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