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