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