| /* |
| * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| package javax.swing.text.html; |
| |
| import sun.awt.AppContext; |
| |
| 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.event.*; |
| import javax.swing.plaf.TextUI; |
| import java.util.*; |
| import javax.accessibility.*; |
| import java.lang.ref.*; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import javax.swing.text.html.parser.ParserDelegator; |
| |
| /** |
| * 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. |
| * <p> |
| * 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. |
| * <dl> |
| * <dt> |
| * Support editing |
| * <dd> |
| * 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. |
| * <p> |
| * The modeling of HTML is provided by the class <code>HTMLDocument</code>. |
| * Its documentation describes the details of how the HTML is modeled. |
| * The editing support leverages heavily off of the text package. |
| * |
| * <dt> |
| * Extendable/Scalable |
| * <dd> |
| * To maximize the usefulness of this kit, a great deal of effort |
| * has gone into making it extendable. These are some of the |
| * features. |
| * <ol> |
| * <li> |
| * The parser is replaceable. 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. |
| * <li> |
| * 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. |
| * <li> |
| * 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 <code>HTMLFactory</code> class. This can |
| * be easily changed by subclassing or replacing the HTMLFactory |
| * and reimplementing the getViewFactory method to return the alternative |
| * factory. |
| * <li> |
| * 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 |
| * </ol> |
| * |
| * <dt> |
| * Asynchronous Loading |
| * <dd> |
| * 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 <code>JEditorPane.setPage</code>. |
| * This is controlled by a property on the document. The method |
| * {@link #createDefaultDocument createDefaultDocument} can |
| * be overriden to change this. The batching of work is done |
| * by the <code>HTMLDocument.HTMLReader</code> class. The actual |
| * work is done by the <code>DefaultStyledDocument</code> and |
| * <code>AbstractDocument</code> classes in the text package. |
| * |
| * <dt> |
| * Customization from current LAF |
| * <dd> |
| * 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. |
| * <p> |
| * The support for this is provided by the <code>StyleSheet</code> |
| * class. The presentation of the HTML can be heavily influenced |
| * by the setting of the StyleSheet property on the EditorKit. |
| * |
| * <dt> |
| * Not lossy |
| * <dd> |
| * 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. |
| * </dl> |
| * |
| * @author Timothy Prinzing |
| */ |
| @SuppressWarnings("serial") // Same-version serialization only |
| 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 <code>text/html</code>. |
| * |
| * @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; |
| } |
| |
| /** |
| * Try to get an HTML parser from the document. If no parser is set for |
| * the document, return the editor kit's default parser. It is an error |
| * if no parser could be obtained from the editor kit. |
| */ |
| private Parser ensureParser(HTMLDocument doc) throws IOException { |
| Parser p = doc.getParser(); |
| if (p == null) { |
| p = getParser(); |
| } |
| if (p == null) { |
| throw new IOException("Can't load parser"); |
| } |
| return p; |
| } |
| |
| /** |
| * Inserts content from the given stream. If <code>doc</code> 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; |
| if (pos > doc.getLength()) { |
| throw new BadLocationException("Invalid location", pos); |
| } |
| |
| Parser p = ensureParser(hdoc); |
| 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 html the HTML string |
| * @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 |
| * |
| * @throws BadLocationException if {@code offset} is invalid |
| * @throws IOException on I/O error |
| * @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 { |
| if (offset > doc.getLength()) { |
| throw new BadLocationException("Invalid location", offset); |
| } |
| |
| Parser p = ensureParser(doc); |
| 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 {@code 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. |
| * |
| * @param s a StyleSheet |
| */ |
| public void setStyleSheet(StyleSheet s) { |
| if (s == null) { |
| AppContext.getAppContext().remove(DEFAULT_STYLES_KEY); |
| } else { |
| AppContext.getAppContext().put(DEFAULT_STYLES_KEY, 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. |
| * |
| * @return the StyleSheet |
| */ |
| public StyleSheet getStyleSheet() { |
| AppContext appContext = AppContext.getAppContext(); |
| StyleSheet defaultStyles = (StyleSheet) appContext.get(DEFAULT_STYLES_KEY); |
| |
| if (defaultStyles == null) { |
| defaultStyles = new StyleSheet(); |
| appContext.put(DEFAULT_STYLES_KEY, defaultStyles); |
| 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(final String name) { |
| return AccessController.doPrivileged( |
| new PrivilegedAction<InputStream>() { |
| public InputStream run() { |
| 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(), defaultActions); |
| } |
| |
| /** |
| * Copies the key/values in <code>element</code>s AttributeSet into |
| * <code>set</code>. 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 <code>set</code>.<p> |
| * 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. |
| * |
| * @param cursor a cursor |
| * |
| * @since 1.3 |
| */ |
| public void setDefaultCursor(Cursor cursor) { |
| defaultCursor = cursor; |
| } |
| |
| /** |
| * Returns the default cursor. |
| * |
| * @return the cursor |
| * |
| * @since 1.3 |
| */ |
| public Cursor getDefaultCursor() { |
| return defaultCursor; |
| } |
| |
| /** |
| * Sets the cursor to use over links. |
| * |
| * @param cursor a cursor |
| * |
| * @since 1.3 |
| */ |
| public void setLinkCursor(Cursor cursor) { |
| linkCursor = cursor; |
| } |
| |
| /** |
| * Returns the cursor to use over hyper links. |
| * |
| * @return the cursor |
| * |
| * @since 1.3 |
| */ |
| public Cursor getLinkCursor() { |
| return linkCursor; |
| } |
| |
| /** |
| * Indicates whether an html form submission is processed automatically |
| * or only <code>FormSubmitEvent</code> 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 <code>FormSubmitEvent</code> is fired. |
| * By default it is set to true. |
| * |
| * @param isAuto if {@code true}, html form submission is processed automatically. |
| * |
| * @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. |
| * |
| * @return the parser |
| */ |
| protected Parser getParser() { |
| if (defaultParser == null) { |
| defaultParser = new ParserDelegator(); |
| } |
| 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 final Object DEFAULT_STYLES_KEY = new Object(); |
| 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. |
| */ |
| @SuppressWarnings("serial") // Same-version serialization only |
| 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 transient 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 |
| */ |
| @SuppressWarnings("deprecation") |
| public void mouseClicked(MouseEvent e) { |
| JEditorPane editor = (JEditorPane) e.getSource(); |
| |
| if (! editor.isEditable() && editor.isEnabled() && |
| SwingUtilities.isLeftMouseButton(e)) { |
| Point pt = new Point(e.getX(), e.getY()); |
| int pos = editor.viewToModel(pt); |
| if (pos >= 0) { |
| activateLink(pos, editor, e); |
| } |
| } |
| } |
| |
| // ignore the drags |
| public void mouseDragged(MouseEvent e) { |
| } |
| |
| // track the moving of the mouse. |
| @SuppressWarnings("deprecation") |
| public void mouseMoved(MouseEvent e) { |
| JEditorPane editor = (JEditorPane) e.getSource(); |
| if (!editor.isEnabled()) { |
| return; |
| } |
| |
| 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, e); |
| 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. |
| */ |
| @SuppressWarnings("deprecation") |
| 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 <code>e</code> contains |
| * the location <code>x</code>, <code>y</code>. <code>offset</code> |
| * gives the offset into the Document to check for. |
| */ |
| @SuppressWarnings("deprecation") |
| 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.<p>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, null); |
| } |
| |
| /** |
| * Calls linkActivated on the associated JEditorPane |
| * if the given position represents a link. If this was the result |
| * of a mouse click, <code>x</code> and |
| * <code>y</code> will give the location of the mouse, otherwise |
| * they will be {@literal <} 0. |
| * |
| * @param pos the position |
| * @param html the editor pane |
| */ |
| void activateLink(int pos, JEditorPane html, MouseEvent mouseEvent) { |
| 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; |
| int x = -1; |
| int y = -1; |
| |
| if (mouseEvent != null) { |
| x = mouseEvent.getX(); |
| y = mouseEvent.getY(); |
| } |
| |
| 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, mouseEvent); |
| } |
| if (linkEvent != null) { |
| html.fireHyperlinkUpdate(linkEvent); |
| } |
| } |
| } |
| |
| /** |
| * Creates and returns a new instance of HyperlinkEvent. If |
| * <code>hdoc</code> is a frame document a HTMLFrameHyperlinkEvent |
| * will be created. |
| */ |
| HyperlinkEvent createHyperlinkEvent(JEditorPane html, |
| HTMLDocument hdoc, String href, |
| AttributeSet anchor, |
| Element element, |
| MouseEvent mouseEvent) { |
| 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; |
| |
| if (!hdoc.isFrameDocument()) { |
| linkEvent = new HyperlinkEvent( |
| html, HyperlinkEvent.EventType.ACTIVATED, u, href, |
| element, mouseEvent); |
| } 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, mouseEvent, target); |
| } |
| return linkEvent; |
| } |
| |
| void fireEvents(JEditorPane editor, HTMLDocument doc, String href, |
| Element lastElem, MouseEvent mouseEvent) { |
| 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, mouseEvent); |
| 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, mouseEvent); |
| 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 abstract static 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. |
| * |
| * @param r a reader |
| * @param cb a parser callback |
| * @param ignoreCharSet if {@code true} charset is ignoring |
| * @throws IOException if an I/O exception occurs |
| */ |
| 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 |
| * <code>flush</code> method will be the last method |
| * called, to give the receiver a chance to flush any |
| * pending data into the document. |
| * <p>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_"; |
| |
| /** |
| * The last method called on the reader. It allows |
| * any pending changes to be flushed into the document. |
| * Since this is currently loading synchronously, the entire |
| * set of changes are pushed in at this point. |
| * |
| * @throws BadLocationException if the given position does not |
| * represent a valid location in the associated document. |
| */ |
| public void flush() throws BadLocationException { |
| } |
| |
| /** |
| * Called by the parser to indicate a block of text was |
| * encountered. |
| * |
| * @param data a data |
| * @param pos a position |
| */ |
| public void handleText(char[] data, int pos) { |
| } |
| |
| /** |
| * Called by the parser to indicate a block of comment was |
| * encountered. |
| * |
| * @param data a data |
| * @param pos a position |
| */ |
| public void handleComment(char[] data, int pos) { |
| } |
| |
| /** |
| * Callback from the parser. Route to the appropriate |
| * handler for the tag. |
| * |
| * @param t an HTML tag |
| * @param a a set of attributes |
| * @param pos a position |
| */ |
| public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) { |
| } |
| |
| /** |
| * Callback from the parser. Route to the appropriate |
| * handler for the tag. |
| * |
| * @param t an HTML tag |
| * @param pos a position |
| */ |
| public void handleEndTag(HTML.Tag t, int pos) { |
| } |
| |
| /** |
| * Callback from the parser. Route to the appropriate |
| * handler for the tag. |
| * |
| * @param t an HTML tag |
| * @param a a set of attributes |
| * @param pos a position |
| */ |
| public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) { |
| } |
| |
| /** |
| * Callback from the parser. Route to the appropriate |
| * handler for the error. |
| * |
| * @param errorMsg a error message |
| * @param pos a position |
| */ |
| public void handleError(String errorMsg, int pos) { |
| } |
| |
| /** |
| * This is invoked after the stream has been parsed, but before |
| * <code>flush</code>. <code>eol</code> will be one of \n, \r |
| * or \r\n, which ever is encountered the most in parsing the |
| * stream. |
| * |
| * @param eol value of eol |
| * |
| * @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. |
| * |
| * <table class="striped"> |
| * <caption>Describes the tag and view created by this factory by default |
| * </caption> |
| * <thead> |
| * <tr> |
| * <th>Tag |
| * <th>View created |
| * </tr> |
| * </thead> |
| * <tbody> |
| * <tr> |
| * <td>HTML.Tag.CONTENT<td>InlineView |
| * </tr><tr> |
| * <td>HTML.Tag.IMPLIED<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.P<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.H1<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.H2<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.H3<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.H4<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.H5<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.H6<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.DT<td>javax.swing.text.html.ParagraphView |
| * </tr><tr> |
| * <td>HTML.Tag.MENU<td>ListView |
| * </tr><tr> |
| * <td>HTML.Tag.DIR<td>ListView |
| * </tr><tr> |
| * <td>HTML.Tag.UL<td>ListView |
| * </tr><tr> |
| * <td>HTML.Tag.OL<td>ListView |
| * </tr><tr> |
| * <td>HTML.Tag.LI<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.DL<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.DD<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.BODY<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.HTML<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.CENTER<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.DIV<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.BLOCKQUOTE<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.PRE<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.BLOCKQUOTE<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.PRE<td>BlockView |
| * </tr><tr> |
| * <td>HTML.Tag.IMG<td>ImageView |
| * </tr><tr> |
| * <td>HTML.Tag.HR<td>HRuleView |
| * </tr><tr> |
| * <td>HTML.Tag.BR<td>BRView |
| * </tr><tr> |
| * <td>HTML.Tag.TABLE<td>javax.swing.text.html.TableView |
| * </tr><tr> |
| * <td>HTML.Tag.INPUT<td>FormView |
| * </tr><tr> |
| * <td>HTML.Tag.SELECT<td>FormView |
| * </tr><tr> |
| * <td>HTML.Tag.TEXTAREA<td>FormView |
| * </tr><tr> |
| * <td>HTML.Tag.OBJECT<td>ObjectView |
| * </tr><tr> |
| * <td>HTML.Tag.FRAMESET<td>FrameSetView |
| * </tr><tr> |
| * <td>HTML.Tag.FRAME<td>FrameView |
| * </tr> |
| * </tbody> |
| * </table> |
| */ |
| public static class HTMLFactory implements ViewFactory { |
| |
| /** |
| * Creates a view from an element. |
| * |
| * @param elem the element |
| * @return the view |
| */ |
| public View create(Element elem) { |
| AttributeSet attrs = elem.getAttributes(); |
| Object elementName = |
| attrs.getAttribute(AbstractDocument.ElementNameAttribute); |
| Object o = (elementName != null) ? |
| null : attrs.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 = (elementName != null) ? (String)elementName : |
| 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; |
| if (cachedViewPort != null) { |
| JViewport cachedObject = cachedViewPort.get(); |
| if (cachedObject != null) { |
| if (cachedObject != viewPort) { |
| cachedObject.removeComponentListener(this); |
| } |
| } else { |
| cachedViewPort = null; |
| } |
| } |
| if (cachedViewPort == null) { |
| viewPort.addComponentListener(this); |
| cachedViewPort = new WeakReference<JViewport>(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) { |
| JViewport cachedObject = cachedViewPort.get(); |
| if (cachedObject != null) { |
| 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<JViewport> 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 = "<table border=1><tr><td></td></tr></table>"; |
| |
| /** HTML used when inserting unordered lists. */ |
| private static final String INSERT_UL_HTML = "<ul><li></li></ul>"; |
| |
| /** HTML used when inserting ordered lists. */ |
| private static final String INSERT_OL_HTML = "<ol><li></li></ol>"; |
| |
| /** HTML used when inserting hr. */ |
| private static final String INSERT_HR_HTML = "<hr>"; |
| |
| /** HTML used when inserting pre. */ |
| private static final String INSERT_PRE_HTML = "<pre></pre>"; |
| |
| private static final NavigateLinkAction nextLinkAction = |
| new NavigateLinkAction("next-link-action"); |
| |
| private static final NavigateLinkAction previousLinkAction = |
| new NavigateLinkAction("previous-link-action"); |
| |
| private static final 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, |
| |
| new BeginAction(beginAction, false), |
| new BeginAction(selectionBeginAction, true) |
| }; |
| |
| // link navigation support |
| private boolean foundLink = false; |
| private int prevHypertextOffset = -1; |
| private Object linkNavigationTag; |
| |
| |
| /** |
| * An abstract Action providing some convenience methods that may |
| * be useful in inserting HTML into an existing document. |
| * <p>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. |
| */ |
| @SuppressWarnings("serial") // Superclass is not serializable across versions |
| public abstract static class HTMLTextAction extends StyledTextAction { |
| |
| /** |
| * Creates a new HTMLTextAction from a string action name. |
| * |
| * @param name the name of the action |
| */ |
| public HTMLTextAction(String name) { |
| super(name); |
| } |
| |
| /** |
| * @param e the JEditorPane |
| * @return HTMLDocument of <code>e</code>. |
| */ |
| protected HTMLDocument getHTMLDocument(JEditorPane e) { |
| Document d = e.getDocument(); |
| if (d instanceof HTMLDocument) { |
| return (HTMLDocument) d; |
| } |
| throw new IllegalArgumentException("document must be HTMLDocument"); |
| } |
| |
| /** |
| * @param e the JEditorPane |
| * @return HTMLEditorKit for <code>e</code>. |
| */ |
| 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 <code>offset</code>. |
| * The first elements corresponds to the root. |
| * |
| * @param doc an instance of HTMLDocument |
| * @param offset value of offset |
| * @return an array of the Elements that contain <code>offset</code> |
| */ |
| 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 <code>tag</code>. This will |
| * return -1 if no elements is found representing <code>tag</code>, |
| * or 0 if the parent of the leaf at <code>offset</code> represents |
| * <code>tag</code>. |
| * |
| * @param doc an instance of HTMLDocument |
| * @param offset an offset to start from |
| * @param tag tag to represent |
| * @return number of elements |
| */ |
| 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 <code>offset</code> matching |
| * <code>tag</code>. |
| * |
| * @param doc an instance of HTMLDocument |
| * @param offset the specified offset >= 0 |
| * @param tag an instance of HTML.Tag |
| * |
| * @return the deepest element |
| */ |
| 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.<p> |
| * 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>. |
| * <p>There is also an option to supply an alternate parentTag and |
| * addTag. These will be checked for if there is no parentTag at |
| * offset. |
| */ |
| @SuppressWarnings("serial") // Superclass is not serializable across versions |
| public static class InsertHTMLTextAction extends HTMLTextAction { |
| |
| /** |
| * Creates a new InsertHTMLTextAction. |
| * |
| * @param name a name of the action |
| * @param html an HTML string |
| * @param parentTag a parent tag |
| * @param addTag the first tag to start inserting into document |
| */ |
| public InsertHTMLTextAction(String name, String html, |
| HTML.Tag parentTag, HTML.Tag addTag) { |
| this(name, html, parentTag, addTag, null, null); |
| } |
| |
| /** |
| * Creates a new InsertHTMLTextAction. |
| * |
| * @param name a name of the action |
| * @param html an HTML string |
| * @param parentTag a parent tag |
| * @param addTag the first tag to start inserting into document |
| * @param alternateParentTag an alternative parent tag |
| * @param alternateAddTag an alternative tag |
| */ |
| 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. |
| * |
| * @param editor an instance of JEditorPane |
| * @param doc the document to insert into |
| * @param offset the offset to insert HTML at |
| * @param html an HTML string |
| * @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 addTag the first tag to start inserting into document |
| */ |
| 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. |
| * |
| * @param editor an instance of JEditorPane |
| * @param doc an instance of HTMLDocument |
| * @param offset an offset to start from |
| * @param insertElement an instance of Element |
| * @param html an HTML string |
| * @param parentTag a parent tag |
| * @param addTag the first tag to start inserting into document |
| * |
| * @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 |
| * |
| * @param editor an instance of JEditorPane |
| * @param doc an instance of HTMLDocument |
| * @param offset an offset to start from |
| * @param insertElement an instance of Element |
| * @param html an HTML string |
| * @param parentTag a parent tag |
| * @param addTag the first tag to start inserting into document |
| */ |
| @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 <code>tag</code> at |
| * <code>offset</code>, this will invoke either insertAtBoundary |
| * or <code>insertHTML</code>. 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. |
| */ |
| @SuppressWarnings("serial") // Superclass is not serializable across versions |
| static class InsertHRAction extends InsertHTMLTextAction { |
| InsertHRAction() { |
| super("InsertHR", "<hr>", 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 |
| */ |
| private static 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. |
| */ |
| @SuppressWarnings("serial") // Superclass is not serializable across versions |
| static class NavigateLinkAction extends TextAction implements CaretListener { |
| |
| private static final FocusHighlightPainter focusPainter = |
| new FocusHighlightPainter(null); |
| private final boolean focusBack; |
| |
| /* |
| * Create this action with the appropriate identifier. |
| */ |
| public NavigateLinkAction(String actionName) { |
| super(actionName); |
| focusBack = "previous-link-action".equals(actionName); |
| } |
| |
| /** |
| * Called when the caret position is updated. |
| * |
| * @param e the caret event |
| */ |
| public void caretUpdate(CaretEvent e) { |
| Object src = e.getSource(); |
| if (src instanceof JTextComponent) { |
| JTextComponent comp = (JTextComponent) src; |
| HTMLEditorKit kit = getHTMLEditorKit(comp); |
| if (kit != null && kit.foundLink) { |
| kit.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. |
| comp.getAccessibleContext().firePropertyChange( |
| AccessibleContext.ACCESSIBLE_HYPERTEXT_OFFSET, |
| Integer.valueOf(kit.prevHypertextOffset), |
| Integer.valueOf(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(); |
| HTMLEditorKit kit = getHTMLEditorKit(comp); |
| if (doc == null || kit == 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; |
| 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) { |
| |
| kit.foundLink = true; |
| comp.setCaretPosition(prevStartOffset); |
| moveCaretPosition(comp, kit, prevStartOffset, |
| prevEndOffset); |
| kit.prevHypertextOffset = prevStartOffset; |
| return; |
| } |
| } else { // focus forward |
| if (elementOffset > currentOffset) { |
| |
| kit.foundLink = true; |
| comp.setCaretPosition(elementOffset); |
| moveCaretPosition(comp, kit, elementOffset, |
| nextElement.getEndOffset()); |
| kit.prevHypertextOffset = elementOffset; |
| return; |
| } |
| } |
| prevStartOffset = nextElement.getStartOffset(); |
| prevEndOffset = nextElement.getEndOffset(); |
| } |
| if (focusBack && prevStartOffset >= 0) { |
| kit.foundLink = true; |
| comp.setCaretPosition(prevStartOffset); |
| moveCaretPosition(comp, kit, prevStartOffset, prevEndOffset); |
| kit.prevHypertextOffset = prevStartOffset; |
| } |
| } |
| |
| /* |
| * Moves the caret from mark to dot |
| */ |
| private void moveCaretPosition(JTextComponent comp, HTMLEditorKit kit, |
| 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 (kit.linkNavigationTag != null) { |
| h.changeHighlight(kit.linkNavigationTag, p0, p1); |
| } else { |
| kit.linkNavigationTag = |
| h.addHighlight(p0, p1, focusPainter); |
| } |
| } catch (BadLocationException e) { |
| } |
| } |
| } |
| |
| private HTMLEditorKit getHTMLEditorKit(JTextComponent comp) { |
| if (comp instanceof JEditorPane) { |
| EditorKit kit = ((JEditorPane) comp).getEditorKit(); |
| if (kit instanceof HTMLEditorKit) { |
| return (HTMLEditorKit) kit; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * A highlight painter that draws a one-pixel border around |
| * the highlighted area. |
| */ |
| static 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. |
| */ |
| @SuppressWarnings("serial") // Superclass is not serializable across versions |
| 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 = 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 |
| * <code>unlock</code>. |
| */ |
| 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 <code>lock</code>. |
| */ |
| 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; |
| 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; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private static int getBodyElementStart(JTextComponent comp) { |
| Element rootElement = comp.getDocument().getRootElements()[0]; |
| for (int i = 0; i < rootElement.getElementCount(); i++) { |
| Element currElement = rootElement.getElement(i); |
| if("body".equals(currElement.getName())) { |
| return currElement.getStartOffset(); |
| } |
| } |
| return 0; |
| } |
| |
| /* |
| * Move the caret to the beginning of the document. |
| * @see DefaultEditorKit#beginAction |
| * @see HTMLEditorKit#getActions |
| */ |
| @SuppressWarnings("serial") // Superclass is not serializable across versions |
| static class BeginAction extends TextAction { |
| |
| /* Create this object with the appropriate identifier. */ |
| BeginAction(String nm, boolean select) { |
| super(nm); |
| this.select = select; |
| } |
| |
| /** The operation to perform when this action is triggered. */ |
| public void actionPerformed(ActionEvent e) { |
| JTextComponent target = getTextComponent(e); |
| int bodyStart = getBodyElementStart(target); |
| |
| if (target != null) { |
| if (select) { |
| target.moveCaretPosition(bodyStart); |
| } else { |
| target.setCaretPosition(bodyStart); |
| } |
| } |
| } |
| |
| private boolean select; |
| } |
| } |