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