001/* 002 * $Id: JXEditorPane.java 4147 2012-02-01 17:13:24Z kschaefe $ 003 * 004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021 022package org.jdesktop.swingx; 023 024import java.awt.Component; 025import java.awt.Rectangle; 026import java.awt.datatransfer.Clipboard; 027import java.awt.datatransfer.DataFlavor; 028import java.awt.datatransfer.Transferable; 029import java.awt.event.ActionEvent; 030import java.awt.event.ItemEvent; 031import java.awt.event.ItemListener; 032import java.beans.PropertyChangeEvent; 033import java.beans.PropertyChangeListener; 034import java.io.IOException; 035import java.io.Reader; 036import java.io.StringReader; 037import java.net.URL; 038import java.util.HashMap; 039import java.util.Map; 040import java.util.Vector; 041import java.util.logging.Level; 042import java.util.logging.Logger; 043import java.util.regex.MatchResult; 044import java.util.regex.Matcher; 045import java.util.regex.Pattern; 046 047import javax.swing.ActionMap; 048import javax.swing.DefaultComboBoxModel; 049import javax.swing.DefaultListCellRenderer; 050import javax.swing.JComboBox; 051import javax.swing.JComponent; 052import javax.swing.JEditorPane; 053import javax.swing.JList; 054import javax.swing.KeyStroke; 055import javax.swing.SwingConstants; 056import javax.swing.SwingUtilities; 057import javax.swing.event.CaretEvent; 058import javax.swing.event.CaretListener; 059import javax.swing.event.UndoableEditEvent; 060import javax.swing.event.UndoableEditListener; 061import javax.swing.text.AttributeSet; 062import javax.swing.text.BadLocationException; 063import javax.swing.text.Document; 064import javax.swing.text.EditorKit; 065import javax.swing.text.Element; 066import javax.swing.text.MutableAttributeSet; 067import javax.swing.text.Segment; 068import javax.swing.text.SimpleAttributeSet; 069import javax.swing.text.StyleConstants; 070import javax.swing.text.StyledDocument; 071import javax.swing.text.StyledEditorKit; 072import javax.swing.text.html.HTML; 073import javax.swing.text.html.HTMLDocument; 074import javax.swing.text.html.HTMLEditorKit; 075import javax.swing.undo.CannotRedoException; 076import javax.swing.undo.CannotUndoException; 077import javax.swing.undo.UndoManager; 078 079import org.jdesktop.beans.JavaBean; 080import org.jdesktop.swingx.action.ActionManager; 081import org.jdesktop.swingx.action.Targetable; 082import org.jdesktop.swingx.action.TargetableSupport; 083import org.jdesktop.swingx.plaf.UIAction; 084import org.jdesktop.swingx.search.SearchFactory; 085import org.jdesktop.swingx.search.Searchable; 086 087/** 088 * <p> 089 * {@code JXEditorPane} offers enhanced functionality over the standard {@code 090 * JEditorPane}. Unlike its parent, {@code JXEdtiorPane} {@link 091 * JEditorPane#HONOR_DISPLAY_PROPERTIES honors display properties} by default. 092 * Users can revert to the behavior of {@code JEditorPane} by setting the 093 * property to {@code false}. 094 * </p> 095 * <h3>Additional Features</h3> 096 * <dl> 097 * <dt> 098 * Improved text editing</dt> 099 * <dd> 100 * The standard text component commands for <i>cut</i>, <i>copy</i>, and 101 * <i>paste</i> used enhanced selection methods. The commands will only be 102 * active if there is text to cut or copy selected or valid text in the 103 * clipboard to paste.</dd> 104 * <dt> 105 * Improved HTML editing</dt> 106 * <dd> 107 * Using the context-sensitive approach for the standard text commands, {@code 108 * JXEditorPane} provides HTML editing commands that alter functionality 109 * depending on the document state. Currently, the user can quick-format the 110 * document with headers (H# tags), paragraphs, and breaks.</dd> 111 * <dt> 112 * Built-in UndoManager</dt> 113 * <dd> 114 * Text components provide {@link UndoableEditEvent}s. {@code JXEditorPane} 115 * places those events in an {@link UndoManager} and provides 116 * <i>undo</i>/<i>redo</i> commands. Undo and redo are context-sensitive (like 117 * the text commands) and will only be active if it is possible to perform the 118 * command.</dd> 119 * <dt> 120 * Built-in search</dt> 121 * <dd> 122 * Using SwingX {@linkplain SearchFactory search mechanisms}, {@code 123 * JXEditorPane} provides search capabilities, allowing the user to find text 124 * within the document.</dd> 125 * </dl> 126 * <h3>Example</h3> 127 * <p> 128 * Creating a {@code JXEditorPane} is no different than creating a {@code 129 * JEditorPane}. However, the following example demonstrates the best way to 130 * access the improved command functionality. 131 * 132 * <pre> 133 * JXEditorPane editorPane = new JXEditorPane("some URL"); 134 * add(editorPane); 135 * JToolBar toolBar = ActionContainerFactory.createToolBar(editorPane.getCommands[]); 136 * toolBar.addSeparator(); 137 * toolBar.add(editorPane.getParagraphSelector()); 138 * setToolBar(toolBar); 139 * </pre> 140 * </p> 141 * 142 * @author Mark Davidson 143 */ 144@JavaBean 145public class JXEditorPane extends JEditorPane implements /*Searchable, */Targetable { 146 147 private static final Logger LOG = Logger.getLogger(JXEditorPane.class 148 .getName()); 149 150 private UndoableEditListener undoHandler; 151 private UndoManager undoManager; 152 private CaretListener caretHandler; 153 private JComboBox selector; 154 155 // The ids of supported actions. Perhaps this should be public. 156 private final static String ACTION_FIND = "find"; 157 private final static String ACTION_UNDO = "undo"; 158 private final static String ACTION_REDO = "redo"; 159 /* 160 * These next 3 actions are part of a *HACK* to get cut/copy/paste 161 * support working in the same way as find, undo and redo. in JTextComponent 162 * the cut/copy/paste actions are _not_ added to the ActionMap. Instead, 163 * a default "transfer handler" system is used, apparently to get the text 164 * onto the system clipboard. 165 * Since there aren't any CUT/COPY/PASTE actions in the JTextComponent's action 166 * map, they cannot be referenced by the action framework the same way that 167 * find/undo/redo are. So, I added the actions here. The really hacky part 168 * is that by defining an Action to go along with the cut/copy/paste keys, 169 * I loose the default handling in the cut/copy/paste routines. So, I have 170 * to remove cut/copy/paste from the action map, call the appropriate 171 * method (cut, copy, or paste) and then add the action back into the 172 * map. Yuck! 173 */ 174 private final static String ACTION_CUT = "cut"; 175 private final static String ACTION_COPY = "copy"; 176 private final static String ACTION_PASTE = "paste"; 177 178 private TargetableSupport targetSupport = new TargetableSupport(this); 179 private Searchable searchable; 180 181 /** 182 * Creates a new <code>JXEditorPane</code>. 183 * The document model is set to <code>null</code>. 184 */ 185 public JXEditorPane() { 186 init(); 187 } 188 189 /** 190 * Creates a <code>JXEditorPane</code> based on a string containing 191 * a URL specification. 192 * 193 * @param url the URL 194 * @exception IOException if the URL is <code>null</code> or 195 * cannot be accessed 196 */ 197 public JXEditorPane(String url) throws IOException { 198 super(url); 199 init(); 200 } 201 202 /** 203 * Creates a <code>JXEditorPane</code> that has been initialized 204 * to the given text. This is a convenience constructor that calls the 205 * <code>setContentType</code> and <code>setText</code> methods. 206 * 207 * @param type mime type of the given text 208 * @param text the text to initialize with; may be <code>null</code> 209 * @exception NullPointerException if the <code>type</code> parameter 210 * is <code>null</code> 211 */ 212 public JXEditorPane(String type, String text) { 213 super(type, text); 214 init(); 215 } 216 217 /** 218 * Creates a <code>JXEditorPane</code> based on a specified URL for input. 219 * 220 * @param initialPage the URL 221 * @exception IOException if the URL is <code>null</code> 222 * or cannot be accessed 223 */ 224 public JXEditorPane(URL initialPage) throws IOException { 225 super(initialPage); 226 init(); 227 } 228 229 private void init() { 230 putClientProperty(HONOR_DISPLAY_PROPERTIES, true); 231 setEditorKitForContentType("text/html", new SloppyHTMLEditorKit()); 232 addPropertyChangeListener(new PropertyHandler()); 233 getDocument().addUndoableEditListener(getUndoableEditListener()); 234 initActions(); 235 } 236 237 private class PropertyHandler implements PropertyChangeListener { 238 @Override 239 public void propertyChange(PropertyChangeEvent evt) { 240 String name = evt.getPropertyName(); 241 if (name.equals("document")) { 242 Document doc = (Document)evt.getOldValue(); 243 if (doc != null) { 244 doc.removeUndoableEditListener(getUndoableEditListener()); 245 } 246 247 doc = (Document)evt.getNewValue(); 248 if (doc != null) { 249 doc.addUndoableEditListener(getUndoableEditListener()); 250 } 251 } 252 } 253 254 } 255 256 // pp for testing 257 CaretListener getCaretListener() { 258 return caretHandler; 259 } 260 261 // pp for testing 262 UndoableEditListener getUndoableEditListener() { 263 if (undoHandler == null) { 264 undoHandler = new UndoHandler(); 265 undoManager = new UndoManager(); 266 } 267 return undoHandler; 268 } 269 270 /** 271 * Overidden to perform document initialization based on type. 272 */ 273 @Override 274 public void setEditorKit(EditorKit kit) { 275 super.setEditorKit(kit); 276 277 if (kit instanceof StyledEditorKit) { 278 if (caretHandler == null) { 279 caretHandler = new CaretHandler(); 280 } 281 addCaretListener(caretHandler); 282 } 283 } 284 285 /** 286 * Register the actions that this class can handle. 287 */ 288 protected void initActions() { 289 ActionMap map = getActionMap(); 290 map.put(ACTION_FIND, new Actions(ACTION_FIND)); 291 map.put(ACTION_UNDO, new Actions(ACTION_UNDO)); 292 map.put(ACTION_REDO, new Actions(ACTION_REDO)); 293 map.put(ACTION_CUT, new Actions(ACTION_CUT)); 294 map.put(ACTION_COPY, new Actions(ACTION_COPY)); 295 map.put(ACTION_PASTE, new Actions(ACTION_PASTE)); 296 297 KeyStroke findStroke = SearchFactory.getInstance().getSearchAccelerator(); 298 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(findStroke, "find"); 299 } 300 301 // undo/redo implementation 302 303 private class UndoHandler implements UndoableEditListener { 304 @Override 305 public void undoableEditHappened(UndoableEditEvent evt) { 306 undoManager.addEdit(evt.getEdit()); 307 updateActionState(); 308 } 309 } 310 311 /** 312 * Updates the state of the actions in response to an undo/redo operation. <p> 313 * 314 */ 315 private void updateActionState() { 316 // Update the state of the undo and redo actions 317 // JW: fiddling with actionManager's actions state? I'm pretty sure 318 // we don't want that: the manager will get nuts with multiple 319 // components with different state. 320 // It's up to whatever manager to listen 321 // to our changes and update itself accordingly. Which is not 322 // well supported with the current design ... nobody 323 // really cares about enabled as it should. 324 // 325 Runnable doEnabled = new Runnable() { 326 @Override 327 public void run() { 328 ActionManager manager = ActionManager.getInstance(); 329 manager.setEnabled(ACTION_UNDO, undoManager.canUndo()); 330 manager.setEnabled(ACTION_REDO, undoManager.canRedo()); 331 } 332 }; 333 SwingUtilities.invokeLater(doEnabled); 334 } 335 336 /** 337 * A small class which dispatches actions. 338 * TODO: Is there a way that we can make this static? 339 * JW: these if-constructs are totally crazy ... we live in OO world! 340 * 341 */ 342 private class Actions extends UIAction { 343 Actions(String name) { 344 super(name); 345 } 346 347 @Override 348 public void actionPerformed(ActionEvent evt) { 349 String name = getName(); 350 if (ACTION_FIND.equals(name)) { 351 find(); 352 } 353 else if (ACTION_UNDO.equals(name)) { 354 try { 355 undoManager.undo(); 356 } catch (CannotUndoException ex) { 357 LOG.info("Could not undo"); 358 } 359 updateActionState(); 360 } 361 else if (ACTION_REDO.equals(name)) { 362 try { 363 undoManager.redo(); 364 } catch (CannotRedoException ex) { 365 LOG.info("Could not redo"); 366 } 367 updateActionState(); 368 } else if (ACTION_CUT.equals(name)) { 369 ActionMap map = getActionMap(); 370 map.remove(ACTION_CUT); 371 cut(); 372 map.put(ACTION_CUT, this); 373 } else if (ACTION_COPY.equals(name)) { 374 ActionMap map = getActionMap(); 375 map.remove(ACTION_COPY); 376 copy(); 377 map.put(ACTION_COPY, this); 378 } else if (ACTION_PASTE.equals(name)) { 379 ActionMap map = getActionMap(); 380 map.remove(ACTION_PASTE); 381 paste(); 382 map.put(ACTION_PASTE, this); 383 } 384 else { 385 LOG.fine("ActionHandled: " + name); 386 } 387 388 } 389 390 @Override 391 public boolean isEnabled(Object sender) { 392 String name = getName(); 393 if (ACTION_UNDO.equals(name)) { 394 return isEditable() && undoManager.canUndo(); 395 } 396 if (ACTION_REDO.equals(name)) { 397 return isEditable() && undoManager.canRedo(); 398 } 399 if (ACTION_PASTE.equals(name)) { 400 if (!isEditable()) return false; 401 // is this always possible? 402 boolean dataOnClipboard = false; 403 try { 404 dataOnClipboard = getToolkit() 405 .getSystemClipboard().getContents(null) != null; 406 } catch (Exception e) { 407 // can't do anything - clipboard unaccessible 408 } 409 return dataOnClipboard; 410 } 411 boolean selectedText = getSelectionEnd() 412 - getSelectionStart() > 0; 413 if (ACTION_CUT.equals(name)) { 414 return isEditable() && selectedText; 415 } 416 if (ACTION_COPY.equals(name)) { 417 return selectedText; 418 } 419 if (ACTION_FIND.equals(name)) { 420 return getDocument().getLength() > 0; 421 } 422 return true; 423 } 424 425 426 } 427 428 /** 429 * Retrieves a component which will be used as the paragraph selector. 430 * This can be placed in the toolbar. 431 * <p> 432 * Note: This is only valid for the HTMLEditorKit 433 */ 434 public JComboBox getParagraphSelector() { 435 if (selector == null) { 436 selector = new ParagraphSelector(); 437 } 438 return selector; 439 } 440 441 /** 442 * A control which should be placed in the toolbar to enable 443 * paragraph selection. 444 */ 445 private class ParagraphSelector extends JComboBox implements ItemListener { 446 447 private Map<HTML.Tag, String> itemMap; 448 449 public ParagraphSelector() { 450 451 // The item map is for rendering 452 itemMap = new HashMap<HTML.Tag, String>(); 453 itemMap.put(HTML.Tag.P, "Paragraph"); 454 itemMap.put(HTML.Tag.H1, "Heading 1"); 455 itemMap.put(HTML.Tag.H2, "Heading 2"); 456 itemMap.put(HTML.Tag.H3, "Heading 3"); 457 itemMap.put(HTML.Tag.H4, "Heading 4"); 458 itemMap.put(HTML.Tag.H5, "Heading 5"); 459 itemMap.put(HTML.Tag.H6, "Heading 6"); 460 itemMap.put(HTML.Tag.PRE, "Preformatted"); 461 462 // The list of items 463 Vector<HTML.Tag> items = new Vector<HTML.Tag>(); 464 items.addElement(HTML.Tag.P); 465 items.addElement(HTML.Tag.H1); 466 items.addElement(HTML.Tag.H2); 467 items.addElement(HTML.Tag.H3); 468 items.addElement(HTML.Tag.H4); 469 items.addElement(HTML.Tag.H5); 470 items.addElement(HTML.Tag.H6); 471 items.addElement(HTML.Tag.PRE); 472 473 setModel(new DefaultComboBoxModel(items)); 474 setRenderer(new ParagraphRenderer()); 475 addItemListener(this); 476 setFocusable(false); 477 } 478 479 @Override 480 public void itemStateChanged(ItemEvent evt) { 481 if (evt.getStateChange() == ItemEvent.SELECTED) { 482 applyTag((HTML.Tag)evt.getItem()); 483 } 484 } 485 486 private class ParagraphRenderer extends DefaultListCellRenderer { 487 488 public ParagraphRenderer() { 489 setOpaque(true); 490 } 491 492 @Override 493 public Component getListCellRendererComponent(JList list, 494 Object value, 495 int index, 496 boolean isSelected, 497 boolean cellHasFocus) { 498 super.getListCellRendererComponent(list, value, index, isSelected, 499 cellHasFocus); 500 501 setText((String)itemMap.get(value)); 502 503 return this; 504 } 505 } 506 507 508 // TODO: Should have a rendererer which does stuff like: 509 // Paragraph, Heading 1, etc... 510 } 511 512 /** 513 * Applys the tag to the current selection 514 */ 515 protected void applyTag(HTML.Tag tag) { 516 Document doc = getDocument(); 517 if (!(doc instanceof HTMLDocument)) { 518 return; 519 } 520 HTMLDocument hdoc = (HTMLDocument)doc; 521 int start = getSelectionStart(); 522 int end = getSelectionEnd(); 523 524 Element element = hdoc.getParagraphElement(start); 525 MutableAttributeSet newAttrs = new SimpleAttributeSet(element.getAttributes()); 526 newAttrs.addAttribute(StyleConstants.NameAttribute, tag); 527 528 hdoc.setParagraphAttributes(start, end - start, newAttrs, true); 529 } 530 531 /** 532 * The paste method has been overloaded to strip off the <html><body> tags 533 * This doesn't really work. 534 */ 535 @Override 536 public void paste() { 537 Clipboard clipboard = getToolkit().getSystemClipboard(); 538 Transferable content = clipboard.getContents(this); 539 if (content != null) { 540 DataFlavor[] flavors = content.getTransferDataFlavors(); 541 try { 542 for (int i = 0; i < flavors.length; i++) { 543 if (String.class.equals(flavors[i].getRepresentationClass())) { 544 Object data = content.getTransferData(flavors[i]); 545 546 if (flavors[i].isMimeTypeEqual("text/plain")) { 547 // This works but we lose all the formatting. 548 replaceSelection(data.toString()); 549 break; 550 } 551 } 552 } 553 } catch (Exception ex) { 554 // TODO change to something meaningful - when can this acutally happen? 555 LOG.log(Level.FINE, "What can produce a problem with data flavor?", ex); 556 } 557 } 558 } 559 560 private void find() { 561 SearchFactory.getInstance().showFindInput(this, getSearchable()); 562 } 563 564 /** 565 * 566 * @return a not-null Searchable for this editor. 567 */ 568 public Searchable getSearchable() { 569 if (searchable == null) { 570 searchable = new DocumentSearchable(); 571 } 572 return searchable; 573 } 574 575 /** 576 * sets the Searchable for this editor. If null, a default 577 * searchable will be used. 578 * 579 * @param searchable 580 */ 581 public void setSearchable(Searchable searchable) { 582 this.searchable = searchable; 583 } 584 585 /** 586 * A {@code Searchable} implementation for {@code Document}s. 587 */ 588 public class DocumentSearchable implements Searchable { 589 @Override 590 public int search(String searchString) { 591 return search(searchString, -1); 592 } 593 594 @Override 595 public int search(String searchString, int columnIndex) { 596 return search(searchString, columnIndex, false); 597 } 598 599 @Override 600 public int search(String searchString, int columnIndex, boolean backward) { 601 Pattern pattern = null; 602 if (!isEmpty(searchString)) { 603 pattern = Pattern.compile(searchString, 0); 604 } 605 return search(pattern, columnIndex, backward); 606 } 607 608 /** 609 * checks if the searchString should be interpreted as empty. 610 * here: returns true if string is null or has zero length. 611 * 612 * TODO: This should be in a utility class. 613 * 614 * @param searchString 615 * @return true if string is null or has zero length 616 */ 617 protected boolean isEmpty(String searchString) { 618 return (searchString == null) || searchString.length() == 0; 619 } 620 621 @Override 622 public int search(Pattern pattern) { 623 return search(pattern, -1); 624 } 625 626 @Override 627 public int search(Pattern pattern, int startIndex) { 628 return search(pattern, startIndex, false); 629 } 630 631 int lastFoundIndex = -1; 632 633 MatchResult lastMatchResult; 634 String lastRegEx; 635 /** 636 * @return start position of matching string or -1 637 */ 638 @Override 639 public int search(Pattern pattern, final int startIndex, 640 boolean backwards) { 641 if ((pattern == null) 642 || (getDocument().getLength() == 0) 643 || ((startIndex > -1) && (getDocument().getLength() < startIndex))) { 644 updateStateAfterNotFound(); 645 return -1; 646 } 647 648 int start = startIndex; 649 if (maybeExtendedMatch(startIndex)) { 650 if (foundExtendedMatch(pattern, start)) { 651 return lastFoundIndex; 652 } 653 start++; 654 } 655 656 int length; 657 if (backwards) { 658 start = 0; 659 if (startIndex < 0) { 660 length = getDocument().getLength() - 1; 661 } else { 662 length = -1 + startIndex; 663 } 664 } else { 665 // start = startIndex + 1; 666 if (start < 0) 667 start = 0; 668 length = getDocument().getLength() - start; 669 } 670 Segment segment = new Segment(); 671 672 try { 673 getDocument().getText(start, length, segment); 674 } catch (BadLocationException ex) { 675 LOG.log(Level.FINE, 676 "this should not happen (calculated the valid start/length) " , ex); 677 } 678 679 Matcher matcher = pattern.matcher(segment.toString()); 680 MatchResult currentResult = getMatchResult(matcher, !backwards); 681 if (currentResult != null) { 682 updateStateAfterFound(currentResult, start); 683 } else { 684 updateStateAfterNotFound(); 685 } 686 return lastFoundIndex; 687 688 } 689 690 /** 691 * Search from same startIndex as the previous search. 692 * Checks if the match is different from the last (either 693 * extended/reduced) at the same position. Returns true 694 * if the current match result represents a different match 695 * than the last, false if no match or the same. 696 * 697 * @param pattern 698 * @param start 699 * @return true if the current match result represents a different 700 * match than the last, false if no match or the same. 701 */ 702 private boolean foundExtendedMatch(Pattern pattern, int start) { 703 // JW: logic still needs cleanup... 704 if (pattern.pattern().equals(lastRegEx)) { 705 return false; 706 } 707 int length = getDocument().getLength() - start; 708 Segment segment = new Segment(); 709 710 try { 711 getDocument().getText(start, length, segment); 712 } catch (BadLocationException ex) { 713 LOG.log(Level.FINE, 714 "this should not happen (calculated the valid start/length) " , ex); 715 } 716 Matcher matcher = pattern.matcher(segment.toString()); 717 MatchResult currentResult = getMatchResult(matcher, true); 718 if (currentResult != null) { 719 // JW: how to compare match results reliably? 720 // the group().equals probably isn't the best idea... 721 // better check pattern? 722 if ((currentResult.start() == 0) && 723 (!lastMatchResult.group().equals(currentResult.group()))) { 724 updateStateAfterFound(currentResult, start); 725 return true; 726 } 727 } 728 return false; 729 } 730 731 /** 732 * Checks if the startIndex is a candidate for trying a re-match. 733 * 734 * 735 * @param startIndex 736 * @return true if the startIndex should be re-matched, false if not. 737 */ 738 private boolean maybeExtendedMatch(final int startIndex) { 739 return (startIndex >= 0) && (startIndex == lastFoundIndex); 740 } 741 742 /** 743 * @param currentResult 744 * @param offset 745 * @return the start position of the selected text 746 */ 747 private int updateStateAfterFound(MatchResult currentResult, final int offset) { 748 int end = currentResult.end() + offset; 749 int found = currentResult.start() + offset; 750 select(found, end); 751 getCaret().setSelectionVisible(true); 752 lastFoundIndex = found; 753 lastMatchResult = currentResult; 754 lastRegEx = ((Matcher) lastMatchResult).pattern().pattern(); 755 return found; 756 } 757 758 /** 759 * @param matcher 760 * @param useFirst whether or not to return after the first match is found. 761 * @return <code>MatchResult</code> or null 762 */ 763 private MatchResult getMatchResult(Matcher matcher, boolean useFirst) { 764 MatchResult currentResult = null; 765 while (matcher.find()) { 766 currentResult = matcher.toMatchResult(); 767 if (useFirst) break; 768 } 769 return currentResult; 770 } 771 772 /** 773 */ 774 private void updateStateAfterNotFound() { 775 lastFoundIndex = -1; 776 lastMatchResult = null; 777 lastRegEx = null; 778 setCaretPosition(getSelectionEnd()); 779 } 780 781 } 782 783 @Override 784 public boolean hasCommand(Object command) { 785 return targetSupport.hasCommand(command); 786 } 787 788 @Override 789 public Object[] getCommands() { 790 return targetSupport.getCommands(); 791 } 792 793 @Override 794 public boolean doCommand(Object command, Object value) { 795 return targetSupport.doCommand(command, value); 796 } 797 798 /** 799 * {@inheritDoc} 800 */ 801 @Override 802 public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { 803 switch(orientation) { 804 case SwingConstants.VERTICAL: 805 return getFontMetrics(getFont()).getHeight(); 806 case SwingConstants.HORIZONTAL: 807 return getFontMetrics(getFont()).charWidth('M'); 808 default: 809 throw new IllegalArgumentException("Invalid orientation: " + orientation); 810 } 811 } 812 813 /** 814 * Listens to the caret placement and adjusts the editing 815 * properties as appropriate. 816 * 817 * Should add more attributes as required. 818 */ 819 private class CaretHandler implements CaretListener { 820 @Override 821 public void caretUpdate(CaretEvent evt) { 822 StyledDocument document = (StyledDocument)getDocument(); 823 int dot = evt.getDot(); 824 //SwingX #257--ensure display shows the valid attributes 825 dot = dot > 0 ? dot - 1 : dot; 826 827 Element elem = document.getCharacterElement(dot); 828 AttributeSet set = elem.getAttributes(); 829 830 // JW: see comment in updateActionState 831 ActionManager manager = ActionManager.getInstance(); 832 manager.setSelected("font-bold", StyleConstants.isBold(set)); 833 manager.setSelected("font-italic", StyleConstants.isItalic(set)); 834 manager.setSelected("font-underline", StyleConstants.isUnderline(set)); 835 836 elem = document.getParagraphElement(dot); 837 set = elem.getAttributes(); 838 839 // Update the paragraph selector if applicable. 840 if (selector != null) { 841 selector.setSelectedItem(set.getAttribute(StyleConstants.NameAttribute)); 842 } 843 844 switch (StyleConstants.getAlignment(set)) { 845 // XXX There is a bug here. the setSelected method 846 // should only affect the UI actions rather than propagate 847 // down into the action map actions. 848 case StyleConstants.ALIGN_LEFT: 849 manager.setSelected("left-justify", true); 850 break; 851 852 case StyleConstants.ALIGN_CENTER: 853 manager.setSelected("center-justify", true); 854 break; 855 856 case StyleConstants.ALIGN_RIGHT: 857 manager.setSelected("right-justify", true); 858 break; 859 } 860 } 861 } 862 863 /** 864 * Handles sloppy HTML. This implementation currently only looks for 865 * tags that have a / at the end (self-closing tags) and fixes them 866 * to work with the version of HTML supported by HTMLEditorKit 867 * <p>TODO: Need to break this functionality out so it can take pluggable 868 * replacement code blocks, allowing people to write custom replacement 869 * routines. The idea is that with some simple modifications a lot more 870 * sloppy HTML can be rendered correctly. 871 * 872 * @author rbair 873 */ 874 private static final class SloppyHTMLEditorKit extends HTMLEditorKit { 875 @Override 876 public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException { 877 //read the reader into a String 878 StringBuffer buffer = new StringBuffer(); 879 int length; 880 char[] data = new char[1024]; 881 while ((length = in.read(data)) != -1) { 882 buffer.append(data, 0, length); 883 } 884 //TODO is this regex right? 885 StringReader reader = new StringReader(buffer.toString().replaceAll("/>", ">")); 886 super.read(reader, doc, pos); 887 } 888 } 889} 890