/* * @(#)HTMLEditorKit.java 1.131 04/05/18 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package javax.swing.text.html; import java.lang.reflect.Method; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import javax.swing.text.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.event.*; import javax.swing.plaf.TextUI; import java.util.*; import javax.accessibility.*; import java.lang.ref.*; /** * The Swing JEditorPane text component supports different kinds * of content via a plug-in mechanism called an EditorKit. Because * HTML is a very popular format of content, some support is provided * by default. The default support is provided by this class, which * supports HTML version 3.2 (with some extensions), and is migrating * toward version 4.0. * The <applet> tag is not supported, but some support is provided * for the <object> tag. *

* There are several goals of the HTML EditorKit provided, that have * an effect upon the way that HTML is modeled. These * have influenced its design in a substantial way. *

*

*

* Support editing *
* It might seem fairly obvious that a plug-in for JEditorPane * should provide editing support, but that fact has several * design considerations. There are a substantial number of HTML * documents that don't properly conform to an HTML specification. * These must be normalized somewhat into a correct form if one * is to edit them. Additionally, users don't like to be presented * with an excessive amount of structure editing, so using traditional * text editing gestures is preferred over using the HTML structure * exactly as defined in the HTML document. *

* The modeling of HTML is provided by the class HTMLDocument. * Its documention describes the details of how the HTML is modeled. * The editing support leverages heavily off of the text package. *

*

* Extendable/Scalable *
* To maximize the usefulness of this kit, a great deal of effort * has gone into making it extendable. These are some of the * features. *
    *
  1. * The parser is replacable. The default parser is the Hot Java * parser which is DTD based. A different DTD can be used, or an * entirely different parser can be used. To change the parser, * reimplement the getParser method. The default parser is * dynamically loaded when first asked for, so the class files * will never be loaded if an alternative parser is used. The * default parser is in a separate package called parser below * this package. *
  2. * The parser drives the ParserCallback, which is provided by * HTMLDocument. To change the callback, subclass HTMLDocument * and reimplement the createDefaultDocument method to return * document that produces a different reader. The reader controls * how the document is structured. Although the Document provides * HTML support by default, there is nothing preventing support of * non-HTML tags that result in alternative element structures. *
  3. * The default view of the models are provided as a hierarchy of * View implementations, so one can easily customize how a particular * element is displayed or add capabilities for new kinds of elements * by providing new View implementations. The default set of views * are provided by the HTMLFactory class. This can * be easily changed by subclassing or replacing the HTMLFactory * and reimplementing the getViewFactory method to return the alternative * factory. *
  4. * The View implementations work primarily off of CSS attributes, * which are kept in the views. This makes it possible to have * multiple views mapped over the same model that appear substantially * different. This can be especially useful for printing. For * most HTML attributes, the HTML attributes are converted to CSS * attributes for display. This helps make the View implementations * more general purpose *
*

*

* Asynchronous Loading *
* Larger documents involve a lot of parsing and take some time * to load. By default, this kit produces documents that will be * loaded asynchronously if loaded using JEditorPane.setPage. * This is controlled by a property on the document. The method * createDefaultDocument can * be overriden to change this. The batching of work is done * by the HTMLDocument.HTMLReader class. The actual * work is done by the DefaultStyledDocument and * AbstractDocument classes in the text package. *

*

* Customization from current LAF *
* HTML provides a well known set of features without exactly * specifying the display characteristics. Swing has a theme * mechanism for its look-and-feel implementations. It is desirable * for the look-and-feel to feed display characteristics into the * HTML views. An user with poor vision for example would want * high contrast and larger than typical fonts. *

* The support for this is provided by the StyleSheet * class. The presentation of the HTML can be heavily influenced * by the setting of the StyleSheet property on the EditorKit. *

*

* Not lossy *
* An EditorKit has the ability to be read and save documents. * It is generally the most pleasing to users if there is no loss * of data between the two operation. The policy of the HTMLEditorKit * will be to store things not recognized or not necessarily visible * so they can be subsequently written out. The model of the HTML document * should therefore contain all information discovered while reading the * document. This is constrained in some ways by the need to support * editing (i.e. incorrect documents sometimes must be normalized). * The guiding principle is that information shouldn't be lost, but * some might be synthesized to produce a more correct model or it might * be rearranged. *
* * @author Timothy Prinzing * @version 1.131 05/18/04 */ public class HTMLEditorKit extends StyledEditorKit implements Accessible { private JEditorPane theEditor; /** * Constructs an HTMLEditorKit, creates a StyleContext, * and loads the style sheet. */ public HTMLEditorKit() { } /** * Get the MIME type of the data that this * kit represents support for. This kit supports * the type text/html. * * @return the type */ public String getContentType() { return "text/html"; } /** * Fetch a factory that is suitable for producing * views of any models that are produced by this * kit. * * @return the factory */ public ViewFactory getViewFactory() { return defaultFactory; } /** * Create an uninitialized text storage model * that is appropriate for this type of editor. * * @return the model */ public Document createDefaultDocument() { StyleSheet styles = getStyleSheet(); StyleSheet ss = new StyleSheet(); ss.addStyleSheet(styles); HTMLDocument doc = new HTMLDocument(ss); doc.setParser(getParser()); doc.setAsynchronousLoadPriority(4); doc.setTokenThreshold(100); return doc; } /** * Inserts content from the given stream. If doc is * an instance of HTMLDocument, this will read * HTML 3.2 text. Inserting HTML into a non-empty document must be inside * the body Element, if you do not insert into the body an exception will * be thrown. When inserting into a non-empty document all tags outside * of the body (head, title) will be dropped. * * @param in the stream to read from * @param doc the destination for the insertion * @param pos the location in the document to place the * content * @exception IOException on any I/O error * @exception BadLocationException if pos represents an invalid * location within the document * @exception RuntimeException (will eventually be a BadLocationException) * if pos is invalid */ public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException { if (doc instanceof HTMLDocument) { HTMLDocument hdoc = (HTMLDocument) doc; Parser p = getParser(); if (p == null) { throw new IOException("Can't load parser"); } if (pos > doc.getLength()) { throw new BadLocationException("Invalid location", pos); } ParserCallback receiver = hdoc.getReader(pos); Boolean ignoreCharset = (Boolean)doc.getProperty("IgnoreCharsetDirective"); p.parse(in, receiver, (ignoreCharset == null) ? false : ignoreCharset.booleanValue()); receiver.flush(); } else { super.read(in, doc, pos); } } /** * Inserts HTML into an existing document. * * @param doc the document to insert into * @param offset the offset to insert HTML at * @param popDepth the number of ElementSpec.EndTagTypes to generate before * inserting * @param pushDepth the number of ElementSpec.StartTagTypes with a direction * of ElementSpec.JoinNextDirection that should be generated * before inserting, but after the end tags have been generated * @param insertTag the first tag to start inserting into document * @exception RuntimeException (will eventually be a BadLocationException) * if pos is invalid */ public void insertHTML(HTMLDocument doc, int offset, String html, int popDepth, int pushDepth, HTML.Tag insertTag) throws BadLocationException, IOException { Parser p = getParser(); if (p == null) { throw new IOException("Can't load parser"); } if (offset > doc.getLength()) { throw new BadLocationException("Invalid location", offset); } ParserCallback receiver = doc.getReader(offset, popDepth, pushDepth, insertTag); Boolean ignoreCharset = (Boolean)doc.getProperty ("IgnoreCharsetDirective"); p.parse(new StringReader(html), receiver, (ignoreCharset == null) ? false : ignoreCharset.booleanValue()); receiver.flush(); } /** * Write content from a document to the given stream * in a format appropriate for this kind of content handler. * * @param out the stream to write to * @param doc the source for the write * @param pos the location in the document to fetch the * content * @param len the amount to write out * @exception IOException on any I/O error * @exception BadLocationException if pos represents an invalid * location within the document */ public void write(Writer out, Document doc, int pos, int len) throws IOException, BadLocationException { if (doc instanceof HTMLDocument) { HTMLWriter w = new HTMLWriter(out, (HTMLDocument)doc, pos, len); w.write(); } else if (doc instanceof StyledDocument) { MinimalHTMLWriter w = new MinimalHTMLWriter(out, (StyledDocument)doc, pos, len); w.write(); } else { super.write(out, doc, pos, len); } } /** * Called when the kit is being installed into the * a JEditorPane. * * @param c the JEditorPane */ public void install(JEditorPane c) { c.addMouseListener(linkHandler); c.addMouseMotionListener(linkHandler); c.addCaretListener(nextLinkAction); super.install(c); theEditor = c; } /** * Called when the kit is being removed from the * JEditorPane. This is used to unregister any * listeners that were attached. * * @param c the JEditorPane */ public void deinstall(JEditorPane c) { c.removeMouseListener(linkHandler); c.removeMouseMotionListener(linkHandler); c.removeCaretListener(nextLinkAction); super.deinstall(c); theEditor = null; } /** * Default Cascading Style Sheet file that sets * up the tag views. */ public static final String DEFAULT_CSS = "default.css"; /** * Set the set of styles to be used to render the various * HTML elements. These styles are specified in terms of * CSS specifications. Each document produced by the kit * will have a copy of the sheet which it can add the * document specific styles to. By default, the StyleSheet * specified is shared by all HTMLEditorKit instances. * This should be reimplemented to provide a finer granularity * if desired. */ public void setStyleSheet(StyleSheet s) { defaultStyles = s; } /** * Get the set of styles currently being used to render the * HTML elements. By default the resource specified by * DEFAULT_CSS gets loaded, and is shared by all HTMLEditorKit * instances. */ public StyleSheet getStyleSheet() { if (defaultStyles == null) { defaultStyles = new StyleSheet(); try { InputStream is = HTMLEditorKit.getResourceAsStream(DEFAULT_CSS); Reader r = new BufferedReader( new InputStreamReader(is, "ISO-8859-1")); defaultStyles.loadRules(r, null); r.close(); } catch (Throwable e) { // on error we simply have no styles... the html // will look mighty wrong but still function. } } return defaultStyles; } /** * Fetch a resource relative to the HTMLEditorKit classfile. * If this is called on 1.2 the loading will occur under the * protection of a doPrivileged call to allow the HTMLEditorKit * to function when used in an applet. * * @param name the name of the resource, relative to the * HTMLEditorKit class * @return a stream representing the resource */ static InputStream getResourceAsStream(String name) { try { return ResourceLoader.getResourceAsStream(name); } catch (Throwable e) { // If the class doesn't exist or we have some other // problem we just try to call getResourceAsStream directly. return HTMLEditorKit.class.getResourceAsStream(name); } } /** * Fetches the command list for the editor. This is * the list of commands supported by the superclass * augmented by the collection of commands defined * locally for style operations. * * @return the command list */ public Action[] getActions() { return TextAction.augmentList(super.getActions(), this.defaultActions); } /** * Copies the key/values in elements AttributeSet into * set. This does not copy component, icon, or element * names attributes. Subclasses may wish to refine what is and what * isn't copied here. But be sure to first remove all the attributes that * are in set.

* This is called anytime the caret moves over a different location. * */ protected void createInputAttributes(Element element, MutableAttributeSet set) { set.removeAttributes(set); set.addAttributes(element.getAttributes()); set.removeAttribute(StyleConstants.ComposedTextAttribute); Object o = set.getAttribute(StyleConstants.NameAttribute); if (o instanceof HTML.Tag) { HTML.Tag tag = (HTML.Tag)o; // PENDING: we need a better way to express what shouldn't be // copied when editing... if(tag == HTML.Tag.IMG) { // Remove the related image attributes, src, width, height set.removeAttribute(HTML.Attribute.SRC); set.removeAttribute(HTML.Attribute.HEIGHT); set.removeAttribute(HTML.Attribute.WIDTH); set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT); } else if (tag == HTML.Tag.HR || tag == HTML.Tag.BR) { // Don't copy HRs or BRs either. set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT); } else if (tag == HTML.Tag.COMMENT) { // Don't copy COMMENTs either set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT); set.removeAttribute(HTML.Attribute.COMMENT); } else if (tag == HTML.Tag.INPUT) { // or INPUT either set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT); set.removeAttribute(HTML.Tag.INPUT); } else if (tag instanceof HTML.UnknownTag) { // Don't copy unknowns either:( set.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT); set.removeAttribute(HTML.Attribute.ENDTAG); } } } /** * Gets the input attributes used for the styled * editing actions. * * @return the attribute set */ public MutableAttributeSet getInputAttributes() { if (input == null) { input = getStyleSheet().addStyle(null, null); } return input; } /** * Sets the default cursor. * * @since 1.3 */ public void setDefaultCursor(Cursor cursor) { defaultCursor = cursor; } /** * Returns the default cursor. * * @since 1.3 */ public Cursor getDefaultCursor() { return defaultCursor; } /** * Sets the cursor to use over links. * * @since 1.3 */ public void setLinkCursor(Cursor cursor) { linkCursor = cursor; } /** * Returns the cursor to use over hyper links. */ public Cursor getLinkCursor() { return linkCursor; } /** * Indicates whether an html form submission is processed automatically * or only FormSubmitEvent is fired. * * @return true if html form submission is processed automatically, * false otherwise. * * @see #setAutoFormSubmission * @since 1.5 */ public boolean isAutoFormSubmission() { return isAutoFormSubmission; } /** * Specifies if an html form submission is processed * automatically or only FormSubmitEvent is fired. * By default it is set to true. * * @see #isAutoFormSubmission * @see FormSubmitEvent * @since 1.5 */ public void setAutoFormSubmission(boolean isAuto) { isAutoFormSubmission = isAuto; } /** * Creates a copy of the editor kit. * * @return the copy */ public Object clone() { HTMLEditorKit o = (HTMLEditorKit)super.clone(); if (o != null) { o.input = null; o.linkHandler = new LinkController(); } return o; } /** * Fetch the parser to use for reading HTML streams. * This can be reimplemented to provide a different * parser. The default implementation is loaded dynamically * to avoid the overhead of loading the default parser if * it's not used. The default parser is the HotJava parser * using an HTML 3.2 DTD. */ protected Parser getParser() { if (defaultParser == null) { try { Class c = Class.forName("javax.swing.text.html.parser.ParserDelegator"); defaultParser = (Parser) c.newInstance(); } catch (Throwable e) { } } return defaultParser; } // ----- Accessibility support ----- private AccessibleContext accessibleContext; /** * returns the AccessibleContext associated with this editor kit * * @return the AccessibleContext associated with this editor kit * @since 1.4 */ public AccessibleContext getAccessibleContext() { if (theEditor == null) { return null; } if (accessibleContext == null) { AccessibleHTML a = new AccessibleHTML(theEditor); accessibleContext = a.getAccessibleContext(); } return accessibleContext; } // --- variables ------------------------------------------ private static final Cursor MoveCursor = Cursor.getPredefinedCursor (Cursor.HAND_CURSOR); private static final Cursor DefaultCursor = Cursor.getPredefinedCursor (Cursor.DEFAULT_CURSOR); /** Shared factory for creating HTML Views. */ private static final ViewFactory defaultFactory = new HTMLFactory(); MutableAttributeSet input; private static StyleSheet defaultStyles = null; private LinkController linkHandler = new LinkController(); private static Parser defaultParser = null; private Cursor defaultCursor = DefaultCursor; private Cursor linkCursor = MoveCursor; private boolean isAutoFormSubmission = true; /** * Class to watch the associated component and fire * hyperlink events on it when appropriate. */ public static class LinkController extends MouseAdapter implements MouseMotionListener, Serializable { private Element curElem = null; /** * If true, the current element (curElem) represents an image. */ private boolean curElemImage = false; private String href = null; /** This is used by viewToModel to avoid allocing a new array each * time. */ private Position.Bias[] bias = new Position.Bias[1]; /** * Current offset. */ private int curOffset; /** * Called for a mouse click event. * If the component is read-only (ie a browser) then * the clicked event is used to drive an attempt to * follow the reference specified by a link. * * @param e the mouse event * @see MouseListener#mouseClicked */ public void mouseClicked(MouseEvent e) { JEditorPane editor = (JEditorPane) e.getSource(); if (! editor.isEditable() && SwingUtilities.isLeftMouseButton(e)) { Point pt = new Point(e.getX(), e.getY()); int pos = editor.viewToModel(pt); if (pos >= 0) { activateLink(pos, editor, e.getX(), e.getY()); } } } // ignore the drags public void mouseDragged(MouseEvent e) { } // track the moving of the mouse. public void mouseMoved(MouseEvent e) { JEditorPane editor = (JEditorPane) e.getSource(); HTMLEditorKit kit = (HTMLEditorKit)editor.getEditorKit(); boolean adjustCursor = true; Cursor newCursor = kit.getDefaultCursor(); if (!editor.isEditable()) { Point pt = new Point(e.getX(), e.getY()); int pos = editor.getUI().viewToModel(editor, pt, bias); if (bias[0] == Position.Bias.Backward && pos > 0) { pos--; } if (pos >= 0 &&(editor.getDocument() instanceof HTMLDocument)){ HTMLDocument hdoc = (HTMLDocument)editor.getDocument(); Element elem = hdoc.getCharacterElement(pos); if (!doesElementContainLocation(editor, elem, pos, e.getX(), e.getY())) { elem = null; } if (curElem != elem || curElemImage) { Element lastElem = curElem; curElem = elem; String href = null; curElemImage = false; if (elem != null) { AttributeSet a = elem.getAttributes(); AttributeSet anchor = (AttributeSet)a. getAttribute(HTML.Tag.A); if (anchor == null) { curElemImage = (a.getAttribute(StyleConstants. NameAttribute) == HTML.Tag.IMG); if (curElemImage) { href = getMapHREF(editor, hdoc, elem, a, pos, e.getX(), e.getY()); } } else { href = (String)anchor.getAttribute (HTML.Attribute.HREF); } } if (href != this.href) { // reference changed, fire event(s) fireEvents(editor, hdoc, href, lastElem); this.href = href; if (href != null) { newCursor = kit.getLinkCursor(); } } else { adjustCursor = false; } } else { adjustCursor = false; } curOffset = pos; } } if (adjustCursor && editor.getCursor() != newCursor) { editor.setCursor(newCursor); } } /** * Returns a string anchor if the passed in element has a * USEMAP that contains the passed in location. */ private String getMapHREF(JEditorPane html, HTMLDocument hdoc, Element elem, AttributeSet attr, int offset, int x, int y) { Object useMap = attr.getAttribute(HTML.Attribute.USEMAP); if (useMap != null && (useMap instanceof String)) { Map m = hdoc.getMap((String)useMap); if (m != null && offset < hdoc.getLength()) { Rectangle bounds; TextUI ui = html.getUI(); try { Shape lBounds = ui.modelToView(html, offset, Position.Bias.Forward); Shape rBounds = ui.modelToView(html, offset + 1, Position.Bias.Backward); bounds = lBounds.getBounds(); bounds.add((rBounds instanceof Rectangle) ? (Rectangle)rBounds : rBounds.getBounds()); } catch (BadLocationException ble) { bounds = null; } if (bounds != null) { AttributeSet area = m.getArea(x - bounds.x, y - bounds.y, bounds.width, bounds.height); if (area != null) { return (String)area.getAttribute(HTML.Attribute. HREF); } } } } return null; } /** * Returns true if the View representing e contains * the location x, y. offset * gives the offset into the Document to check for. */ private boolean doesElementContainLocation(JEditorPane editor, Element e, int offset, int x, int y) { if (e != null && offset > 0 && e.getStartOffset() == offset) { try { TextUI ui = editor.getUI(); Shape s1 = ui.modelToView(editor, offset, Position.Bias.Forward); if (s1 == null) { return false; } Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle)s1 : s1.getBounds(); Shape s2 = ui.modelToView(editor, e.getEndOffset(), Position.Bias.Backward); if (s2 != null) { Rectangle r2 = (s2 instanceof Rectangle) ? (Rectangle)s2 : s2.getBounds(); r1.add(r2); } return r1.contains(x, y); } catch (BadLocationException ble) { } } return true; } /** * Calls linkActivated on the associated JEditorPane * if the given position represents a link.

This is implemented * to forward to the method with the same name, but with the following * args both == -1. * * @param pos the position * @param editor the editor pane */ protected void activateLink(int pos, JEditorPane editor) { activateLink(pos, editor, -1, -1); } /** * Calls linkActivated on the associated JEditorPane * if the given position represents a link. If this was the result * of a mouse click, x and * y will give the location of the mouse, otherwise * they will be < 0. * * @param pos the position * @param html the editor pane */ void activateLink(int pos, JEditorPane html, int x, int y) { Document doc = html.getDocument(); if (doc instanceof HTMLDocument) { HTMLDocument hdoc = (HTMLDocument) doc; Element e = hdoc.getCharacterElement(pos); AttributeSet a = e.getAttributes(); AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A); HyperlinkEvent linkEvent = null; String description; if (anchor == null) { href = getMapHREF(html, hdoc, e, a, pos, x, y); } else { href = (String)anchor.getAttribute(HTML.Attribute.HREF); } if (href != null) { linkEvent = createHyperlinkEvent(html, hdoc, href, anchor, e); } if (linkEvent != null) { html.fireHyperlinkUpdate(linkEvent); } } } /** * Creates and returns a new instance of HyperlinkEvent. If * hdoc is a frame document a HTMLFrameHyperlinkEvent * will be created. */ HyperlinkEvent createHyperlinkEvent(JEditorPane html, HTMLDocument hdoc, String href, AttributeSet anchor, Element element) { URL u; try { URL base = hdoc.getBase(); u = new URL(base, href); // Following is a workaround for 1.2, in which // new URL("file://...", "#...") causes the filename to // be lost. if (href != null && "file".equals(u.getProtocol()) && href.startsWith("#")) { String baseFile = base.getFile(); String newFile = u.getFile(); if (baseFile != null && newFile != null && !newFile.startsWith(baseFile)) { u = new URL(base, baseFile + href); } } } catch (MalformedURLException m) { u = null; } HyperlinkEvent linkEvent = null; if (!hdoc.isFrameDocument()) { linkEvent = new HyperlinkEvent(html, HyperlinkEvent.EventType. ACTIVATED, u, href, element); } else { String target = (anchor != null) ? (String)anchor.getAttribute(HTML.Attribute.TARGET) : null; if ((target == null) || (target.equals(""))) { target = hdoc.getBaseTarget(); } if ((target == null) || (target.equals(""))) { target = "_self"; } linkEvent = new HTMLFrameHyperlinkEvent(html, HyperlinkEvent. EventType.ACTIVATED, u, href, element, target); } return linkEvent; } void fireEvents(JEditorPane editor, HTMLDocument doc, String href, Element lastElem) { if (this.href != null) { // fire an exited event on the old link URL u; try { u = new URL(doc.getBase(), this.href); } catch (MalformedURLException m) { u = null; } HyperlinkEvent exit = new HyperlinkEvent(editor, HyperlinkEvent.EventType.EXITED, u, this.href, lastElem); editor.fireHyperlinkUpdate(exit); } if (href != null) { // fire an entered event on the new link URL u; try { u = new URL(doc.getBase(), href); } catch (MalformedURLException m) { u = null; } HyperlinkEvent entered = new HyperlinkEvent(editor, HyperlinkEvent.EventType.ENTERED, u, href, curElem); editor.fireHyperlinkUpdate(entered); } } } /** * Interface to be supported by the parser. This enables * providing a different parser while reusing some of the * implementation provided by this editor kit. */ public static abstract class Parser { /** * Parse the given stream and drive the given callback * with the results of the parse. This method should * be implemented to be thread-safe. */ public abstract void parse(Reader r, ParserCallback cb, boolean ignoreCharSet) throws IOException; } /** * The result of parsing drives these callback methods. * The open and close actions should be balanced. The * flush method will be the last method * called, to give the receiver a chance to flush any * pending data into the document. *

Refer to DocumentParser, the default parser used, for further * information on the contents of the AttributeSets, the positions, and * other info. * * @see javax.swing.text.html.parser.DocumentParser */ public static class ParserCallback { /** * This is passed as an attribute in the attributeset to indicate * the element is implied eg, the string '<>foo<\t>' * contains an implied html element and an implied body element. * * @since 1.3 */ public static final Object IMPLIED = "_implied_"; public void flush() throws BadLocationException { } public void handleText(char[] data, int pos) { } public void handleComment(char[] data, int pos) { } public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) { } public void handleEndTag(HTML.Tag t, int pos) { } public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) { } public void handleError(String errorMsg, int pos){ } /** * This is invoked after the stream has been parsed, but before * flush. eol will be one of \n, \r * or \r\n, which ever is encountered the most in parsing the * stream. * * @since 1.3 */ public void handleEndOfLineString(String eol) { } } /** * A factory to build views for HTML. The following * table describes what this factory will build by * default. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
TagView created *
HTML.Tag.CONTENTInlineView *
HTML.Tag.IMPLIEDjavax.swing.text.html.ParagraphView *
HTML.Tag.Pjavax.swing.text.html.ParagraphView *
HTML.Tag.H1javax.swing.text.html.ParagraphView *
HTML.Tag.H2javax.swing.text.html.ParagraphView *
HTML.Tag.H3javax.swing.text.html.ParagraphView *
HTML.Tag.H4javax.swing.text.html.ParagraphView *
HTML.Tag.H5javax.swing.text.html.ParagraphView *
HTML.Tag.H6javax.swing.text.html.ParagraphView *
HTML.Tag.DTjavax.swing.text.html.ParagraphView *
HTML.Tag.MENUListView *
HTML.Tag.DIRListView *
HTML.Tag.ULListView *
HTML.Tag.OLListView *
HTML.Tag.LIBlockView *
HTML.Tag.DLBlockView *
HTML.Tag.DDBlockView *
HTML.Tag.BODYBlockView *
HTML.Tag.HTMLBlockView *
HTML.Tag.CENTERBlockView *
HTML.Tag.DIVBlockView *
HTML.Tag.BLOCKQUOTEBlockView *
HTML.Tag.PREBlockView *
HTML.Tag.BLOCKQUOTEBlockView *
HTML.Tag.PREBlockView *
HTML.Tag.IMGImageView *
HTML.Tag.HRHRuleView *
HTML.Tag.BRBRView *
HTML.Tag.TABLEjavax.swing.text.html.TableView *
HTML.Tag.INPUTFormView *
HTML.Tag.SELECTFormView *
HTML.Tag.TEXTAREAFormView *
HTML.Tag.OBJECTObjectView *
HTML.Tag.FRAMESETFrameSetView *
HTML.Tag.FRAMEFrameView *
*/ public static class HTMLFactory implements ViewFactory { /** * Creates a view from an element. * * @param elem the element * @return the view */ public View create(Element elem) { Object o = elem.getAttributes().getAttribute(StyleConstants.NameAttribute); if (o instanceof HTML.Tag) { HTML.Tag kind = (HTML.Tag) o; if (kind == HTML.Tag.CONTENT) { return new InlineView(elem); } else if (kind == HTML.Tag.IMPLIED) { String ws = (String) elem.getAttributes().getAttribute( CSS.Attribute.WHITE_SPACE); if ((ws != null) && ws.equals("pre")) { return new LineView(elem); } return new javax.swing.text.html.ParagraphView(elem); } else if ((kind == HTML.Tag.P) || (kind == HTML.Tag.H1) || (kind == HTML.Tag.H2) || (kind == HTML.Tag.H3) || (kind == HTML.Tag.H4) || (kind == HTML.Tag.H5) || (kind == HTML.Tag.H6) || (kind == HTML.Tag.DT)) { // paragraph return new javax.swing.text.html.ParagraphView(elem); } else if ((kind == HTML.Tag.MENU) || (kind == HTML.Tag.DIR) || (kind == HTML.Tag.UL) || (kind == HTML.Tag.OL)) { return new ListView(elem); } else if (kind == HTML.Tag.BODY) { return new BodyBlockView(elem); } else if (kind == HTML.Tag.HTML) { return new BlockView(elem, View.Y_AXIS); } else if ((kind == HTML.Tag.LI) || (kind == HTML.Tag.CENTER) || (kind == HTML.Tag.DL) || (kind == HTML.Tag.DD) || (kind == HTML.Tag.DIV) || (kind == HTML.Tag.BLOCKQUOTE) || (kind == HTML.Tag.PRE) || (kind == HTML.Tag.FORM)) { // vertical box return new BlockView(elem, View.Y_AXIS); } else if (kind == HTML.Tag.NOFRAMES) { return new NoFramesView(elem, View.Y_AXIS); } else if (kind==HTML.Tag.IMG) { return new ImageView(elem); } else if (kind == HTML.Tag.ISINDEX) { return new IsindexView(elem); } else if (kind == HTML.Tag.HR) { return new HRuleView(elem); } else if (kind == HTML.Tag.BR) { return new BRView(elem); } else if (kind == HTML.Tag.TABLE) { return new javax.swing.text.html.TableView(elem); } else if ((kind == HTML.Tag.INPUT) || (kind == HTML.Tag.SELECT) || (kind == HTML.Tag.TEXTAREA)) { return new FormView(elem); } else if (kind == HTML.Tag.OBJECT) { return new ObjectView(elem); } else if (kind == HTML.Tag.FRAMESET) { if (elem.getAttributes().isDefined(HTML.Attribute.ROWS)) { return new FrameSetView(elem, View.Y_AXIS); } else if (elem.getAttributes().isDefined(HTML.Attribute.COLS)) { return new FrameSetView(elem, View.X_AXIS); } throw new RuntimeException("Can't build a" + kind + ", " + elem + ":" + "no ROWS or COLS defined."); } else if (kind == HTML.Tag.FRAME) { return new FrameView(elem); } else if (kind instanceof HTML.UnknownTag) { return new HiddenTagView(elem); } else if (kind == HTML.Tag.COMMENT) { return new CommentView(elem); } else if (kind == HTML.Tag.HEAD) { // Make the head never visible, and never load its // children. For Cursor positioning, // getNextVisualPositionFrom is overriden to always return // the end offset of the element. return new BlockView(elem, View.X_AXIS) { public float getPreferredSpan(int axis) { return 0; } public float getMinimumSpan(int axis) { return 0; } public float getMaximumSpan(int axis) { return 0; } protected void loadChildren(ViewFactory f) { } public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { return a; } public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a, int direction, Position.Bias[] biasRet) { return getElement().getEndOffset(); } }; } else if ((kind == HTML.Tag.TITLE) || (kind == HTML.Tag.META) || (kind == HTML.Tag.LINK) || (kind == HTML.Tag.STYLE) || (kind == HTML.Tag.SCRIPT) || (kind == HTML.Tag.AREA) || (kind == HTML.Tag.MAP) || (kind == HTML.Tag.PARAM) || (kind == HTML.Tag.APPLET)) { return new HiddenTagView(elem); } } // If we get here, it's either an element we don't know about // or something from StyledDocument that doesn't have a mapping to HTML. String nm = elem.getName(); if (nm != null) { if (nm.equals(AbstractDocument.ContentElementName)) { return new LabelView(elem); } else if (nm.equals(AbstractDocument.ParagraphElementName)) { return new ParagraphView(elem); } else if (nm.equals(AbstractDocument.SectionElementName)) { return new BoxView(elem, View.Y_AXIS); } else if (nm.equals(StyleConstants.ComponentElementName)) { return new ComponentView(elem); } else if (nm.equals(StyleConstants.IconElementName)) { return new IconView(elem); } } // default to text display return new LabelView(elem); } static class BodyBlockView extends BlockView implements ComponentListener { public BodyBlockView(Element elem) { super(elem,View.Y_AXIS); } // reimplement major axis requirements to indicate that the // block is flexible for the body element... so that it can // be stretched to fill the background properly. protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) { r = super.calculateMajorAxisRequirements(axis, r); r.maximum = Integer.MAX_VALUE; return r; } protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { Container container = getContainer(); Container parentContainer; if (container != null && (container instanceof javax.swing.JEditorPane) && (parentContainer = container.getParent()) != null && (parentContainer instanceof javax.swing.JViewport)) { JViewport viewPort = (JViewport)parentContainer; Object cachedObject; if (cachedViewPort != null) { if ((cachedObject = cachedViewPort.get()) != null) { if (cachedObject != viewPort) { ((JComponent)cachedObject).removeComponentListener(this); } } else { cachedViewPort = null; } } if (cachedViewPort == null) { viewPort.addComponentListener(this); cachedViewPort = new WeakReference(viewPort); } componentVisibleWidth = viewPort.getExtentSize().width; if (componentVisibleWidth > 0) { Insets insets = container.getInsets(); viewVisibleWidth = componentVisibleWidth - insets.left - getLeftInset(); //try to use viewVisibleWidth if it is smaller than targetSpan targetSpan = Math.min(targetSpan, viewVisibleWidth); } } else { if (cachedViewPort != null) { Object cachedObject; if ((cachedObject = cachedViewPort.get()) != null) { ((JComponent)cachedObject).removeComponentListener(this); } cachedViewPort = null; } } super.layoutMinorAxis(targetSpan, axis, offsets, spans); } public void setParent(View parent) { //if parent == null unregister component listener if (parent == null) { if (cachedViewPort != null) { Object cachedObject; if ((cachedObject = cachedViewPort.get()) != null) { ((JComponent)cachedObject).removeComponentListener(this); } cachedViewPort = null; } } super.setParent(parent); } public void componentResized(ComponentEvent e) { if ( !(e.getSource() instanceof JViewport) ) { return; } JViewport viewPort = (JViewport)e.getSource(); if (componentVisibleWidth != viewPort.getExtentSize().width) { Document doc = getDocument(); if (doc instanceof AbstractDocument) { AbstractDocument document = (AbstractDocument)getDocument(); document.readLock(); try { layoutChanged(X_AXIS); preferenceChanged(null, true, true); } finally { document.readUnlock(); } } } } public void componentHidden(ComponentEvent e) { } public void componentMoved(ComponentEvent e) { } public void componentShown(ComponentEvent e) { } /* * we keep weak reference to viewPort if and only if BodyBoxView is listening for ComponentEvents * only in that case cachedViewPort is not equal to null. * we need to keep this reference in order to remove BodyBoxView from viewPort listeners. * */ private Reference cachedViewPort = null; private boolean isListening = false; private int viewVisibleWidth = Integer.MAX_VALUE; private int componentVisibleWidth = Integer.MAX_VALUE; } } // --- Action implementations ------------------------------ /** The bold action identifier */ public static final String BOLD_ACTION = "html-bold-action"; /** The italic action identifier */ public static final String ITALIC_ACTION = "html-italic-action"; /** The paragraph left indent action identifier */ public static final String PARA_INDENT_LEFT = "html-para-indent-left"; /** The paragraph right indent action identifier */ public static final String PARA_INDENT_RIGHT = "html-para-indent-right"; /** The font size increase to next value action identifier */ public static final String FONT_CHANGE_BIGGER = "html-font-bigger"; /** The font size decrease to next value action identifier */ public static final String FONT_CHANGE_SMALLER = "html-font-smaller"; /** The Color choice action identifier The color is passed as an argument */ public static final String COLOR_ACTION = "html-color-action"; /** The logical style choice action identifier The logical style is passed in as an argument */ public static final String LOGICAL_STYLE_ACTION = "html-logical-style-action"; /** * Align images at the top. */ public static final String IMG_ALIGN_TOP = "html-image-align-top"; /** * Align images in the middle. */ public static final String IMG_ALIGN_MIDDLE = "html-image-align-middle"; /** * Align images at the bottom. */ public static final String IMG_ALIGN_BOTTOM = "html-image-align-bottom"; /** * Align images at the border. */ public static final String IMG_BORDER = "html-image-border"; /** HTML used when inserting tables. */ private static final String INSERT_TABLE_HTML = "
"; /** HTML used when inserting unordered lists. */ private static final String INSERT_UL_HTML = "

"; /** HTML used when inserting ordered lists. */ private static final String INSERT_OL_HTML = "
"; /** HTML used when inserting hr. */ private static final String INSERT_HR_HTML = "
"; /** HTML used when inserting pre. */ private static final String INSERT_PRE_HTML = "
";

    private static NavigateLinkAction nextLinkAction = 
	new NavigateLinkAction("next-link-action");

    private static NavigateLinkAction previousLinkAction = 
	new NavigateLinkAction("previous-link-action");

    private static ActivateLinkAction activateLinkAction = 
	new ActivateLinkAction("activate-link-action");
	
    private static final Action[] defaultActions = {
	new InsertHTMLTextAction("InsertTable", INSERT_TABLE_HTML,
				 HTML.Tag.BODY, HTML.Tag.TABLE),
	new InsertHTMLTextAction("InsertTableRow", INSERT_TABLE_HTML,
				 HTML.Tag.TABLE, HTML.Tag.TR,
				 HTML.Tag.BODY, HTML.Tag.TABLE),
	new InsertHTMLTextAction("InsertTableDataCell", INSERT_TABLE_HTML,
				 HTML.Tag.TR, HTML.Tag.TD,
				 HTML.Tag.BODY, HTML.Tag.TABLE),
	new InsertHTMLTextAction("InsertUnorderedList", INSERT_UL_HTML,
				 HTML.Tag.BODY, HTML.Tag.UL),
	new InsertHTMLTextAction("InsertUnorderedListItem", INSERT_UL_HTML,
				 HTML.Tag.UL, HTML.Tag.LI,
				 HTML.Tag.BODY, HTML.Tag.UL),
	new InsertHTMLTextAction("InsertOrderedList", INSERT_OL_HTML,
				 HTML.Tag.BODY, HTML.Tag.OL),
	new InsertHTMLTextAction("InsertOrderedListItem", INSERT_OL_HTML,
				 HTML.Tag.OL, HTML.Tag.LI,
				 HTML.Tag.BODY, HTML.Tag.OL),
	new InsertHRAction(),
	new InsertHTMLTextAction("InsertPre", INSERT_PRE_HTML,
				 HTML.Tag.BODY, HTML.Tag.PRE),
	nextLinkAction, previousLinkAction, activateLinkAction
    };


    /**
     * An abstract Action providing some convenience methods that may
     * be useful in inserting HTML into an existing document.
     * 

NOTE: None of the convenience methods obtain a lock on the * document. If you have another thread modifying the text these * methods may have inconsistent behavior, or return the wrong thing. */ public static abstract class HTMLTextAction extends StyledTextAction { public HTMLTextAction(String name) { super(name); } /** * @return HTMLDocument of e. */ protected HTMLDocument getHTMLDocument(JEditorPane e) { Document d = e.getDocument(); if (d instanceof HTMLDocument) { return (HTMLDocument) d; } throw new IllegalArgumentException("document must be HTMLDocument"); } /** * @return HTMLEditorKit for e. */ protected HTMLEditorKit getHTMLEditorKit(JEditorPane e) { EditorKit k = e.getEditorKit(); if (k instanceof HTMLEditorKit) { return (HTMLEditorKit) k; } throw new IllegalArgumentException("EditorKit must be HTMLEditorKit"); } /** * Returns an array of the Elements that contain offset. * The first elements corresponds to the root. */ protected Element[] getElementsAt(HTMLDocument doc, int offset) { return getElementsAt(doc.getDefaultRootElement(), offset, 0); } /** * Recursive method used by getElementsAt. */ private Element[] getElementsAt(Element parent, int offset, int depth) { if (parent.isLeaf()) { Element[] retValue = new Element[depth + 1]; retValue[depth] = parent; return retValue; } Element[] retValue = getElementsAt(parent.getElement (parent.getElementIndex(offset)), offset, depth + 1); retValue[depth] = parent; return retValue; } /** * Returns number of elements, starting at the deepest leaf, needed * to get to an element representing tag. This will * return -1 if no elements is found representing tag, * or 0 if the parent of the leaf at offset represents * tag. */ protected int elementCountToTag(HTMLDocument doc, int offset, HTML.Tag tag) { int depth = -1; Element e = doc.getCharacterElement(offset); while (e != null && e.getAttributes().getAttribute (StyleConstants.NameAttribute) != tag) { e = e.getParentElement(); depth++; } if (e == null) { return -1; } return depth; } /** * Returns the deepest element at offset matching * tag. */ protected Element findElementMatchingTag(HTMLDocument doc, int offset, HTML.Tag tag) { Element e = doc.getDefaultRootElement(); Element lastMatch = null; while (e != null) { if (e.getAttributes().getAttribute (StyleConstants.NameAttribute) == tag) { lastMatch = e; } e = e.getElement(e.getElementIndex(offset)); } return lastMatch; } } /** * InsertHTMLTextAction can be used to insert an arbitrary string of HTML * into an existing HTML document. At least two HTML.Tags need to be * supplied. The first Tag, parentTag, identifies the parent in * the document to add the elements to. The second tag, addTag, * identifies the first tag that should be added to the document as * seen in the HTML string. One important thing to remember, is that * the parser is going to generate all the appropriate tags, even if * they aren't in the HTML string passed in.

* For example, lets say you wanted to create an action to insert * a table into the body. The parentTag would be HTML.Tag.BODY, * addTag would be HTML.Tag.TABLE, and the string could be something * like <table><tr><td></td></tr></table>. *

There is also an option to supply an alternate parentTag and * addTag. These will be checked for if there is no parentTag at * offset. */ public static class InsertHTMLTextAction extends HTMLTextAction { public InsertHTMLTextAction(String name, String html, HTML.Tag parentTag, HTML.Tag addTag) { this(name, html, parentTag, addTag, null, null); } public InsertHTMLTextAction(String name, String html, HTML.Tag parentTag, HTML.Tag addTag, HTML.Tag alternateParentTag, HTML.Tag alternateAddTag) { this(name, html, parentTag, addTag, alternateParentTag, alternateAddTag, true); } /* public */ InsertHTMLTextAction(String name, String html, HTML.Tag parentTag, HTML.Tag addTag, HTML.Tag alternateParentTag, HTML.Tag alternateAddTag, boolean adjustSelection) { super(name); this.html = html; this.parentTag = parentTag; this.addTag = addTag; this.alternateParentTag = alternateParentTag; this.alternateAddTag = alternateAddTag; this.adjustSelection = adjustSelection; } /** * A cover for HTMLEditorKit.insertHTML. If an exception it * thrown it is wrapped in a RuntimeException and thrown. */ protected void insertHTML(JEditorPane editor, HTMLDocument doc, int offset, String html, int popDepth, int pushDepth, HTML.Tag addTag) { try { getHTMLEditorKit(editor).insertHTML(doc, offset, html, popDepth, pushDepth, addTag); } catch (IOException ioe) { throw new RuntimeException("Unable to insert: " + ioe); } catch (BadLocationException ble) { throw new RuntimeException("Unable to insert: " + ble); } } /** * This is invoked when inserting at a boundary. It determines * the number of pops, and then the number of pushes that need * to be performed, and then invokes insertHTML. * @since 1.3 */ protected void insertAtBoundary(JEditorPane editor, HTMLDocument doc, int offset, Element insertElement, String html, HTML.Tag parentTag, HTML.Tag addTag) { insertAtBoundry(editor, doc, offset, insertElement, html, parentTag, addTag); } /** * This is invoked when inserting at a boundary. It determines * the number of pops, and then the number of pushes that need * to be performed, and then invokes insertHTML. * @deprecated As of Java 2 platform v1.3, use insertAtBoundary */ @Deprecated protected void insertAtBoundry(JEditorPane editor, HTMLDocument doc, int offset, Element insertElement, String html, HTML.Tag parentTag, HTML.Tag addTag) { // Find the common parent. Element e; Element commonParent; boolean isFirst = (offset == 0); if (offset > 0 || insertElement == null) { e = doc.getDefaultRootElement(); while (e != null && e.getStartOffset() != offset && !e.isLeaf()) { e = e.getElement(e.getElementIndex(offset)); } commonParent = (e != null) ? e.getParentElement() : null; } else { // If inserting at the origin, the common parent is the // insertElement. commonParent = insertElement; } if (commonParent != null) { // Determine how many pops to do. int pops = 0; int pushes = 0; if (isFirst && insertElement != null) { e = commonParent; while (e != null && !e.isLeaf()) { e = e.getElement(e.getElementIndex(offset)); pops++; } } else { e = commonParent; offset--; while (e != null && !e.isLeaf()) { e = e.getElement(e.getElementIndex(offset)); pops++; } // And how many pushes e = commonParent; offset++; while (e != null && e != insertElement) { e = e.getElement(e.getElementIndex(offset)); pushes++; } } pops = Math.max(0, pops - 1); // And insert! insertHTML(editor, doc, offset, html, pops, pushes, addTag); } } /** * If there is an Element with name tag at * offset, this will invoke either insertAtBoundary * or insertHTML. This returns true if there is * a match, and one of the inserts is invoked. */ /*protected*/ boolean insertIntoTag(JEditorPane editor, HTMLDocument doc, int offset, HTML.Tag tag, HTML.Tag addTag) { Element e = findElementMatchingTag(doc, offset, tag); if (e != null && e.getStartOffset() == offset) { insertAtBoundary(editor, doc, offset, e, html, tag, addTag); return true; } else if (offset > 0) { int depth = elementCountToTag(doc, offset - 1, tag); if (depth != -1) { insertHTML(editor, doc, offset, html, depth, 0, addTag); return true; } } return false; } /** * Called after an insertion to adjust the selection. */ /* protected */ void adjustSelection(JEditorPane pane, HTMLDocument doc, int startOffset, int oldLength) { int newLength = doc.getLength(); if (newLength != oldLength && startOffset < newLength) { if (startOffset > 0) { String text; try { text = doc.getText(startOffset - 1, 1); } catch (BadLocationException ble) { text = null; } if (text != null && text.length() > 0 && text.charAt(0) == '\n') { pane.select(startOffset, startOffset); } else { pane.select(startOffset + 1, startOffset + 1); } } else { pane.select(1, 1); } } } /** * Inserts the HTML into the document. * * @param ae the event */ public void actionPerformed(ActionEvent ae) { JEditorPane editor = getEditor(ae); if (editor != null) { HTMLDocument doc = getHTMLDocument(editor); int offset = editor.getSelectionStart(); int length = doc.getLength(); boolean inserted; // Try first choice if (!insertIntoTag(editor, doc, offset, parentTag, addTag) && alternateParentTag != null) { // Then alternate. inserted = insertIntoTag(editor, doc, offset, alternateParentTag, alternateAddTag); } else { inserted = true; } if (adjustSelection && inserted) { adjustSelection(editor, doc, offset, length); } } } /** HTML to insert. */ protected String html; /** Tag to check for in the document. */ protected HTML.Tag parentTag; /** Tag in HTML to start adding tags from. */ protected HTML.Tag addTag; /** Alternate Tag to check for in the document if parentTag is * not found. */ protected HTML.Tag alternateParentTag; /** Alternate tag in HTML to start adding tags from if parentTag * is not found and alternateParentTag is found. */ protected HTML.Tag alternateAddTag; /** True indicates the selection should be adjusted after an insert. */ boolean adjustSelection; } /** * InsertHRAction is special, at actionPerformed time it will determine * the parent HTML.Tag based on the paragraph element at the selection * start. */ static class InsertHRAction extends InsertHTMLTextAction { InsertHRAction() { super("InsertHR", "


", null, HTML.Tag.IMPLIED, null, null, false); } /** * Inserts the HTML into the document. * * @param ae the event */ public void actionPerformed(ActionEvent ae) { JEditorPane editor = getEditor(ae); if (editor != null) { HTMLDocument doc = getHTMLDocument(editor); int offset = editor.getSelectionStart(); Element paragraph = doc.getParagraphElement(offset); if (paragraph.getParentElement() != null) { parentTag = (HTML.Tag)paragraph.getParentElement(). getAttributes().getAttribute (StyleConstants.NameAttribute); super.actionPerformed(ae); } } } } /* * Returns the object in an AttributeSet matching a key */ static private Object getAttrValue(AttributeSet attr, HTML.Attribute key) { Enumeration names = attr.getAttributeNames(); while (names.hasMoreElements()) { Object nextKey = names.nextElement(); Object nextVal = attr.getAttribute(nextKey); if (nextVal instanceof AttributeSet) { Object value = getAttrValue((AttributeSet)nextVal, key); if (value != null) { return value; } } else if (nextKey == key) { return nextVal; } } return null; } /* * Action to move the focus on the next or previous hypertext link * or object. TODO: This method relies on support from the * javax.accessibility package. The text package should support * keyboard navigation of text elements directly. */ static class NavigateLinkAction extends TextAction implements CaretListener { private static int prevHypertextOffset = -1; private static boolean foundLink = false; private FocusHighlightPainter focusPainter = new FocusHighlightPainter(null); private Object selectionTag; private boolean focusBack = false; /* * Create this action with the appropriate identifier. */ public NavigateLinkAction(String actionName) { super(actionName); if ("previous-link-action".equals(actionName)) { focusBack = true; } } /** * Called when the caret position is updated. * * @param e the caret event */ public void caretUpdate(CaretEvent e) { if (foundLink) { foundLink = false; // TODO: The AccessibleContext for the editor should register // as a listener for CaretEvents and forward the events to // assistive technologies listening for such events. Object src = e.getSource(); if (src instanceof JTextComponent) { ((JTextComponent)src).getAccessibleContext().firePropertyChange( AccessibleContext.ACCESSIBLE_HYPERTEXT_OFFSET, new Integer(prevHypertextOffset), new Integer(e.getDot())); } } } /* * The operation to perform when this action is triggered. */ public void actionPerformed(ActionEvent e) { JTextComponent comp = getTextComponent(e); if (comp == null || comp.isEditable()) { return; } Document doc = comp.getDocument(); if (doc == null) { return; } // TODO: Should start successive iterations from the // current caret position. ElementIterator ei = new ElementIterator(doc); int currentOffset = comp.getCaretPosition(); int prevStartOffset = -1; int prevEndOffset = -1; // highlight the next link or object after the current caret position Element nextElement = null; while ((nextElement = ei.next()) != null) { String name = nextElement.getName(); AttributeSet attr = nextElement.getAttributes(); Object href = getAttrValue(attr, HTML.Attribute.HREF); if (!(name.equals(HTML.Tag.OBJECT.toString())) && href == null) { continue; } int elementOffset = nextElement.getStartOffset(); if (focusBack) { if (elementOffset >= currentOffset && prevStartOffset >= 0) { foundLink = true; comp.setCaretPosition(prevStartOffset); moveCaretPosition(comp, prevStartOffset, prevEndOffset); prevHypertextOffset = prevStartOffset; return; } } else { // focus forward if (elementOffset > currentOffset) { foundLink = true; comp.setCaretPosition(elementOffset); moveCaretPosition(comp, elementOffset, nextElement.getEndOffset()); prevHypertextOffset = elementOffset; return; } } prevStartOffset = nextElement.getStartOffset(); prevEndOffset = nextElement.getEndOffset(); } if (focusBack && prevStartOffset >= 0) { foundLink = true; comp.setCaretPosition(prevStartOffset); moveCaretPosition(comp, prevStartOffset, prevEndOffset); prevHypertextOffset = prevStartOffset; return; } } /* * Moves the caret from mark to dot */ private void moveCaretPosition(JTextComponent comp, int mark, int dot) { Highlighter h = comp.getHighlighter(); if (h != null) { int p0 = Math.min(dot, mark); int p1 = Math.max(dot, mark); try { if (selectionTag != null) { h.changeHighlight(selectionTag, p0, p1); } else { Highlighter.HighlightPainter p = focusPainter; selectionTag = h.addHighlight(p0, p1, p); } } catch (BadLocationException e) { } } } /** * A highlight painter that draws a one-pixel border around * the highlighted area. */ class FocusHighlightPainter extends DefaultHighlighter.DefaultHighlightPainter { FocusHighlightPainter(Color color) { super(color); } /** * Paints a portion of a highlight. * * @param g the graphics context * @param offs0 the starting model offset >= 0 * @param offs1 the ending model offset >= offs1 * @param bounds the bounding box of the view, which is not * necessarily the region to paint. * @param c the editor * @param view View painting for * @return region in which drawing occurred */ public Shape paintLayer(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c, View view) { Color color = getColor(); if (color == null) { g.setColor(c.getSelectionColor()); } else { g.setColor(color); } if (offs0 == view.getStartOffset() && offs1 == view.getEndOffset()) { // Contained in view, can just use bounds. Rectangle alloc; if (bounds instanceof Rectangle) { alloc = (Rectangle)bounds; } else { alloc = bounds.getBounds(); } g.drawRect(alloc.x, alloc.y, alloc.width - 1, alloc.height); return alloc; } else { // Should only render part of View. try { // --- determine locations --- Shape shape = view.modelToView(offs0, Position.Bias.Forward, offs1,Position.Bias.Backward, bounds); Rectangle r = (shape instanceof Rectangle) ? (Rectangle)shape : shape.getBounds(); g.drawRect(r.x, r.y, r.width - 1, r.height); return r; } catch (BadLocationException e) { // can't render } } // Only if exception return null; } } } /* * Action to activate the hypertext link that has focus. * TODO: This method relies on support from the * javax.accessibility package. The text package should support * keyboard navigation of text elements directly. */ static class ActivateLinkAction extends TextAction { /** * Create this action with the appropriate identifier. */ public ActivateLinkAction(String actionName) { super(actionName); } /* * activates the hyperlink at offset */ private void activateLink(String href, HTMLDocument doc, JEditorPane editor, int offset) { try { URL page = (URL)doc.getProperty(Document.StreamDescriptionProperty); URL url = new URL(page, href); HyperlinkEvent linkEvent = new HyperlinkEvent (editor, HyperlinkEvent.EventType. ACTIVATED, url, url.toExternalForm(), doc.getCharacterElement(offset)); editor.fireHyperlinkUpdate(linkEvent); } catch (MalformedURLException m) { } } /* * Invokes default action on the object in an element */ private void doObjectAction(JEditorPane editor, Element elem) { View view = getView(editor, elem); if (view != null && view instanceof ObjectView) { Component comp = ((ObjectView)view).getComponent(); if (comp != null && comp instanceof Accessible) { AccessibleContext ac = ((Accessible)comp).getAccessibleContext(); if (ac != null) { AccessibleAction aa = ac.getAccessibleAction(); if (aa != null) { aa.doAccessibleAction(0); } } } } } /* * Returns the root view for a document */ private View getRootView(JEditorPane editor) { return editor.getUI().getRootView(editor); } /* * Returns a view associated with an element */ private View getView(JEditorPane editor, Element elem) { Object lock = lock(editor); try { View rootView = getRootView(editor); int start = elem.getStartOffset(); if (rootView != null) { return getView(rootView, elem, start); } return null; } finally { unlock(lock); } } private View getView(View parent, Element elem, int start) { if (parent.getElement() == elem) { return parent; } int index = parent.getViewIndex(start, Position.Bias.Forward); if (index != -1 && index < parent.getViewCount()) { return getView(parent.getView(index), elem, start); } return null; } /* * If possible acquires a lock on the Document. If a lock has been * obtained a key will be retured that should be passed to * unlock. */ private Object lock(JEditorPane editor) { Document document = editor.getDocument(); if (document instanceof AbstractDocument) { ((AbstractDocument)document).readLock(); return document; } return null; } /* * Releases a lock previously obtained via lock. */ private void unlock(Object key) { if (key != null) { ((AbstractDocument)key).readUnlock(); } } /* * The operation to perform when this action is triggered. */ public void actionPerformed(ActionEvent e) { JTextComponent c = getTextComponent(e); if (c.isEditable() || !(c instanceof JEditorPane)) { return; } JEditorPane editor = (JEditorPane)c; Document d = editor.getDocument(); if (d == null || !(d instanceof HTMLDocument)) { return; } HTMLDocument doc = (HTMLDocument)d; ElementIterator ei = new ElementIterator(doc); int currentOffset = editor.getCaretPosition(); // invoke the next link or object action String urlString = null; String objString = null; Element currentElement = null; while ((currentElement = ei.next()) != null) { String name = currentElement.getName(); AttributeSet attr = currentElement.getAttributes(); Object href = getAttrValue(attr, HTML.Attribute.HREF); if (href != null) { if (currentOffset >= currentElement.getStartOffset() && currentOffset <= currentElement.getEndOffset()) { activateLink((String)href, doc, editor, currentOffset); return; } } else if (name.equals(HTML.Tag.OBJECT.toString())) { Object obj = getAttrValue(attr, HTML.Attribute.CLASSID); if (obj != null) { if (currentOffset >= currentElement.getStartOffset() && currentOffset <= currentElement.getEndOffset()) { doObjectAction(editor, currentElement); return; } } } } } } }