From 59308f67f9b7038cfa2ceb9ee9ba27645b927cb5 Mon Sep 17 00:00:00 2001 From: duke Date: Sat, 1 Dec 2007 00:00:00 +0000 Subject: Initial load --- .../javax/swing/text/html/AccessibleHTML.java | 3058 ++++++++++++++ .../classes/javax/swing/text/html/BRView.java | 57 + .../classes/javax/swing/text/html/BlockView.java | 443 +++ src/share/classes/javax/swing/text/html/CSS.java | 3383 ++++++++++++++++ .../classes/javax/swing/text/html/CSSBorder.java | 435 ++ .../classes/javax/swing/text/html/CSSParser.java | 854 ++++ .../classes/javax/swing/text/html/CommentView.java | 141 + .../javax/swing/text/html/EditableView.java | 128 + .../javax/swing/text/html/FormSubmitEvent.java | 92 + .../classes/javax/swing/text/html/FormView.java | 932 +++++ .../javax/swing/text/html/FrameSetView.java | 322 ++ .../classes/javax/swing/text/html/FrameView.java | 479 +++ .../classes/javax/swing/text/html/HRuleView.java | 316 ++ src/share/classes/javax/swing/text/html/HTML.java | 697 ++++ .../javax/swing/text/html/HTMLDocument.java | 4185 ++++++++++++++++++++ .../javax/swing/text/html/HTMLEditorKit.java | 2279 +++++++++++ .../swing/text/html/HTMLFrameHyperlinkEvent.java | 134 + .../classes/javax/swing/text/html/HTMLWriter.java | 1276 ++++++ .../javax/swing/text/html/HiddenTagView.java | 361 ++ .../classes/javax/swing/text/html/ImageView.java | 997 +++++ .../classes/javax/swing/text/html/InlineView.java | 225 ++ .../classes/javax/swing/text/html/IsindexView.java | 114 + .../classes/javax/swing/text/html/LineView.java | 186 + .../classes/javax/swing/text/html/ListView.java | 122 + src/share/classes/javax/swing/text/html/Map.java | 505 +++ .../javax/swing/text/html/MinimalHTMLWriter.java | 726 ++++ .../javax/swing/text/html/MuxingAttributeSet.java | 311 ++ .../javax/swing/text/html/NoFramesView.java | 173 + .../classes/javax/swing/text/html/ObjectView.java | 182 + .../classes/javax/swing/text/html/Option.java | 120 + .../javax/swing/text/html/OptionComboBoxModel.java | 63 + .../javax/swing/text/html/OptionListModel.java | 571 +++ .../javax/swing/text/html/ParagraphView.java | 294 ++ .../javax/swing/text/html/ResourceLoader.java | 60 + .../classes/javax/swing/text/html/StyleSheet.java | 3340 ++++++++++++++++ .../classes/javax/swing/text/html/TableView.java | 1801 +++++++++ .../javax/swing/text/html/TextAreaDocument.java | 68 + .../classes/javax/swing/text/html/default.css | 267 ++ .../classes/javax/swing/text/html/package.html | 59 + .../swing/text/html/parser/AttributeList.java | 172 + .../javax/swing/text/html/parser/ContentModel.java | 255 ++ .../swing/text/html/parser/ContentModelState.java | 295 ++ .../classes/javax/swing/text/html/parser/DTD.java | 451 +++ .../javax/swing/text/html/parser/DTDConstants.java | 82 + .../swing/text/html/parser/DocumentParser.java | 281 ++ .../javax/swing/text/html/parser/Element.java | 175 + .../javax/swing/text/html/parser/Entity.java | 139 + .../javax/swing/text/html/parser/Parser.java | 2312 +++++++++++ .../swing/text/html/parser/ParserDelegator.java | 120 + .../swing/text/html/parser/ResourceLoader.java | 60 + .../javax/swing/text/html/parser/TagElement.java | 74 + .../javax/swing/text/html/parser/TagStack.java | 220 + .../javax/swing/text/html/parser/html32.bdtd | Bin 0 -> 16173 bytes .../javax/swing/text/html/parser/package.html | 54 + 54 files changed, 34446 insertions(+) create mode 100644 src/share/classes/javax/swing/text/html/AccessibleHTML.java create mode 100644 src/share/classes/javax/swing/text/html/BRView.java create mode 100644 src/share/classes/javax/swing/text/html/BlockView.java create mode 100644 src/share/classes/javax/swing/text/html/CSS.java create mode 100644 src/share/classes/javax/swing/text/html/CSSBorder.java create mode 100644 src/share/classes/javax/swing/text/html/CSSParser.java create mode 100644 src/share/classes/javax/swing/text/html/CommentView.java create mode 100644 src/share/classes/javax/swing/text/html/EditableView.java create mode 100644 src/share/classes/javax/swing/text/html/FormSubmitEvent.java create mode 100644 src/share/classes/javax/swing/text/html/FormView.java create mode 100644 src/share/classes/javax/swing/text/html/FrameSetView.java create mode 100644 src/share/classes/javax/swing/text/html/FrameView.java create mode 100644 src/share/classes/javax/swing/text/html/HRuleView.java create mode 100644 src/share/classes/javax/swing/text/html/HTML.java create mode 100644 src/share/classes/javax/swing/text/html/HTMLDocument.java create mode 100644 src/share/classes/javax/swing/text/html/HTMLEditorKit.java create mode 100644 src/share/classes/javax/swing/text/html/HTMLFrameHyperlinkEvent.java create mode 100644 src/share/classes/javax/swing/text/html/HTMLWriter.java create mode 100644 src/share/classes/javax/swing/text/html/HiddenTagView.java create mode 100644 src/share/classes/javax/swing/text/html/ImageView.java create mode 100644 src/share/classes/javax/swing/text/html/InlineView.java create mode 100644 src/share/classes/javax/swing/text/html/IsindexView.java create mode 100644 src/share/classes/javax/swing/text/html/LineView.java create mode 100644 src/share/classes/javax/swing/text/html/ListView.java create mode 100644 src/share/classes/javax/swing/text/html/Map.java create mode 100644 src/share/classes/javax/swing/text/html/MinimalHTMLWriter.java create mode 100644 src/share/classes/javax/swing/text/html/MuxingAttributeSet.java create mode 100644 src/share/classes/javax/swing/text/html/NoFramesView.java create mode 100644 src/share/classes/javax/swing/text/html/ObjectView.java create mode 100644 src/share/classes/javax/swing/text/html/Option.java create mode 100644 src/share/classes/javax/swing/text/html/OptionComboBoxModel.java create mode 100644 src/share/classes/javax/swing/text/html/OptionListModel.java create mode 100644 src/share/classes/javax/swing/text/html/ParagraphView.java create mode 100644 src/share/classes/javax/swing/text/html/ResourceLoader.java create mode 100644 src/share/classes/javax/swing/text/html/StyleSheet.java create mode 100644 src/share/classes/javax/swing/text/html/TableView.java create mode 100644 src/share/classes/javax/swing/text/html/TextAreaDocument.java create mode 100644 src/share/classes/javax/swing/text/html/default.css create mode 100644 src/share/classes/javax/swing/text/html/package.html create mode 100644 src/share/classes/javax/swing/text/html/parser/AttributeList.java create mode 100644 src/share/classes/javax/swing/text/html/parser/ContentModel.java create mode 100644 src/share/classes/javax/swing/text/html/parser/ContentModelState.java create mode 100644 src/share/classes/javax/swing/text/html/parser/DTD.java create mode 100644 src/share/classes/javax/swing/text/html/parser/DTDConstants.java create mode 100644 src/share/classes/javax/swing/text/html/parser/DocumentParser.java create mode 100644 src/share/classes/javax/swing/text/html/parser/Element.java create mode 100644 src/share/classes/javax/swing/text/html/parser/Entity.java create mode 100644 src/share/classes/javax/swing/text/html/parser/Parser.java create mode 100644 src/share/classes/javax/swing/text/html/parser/ParserDelegator.java create mode 100644 src/share/classes/javax/swing/text/html/parser/ResourceLoader.java create mode 100644 src/share/classes/javax/swing/text/html/parser/TagElement.java create mode 100644 src/share/classes/javax/swing/text/html/parser/TagStack.java create mode 100644 src/share/classes/javax/swing/text/html/parser/html32.bdtd create mode 100644 src/share/classes/javax/swing/text/html/parser/package.html (limited to 'src/share/classes/javax/swing/text/html') diff --git a/src/share/classes/javax/swing/text/html/AccessibleHTML.java b/src/share/classes/javax/swing/text/html/AccessibleHTML.java new file mode 100644 index 000000000..a4680179c --- /dev/null +++ b/src/share/classes/javax/swing/text/html/AccessibleHTML.java @@ -0,0 +1,3058 @@ +/* + * Copyright 2000-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html; + +import java.awt.*; +import java.awt.event.*; +import java.beans.*; +import java.util.*; +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.text.*; +import javax.accessibility.*; +import java.text.BreakIterator; + +/* + * The AccessibleHTML class provide information about the contents + * of a HTML document to assistive technologies. + * + * @author Lynn Monsanto + */ +class AccessibleHTML implements Accessible { + + /** + * The editor. + */ + private JEditorPane editor; + /** + * Current model. + */ + private Document model; + /** + * DocumentListener installed on the current model. + */ + private DocumentListener docListener; + /** + * PropertyChangeListener installed on the editor + */ + private PropertyChangeListener propChangeListener; + /** + * The root ElementInfo for the document + */ + private ElementInfo rootElementInfo; + /* + * The root accessible context for the document + */ + private RootHTMLAccessibleContext rootHTMLAccessibleContext; + + public AccessibleHTML(JEditorPane pane) { + editor = pane; + propChangeListener = new PropertyChangeHandler(); + setDocument(editor.getDocument()); + + docListener = new DocumentHandler(); + } + + /** + * Sets the document. + */ + private void setDocument(Document document) { + if (model != null) { + model.removeDocumentListener(docListener); + } + if (editor != null) { + editor.removePropertyChangeListener(propChangeListener); + } + this.model = document; + if (model != null) { + if (rootElementInfo != null) { + rootElementInfo.invalidate(false); + } + buildInfo(); + model.addDocumentListener(docListener); + } + else { + rootElementInfo = null; + } + if (editor != null) { + editor.addPropertyChangeListener(propChangeListener); + } + } + + /** + * Returns the Document currently presenting information for. + */ + private Document getDocument() { + return model; + } + + /** + * Returns the JEditorPane providing information for. + */ + private JEditorPane getTextComponent() { + return editor; + } + + /** + * Returns the ElementInfo representing the root Element. + */ + private ElementInfo getRootInfo() { + return rootElementInfo; + } + + /** + * Returns the root View associated with the current text + * component. + */ + private View getRootView() { + return getTextComponent().getUI().getRootView(getTextComponent()); + } + + /** + * Returns the bounds the root View will be rendered in. + */ + private Rectangle getRootEditorRect() { + Rectangle alloc = getTextComponent().getBounds(); + if ((alloc.width > 0) && (alloc.height > 0)) { + alloc.x = alloc.y = 0; + Insets insets = editor.getInsets(); + alloc.x += insets.left; + alloc.y += insets.top; + alloc.width -= insets.left + insets.right; + alloc.height -= insets.top + insets.bottom; + return alloc; + } + 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() { + Document document = 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(); + } + } + + /** + * Rebuilds the information from the current info. + */ + private void buildInfo() { + Object lock = lock(); + + try { + Document doc = getDocument(); + Element root = doc.getDefaultRootElement(); + + rootElementInfo = new ElementInfo(root); + rootElementInfo.validate(); + } finally { + unlock(lock); + } + } + + /* + * Create an ElementInfo subclass based on the passed in Element. + */ + ElementInfo createElementInfo(Element e, ElementInfo parent) { + AttributeSet attrs = e.getAttributes(); + + if (attrs != null) { + Object name = attrs.getAttribute(StyleConstants.NameAttribute); + + if (name == HTML.Tag.IMG) { + return new IconElementInfo(e, parent); + } + else if (name == HTML.Tag.CONTENT || name == HTML.Tag.CAPTION) { + return new TextElementInfo(e, parent); + } + else if (name == HTML.Tag.TABLE) { + return new TableElementInfo(e, parent); + } + } + return null; + } + + /** + * Returns the root AccessibleContext for the document + */ + public AccessibleContext getAccessibleContext() { + if (rootHTMLAccessibleContext == null) { + rootHTMLAccessibleContext = + new RootHTMLAccessibleContext(rootElementInfo); + } + return rootHTMLAccessibleContext; + } + + /* + * The roow AccessibleContext for the document + */ + private class RootHTMLAccessibleContext extends HTMLAccessibleContext { + + public RootHTMLAccessibleContext(ElementInfo elementInfo) { + super(elementInfo); + } + + /** + * Gets the accessibleName property of this object. The accessibleName + * property of an object is a localized String that designates the purpose + * of the object. For example, the accessibleName property of a label + * or button might be the text of the label or button itself. In the + * case of an object that doesn't display its name, the accessibleName + * should still be set. For example, in the case of a text field used + * to enter the name of a city, the accessibleName for the en_US locale + * could be 'city.' + * + * @return the localized name of the object; null if this + * object does not have a name + * + * @see #setAccessibleName + */ + public String getAccessibleName() { + if (model != null) { + return (String)model.getProperty(Document.TitleProperty); + } else { + return null; + } + } + + /** + * Gets the accessibleDescription property of this object. If this + * property isn't set, returns the content type of this + * JEditorPane instead (e.g. "plain/text", "html/text"). + * + * @return the localized description of the object; null + * if this object does not have a description + * + * @see #setAccessibleName + */ + public String getAccessibleDescription() { + return editor.getContentType(); + } + + /** + * Gets the role of this object. The role of the object is the generic + * purpose or use of the class of this object. For example, the role + * of a push button is AccessibleRole.PUSH_BUTTON. The roles in + * AccessibleRole are provided so component developers can pick from + * a set of predefined roles. This enables assistive technologies to + * provide a consistent interface to various tweaked subclasses of + * components (e.g., use AccessibleRole.PUSH_BUTTON for all components + * that act like a push button) as well as distinguish between sublasses + * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes + * and AccessibleRole.RADIO_BUTTON for radio buttons). + *

Note that the AccessibleRole class is also extensible, so + * custom component developers can define their own AccessibleRole's + * if the set of predefined roles is inadequate. + * + * @return an instance of AccessibleRole describing the role of the object + * @see AccessibleRole + */ + public AccessibleRole getAccessibleRole() { + return AccessibleRole.TEXT; + } + } + + /* + * Base AccessibleContext class for HTML elements + */ + protected abstract class HTMLAccessibleContext extends AccessibleContext + implements Accessible, AccessibleComponent { + + protected ElementInfo elementInfo; + + public HTMLAccessibleContext(ElementInfo elementInfo) { + this.elementInfo = elementInfo; + } + + // begin AccessibleContext implementation ... + public AccessibleContext getAccessibleContext() { + return this; + } + + /** + * Gets the state set of this object. + * + * @return an instance of AccessibleStateSet describing the states + * of the object + * @see AccessibleStateSet + */ + public AccessibleStateSet getAccessibleStateSet() { + AccessibleStateSet states = new AccessibleStateSet(); + Component comp = getTextComponent(); + + if (comp.isEnabled()) { + states.add(AccessibleState.ENABLED); + } + if (comp instanceof JTextComponent && + ((JTextComponent)comp).isEditable()) { + + states.add(AccessibleState.EDITABLE); + states.add(AccessibleState.FOCUSABLE); + } + if (comp.isVisible()) { + states.add(AccessibleState.VISIBLE); + } + if (comp.isShowing()) { + states.add(AccessibleState.SHOWING); + } + return states; + } + + /** + * Gets the 0-based index of this object in its accessible parent. + * + * @return the 0-based index of this object in its parent; -1 if this + * object does not have an accessible parent. + * + * @see #getAccessibleParent + * @see #getAccessibleChildrenCount + * @see #getAccessibleChild + */ + public int getAccessibleIndexInParent() { + return elementInfo.getIndexInParent(); + } + + /** + * Returns the number of accessible children of the object. + * + * @return the number of accessible children of the object. + */ + public int getAccessibleChildrenCount() { + return elementInfo.getChildCount(); + } + + /** + * Returns the specified Accessible child of the object. The Accessible + * children of an Accessible object are zero-based, so the first child + * of an Accessible child is at index 0, the second child is at index 1, + * and so on. + * + * @param i zero-based index of child + * @return the Accessible child of the object + * @see #getAccessibleChildrenCount + */ + public Accessible getAccessibleChild(int i) { + ElementInfo childInfo = elementInfo.getChild(i); + if (childInfo != null && childInfo instanceof Accessible) { + return (Accessible)childInfo; + } else { + return null; + } + } + + /** + * Gets the locale of the component. If the component does not have a + * locale, then the locale of its parent is returned. + * + * @return this component's locale. If this component does not have + * a locale, the locale of its parent is returned. + * + * @exception IllegalComponentStateException + * If the Component does not have its own locale and has not yet been + * added to a containment hierarchy such that the locale can be + * determined from the containing parent. + */ + public Locale getLocale() throws IllegalComponentStateException { + return editor.getLocale(); + } + // ... end AccessibleContext implementation + + // begin AccessibleComponent implementation ... + public AccessibleComponent getAccessibleComponent() { + return this; + } + + /** + * Gets the background color of this object. + * + * @return the background color, if supported, of the object; + * otherwise, null + * @see #setBackground + */ + public Color getBackground() { + return getTextComponent().getBackground(); + } + + /** + * Sets the background color of this object. + * + * @param c the new Color for the background + * @see #setBackground + */ + public void setBackground(Color c) { + getTextComponent().setBackground(c); + } + + /** + * Gets the foreground color of this object. + * + * @return the foreground color, if supported, of the object; + * otherwise, null + * @see #setForeground + */ + public Color getForeground() { + return getTextComponent().getForeground(); + } + + /** + * Sets the foreground color of this object. + * + * @param c the new Color for the foreground + * @see #getForeground + */ + public void setForeground(Color c) { + getTextComponent().setForeground(c); + } + + /** + * Gets the Cursor of this object. + * + * @return the Cursor, if supported, of the object; otherwise, null + * @see #setCursor + */ + public Cursor getCursor() { + return getTextComponent().getCursor(); + } + + /** + * Sets the Cursor of this object. + * + * @param c the new Cursor for the object + * @see #getCursor + */ + public void setCursor(Cursor cursor) { + getTextComponent().setCursor(cursor); + } + + /** + * Gets the Font of this object. + * + * @return the Font,if supported, for the object; otherwise, null + * @see #setFont + */ + public Font getFont() { + return getTextComponent().getFont(); + } + + /** + * Sets the Font of this object. + * + * @param f the new Font for the object + * @see #getFont + */ + public void setFont(Font f) { + getTextComponent().setFont(f); + } + + /** + * Gets the FontMetrics of this object. + * + * @param f the Font + * @return the FontMetrics, if supported, the object; otherwise, null + * @see #getFont + */ + public FontMetrics getFontMetrics(Font f) { + return getTextComponent().getFontMetrics(f); + } + + /** + * Determines if the object is enabled. Objects that are enabled + * will also have the AccessibleState.ENABLED state set in their + * AccessibleStateSets. + * + * @return true if object is enabled; otherwise, false + * @see #setEnabled + * @see AccessibleContext#getAccessibleStateSet + * @see AccessibleState#ENABLED + * @see AccessibleStateSet + */ + public boolean isEnabled() { + return getTextComponent().isEnabled(); + } + + /** + * Sets the enabled state of the object. + * + * @param b if true, enables this object; otherwise, disables it + * @see #isEnabled + */ + public void setEnabled(boolean b) { + getTextComponent().setEnabled(b); + } + + /** + * Determines if the object is visible. Note: this means that the + * object intends to be visible; however, it may not be + * showing on the screen because one of the objects that this object + * is contained by is currently not visible. To determine if an object + * is showing on the screen, use isShowing(). + *

Objects that are visible will also have the + * AccessibleState.VISIBLE state set in their AccessibleStateSets. + * + * @return true if object is visible; otherwise, false + * @see #setVisible + * @see AccessibleContext#getAccessibleStateSet + * @see AccessibleState#VISIBLE + * @see AccessibleStateSet + */ + public boolean isVisible() { + return getTextComponent().isVisible(); + } + + /** + * Sets the visible state of the object. + * + * @param b if true, shows this object; otherwise, hides it + * @see #isVisible + */ + public void setVisible(boolean b) { + getTextComponent().setVisible(b); + } + + /** + * Determines if the object is showing. This is determined by checking + * the visibility of the object and its ancestors. + * Note: this + * will return true even if the object is obscured by another (for + * example, it is underneath a menu that was pulled down). + * + * @return true if object is showing; otherwise, false + */ + public boolean isShowing() { + return getTextComponent().isShowing(); + } + + /** + * Checks whether the specified point is within this object's bounds, + * where the point's x and y coordinates are defined to be relative + * to the coordinate system of the object. + * + * @param p the Point relative to the coordinate system of the object + * @return true if object contains Point; otherwise false + * @see #getBounds + */ + public boolean contains(Point p) { + Rectangle r = getBounds(); + if (r != null) { + return r.contains(p.x, p.y); + } else { + return false; + } + } + + /** + * Returns the location of the object on the screen. + * + * @return the location of the object on screen; null if this object + * is not on the screen + * @see #getBounds + * @see #getLocation + */ + public Point getLocationOnScreen() { + Point editorLocation = getTextComponent().getLocationOnScreen(); + Rectangle r = getBounds(); + if (r != null) { + return new Point(editorLocation.x + r.x, + editorLocation.y + r.y); + } else { + return null; + } + } + + /** + * Gets the location of the object relative to the parent in the form + * of a point specifying the object's top-left corner in the screen's + * coordinate space. + * + * @return An instance of Point representing the top-left corner of the + * object's bounds in the coordinate space of the screen; null if + * this object or its parent are not on the screen + * @see #getBounds + * @see #getLocationOnScreen + */ + public Point getLocation() { + Rectangle r = getBounds(); + if (r != null) { + return new Point(r.x, r.y); + } else { + return null; + } + } + + /** + * Sets the location of the object relative to the parent. + * @param p the new position for the top-left corner + * @see #getLocation + */ + public void setLocation(Point p) { + } + + /** + * Gets the bounds of this object in the form of a Rectangle object. + * The bounds specify this object's width, height, and location + * relative to its parent. + * + * @return A rectangle indicating this component's bounds; null if + * this object is not on the screen. + * @see #contains + */ + public Rectangle getBounds() { + return elementInfo.getBounds(); + } + + /** + * Sets the bounds of this object in the form of a Rectangle object. + * The bounds specify this object's width, height, and location + * relative to its parent. + * + * @param r rectangle indicating this component's bounds + * @see #getBounds + */ + public void setBounds(Rectangle r) { + } + + /** + * Returns the size of this object in the form of a Dimension object. + * The height field of the Dimension object contains this object's + * height, and the width field of the Dimension object contains this + * object's width. + * + * @return A Dimension object that indicates the size of this component; + * null if this object is not on the screen + * @see #setSize + */ + public Dimension getSize() { + Rectangle r = getBounds(); + if (r != null) { + return new Dimension(r.width, r.height); + } else { + return null; + } + } + + /** + * Resizes this object so that it has width and height. + * + * @param d The dimension specifying the new size of the object. + * @see #getSize + */ + public void setSize(Dimension d) { + Component comp = getTextComponent(); + comp.setSize(d); + } + + /** + * Returns the Accessible child, if one exists, contained at the local + * coordinate Point. + * + * @param p The point relative to the coordinate system of this object. + * @return the Accessible, if it exists, at the specified location; + * otherwise null + */ + public Accessible getAccessibleAt(Point p) { + ElementInfo innerMostElement = getElementInfoAt(rootElementInfo, p); + if (innerMostElement instanceof Accessible) { + return (Accessible)innerMostElement; + } else { + return null; + } + } + + private ElementInfo getElementInfoAt(ElementInfo elementInfo, Point p) { + if (elementInfo.getBounds() == null) { + return null; + } + if (elementInfo.getChildCount() == 0 && + elementInfo.getBounds().contains(p)) { + return elementInfo; + + } else { + if (elementInfo instanceof TableElementInfo) { + // Handle table caption as a special case since it's the + // only table child that is not a table row. + ElementInfo captionInfo = + ((TableElementInfo)elementInfo).getCaptionInfo(); + if (captionInfo != null) { + Rectangle bounds = captionInfo.getBounds(); + if (bounds != null && bounds.contains(p)) { + return captionInfo; + } + } + } + for (int i = 0; i < elementInfo.getChildCount(); i++) +{ + ElementInfo childInfo = elementInfo.getChild(i); + ElementInfo retValue = getElementInfoAt(childInfo, p); + if (retValue != null) { + return retValue; + } + } + } + return null; + } + + /** + * Returns whether this object can accept focus or not. Objects that + * can accept focus will also have the AccessibleState.FOCUSABLE state + * set in their AccessibleStateSets. + * + * @return true if object can accept focus; otherwise false + * @see AccessibleContext#getAccessibleStateSet + * @see AccessibleState#FOCUSABLE + * @see AccessibleState#FOCUSED + * @see AccessibleStateSet + */ + public boolean isFocusTraversable() { + Component comp = getTextComponent(); + if (comp instanceof JTextComponent) { + if (((JTextComponent)comp).isEditable()) { + return true; + } + } + return false; + } + + /** + * Requests focus for this object. If this object cannot accept focus, + * nothing will happen. Otherwise, the object will attempt to take + * focus. + * @see #isFocusTraversable + */ + public void requestFocus() { + // TIGER - 4856191 + if (! isFocusTraversable()) { + return; + } + + Component comp = getTextComponent(); + if (comp instanceof JTextComponent) { + + comp.requestFocusInWindow(); + + try { + if (elementInfo.validateIfNecessary()) { + // set the caret position to the start of this component + Element elem = elementInfo.getElement(); + ((JTextComponent)comp).setCaretPosition(elem.getStartOffset()); + + // fire a AccessibleState.FOCUSED property change event + AccessibleContext ac = editor.getAccessibleContext(); + PropertyChangeEvent pce = new PropertyChangeEvent(this, + AccessibleContext.ACCESSIBLE_STATE_PROPERTY, + null, AccessibleState.FOCUSED); + ac.firePropertyChange( + AccessibleContext.ACCESSIBLE_STATE_PROPERTY, + null, pce); + } + } catch (IllegalArgumentException e) { + // don't fire property change event + } + } + } + + /** + * Adds the specified focus listener to receive focus events from this + * component. + * + * @param l the focus listener + * @see #removeFocusListener + */ + public void addFocusListener(FocusListener l) { + getTextComponent().addFocusListener(l); + } + + /** + * Removes the specified focus listener so it no longer receives focus + * events from this component. + * + * @param l the focus listener + * @see #addFocusListener + */ + public void removeFocusListener(FocusListener l) { + getTextComponent().removeFocusListener(l); + } + // ... end AccessibleComponent implementation + } // ... end HTMLAccessibleContext + + + + /* + * ElementInfo for text + */ + class TextElementInfo extends ElementInfo implements Accessible { + + TextElementInfo(Element element, ElementInfo parent) { + super(element, parent); + } + + // begin AccessibleText implementation ... + private AccessibleContext accessibleContext; + + public AccessibleContext getAccessibleContext() { + if (accessibleContext == null) { + accessibleContext = new TextAccessibleContext(this); + } + return accessibleContext; + } + + /* + * AccessibleContext for text elements + */ + public class TextAccessibleContext extends HTMLAccessibleContext + implements AccessibleText { + + public TextAccessibleContext(ElementInfo elementInfo) { + super(elementInfo); + } + + public AccessibleText getAccessibleText() { + return this; + } + + /** + * Gets the accessibleName property of this object. The accessibleName + * property of an object is a localized String that designates the purpose + * of the object. For example, the accessibleName property of a label + * or button might be the text of the label or button itself. In the + * case of an object that doesn't display its name, the accessibleName + * should still be set. For example, in the case of a text field used + * to enter the name of a city, the accessibleName for the en_US locale + * could be 'city.' + * + * @return the localized name of the object; null if this + * object does not have a name + * + * @see #setAccessibleName + */ + public String getAccessibleName() { + if (model != null) { + return (String)model.getProperty(Document.TitleProperty); + } else { + return null; + } + } + + /** + * Gets the accessibleDescription property of this object. If this + * property isn't set, returns the content type of this + * JEditorPane instead (e.g. "plain/text", "html/text"). + * + * @return the localized description of the object; null + * if this object does not have a description + * + * @see #setAccessibleName + */ + public String getAccessibleDescription() { + return editor.getContentType(); + } + + /** + * Gets the role of this object. The role of the object is the generic + * purpose or use of the class of this object. For example, the role + * of a push button is AccessibleRole.PUSH_BUTTON. The roles in + * AccessibleRole are provided so component developers can pick from + * a set of predefined roles. This enables assistive technologies to + * provide a consistent interface to various tweaked subclasses of + * components (e.g., use AccessibleRole.PUSH_BUTTON for all components + * that act like a push button) as well as distinguish between sublasses + * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes + * and AccessibleRole.RADIO_BUTTON for radio buttons). + *

Note that the AccessibleRole class is also extensible, so + * custom component developers can define their own AccessibleRole's + * if the set of predefined roles is inadequate. + * + * @return an instance of AccessibleRole describing the role of the object + * @see AccessibleRole + */ + public AccessibleRole getAccessibleRole() { + return AccessibleRole.TEXT; + } + + /** + * Given a point in local coordinates, return the zero-based index + * of the character under that Point. If the point is invalid, + * this method returns -1. + * + * @param p the Point in local coordinates + * @return the zero-based index of the character under Point p; if + * Point is invalid returns -1. + */ + public int getIndexAtPoint(Point p) { + View v = getView(); + if (v != null) { + return v.viewToModel(p.x, p.y, getBounds()); + } else { + return -1; + } + } + + /** + * Determine the bounding box of the character at the given + * index into the string. The bounds are returned in local + * coordinates. If the index is invalid an empty rectangle is + * returned. + * + * @param i the index into the String + * @return the screen coordinates of the character's the bounding box, + * if index is invalid returns an empty rectangle. + */ + public Rectangle getCharacterBounds(int i) { + try { + return editor.getUI().modelToView(editor, i); + } catch (BadLocationException e) { + return null; + } + } + + /** + * Return the number of characters (valid indicies) + * + * @return the number of characters + */ + public int getCharCount() { + if (validateIfNecessary()) { + Element elem = elementInfo.getElement(); + return elem.getEndOffset() - elem.getStartOffset(); + } + return 0; + } + + /** + * Return the zero-based offset of the caret. + * + * Note: That to the right of the caret will have the same index + * value as the offset (the caret is between two characters). + * @return the zero-based offset of the caret. + */ + public int getCaretPosition() { + View v = getView(); + if (v == null) { + return -1; + } + Container c = v.getContainer(); + if (c == null) { + return -1; + } + if (c instanceof JTextComponent) { + return ((JTextComponent)c).getCaretPosition(); + } else { + return -1; + } + } + + /** + * IndexedSegment extends Segment adding the offset into the + * the model the Segment was asked for. + */ + private class IndexedSegment extends Segment { + /** + * Offset into the model that the position represents. + */ + public int modelOffset; + } + + public String getAtIndex(int part, int index) { + return getAtIndex(part, index, 0); + } + + + public String getAfterIndex(int part, int index) { + return getAtIndex(part, index, 1); + } + + public String getBeforeIndex(int part, int index) { + return getAtIndex(part, index, -1); + } + + /** + * Gets the word, sentence, or character at index. + * If direction is non-null this will find the + * next/previous word/sentence/character. + */ + private String getAtIndex(int part, int index, int direction) { + if (model instanceof AbstractDocument) { + ((AbstractDocument)model).readLock(); + } + try { + if (index < 0 || index >= model.getLength()) { + return null; + } + switch (part) { + case AccessibleText.CHARACTER: + if (index + direction < model.getLength() && + index + direction >= 0) { + return model.getText(index + direction, 1); + } + break; + + + case AccessibleText.WORD: + case AccessibleText.SENTENCE: + IndexedSegment seg = getSegmentAt(part, index); + if (seg != null) { + if (direction != 0) { + int next; + + + if (direction < 0) { + next = seg.modelOffset - 1; + } + else { + next = seg.modelOffset + direction * seg.count; + } + if (next >= 0 && next <= model.getLength()) { + seg = getSegmentAt(part, next); + } + else { + seg = null; + } + } + if (seg != null) { + return new String(seg.array, seg.offset, + seg.count); + } + } + break; + + default: + break; + } + } catch (BadLocationException e) { + } finally { + if (model instanceof AbstractDocument) { + ((AbstractDocument)model).readUnlock(); + } + } + return null; + } + + /* + * Returns the paragraph element for the specified index. + */ + private Element getParagraphElement(int index) { + if (model instanceof PlainDocument ) { + PlainDocument sdoc = (PlainDocument)model; + return sdoc.getParagraphElement(index); + } else if (model instanceof StyledDocument) { + StyledDocument sdoc = (StyledDocument)model; + return sdoc.getParagraphElement(index); + } else { + Element para = null; + for (para = model.getDefaultRootElement(); ! para.isLeaf(); ) { + int pos = para.getElementIndex(index); + para = para.getElement(pos); + } + if (para == null) { + return null; + } + return para.getParentElement(); + } + } + + /* + * Returns a Segment containing the paragraph text + * at index, or null if index isn't + * valid. + */ + private IndexedSegment getParagraphElementText(int index) + throws BadLocationException { + Element para = getParagraphElement(index); + + + if (para != null) { + IndexedSegment segment = new IndexedSegment(); + try { + int length = para.getEndOffset() - para.getStartOffset(); + model.getText(para.getStartOffset(), length, segment); + } catch (BadLocationException e) { + return null; + } + segment.modelOffset = para.getStartOffset(); + return segment; + } + return null; + } + + + /** + * Returns the Segment at index representing either + * the paragraph or sentence as identified by part, or + * null if a valid paragraph/sentence can't be found. The offset + * will point to the start of the word/sentence in the array, and + * the modelOffset will point to the location of the word/sentence + * in the model. + */ + private IndexedSegment getSegmentAt(int part, int index) + throws BadLocationException { + + IndexedSegment seg = getParagraphElementText(index); + if (seg == null) { + return null; + } + BreakIterator iterator; + switch (part) { + case AccessibleText.WORD: + iterator = BreakIterator.getWordInstance(getLocale()); + break; + case AccessibleText.SENTENCE: + iterator = BreakIterator.getSentenceInstance(getLocale()); + break; + default: + return null; + } + seg.first(); + iterator.setText(seg); + int end = iterator.following(index - seg.modelOffset + seg.offset); + if (end == BreakIterator.DONE) { + return null; + } + if (end > seg.offset + seg.count) { + return null; + } + int begin = iterator.previous(); + if (begin == BreakIterator.DONE || + begin >= seg.offset + seg.count) { + return null; + } + seg.modelOffset = seg.modelOffset + begin - seg.offset; + seg.offset = begin; + seg.count = end - begin; + return seg; + } + + /** + * Return the AttributeSet for a given character at a given index + * + * @param i the zero-based index into the text + * @return the AttributeSet of the character + */ + public AttributeSet getCharacterAttribute(int i) { + if (model instanceof StyledDocument) { + StyledDocument doc = (StyledDocument)model; + Element elem = doc.getCharacterElement(i); + if (elem != null) { + return elem.getAttributes(); + } + } + return null; + } + + /** + * Returns the start offset within the selected text. + * If there is no selection, but there is + * a caret, the start and end offsets will be the same. + * + * @return the index into the text of the start of the selection + */ + public int getSelectionStart() { + return editor.getSelectionStart(); + } + + /** + * Returns the end offset within the selected text. + * If there is no selection, but there is + * a caret, the start and end offsets will be the same. + * + * @return the index into teh text of the end of the selection + */ + public int getSelectionEnd() { + return editor.getSelectionEnd(); + } + + /** + * Returns the portion of the text that is selected. + * + * @return the String portion of the text that is selected + */ + public String getSelectedText() { + return editor.getSelectedText(); + } + + /* + * Returns the text substring starting at the specified + * offset with the specified length. + */ + private String getText(int offset, int length) + throws BadLocationException { + + if (model != null && model instanceof StyledDocument) { + StyledDocument doc = (StyledDocument)model; + return model.getText(offset, length); + } else { + return null; + } + } + } + } + + /* + * ElementInfo for images + */ + private class IconElementInfo extends ElementInfo implements Accessible { + + private int width = -1; + private int height = -1; + + IconElementInfo(Element element, ElementInfo parent) { + super(element, parent); + } + + protected void invalidate(boolean first) { + super.invalidate(first); + width = height = -1; + } + + private int getImageSize(Object key) { + if (validateIfNecessary()) { + int size = getIntAttr(getAttributes(), key, -1); + + if (size == -1) { + View v = getView(); + + size = 0; + if (v instanceof ImageView) { + Image img = ((ImageView)v).getImage(); + if (img != null) { + if (key == HTML.Attribute.WIDTH) { + size = img.getWidth(null); + } + else { + size = img.getHeight(null); + } + } + } + } + return size; + } + return 0; + } + + // begin AccessibleIcon implementation ... + private AccessibleContext accessibleContext; + + public AccessibleContext getAccessibleContext() { + if (accessibleContext == null) { + accessibleContext = new IconAccessibleContext(this); + } + return accessibleContext; + } + + /* + * AccessibleContext for images + */ + protected class IconAccessibleContext extends HTMLAccessibleContext + implements AccessibleIcon { + + public IconAccessibleContext(ElementInfo elementInfo) { + super(elementInfo); + } + + /** + * Gets the accessibleName property of this object. The accessibleName + * property of an object is a localized String that designates the purpose + * of the object. For example, the accessibleName property of a label + * or button might be the text of the label or button itself. In the + * case of an object that doesn't display its name, the accessibleName + * should still be set. For example, in the case of a text field used + * to enter the name of a city, the accessibleName for the en_US locale + * could be 'city.' + * + * @return the localized name of the object; null if this + * object does not have a name + * + * @see #setAccessibleName + */ + public String getAccessibleName() { + return getAccessibleIconDescription(); + } + + /** + * Gets the accessibleDescription property of this object. If this + * property isn't set, returns the content type of this + * JEditorPane instead (e.g. "plain/text", "html/text"). + * + * @return the localized description of the object; null + * if this object does not have a description + * + * @see #setAccessibleName + */ + public String getAccessibleDescription() { + return editor.getContentType(); + } + + /** + * Gets the role of this object. The role of the object is the generic + * purpose or use of the class of this object. For example, the role + * of a push button is AccessibleRole.PUSH_BUTTON. The roles in + * AccessibleRole are provided so component developers can pick from + * a set of predefined roles. This enables assistive technologies to + * provide a consistent interface to various tweaked subclasses of + * components (e.g., use AccessibleRole.PUSH_BUTTON for all components + * that act like a push button) as well as distinguish between sublasses + * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes + * and AccessibleRole.RADIO_BUTTON for radio buttons). + *

Note that the AccessibleRole class is also extensible, so + * custom component developers can define their own AccessibleRole's + * if the set of predefined roles is inadequate. + * + * @return an instance of AccessibleRole describing the role of the object + * @see AccessibleRole + */ + public AccessibleRole getAccessibleRole() { + return AccessibleRole.ICON; + } + + public AccessibleIcon [] getAccessibleIcon() { + AccessibleIcon [] icons = new AccessibleIcon[1]; + icons[0] = this; + return icons; + } + + /** + * Gets the description of the icon. This is meant to be a brief + * textual description of the object. For example, it might be + * presented to a blind user to give an indication of the purpose + * of the icon. + * + * @return the description of the icon + */ + public String getAccessibleIconDescription() { + return ((ImageView)getView()).getAltText(); + } + + /** + * Sets the description of the icon. This is meant to be a brief + * textual description of the object. For example, it might be + * presented to a blind user to give an indication of the purpose + * of the icon. + * + * @param description the description of the icon + */ + public void setAccessibleIconDescription(String description) { + } + + /** + * Gets the width of the icon + * + * @return the width of the icon. + */ + public int getAccessibleIconWidth() { + if (width == -1) { + width = getImageSize(HTML.Attribute.WIDTH); + } + return width; + } + + /** + * Gets the height of the icon + * + * @return the height of the icon. + */ + public int getAccessibleIconHeight() { + if (height == -1) { + height = getImageSize(HTML.Attribute.HEIGHT); + } + return height; + } + } + // ... end AccessibleIconImplementation + } + + + /** + * TableElementInfo encapsulates information about a HTML.Tag.TABLE. + * To make access fast it crates a grid containing the children to + * allow for access by row, column. TableElementInfo will contain + * TableRowElementInfos, which will contain TableCellElementInfos. + * Any time one of the rows or columns becomes invalid the table is + * invalidated. This is because any time one of the child attributes + * changes the size of the grid may have changed. + */ + private class TableElementInfo extends ElementInfo + implements Accessible { + + protected ElementInfo caption; + + /** + * Allocation of the table by row x column. There may be holes (eg + * nulls) depending upon the html, any cell that has a rowspan/colspan + * > 1 will be contained multiple times in the grid. + */ + private TableCellElementInfo[][] grid; + + + TableElementInfo(Element e, ElementInfo parent) { + super(e, parent); + } + + public ElementInfo getCaptionInfo() { + return caption; + } + + /** + * Overriden to update the grid when validating. + */ + protected void validate() { + super.validate(); + updateGrid(); + } + + /** + * Overriden to only alloc instances of TableRowElementInfos. + */ + protected void loadChildren(Element e) { + + for (int counter = 0; counter < e.getElementCount(); counter++) { + Element child = e.getElement(counter); + AttributeSet attrs = child.getAttributes(); + + if (attrs.getAttribute(StyleConstants.NameAttribute) == + HTML.Tag.TR) { + addChild(new TableRowElementInfo(child, this, counter)); + + } else if (attrs.getAttribute(StyleConstants.NameAttribute) == + HTML.Tag.CAPTION) { + // Handle captions as a special case since all other + // children are table rows. + caption = createElementInfo(child, this); + } + } + } + + /** + * Updates the grid. + */ + private void updateGrid() { + // Determine the max row/col count. + int delta = 0; + int maxCols = 0; + int rows = 0; + for (int counter = 0; counter < getChildCount(); counter++) { + TableRowElementInfo row = getRow(counter); + int prev = 0; + for (int y = 0; y < delta; y++) { + prev = Math.max(prev, getRow(counter - y - 1). + getColumnCount(y + 2)); + } + delta = Math.max(row.getRowCount(), delta); + delta--; + maxCols = Math.max(maxCols, row.getColumnCount() + prev); + } + rows = getChildCount() + delta; + + // Alloc + grid = new TableCellElementInfo[rows][]; + for (int counter = 0; counter < rows; counter++) { + grid[counter] = new TableCellElementInfo[maxCols]; + } + // Update + for (int counter = 0; counter < rows; counter++) { + getRow(counter).updateGrid(counter); + } + } + + /** + * Returns the TableCellElementInfo at the specified index. + */ + public TableRowElementInfo getRow(int index) { + return (TableRowElementInfo)getChild(index); + } + + /** + * Returns the TableCellElementInfo by row and column. + */ + public TableCellElementInfo getCell(int r, int c) { + if (validateIfNecessary() && r < grid.length && + c < grid[0].length) { + return grid[r][c]; + } + return null; + } + + /** + * Returns the rowspan of the specified entry. + */ + public int getRowExtentAt(int r, int c) { + TableCellElementInfo cell = getCell(r, c); + + if (cell != null) { + int rows = cell.getRowCount(); + int delta = 1; + + while ((r - delta) >= 0 && grid[r - delta][c] == cell) { + delta++; + } + return rows - delta + 1; + } + return 0; + } + + /** + * Returns the colspan of the specified entry. + */ + public int getColumnExtentAt(int r, int c) { + TableCellElementInfo cell = getCell(r, c); + + if (cell != null) { + int cols = cell.getColumnCount(); + int delta = 1; + + while ((c - delta) >= 0 && grid[r][c - delta] == cell) { + delta++; + } + return cols - delta + 1; + } + return 0; + } + + /** + * Returns the number of rows in the table. + */ + public int getRowCount() { + if (validateIfNecessary()) { + return grid.length; + } + return 0; + } + + /** + * Returns the number of columns in the table. + */ + public int getColumnCount() { + if (validateIfNecessary() && grid.length > 0) { + return grid[0].length; + } + return 0; + } + + // begin AccessibleTable implementation ... + private AccessibleContext accessibleContext; + + public AccessibleContext getAccessibleContext() { + if (accessibleContext == null) { + accessibleContext = new TableAccessibleContext(this); + } + return accessibleContext; + } + + /* + * AccessibleContext for tables + */ + public class TableAccessibleContext extends HTMLAccessibleContext + implements AccessibleTable { + + private AccessibleHeadersTable rowHeadersTable; + + public TableAccessibleContext(ElementInfo elementInfo) { + super(elementInfo); + } + + /** + * Gets the accessibleName property of this object. The accessibleName + * property of an object is a localized String that designates the purpose + * of the object. For example, the accessibleName property of a label + * or button might be the text of the label or button itself. In the + * case of an object that doesn't display its name, the accessibleName + * should still be set. For example, in the case of a text field used + * to enter the name of a city, the accessibleName for the en_US locale + * could be 'city.' + * + * @return the localized name of the object; null if this + * object does not have a name + * + * @see #setAccessibleName + */ + public String getAccessibleName() { + // return the role of the object + return getAccessibleRole().toString(); + } + + /** + * Gets the accessibleDescription property of this object. If this + * property isn't set, returns the content type of this + * JEditorPane instead (e.g. "plain/text", "html/text"). + * + * @return the localized description of the object; null + * if this object does not have a description + * + * @see #setAccessibleName + */ + public String getAccessibleDescription() { + return editor.getContentType(); + } + + /** + * Gets the role of this object. The role of the object is the generic + * purpose or use of the class of this object. For example, the role + * of a push button is AccessibleRole.PUSH_BUTTON. The roles in + * AccessibleRole are provided so component developers can pick from + * a set of predefined roles. This enables assistive technologies to + * provide a consistent interface to various tweaked subclasses of + * components (e.g., use AccessibleRole.PUSH_BUTTON for all components + * that act like a push button) as well as distinguish between sublasses + * that behave differently (e.g., AccessibleRole.CHECK_BOX for check boxes + * and AccessibleRole.RADIO_BUTTON for radio buttons). + *

Note that the AccessibleRole class is also extensible, so + * custom component developers can define their own AccessibleRole's + * if the set of predefined roles is inadequate. + * + * @return an instance of AccessibleRole describing the role of the object + * @see AccessibleRole + */ + public AccessibleRole getAccessibleRole() { + return AccessibleRole.TABLE; + } + + /** + * Gets the 0-based index of this object in its accessible parent. + * + * @return the 0-based index of this object in its parent; -1 if this + * object does not have an accessible parent. + * + * @see #getAccessibleParent + * @see #getAccessibleChildrenCount + * @gsee #getAccessibleChild + */ + public int getAccessibleIndexInParent() { + return elementInfo.getIndexInParent(); + } + + /** + * Returns the number of accessible children of the object. + * + * @return the number of accessible children of the object. + */ + public int getAccessibleChildrenCount() { + return ((TableElementInfo)elementInfo).getRowCount() * + ((TableElementInfo)elementInfo).getColumnCount(); + } + + /** + * Returns the specified Accessible child of the object. The Accessible + * children of an Accessible object are zero-based, so the first child + * of an Accessible child is at index 0, the second child is at index 1, + * and so on. + * + * @param i zero-based index of child + * @return the Accessible child of the object + * @see #getAccessibleChildrenCount + */ + public Accessible getAccessibleChild(int i) { + int rowCount = ((TableElementInfo)elementInfo).getRowCount(); + int columnCount = ((TableElementInfo)elementInfo).getColumnCount(); + int r = i / rowCount; + int c = i % columnCount; + if (r < 0 || r >= rowCount || c < 0 || c >= columnCount) { + return null; + } else { + return getAccessibleAt(r, c); + } + } + + public AccessibleTable getAccessibleTable() { + return this; + } + + /** + * Returns the caption for the table. + * + * @return the caption for the table + */ + public Accessible getAccessibleCaption() { + ElementInfo captionInfo = getCaptionInfo(); + if (captionInfo instanceof Accessible) { + return (Accessible)caption; + } else { + return null; + } + } + + /** + * Sets the caption for the table. + * + * @param a the caption for the table + */ + public void setAccessibleCaption(Accessible a) { + } + + /** + * Returns the summary description of the table. + * + * @return the summary description of the table + */ + public Accessible getAccessibleSummary() { + return null; + } + + /** + * Sets the summary description of the table + * + * @param a the summary description of the table + */ + public void setAccessibleSummary(Accessible a) { + } + + /** + * Returns the number of rows in the table. + * + * @return the number of rows in the table + */ + public int getAccessibleRowCount() { + return ((TableElementInfo)elementInfo).getRowCount(); + } + + /** + * Returns the number of columns in the table. + * + * @return the number of columns in the table + */ + public int getAccessibleColumnCount() { + return ((TableElementInfo)elementInfo).getColumnCount(); + } + + /** + * Returns the Accessible at a specified row and column + * in the table. + * + * @param r zero-based row of the table + * @param c zero-based column of the table + * @return the Accessible at the specified row and column + */ + public Accessible getAccessibleAt(int r, int c) { + TableCellElementInfo cellInfo = getCell(r, c); + if (cellInfo != null) { + return cellInfo.getAccessible(); + } else { + return null; + } + } + + /** + * Returns the number of rows occupied by the Accessible at + * a specified row and column in the table. + * + * @return the number of rows occupied by the Accessible at a + * given specified (row, column) + */ + public int getAccessibleRowExtentAt(int r, int c) { + return ((TableElementInfo)elementInfo).getRowExtentAt(r, c); + } + + /** + * Returns the number of columns occupied by the Accessible at + * a specified row and column in the table. + * + * @return the number of columns occupied by the Accessible at a + * given specified row and column + */ + public int getAccessibleColumnExtentAt(int r, int c) { + return ((TableElementInfo)elementInfo).getColumnExtentAt(r, c); + } + + /** + * Returns the row headers as an AccessibleTable. + * + * @return an AccessibleTable representing the row + * headers + */ + public AccessibleTable getAccessibleRowHeader() { + return rowHeadersTable; + } + + /** + * Sets the row headers. + * + * @param table an AccessibleTable representing the + * row headers + */ + public void setAccessibleRowHeader(AccessibleTable table) { + } + + /** + * Returns the column headers as an AccessibleTable. + * + * @return an AccessibleTable representing the column + * headers + */ + public AccessibleTable getAccessibleColumnHeader() { + return null; + } + + /** + * Sets the column headers. + * + * @param table an AccessibleTable representing the + * column headers + */ + public void setAccessibleColumnHeader(AccessibleTable table) { + } + + /** + * Returns the description of the specified row in the table. + * + * @param r zero-based row of the table + * @return the description of the row + */ + public Accessible getAccessibleRowDescription(int r) { + return null; + } + + /** + * Sets the description text of the specified row of the table. + * + * @param r zero-based row of the table + * @param a the description of the row + */ + public void setAccessibleRowDescription(int r, Accessible a) { + } + + /** + * Returns the description text of the specified column in the table. + * + * @param c zero-based column of the table + * @return the text description of the column + */ + public Accessible getAccessibleColumnDescription(int c) { + return null; + } + + /** + * Sets the description text of the specified column in the table. + * + * @param c zero-based column of the table + * @param a the text description of the column + */ + public void setAccessibleColumnDescription(int c, Accessible a) { + } + + /** + * Returns a boolean value indicating whether the accessible at + * a specified row and column is selected. + * + * @param r zero-based row of the table + * @param c zero-based column of the table + * @return the boolean value true if the accessible at the + * row and column is selected. Otherwise, the boolean value + * false + */ + public boolean isAccessibleSelected(int r, int c) { + if (validateIfNecessary()) { + if (r < 0 || r >= getAccessibleRowCount() || + c < 0 || c >= getAccessibleColumnCount()) { + return false; + } + TableCellElementInfo cell = getCell(r, c); + if (cell != null) { + Element elem = cell.getElement(); + int start = elem.getStartOffset(); + int end = elem.getEndOffset(); + return start >= editor.getSelectionStart() && + end <= editor.getSelectionEnd(); + } + } + return false; + } + + /** + * Returns a boolean value indicating whether the specified row + * is selected. + * + * @param r zero-based row of the table + * @return the boolean value true if the specified row is selected. + * Otherwise, false. + */ + public boolean isAccessibleRowSelected(int r) { + if (validateIfNecessary()) { + if (r < 0 || r >= getAccessibleRowCount()) { + return false; + } + int nColumns = getAccessibleColumnCount(); + + TableCellElementInfo startCell = getCell(r, 0); + if (startCell == null) { + return false; + } + int start = startCell.getElement().getStartOffset(); + + TableCellElementInfo endCell = getCell(r, nColumns-1); + if (endCell == null) { + return false; + } + int end = endCell.getElement().getEndOffset(); + + return start >= editor.getSelectionStart() && + end <= editor.getSelectionEnd(); + } + return false; + } + + /** + * Returns a boolean value indicating whether the specified column + * is selected. + * + * @param r zero-based column of the table + * @return the boolean value true if the specified column is selected. + * Otherwise, false. + */ + public boolean isAccessibleColumnSelected(int c) { + if (validateIfNecessary()) { + if (c < 0 || c >= getAccessibleColumnCount()) { + return false; + } + int nRows = getAccessibleRowCount(); + + TableCellElementInfo startCell = getCell(0, c); + if (startCell == null) { + return false; + } + int start = startCell.getElement().getStartOffset(); + + TableCellElementInfo endCell = getCell(nRows-1, c); + if (endCell == null) { + return false; + } + int end = endCell.getElement().getEndOffset(); + return start >= editor.getSelectionStart() && + end <= editor.getSelectionEnd(); + } + return false; + } + + /** + * Returns the selected rows in a table. + * + * @return an array of selected rows where each element is a + * zero-based row of the table + */ + public int [] getSelectedAccessibleRows() { + if (validateIfNecessary()) { + int nRows = getAccessibleRowCount(); + Vector vec = new Vector(); + + for (int i = 0; i < nRows; i++) { + if (isAccessibleRowSelected(i)) { + vec.addElement(new Integer(i)); + } + } + int retval[] = new int[vec.size()]; + for (int i = 0; i < retval.length; i++) { + retval[i] = ((Integer)vec.elementAt(i)).intValue(); + } + return retval; + } + return new int[0]; + } + + /** + * Returns the selected columns in a table. + * + * @return an array of selected columns where each element is a + * zero-based column of the table + */ + public int [] getSelectedAccessibleColumns() { + if (validateIfNecessary()) { + int nColumns = getAccessibleRowCount(); + Vector vec = new Vector(); + + for (int i = 0; i < nColumns; i++) { + if (isAccessibleColumnSelected(i)) { + vec.addElement(new Integer(i)); + } + } + int retval[] = new int[vec.size()]; + for (int i = 0; i < retval.length; i++) { + retval[i] = ((Integer)vec.elementAt(i)).intValue(); + } + return retval; + } + return new int[0]; + } + + // begin AccessibleExtendedTable implementation ------------- + + /** + * Returns the row number of an index in the table. + * + * @param index the zero-based index in the table + * @return the zero-based row of the table if one exists; + * otherwise -1. + */ + public int getAccessibleRow(int index) { + if (validateIfNecessary()) { + int numCells = getAccessibleColumnCount() * + getAccessibleRowCount(); + if (index >= numCells) { + return -1; + } else { + return index / getAccessibleColumnCount(); + } + } + return -1; + } + + /** + * Returns the column number of an index in the table. + * + * @param index the zero-based index in the table + * @return the zero-based column of the table if one exists; + * otherwise -1. + */ + public int getAccessibleColumn(int index) { + if (validateIfNecessary()) { + int numCells = getAccessibleColumnCount() * + getAccessibleRowCount(); + if (index >= numCells) { + return -1; + } else { + return index % getAccessibleColumnCount(); + } + } + return -1; + } + + /** + * Returns the index at a row and column in the table. + * + * @param r zero-based row of the table + * @param c zero-based column of the table + * @return the zero-based index in the table if one exists; + * otherwise -1. + */ + public int getAccessibleIndex(int r, int c) { + if (validateIfNecessary()) { + if (r >= getAccessibleRowCount() || + c >= getAccessibleColumnCount()) { + return -1; + } else { + return r * getAccessibleColumnCount() + c; + } + } + return -1; + } + + /** + * Returns the row header at a row in a table. + * @param r zero-based row of the table + * + * @return a String representing the row header + * if one exists; otherwise null. + */ + public String getAccessibleRowHeader(int r) { + if (validateIfNecessary()) { + TableCellElementInfo cellInfo = getCell(r, 0); + if (cellInfo.isHeaderCell()) { + View v = cellInfo.getView(); + if (v != null && model != null) { + try { + return model.getText(v.getStartOffset(), + v.getEndOffset() - + v.getStartOffset()); + } catch (BadLocationException e) { + return null; + } + } + } + } + return null; + } + + /** + * Returns the column header at a column in a table. + * @param c zero-based column of the table + * + * @return a String representing the column header + * if one exists; otherwise null. + */ + public String getAccessibleColumnHeader(int c) { + if (validateIfNecessary()) { + TableCellElementInfo cellInfo = getCell(0, c); + if (cellInfo.isHeaderCell()) { + View v = cellInfo.getView(); + if (v != null && model != null) { + try { + return model.getText(v.getStartOffset(), + v.getEndOffset() - + v.getStartOffset()); + } catch (BadLocationException e) { + return null; + } + } + } + } + return null; + } + + public void addRowHeader(TableCellElementInfo cellInfo, int rowNumber) { + if (rowHeadersTable == null) { + rowHeadersTable = new AccessibleHeadersTable(); + } + rowHeadersTable.addHeader(cellInfo, rowNumber); + } + // end of AccessibleExtendedTable implementation ------------ + + protected class AccessibleHeadersTable implements AccessibleTable { + + // Header information is modeled as a Hashtable of + // ArrayLists where each Hashtable entry represents + // a row containing one or more headers. + private Hashtable headers = new Hashtable(); + private int rowCount = 0; + private int columnCount = 0; + + public void addHeader(TableCellElementInfo cellInfo, int rowNumber) { + Integer rowInteger = new Integer(rowNumber); + ArrayList list = (ArrayList)headers.get(rowInteger); + if (list == null) { + list = new ArrayList(); + headers.put(rowInteger, list); + } + list.add(cellInfo); + } + + /** + * Returns the caption for the table. + * + * @return the caption for the table + */ + public Accessible getAccessibleCaption() { + return null; + } + + /** + * Sets the caption for the table. + * + * @param a the caption for the table + */ + public void setAccessibleCaption(Accessible a) { + } + + /** + * Returns the summary description of the table. + * + * @return the summary description of the table + */ + public Accessible getAccessibleSummary() { + return null; + } + + /** + * Sets the summary description of the table + * + * @param a the summary description of the table + */ + public void setAccessibleSummary(Accessible a) { + } + + /** + * Returns the number of rows in the table. + * + * @return the number of rows in the table + */ + public int getAccessibleRowCount() { + return rowCount; + } + + /** + * Returns the number of columns in the table. + * + * @return the number of columns in the table + */ + public int getAccessibleColumnCount() { + return columnCount; + } + + private TableCellElementInfo getElementInfoAt(int r, int c) { + ArrayList list = (ArrayList)headers.get(new Integer(r)); + if (list != null) { + return (TableCellElementInfo)list.get(c); + } else { + return null; + } + } + + /** + * Returns the Accessible at a specified row and column + * in the table. + * + * @param r zero-based row of the table + * @param c zero-based column of the table + * @return the Accessible at the specified row and column + */ + public Accessible getAccessibleAt(int r, int c) { + ElementInfo elementInfo = getElementInfoAt(r, c); + if (elementInfo instanceof Accessible) { + return (Accessible)elementInfo; + } else { + return null; + } + } + + /** + * Returns the number of rows occupied by the Accessible at + * a specified row and column in the table. + * + * @return the number of rows occupied by the Accessible at a + * given specified (row, column) + */ + public int getAccessibleRowExtentAt(int r, int c) { + TableCellElementInfo elementInfo = getElementInfoAt(r, c); + if (elementInfo != null) { + return elementInfo.getRowCount(); + } else { + return 0; + } + } + + /** + * Returns the number of columns occupied by the Accessible at + * a specified row and column in the table. + * + * @return the number of columns occupied by the Accessible at a + * given specified row and column + */ + public int getAccessibleColumnExtentAt(int r, int c) { + TableCellElementInfo elementInfo = getElementInfoAt(r, c); + if (elementInfo != null) { + return elementInfo.getRowCount(); + } else { + return 0; + } + } + + /** + * Returns the row headers as an AccessibleTable. + * + * @return an AccessibleTable representing the row + * headers + */ + public AccessibleTable getAccessibleRowHeader() { + return null; + } + + /** + * Sets the row headers. + * + * @param table an AccessibleTable representing the + * row headers + */ + public void setAccessibleRowHeader(AccessibleTable table) { + } + + /** + * Returns the column headers as an AccessibleTable. + * + * @return an AccessibleTable representing the column + * headers + */ + public AccessibleTable getAccessibleColumnHeader() { + return null; + } + + /** + * Sets the column headers. + * + * @param table an AccessibleTable representing the + * column headers + */ + public void setAccessibleColumnHeader(AccessibleTable table) { + } + + /** + * Returns the description of the specified row in the table. + * + * @param r zero-based row of the table + * @return the description of the row + */ + public Accessible getAccessibleRowDescription(int r) { + return null; + } + + /** + * Sets the description text of the specified row of the table. + * + * @param r zero-based row of the table + * @param a the description of the row + */ + public void setAccessibleRowDescription(int r, Accessible a) { + } + + /** + * Returns the description text of the specified column in the table. + * + * @param c zero-based column of the table + * @return the text description of the column + */ + public Accessible getAccessibleColumnDescription(int c) { + return null; + } + + /** + * Sets the description text of the specified column in the table. + * + * @param c zero-based column of the table + * @param a the text description of the column + */ + public void setAccessibleColumnDescription(int c, Accessible a) { + } + + /** + * Returns a boolean value indicating whether the accessible at + * a specified row and column is selected. + * + * @param r zero-based row of the table + * @param c zero-based column of the table + * @return the boolean value true if the accessible at the + * row and column is selected. Otherwise, the boolean value + * false + */ + public boolean isAccessibleSelected(int r, int c) { + return false; + } + + /** + * Returns a boolean value indicating whether the specified row + * is selected. + * + * @param r zero-based row of the table + * @return the boolean value true if the specified row is selected. + * Otherwise, false. + */ + public boolean isAccessibleRowSelected(int r) { + return false; + } + + /** + * Returns a boolean value indicating whether the specified column + * is selected. + * + * @param r zero-based column of the table + * @return the boolean value true if the specified column is selected. + * Otherwise, false. + */ + public boolean isAccessibleColumnSelected(int c) { + return false; + } + + /** + * Returns the selected rows in a table. + * + * @return an array of selected rows where each element is a + * zero-based row of the table + */ + public int [] getSelectedAccessibleRows() { + return new int [0]; + } + + /** + * Returns the selected columns in a table. + * + * @return an array of selected columns where each element is a + * zero-based column of the table + */ + public int [] getSelectedAccessibleColumns() { + return new int [0]; + } + } + } // ... end AccessibleHeadersTable + + /* + * ElementInfo for table rows + */ + private class TableRowElementInfo extends ElementInfo { + + private TableElementInfo parent; + private int rowNumber; + + TableRowElementInfo(Element e, TableElementInfo parent, int rowNumber) { + super(e, parent); + this.parent = parent; + this.rowNumber = rowNumber; + } + + protected void loadChildren(Element e) { + for (int x = 0; x < e.getElementCount(); x++) { + AttributeSet attrs = e.getElement(x).getAttributes(); + + if (attrs.getAttribute(StyleConstants.NameAttribute) == + HTML.Tag.TH) { + TableCellElementInfo headerElementInfo = + new TableCellElementInfo(e.getElement(x), this, true); + addChild(headerElementInfo); + + AccessibleTable at = + parent.getAccessibleContext().getAccessibleTable(); + TableAccessibleContext tableElement = + (TableAccessibleContext)at; + tableElement.addRowHeader(headerElementInfo, rowNumber); + + } else if (attrs.getAttribute(StyleConstants.NameAttribute) == + HTML.Tag.TD) { + addChild(new TableCellElementInfo(e.getElement(x), this, + false)); + } + } + } + + /** + * Returns the max of the rowspans of the cells in this row. + */ + public int getRowCount() { + int rowCount = 1; + if (validateIfNecessary()) { + for (int counter = 0; counter < getChildCount(); + counter++) { + + TableCellElementInfo cell = (TableCellElementInfo) + getChild(counter); + + if (cell.validateIfNecessary()) { + rowCount = Math.max(rowCount, cell.getRowCount()); + } + } + } + return rowCount; + } + + /** + * Returns the sum of the column spans of the individual + * cells in this row. + */ + public int getColumnCount() { + int colCount = 0; + if (validateIfNecessary()) { + for (int counter = 0; counter < getChildCount(); + counter++) { + TableCellElementInfo cell = (TableCellElementInfo) + getChild(counter); + + if (cell.validateIfNecessary()) { + colCount += cell.getColumnCount(); + } + } + } + return colCount; + } + + /** + * Overriden to invalidate the table as well as + * TableRowElementInfo. + */ + protected void invalidate(boolean first) { + super.invalidate(first); + getParent().invalidate(true); + } + + /** + * Places the TableCellElementInfos for this element in + * the grid. + */ + private void updateGrid(int row) { + if (validateIfNecessary()) { + boolean emptyRow = false; + + while (!emptyRow) { + for (int counter = 0; counter < grid[row].length; + counter++) { + if (grid[row][counter] == null) { + emptyRow = true; + break; + } + } + if (!emptyRow) { + row++; + } + } + for (int col = 0, counter = 0; counter < getChildCount(); + counter++) { + TableCellElementInfo cell = (TableCellElementInfo) + getChild(counter); + + while (grid[row][col] != null) { + col++; + } + for (int rowCount = cell.getRowCount() - 1; + rowCount >= 0; rowCount--) { + for (int colCount = cell.getColumnCount() - 1; + colCount >= 0; colCount--) { + grid[row + rowCount][col + colCount] = cell; + } + } + col += cell.getColumnCount(); + } + } + } + + /** + * Returns the column count of the number of columns that have + * a rowcount >= rowspan. + */ + private int getColumnCount(int rowspan) { + if (validateIfNecessary()) { + int cols = 0; + for (int counter = 0; counter < getChildCount(); + counter++) { + TableCellElementInfo cell = (TableCellElementInfo) + getChild(counter); + + if (cell.getRowCount() >= rowspan) { + cols += cell.getColumnCount(); + } + } + return cols; + } + return 0; + } + } + + /** + * TableCellElementInfo is used to represents the cells of + * the table. + */ + private class TableCellElementInfo extends ElementInfo { + + private Accessible accessible; + private boolean isHeaderCell; + + TableCellElementInfo(Element e, ElementInfo parent) { + super(e, parent); + this.isHeaderCell = false; + } + + TableCellElementInfo(Element e, ElementInfo parent, + boolean isHeaderCell) { + super(e, parent); + this.isHeaderCell = isHeaderCell; + } + + /* + * Returns whether this table cell is a header + */ + public boolean isHeaderCell() { + return this.isHeaderCell; + } + + /* + * Returns the Accessible representing this table cell + */ + public Accessible getAccessible() { + accessible = null; + getAccessible(this); + return accessible; + } + + /* + * Gets the outermost Accessible in the table cell + */ + private void getAccessible(ElementInfo elementInfo) { + if (elementInfo instanceof Accessible) { + accessible = (Accessible)elementInfo; + return; + } else { + for (int i = 0; i < elementInfo.getChildCount(); i++) { + getAccessible(elementInfo.getChild(i)); + } + } + } + + /** + * Returns the rowspan attribute. + */ + public int getRowCount() { + if (validateIfNecessary()) { + return Math.max(1, getIntAttr(getAttributes(), + HTML.Attribute.ROWSPAN, 1)); + } + return 0; + } + + /** + * Returns the colspan attribute. + */ + public int getColumnCount() { + if (validateIfNecessary()) { + return Math.max(1, getIntAttr(getAttributes(), + HTML.Attribute.COLSPAN, 1)); + } + return 0; + } + + /** + * Overriden to invalidate the TableRowElementInfo as well as + * the TableCellElementInfo. + */ + protected void invalidate(boolean first) { + super.invalidate(first); + getParent().invalidate(true); + } + } + } + + + /** + * ElementInfo provides a slim down view of an Element. Each ElementInfo + * can have any number of child ElementInfos that are not necessarily + * direct children of the Element. As the Document changes various + * ElementInfos become invalidated. Before accessing a particular portion + * of an ElementInfo you should make sure it is valid by invoking + * validateIfNecessary, this will return true if + * successful, on the other hand a false return value indicates the + * ElementInfo is not valid and can never become valid again (usually + * the result of the Element the ElementInfo encapsulates being removed). + */ + private class ElementInfo { + + /** + * The children of this ElementInfo. + */ + private ArrayList children; + /** + * The Element this ElementInfo is providing information for. + */ + private Element element; + /** + * The parent ElementInfo, will be null for the root. + */ + private ElementInfo parent; + /** + * Indicates the validity of the ElementInfo. + */ + private boolean isValid; + /** + * Indicates if the ElementInfo can become valid. + */ + private boolean canBeValid; + + + /** + * Creates the root ElementInfo. + */ + ElementInfo(Element element) { + this(element, null); + } + + /** + * Creates an ElementInfo representing element with + * the specified parent. + */ + ElementInfo(Element element, ElementInfo parent) { + this.element = element; + this.parent = parent; + isValid = false; + canBeValid = true; + } + + /** + * Validates the receiver. This recreates the children as well. This + * will be invoked within a readLock. If this is overriden + * it MUST invoke supers implementation first! + */ + protected void validate() { + isValid = true; + loadChildren(getElement()); + } + + /** + * Recreates the direct children of info. + */ + protected void loadChildren(Element parent) { + if (!parent.isLeaf()) { + for (int counter = 0, maxCounter = parent.getElementCount(); + counter < maxCounter; counter++) { + Element e = parent.getElement(counter); + ElementInfo childInfo = createElementInfo(e, this); + + if (childInfo != null) { + addChild(childInfo); + } + else { + loadChildren(e); + } + } + } + } + + /** + * Returns the index of the child in the parent, or -1 for the + * root or if the parent isn't valid. + */ + public int getIndexInParent() { + if (parent == null || !parent.isValid()) { + return -1; + } + return parent.indexOf(this); + } + + /** + * Returns the Element this ElementInfo represents. + */ + public Element getElement() { + return element; + } + + /** + * Returns the parent of this Element, or null for the root. + */ + public ElementInfo getParent() { + return parent; + } + + /** + * Returns the index of the specified child, or -1 if + * child isn't a valid child. + */ + public int indexOf(ElementInfo child) { + ArrayList children = this.children; + + if (children != null) { + return children.indexOf(child); + } + return -1; + } + + /** + * Returns the child ElementInfo at index, or null + * if index isn't a valid index. + */ + public ElementInfo getChild(int index) { + if (validateIfNecessary()) { + ArrayList children = this.children; + + if (children != null && index >= 0 && + index < children.size()) { + return (ElementInfo)children.get(index); + } + } + return null; + } + + /** + * Returns the number of children the ElementInfo contains. + */ + public int getChildCount() { + validateIfNecessary(); + return (children == null) ? 0 : children.size(); + } + + /** + * Adds a new child to this ElementInfo. + */ + protected void addChild(ElementInfo child) { + if (children == null) { + children = new ArrayList(); + } + children.add(child); + } + + /** + * Returns the View corresponding to this ElementInfo, or null + * if the ElementInfo can't be validated. + */ + protected View getView() { + if (!validateIfNecessary()) { + return null; + } + Object lock = lock(); + try { + View rootView = getRootView(); + Element e = getElement(); + int start = e.getStartOffset(); + + if (rootView != null) { + return getView(rootView, e, start); + } + return null; + } finally { + unlock(lock); + } + } + + /** + * Returns the Bounds for this ElementInfo, or null + * if the ElementInfo can't be validated. + */ + public Rectangle getBounds() { + if (!validateIfNecessary()) { + return null; + } + Object lock = lock(); + try { + Rectangle bounds = getRootEditorRect(); + View rootView = getRootView(); + Element e = getElement(); + + if (bounds != null && rootView != null) { + try { + return rootView.modelToView(e.getStartOffset(), + Position.Bias.Forward, + e.getEndOffset(), + Position.Bias.Backward, + bounds).getBounds(); + } catch (BadLocationException ble) { } + } + } finally { + unlock(lock); + } + return null; + } + + /** + * Returns true if this ElementInfo is valid. + */ + protected boolean isValid() { + return isValid; + } + + /** + * Returns the AttributeSet associated with the Element, this will + * return null if the ElementInfo can't be validated. + */ + protected AttributeSet getAttributes() { + if (validateIfNecessary()) { + return getElement().getAttributes(); + } + return null; + } + + /** + * Returns the AttributeSet associated with the View that is + * representing this Element, this will + * return null if the ElementInfo can't be validated. + */ + protected AttributeSet getViewAttributes() { + if (validateIfNecessary()) { + View view = getView(); + + if (view != null) { + return view.getElement().getAttributes(); + } + return getElement().getAttributes(); + } + return null; + } + + /** + * Convenience method for getting an integer attribute from the passed + * in AttributeSet. + */ + protected int getIntAttr(AttributeSet attrs, Object key, int deflt) { + if (attrs != null && attrs.isDefined(key)) { + int i; + String val = (String)attrs.getAttribute(key); + if (val == null) { + i = deflt; + } + else { + try { + i = Math.max(0, Integer.parseInt(val)); + } catch (NumberFormatException x) { + i = deflt; + } + } + return i; + } + return deflt; + } + + /** + * Validates the ElementInfo if necessary. Some ElementInfos may + * never be valid again. You should check isValid before + * using one. This will reload the children and invoke + * validate if the ElementInfo is invalid and can become + * valid again. This will return true if the receiver is valid. + */ + protected boolean validateIfNecessary() { + if (!isValid() && canBeValid) { + children = null; + Object lock = lock(); + + try { + validate(); + } finally { + unlock(lock); + } + } + return isValid(); + } + + /** + * Invalidates the ElementInfo. Subclasses should override this + * if they need to reset state once invalid. + */ + protected void invalidate(boolean first) { + if (!isValid()) { + if (canBeValid && !first) { + canBeValid = false; + } + return; + } + isValid = false; + canBeValid = first; + if (children != null) { + for (int counter = 0; counter < children.size(); counter++) { + ((ElementInfo)children.get(counter)).invalidate(false); + } + children = null; + } + } + + private View getView(View parent, Element e, int start) { + if (parent.getElement() == e) { + return parent; + } + int index = parent.getViewIndex(start, Position.Bias.Forward); + + if (index != -1 && index < parent.getViewCount()) { + return getView(parent.getView(index), e, start); + } + return null; + } + + private int getClosestInfoIndex(int index) { + for (int counter = 0; counter < getChildCount(); counter++) { + ElementInfo info = getChild(counter); + + if (index < info.getElement().getEndOffset() || + index == info.getElement().getStartOffset()) { + return counter; + } + } + return -1; + } + + private void update(DocumentEvent e) { + if (!isValid()) { + return; + } + ElementInfo parent = getParent(); + Element element = getElement(); + + do { + DocumentEvent.ElementChange ec = e.getChange(element); + if (ec != null) { + if (element == getElement()) { + // One of our children changed. + invalidate(true); + } + else if (parent != null) { + parent.invalidate(parent == getRootInfo()); + } + return; + } + element = element.getParentElement(); + } while (parent != null && element != null && + element != parent.getElement()); + + if (getChildCount() > 0) { + Element elem = getElement(); + int pos = e.getOffset(); + int index0 = getClosestInfoIndex(pos); + if (index0 == -1 && + e.getType() == DocumentEvent.EventType.REMOVE && + pos >= elem.getEndOffset()) { + // Event beyond our offsets. We may have represented this, + // that is the remove may have removed one of our child + // Elements that represented this, so, we should foward + // to last element. + index0 = getChildCount() - 1; + } + ElementInfo info = (index0 >= 0) ? getChild(index0) : null; + if (info != null && + (info.getElement().getStartOffset() == pos) && (pos > 0)) { + // If at a boundary, forward the event to the previous + // ElementInfo too. + index0 = Math.max(index0 - 1, 0); + } + int index1; + if (e.getType() != DocumentEvent.EventType.REMOVE) { + index1 = getClosestInfoIndex(pos + e.getLength()); + if (index1 < 0) { + index1 = getChildCount() - 1; + } + } + else { + index1 = index0; + // A remove may result in empty elements. + while ((index1 + 1) < getChildCount() && + getChild(index1 + 1).getElement().getEndOffset() == + getChild(index1 + 1).getElement().getStartOffset()){ + index1++; + } + } + index0 = Math.max(index0, 0); + // The check for isValid is here as in the process of + // forwarding update our child may invalidate us. + for (int i = index0; i <= index1 && isValid(); i++) { + getChild(i).update(e); + } + } + } + } + + /** + * DocumentListener installed on the current Document. Will invoke + * update on the RootInfo in response to + * any event. + */ + private class DocumentHandler implements DocumentListener { + public void insertUpdate(DocumentEvent e) { + getRootInfo().update(e); + } + public void removeUpdate(DocumentEvent e) { + getRootInfo().update(e); + } + public void changedUpdate(DocumentEvent e) { + getRootInfo().update(e); + } + } + + /* + * PropertyChangeListener installed on the editor. + */ + private class PropertyChangeHandler implements PropertyChangeListener { + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals("document")) { + // handle the document change + setDocument(editor.getDocument()); + } + } + } +} diff --git a/src/share/classes/javax/swing/text/html/BRView.java b/src/share/classes/javax/swing/text/html/BRView.java new file mode 100644 index 000000000..77aa095c5 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/BRView.java @@ -0,0 +1,57 @@ +/* + * Copyright 1998-2004 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import javax.swing.text.*; + +/** + * Processes the <BR> tag. In other words, forces a line break. + * + * @author Sunita Mani + */ +class BRView extends InlineView { + + /** + * Creates a new view that represents a <BR> element. + * + * @param elem the element to create a view for + */ + public BRView(Element elem) { + super(elem); + } + + /** + * Forces a line break. + * + * @return View.ForcedBreakWeight + */ + public int getBreakWeight(int axis, float pos, float len) { + if (axis == X_AXIS) { + return ForcedBreakWeight; + } else { + return super.getBreakWeight(axis, pos, len); + } + } +} diff --git a/src/share/classes/javax/swing/text/html/BlockView.java b/src/share/classes/javax/swing/text/html/BlockView.java new file mode 100644 index 000000000..8e453bddd --- /dev/null +++ b/src/share/classes/javax/swing/text/html/BlockView.java @@ -0,0 +1,443 @@ +/* + * Copyright 1997-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.util.Enumeration; +import java.awt.*; +import javax.swing.SizeRequirements; +import javax.swing.border.*; +import javax.swing.event.DocumentEvent; +import javax.swing.text.*; + +/** + * A view implementation to display a block (as a box) + * with CSS specifications. + * + * @author Timothy Prinzing + */ +public class BlockView extends BoxView { + + /** + * Creates a new view that represents an + * html box. This can be used for a number + * of elements. + * + * @param elem the element to create a view for + * @param axis either View.X_AXIS or View.Y_AXIS + */ + public BlockView(Element elem, int axis) { + super(elem, axis); + } + + /** + * Establishes the parent view for this view. This is + * guaranteed to be called before any other methods if the + * parent view is functioning properly. + *

+ * This is implemented + * to forward to the superclass as well as call the + * {@link #setPropertiesFromAttributes()} + * method to set the paragraph properties from the css + * attributes. The call is made at this time to ensure + * the ability to resolve upward through the parents + * view attributes. + * + * @param parent the new parent, or null if the view is + * being removed from a parent it was previously added + * to + */ + public void setParent(View parent) { + super.setParent(parent); + if (parent != null) { + setPropertiesFromAttributes(); + } + } + + /** + * Calculate the requirements of the block along the major + * axis (i.e. the axis along with it tiles). This is implemented + * to provide the superclass behavior and then adjust it if the + * CSS width or height attribute is specified and applicable to + * the axis. + */ + protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) { + if (r == null) { + r = new SizeRequirements(); + } + if (! spanSetFromAttributes(axis, r, cssWidth, cssHeight)) { + r = super.calculateMajorAxisRequirements(axis, r); + } + else { + // Offset by the margins so that pref/min/max return the + // right value. + SizeRequirements parentR = super.calculateMajorAxisRequirements( + axis, null); + int margin = (axis == X_AXIS) ? getLeftInset() + getRightInset() : + getTopInset() + getBottomInset(); + r.minimum -= margin; + r.preferred -= margin; + r.maximum -= margin; + constrainSize(axis, r, parentR); + } + return r; + } + + /** + * Calculate the requirements of the block along the minor + * axis (i.e. the axis orthoginal to the axis along with it tiles). + * This is implemented + * to provide the superclass behavior and then adjust it if the + * CSS width or height attribute is specified and applicable to + * the axis. + */ + protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { + if (r == null) { + r = new SizeRequirements(); + } + + if (! spanSetFromAttributes(axis, r, cssWidth, cssHeight)) { + + /* + * The requirements were not directly specified by attributes, so + * compute the aggregate of the requirements of the children. The + * children that have a percentage value specified will be treated + * as completely stretchable since that child is not limited in any + * way. + */ +/* + int min = 0; + long pref = 0; + int max = 0; + int n = getViewCount(); + for (int i = 0; i < n; i++) { + View v = getView(i); + min = Math.max((int) v.getMinimumSpan(axis), min); + pref = Math.max((int) v.getPreferredSpan(axis), pref); + if ( + max = Math.max((int) v.getMaximumSpan(axis), max); + + } + r.preferred = (int) pref; + r.minimum = min; + r.maximum = max; + */ + r = super.calculateMinorAxisRequirements(axis, r); + } + else { + // Offset by the margins so that pref/min/max return the + // right value. + SizeRequirements parentR = super.calculateMinorAxisRequirements( + axis, null); + int margin = (axis == X_AXIS) ? getLeftInset() + getRightInset() : + getTopInset() + getBottomInset(); + r.minimum -= margin; + r.preferred -= margin; + r.maximum -= margin; + constrainSize(axis, r, parentR); + } + + /* + * Set the alignment based upon the CSS properties if it is + * specified. For X_AXIS this would be text-align, for + * Y_AXIS this would be vertical-align. + */ + if (axis == X_AXIS) { + Object o = getAttributes().getAttribute(CSS.Attribute.TEXT_ALIGN); + if (o != null) { + String align = o.toString(); + if (align.equals("center")) { + r.alignment = 0.5f; + } else if (align.equals("right")) { + r.alignment = 1.0f; + } else { + r.alignment = 0.0f; + } + } + } + // Y_AXIS TBD + return r; + } + + boolean isPercentage(int axis, AttributeSet a) { + if (axis == X_AXIS) { + if (cssWidth != null) { + return cssWidth.isPercentage(); + } + } else { + if (cssHeight != null) { + return cssHeight.isPercentage(); + } + } + return false; + } + + /** + * Adjust the given requirements to the CSS width or height if + * it is specified along the applicable axis. Return true if the + * size is exactly specified, false if the span is not specified + * in an attribute or the size specified is a percentage. + */ + static boolean spanSetFromAttributes(int axis, SizeRequirements r, + CSS.LengthValue cssWidth, + CSS.LengthValue cssHeight) { + if (axis == X_AXIS) { + if ((cssWidth != null) && (! cssWidth.isPercentage())) { + r.minimum = r.preferred = r.maximum = (int) cssWidth.getValue(); + return true; + } + } else { + if ((cssHeight != null) && (! cssHeight.isPercentage())) { + r.minimum = r.preferred = r.maximum = (int) cssHeight.getValue(); + return true; + } + } + return false; + } + + /** + * Performs layout for the minor axis of the box (i.e. the + * axis orthoginal to the axis that it represents). The results + * of the layout (the offset and span for each children) are + * placed in the given arrays which represent the allocations to + * the children along the minor axis. + * + * @param targetSpan the total span given to the view, which + * whould be used to layout the childre. + * @param axis the axis being layed out + * @param offsets the offsets from the origin of the view for + * each of the child views; this is a return value and is + * filled in by the implementation of this method + * @param spans the span of each child view; this is a return + * value and is filled in by the implementation of this method + */ + protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { + int n = getViewCount(); + Object key = (axis == X_AXIS) ? CSS.Attribute.WIDTH : CSS.Attribute.HEIGHT; + for (int i = 0; i < n; i++) { + View v = getView(i); + int min = (int) v.getMinimumSpan(axis); + int max; + + // check for percentage span + AttributeSet a = v.getAttributes(); + CSS.LengthValue lv = (CSS.LengthValue) a.getAttribute(key); + if ((lv != null) && lv.isPercentage()) { + // bound the span to the percentage specified + min = Math.max((int) lv.getValue(targetSpan), min); + max = min; + } else { + max = (int)v.getMaximumSpan(axis); + } + + // assign the offset and span for the child + if (max < targetSpan) { + // can't make the child this wide, align it + float align = v.getAlignment(axis); + offsets[i] = (int) ((targetSpan - max) * align); + spans[i] = max; + } else { + // make it the target width, or as small as it can get. + offsets[i] = 0; + spans[i] = Math.max(min, targetSpan); + } + } + } + + + /** + * Renders using the given rendering surface and area on that + * surface. This is implemented to delegate to the css box + * painter to paint the border and background prior to the + * interior. + * + * @param g the rendering surface to use + * @param allocation the allocated region to render into + * @see View#paint + */ + public void paint(Graphics g, Shape allocation) { + Rectangle a = (Rectangle) allocation; + painter.paint(g, a.x, a.y, a.width, a.height, this); + super.paint(g, a); + } + + /** + * Fetches the attributes to use when rendering. This is + * implemented to multiplex the attributes specified in the + * model with a StyleSheet. + */ + public AttributeSet getAttributes() { + if (attr == null) { + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + } + return attr; + } + + /** + * Gets the resize weight. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the weight + * @exception IllegalArgumentException for an invalid axis + */ + public int getResizeWeight(int axis) { + switch (axis) { + case View.X_AXIS: + return 1; + case View.Y_AXIS: + return 0; + default: + throw new IllegalArgumentException("Invalid axis: " + axis); + } + } + + /** + * Gets the alignment. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the alignment + */ + public float getAlignment(int axis) { + switch (axis) { + case View.X_AXIS: + return 0; + case View.Y_AXIS: + if (getViewCount() == 0) { + return 0; + } + float span = getPreferredSpan(View.Y_AXIS); + View v = getView(0); + float above = v.getPreferredSpan(View.Y_AXIS); + float a = (((int)span) != 0) ? (above * v.getAlignment(View.Y_AXIS)) / span: 0; + return a; + default: + throw new IllegalArgumentException("Invalid axis: " + axis); + } + } + + public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) { + super.changedUpdate(changes, a, f); + int pos = changes.getOffset(); + if (pos <= getStartOffset() && (pos + changes.getLength()) >= + getEndOffset()) { + setPropertiesFromAttributes(); + } + } + + /** + * Determines the preferred span for this view along an + * axis. + * + * @param axis may be either View.X_AXIS + * or View.Y_AXIS + * @return the span the view would like to be rendered into >= 0; + * typically the view is told to render into the span + * that is returned, although there is no guarantee; + * the parent may choose to resize or break the view + * @exception IllegalArgumentException for an invalid axis type + */ + public float getPreferredSpan(int axis) { + return super.getPreferredSpan(axis); + } + + /** + * Determines the minimum span for this view along an + * axis. + * + * @param axis may be either View.X_AXIS + * or View.Y_AXIS + * @return the span the view would like to be rendered into >= 0; + * typically the view is told to render into the span + * that is returned, although there is no guarantee; + * the parent may choose to resize or break the view + * @exception IllegalArgumentException for an invalid axis type + */ + public float getMinimumSpan(int axis) { + return super.getMinimumSpan(axis); + } + + /** + * Determines the maximum span for this view along an + * axis. + * + * @param axis may be either View.X_AXIS + * or View.Y_AXIS + * @return the span the view would like to be rendered into >= 0; + * typically the view is told to render into the span + * that is returned, although there is no guarantee; + * the parent may choose to resize or break the view + * @exception IllegalArgumentException for an invalid axis type + */ + public float getMaximumSpan(int axis) { + return super.getMaximumSpan(axis); + } + + /** + * Update any cached values that come from attributes. + */ + protected void setPropertiesFromAttributes() { + + // update attributes + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + + // Reset the painter + painter = sheet.getBoxPainter(attr); + if (attr != null) { + setInsets((short) painter.getInset(TOP, this), + (short) painter.getInset(LEFT, this), + (short) painter.getInset(BOTTOM, this), + (short) painter.getInset(RIGHT, this)); + } + + // Get the width/height + cssWidth = (CSS.LengthValue) attr.getAttribute(CSS.Attribute.WIDTH); + cssHeight = (CSS.LengthValue) attr.getAttribute(CSS.Attribute.HEIGHT); + } + + protected StyleSheet getStyleSheet() { + HTMLDocument doc = (HTMLDocument) getDocument(); + return doc.getStyleSheet(); + } + + /** + * Constrains want to fit in the minimum size specified + * by min. + */ + private void constrainSize(int axis, SizeRequirements want, + SizeRequirements min) { + if (min.minimum > want.minimum) { + want.minimum = want.preferred = min.minimum; + want.maximum = Math.max(want.maximum, min.maximum); + } + } + + private AttributeSet attr; + private StyleSheet.BoxPainter painter; + + private CSS.LengthValue cssWidth; + private CSS.LengthValue cssHeight; + +} diff --git a/src/share/classes/javax/swing/text/html/CSS.java b/src/share/classes/javax/swing/text/html/CSS.java new file mode 100644 index 000000000..40b5977e0 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/CSS.java @@ -0,0 +1,3383 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.Color; +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.awt.Toolkit; +import java.awt.HeadlessException; +import java.awt.Image; +import java.io.*; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.MalformedURLException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; +import java.util.Locale; +import javax.swing.ImageIcon; +import javax.swing.SizeRequirements; +import javax.swing.text.*; + +/** + * Defines a set of + * CSS attributes + * as a typesafe enumeration. The HTML View implementations use + * CSS attributes to determine how they will render. This also defines + * methods to map between CSS/HTML/StyleConstants. Any shorthand + * properties, such as font, are mapped to the intrinsic properties. + *

The following describes the CSS properties that are suppored by the + * rendering engine: + *

+ * The following are modeled, but currently not rendered. + * + *

Note: for the time being we do not fully support relative units, + * unless noted, so that + * p { margin-top: 10% } will be treated as if no margin-top was specified. + * + * @author Timothy Prinzing + * @author Scott Violet + * @see StyleSheet + */ +public class CSS implements Serializable { + + /** + * Definitions to be used as a key on AttributeSet's + * that might hold CSS attributes. Since this is a + * closed set (i.e. defined exactly by the specification), + * it is final and cannot be extended. + */ + public static final class Attribute { + + private Attribute(String name, String defaultValue, boolean inherited) { + this.name = name; + this.defaultValue = defaultValue; + this.inherited = inherited; + } + + /** + * The string representation of the attribute. This + * should exactly match the string specified in the + * CSS specification. + */ + public String toString() { + return name; + } + + /** + * Fetch the default value for the attribute. + * If there is no default value (such as for + * composite attributes), null will be returned. + */ + public String getDefaultValue() { + return defaultValue; + } + + /** + * Indicates if the attribute should be inherited + * from the parent or not. + */ + public boolean isInherited() { + return inherited; + } + + private String name; + private String defaultValue; + private boolean inherited; + + + public static final Attribute BACKGROUND = + new Attribute("background", null, false); + + public static final Attribute BACKGROUND_ATTACHMENT = + new Attribute("background-attachment", "scroll", false); + + public static final Attribute BACKGROUND_COLOR = + new Attribute("background-color", "transparent", false); + + public static final Attribute BACKGROUND_IMAGE = + new Attribute("background-image", "none", false); + + public static final Attribute BACKGROUND_POSITION = + new Attribute("background-position", null, false); + + public static final Attribute BACKGROUND_REPEAT = + new Attribute("background-repeat", "repeat", false); + + public static final Attribute BORDER = + new Attribute("border", null, false); + + public static final Attribute BORDER_BOTTOM = + new Attribute("border-bottom", null, false); + + public static final Attribute BORDER_BOTTOM_COLOR = + new Attribute("border-bottom-color", null, false); + + public static final Attribute BORDER_BOTTOM_STYLE = + new Attribute("border-bottom-style", "none", false); + + public static final Attribute BORDER_BOTTOM_WIDTH = + new Attribute("border-bottom-width", "medium", false); + + public static final Attribute BORDER_COLOR = + new Attribute("border-color", null, false); + + public static final Attribute BORDER_LEFT = + new Attribute("border-left", null, false); + + public static final Attribute BORDER_LEFT_COLOR = + new Attribute("border-left-color", null, false); + + public static final Attribute BORDER_LEFT_STYLE = + new Attribute("border-left-style", "none", false); + + public static final Attribute BORDER_LEFT_WIDTH = + new Attribute("border-left-width", "medium", false); + + public static final Attribute BORDER_RIGHT = + new Attribute("border-right", null, false); + + public static final Attribute BORDER_RIGHT_COLOR = + new Attribute("border-right-color", null, false); + + public static final Attribute BORDER_RIGHT_STYLE = + new Attribute("border-right-style", "none", false); + + public static final Attribute BORDER_RIGHT_WIDTH = + new Attribute("border-right-width", "medium", false); + + public static final Attribute BORDER_STYLE = + new Attribute("border-style", "none", false); + + public static final Attribute BORDER_TOP = + new Attribute("border-top", null, false); + + public static final Attribute BORDER_TOP_COLOR = + new Attribute("border-top-color", null, false); + + public static final Attribute BORDER_TOP_STYLE = + new Attribute("border-top-style", "none", false); + + public static final Attribute BORDER_TOP_WIDTH = + new Attribute("border-top-width", "medium", false); + + public static final Attribute BORDER_WIDTH = + new Attribute("border-width", "medium", false); + + public static final Attribute CLEAR = + new Attribute("clear", "none", false); + + public static final Attribute COLOR = + new Attribute("color", "black", true); + + public static final Attribute DISPLAY = + new Attribute("display", "block", false); + + public static final Attribute FLOAT = + new Attribute("float", "none", false); + + public static final Attribute FONT = + new Attribute("font", null, true); + + public static final Attribute FONT_FAMILY = + new Attribute("font-family", null, true); + + public static final Attribute FONT_SIZE = + new Attribute("font-size", "medium", true); + + public static final Attribute FONT_STYLE = + new Attribute("font-style", "normal", true); + + public static final Attribute FONT_VARIANT = + new Attribute("font-variant", "normal", true); + + public static final Attribute FONT_WEIGHT = + new Attribute("font-weight", "normal", true); + + public static final Attribute HEIGHT = + new Attribute("height", "auto", false); + + public static final Attribute LETTER_SPACING = + new Attribute("letter-spacing", "normal", true); + + public static final Attribute LINE_HEIGHT = + new Attribute("line-height", "normal", true); + + public static final Attribute LIST_STYLE = + new Attribute("list-style", null, true); + + public static final Attribute LIST_STYLE_IMAGE = + new Attribute("list-style-image", "none", true); + + public static final Attribute LIST_STYLE_POSITION = + new Attribute("list-style-position", "outside", true); + + public static final Attribute LIST_STYLE_TYPE = + new Attribute("list-style-type", "disc", true); + + public static final Attribute MARGIN = + new Attribute("margin", null, false); + + public static final Attribute MARGIN_BOTTOM = + new Attribute("margin-bottom", "0", false); + + public static final Attribute MARGIN_LEFT = + new Attribute("margin-left", "0", false); + + public static final Attribute MARGIN_RIGHT = + new Attribute("margin-right", "0", false); + + /* + * made up css attributes to describe orientation depended + * margins. used for

, ,
    etc. see + * 5088268 for more details + */ + static final Attribute MARGIN_LEFT_LTR = + new Attribute("margin-left-ltr", + Integer.toString(Integer.MIN_VALUE), false); + + static final Attribute MARGIN_LEFT_RTL = + new Attribute("margin-left-rtl", + Integer.toString(Integer.MIN_VALUE), false); + + static final Attribute MARGIN_RIGHT_LTR = + new Attribute("margin-right-ltr", + Integer.toString(Integer.MIN_VALUE), false); + + static final Attribute MARGIN_RIGHT_RTL = + new Attribute("margin-right-rtl", + Integer.toString(Integer.MIN_VALUE), false); + + + public static final Attribute MARGIN_TOP = + new Attribute("margin-top", "0", false); + + public static final Attribute PADDING = + new Attribute("padding", null, false); + + public static final Attribute PADDING_BOTTOM = + new Attribute("padding-bottom", "0", false); + + public static final Attribute PADDING_LEFT = + new Attribute("padding-left", "0", false); + + public static final Attribute PADDING_RIGHT = + new Attribute("padding-right", "0", false); + + public static final Attribute PADDING_TOP = + new Attribute("padding-top", "0", false); + + public static final Attribute TEXT_ALIGN = + new Attribute("text-align", null, true); + + public static final Attribute TEXT_DECORATION = + new Attribute("text-decoration", "none", true); + + public static final Attribute TEXT_INDENT = + new Attribute("text-indent", "0", true); + + public static final Attribute TEXT_TRANSFORM = + new Attribute("text-transform", "none", true); + + public static final Attribute VERTICAL_ALIGN = + new Attribute("vertical-align", "baseline", false); + + public static final Attribute WORD_SPACING = + new Attribute("word-spacing", "normal", true); + + public static final Attribute WHITE_SPACE = + new Attribute("white-space", "normal", true); + + public static final Attribute WIDTH = + new Attribute("width", "auto", false); + + /*public*/ static final Attribute BORDER_SPACING = + new Attribute("border-spacing", "0", true); + + /*public*/ static final Attribute CAPTION_SIDE = + new Attribute("caption-side", "left", true); + + // All possible CSS attribute keys. + static final Attribute[] allAttributes = { + BACKGROUND, BACKGROUND_ATTACHMENT, BACKGROUND_COLOR, + BACKGROUND_IMAGE, BACKGROUND_POSITION, BACKGROUND_REPEAT, + BORDER, BORDER_BOTTOM, BORDER_BOTTOM_WIDTH, BORDER_COLOR, + BORDER_LEFT, BORDER_LEFT_WIDTH, BORDER_RIGHT, BORDER_RIGHT_WIDTH, + BORDER_STYLE, BORDER_TOP, BORDER_TOP_WIDTH, BORDER_WIDTH, + BORDER_TOP_STYLE, BORDER_RIGHT_STYLE, BORDER_BOTTOM_STYLE, + BORDER_LEFT_STYLE, + BORDER_TOP_COLOR, BORDER_RIGHT_COLOR, BORDER_BOTTOM_COLOR, + BORDER_LEFT_COLOR, + CLEAR, COLOR, DISPLAY, FLOAT, FONT, FONT_FAMILY, FONT_SIZE, + FONT_STYLE, FONT_VARIANT, FONT_WEIGHT, HEIGHT, LETTER_SPACING, + LINE_HEIGHT, LIST_STYLE, LIST_STYLE_IMAGE, LIST_STYLE_POSITION, + LIST_STYLE_TYPE, MARGIN, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, + MARGIN_TOP, PADDING, PADDING_BOTTOM, PADDING_LEFT, PADDING_RIGHT, + PADDING_TOP, TEXT_ALIGN, TEXT_DECORATION, TEXT_INDENT, TEXT_TRANSFORM, + VERTICAL_ALIGN, WORD_SPACING, WHITE_SPACE, WIDTH, + BORDER_SPACING, CAPTION_SIDE, + MARGIN_LEFT_LTR, MARGIN_LEFT_RTL, MARGIN_RIGHT_LTR, MARGIN_RIGHT_RTL + }; + + private static final Attribute[] ALL_MARGINS = + { MARGIN_TOP, MARGIN_RIGHT, MARGIN_BOTTOM, MARGIN_LEFT }; + private static final Attribute[] ALL_PADDING = + { PADDING_TOP, PADDING_RIGHT, PADDING_BOTTOM, PADDING_LEFT }; + private static final Attribute[] ALL_BORDER_WIDTHS = + { BORDER_TOP_WIDTH, BORDER_RIGHT_WIDTH, BORDER_BOTTOM_WIDTH, + BORDER_LEFT_WIDTH }; + private static final Attribute[] ALL_BORDER_STYLES = + { BORDER_TOP_STYLE, BORDER_RIGHT_STYLE, BORDER_BOTTOM_STYLE, + BORDER_LEFT_STYLE }; + private static final Attribute[] ALL_BORDER_COLORS = + { BORDER_TOP_COLOR, BORDER_RIGHT_COLOR, BORDER_BOTTOM_COLOR, + BORDER_LEFT_COLOR }; + + } + + static final class Value { + + private Value(String name) { + this.name = name; + } + + /** + * The string representation of the attribute. This + * should exactly match the string specified in the + * CSS specification. + */ + public String toString() { + return name; + } + + static final Value INHERITED = new Value("inherited"); + static final Value NONE = new Value("none"); + static final Value HIDDEN = new Value("hidden"); + static final Value DOTTED = new Value("dotted"); + static final Value DASHED = new Value("dashed"); + static final Value SOLID = new Value("solid"); + static final Value DOUBLE = new Value("double"); + static final Value GROOVE = new Value("groove"); + static final Value RIDGE = new Value("ridge"); + static final Value INSET = new Value("inset"); + static final Value OUTSET = new Value("outset"); + // Lists. + static final Value DISC = new Value("disc"); + static final Value CIRCLE = new Value("circle"); + static final Value SQUARE = new Value("square"); + static final Value DECIMAL = new Value("decimal"); + static final Value LOWER_ROMAN = new Value("lower-roman"); + static final Value UPPER_ROMAN = new Value("upper-roman"); + static final Value LOWER_ALPHA = new Value("lower-alpha"); + static final Value UPPER_ALPHA = new Value("upper-alpha"); + // background-repeat + static final Value BACKGROUND_NO_REPEAT = new Value("no-repeat"); + static final Value BACKGROUND_REPEAT = new Value("repeat"); + static final Value BACKGROUND_REPEAT_X = new Value("repeat-x"); + static final Value BACKGROUND_REPEAT_Y = new Value("repeat-y"); + // background-attachment + static final Value BACKGROUND_SCROLL = new Value("scroll"); + static final Value BACKGROUND_FIXED = new Value("fixed"); + + private String name; + + static final Value[] allValues = { + INHERITED, NONE, DOTTED, DASHED, SOLID, DOUBLE, GROOVE, + RIDGE, INSET, OUTSET, DISC, CIRCLE, SQUARE, DECIMAL, + LOWER_ROMAN, UPPER_ROMAN, LOWER_ALPHA, UPPER_ALPHA, + BACKGROUND_NO_REPEAT, BACKGROUND_REPEAT, + BACKGROUND_REPEAT_X, BACKGROUND_REPEAT_Y, + BACKGROUND_FIXED, BACKGROUND_FIXED + }; + } + + public CSS() { + baseFontSize = baseFontSizeIndex + 1; + // setup the css conversion table + valueConvertor = new Hashtable(); + valueConvertor.put(CSS.Attribute.FONT_SIZE, new FontSize()); + valueConvertor.put(CSS.Attribute.FONT_FAMILY, new FontFamily()); + valueConvertor.put(CSS.Attribute.FONT_WEIGHT, new FontWeight()); + Object bs = new BorderStyle(); + valueConvertor.put(CSS.Attribute.BORDER_TOP_STYLE, bs); + valueConvertor.put(CSS.Attribute.BORDER_RIGHT_STYLE, bs); + valueConvertor.put(CSS.Attribute.BORDER_BOTTOM_STYLE, bs); + valueConvertor.put(CSS.Attribute.BORDER_LEFT_STYLE, bs); + Object cv = new ColorValue(); + valueConvertor.put(CSS.Attribute.COLOR, cv); + valueConvertor.put(CSS.Attribute.BACKGROUND_COLOR, cv); + valueConvertor.put(CSS.Attribute.BORDER_TOP_COLOR, cv); + valueConvertor.put(CSS.Attribute.BORDER_RIGHT_COLOR, cv); + valueConvertor.put(CSS.Attribute.BORDER_BOTTOM_COLOR, cv); + valueConvertor.put(CSS.Attribute.BORDER_LEFT_COLOR, cv); + Object lv = new LengthValue(); + valueConvertor.put(CSS.Attribute.MARGIN_TOP, lv); + valueConvertor.put(CSS.Attribute.MARGIN_BOTTOM, lv); + valueConvertor.put(CSS.Attribute.MARGIN_LEFT, lv); + valueConvertor.put(CSS.Attribute.MARGIN_LEFT_LTR, lv); + valueConvertor.put(CSS.Attribute.MARGIN_LEFT_RTL, lv); + valueConvertor.put(CSS.Attribute.MARGIN_RIGHT, lv); + valueConvertor.put(CSS.Attribute.MARGIN_RIGHT_LTR, lv); + valueConvertor.put(CSS.Attribute.MARGIN_RIGHT_RTL, lv); + valueConvertor.put(CSS.Attribute.PADDING_TOP, lv); + valueConvertor.put(CSS.Attribute.PADDING_BOTTOM, lv); + valueConvertor.put(CSS.Attribute.PADDING_LEFT, lv); + valueConvertor.put(CSS.Attribute.PADDING_RIGHT, lv); + Object bv = new BorderWidthValue(null, 0); + valueConvertor.put(CSS.Attribute.BORDER_TOP_WIDTH, bv); + valueConvertor.put(CSS.Attribute.BORDER_BOTTOM_WIDTH, bv); + valueConvertor.put(CSS.Attribute.BORDER_LEFT_WIDTH, bv); + valueConvertor.put(CSS.Attribute.BORDER_RIGHT_WIDTH, bv); + Object nlv = new LengthValue(true); + valueConvertor.put(CSS.Attribute.TEXT_INDENT, nlv); + valueConvertor.put(CSS.Attribute.WIDTH, lv); + valueConvertor.put(CSS.Attribute.HEIGHT, lv); + valueConvertor.put(CSS.Attribute.BORDER_SPACING, lv); + Object sv = new StringValue(); + valueConvertor.put(CSS.Attribute.FONT_STYLE, sv); + valueConvertor.put(CSS.Attribute.TEXT_DECORATION, sv); + valueConvertor.put(CSS.Attribute.TEXT_ALIGN, sv); + valueConvertor.put(CSS.Attribute.VERTICAL_ALIGN, sv); + Object valueMapper = new CssValueMapper(); + valueConvertor.put(CSS.Attribute.LIST_STYLE_TYPE, + valueMapper); + valueConvertor.put(CSS.Attribute.BACKGROUND_IMAGE, + new BackgroundImage()); + valueConvertor.put(CSS.Attribute.BACKGROUND_POSITION, + new BackgroundPosition()); + valueConvertor.put(CSS.Attribute.BACKGROUND_REPEAT, + valueMapper); + valueConvertor.put(CSS.Attribute.BACKGROUND_ATTACHMENT, + valueMapper); + Object generic = new CssValue(); + int n = CSS.Attribute.allAttributes.length; + for (int i = 0; i < n; i++) { + CSS.Attribute key = CSS.Attribute.allAttributes[i]; + if (valueConvertor.get(key) == null) { + valueConvertor.put(key, generic); + } + } + } + + /** + * Sets the base font size. sz is a CSS value, and is + * not necessarily the point size. Use getPointSize to determine the + * point size corresponding to sz. + */ + void setBaseFontSize(int sz) { + if (sz < 1) + baseFontSize = 0; + else if (sz > 7) + baseFontSize = 7; + else + baseFontSize = sz; + } + + /** + * Sets the base font size from the passed in string. + */ + void setBaseFontSize(String size) { + int relSize, absSize, diff; + + if (size != null) { + if (size.startsWith("+")) { + relSize = Integer.valueOf(size.substring(1)).intValue(); + setBaseFontSize(baseFontSize + relSize); + } else if (size.startsWith("-")) { + relSize = -Integer.valueOf(size.substring(1)).intValue(); + setBaseFontSize(baseFontSize + relSize); + } else { + setBaseFontSize(Integer.valueOf(size).intValue()); + } + } + } + + /** + * Returns the base font size. + */ + int getBaseFontSize() { + return baseFontSize; + } + + /** + * Parses the CSS property key with value + * value placing the result in att. + */ + void addInternalCSSValue(MutableAttributeSet attr, + CSS.Attribute key, String value) { + if (key == CSS.Attribute.FONT) { + ShorthandFontParser.parseShorthandFont(this, value, attr); + } + else if (key == CSS.Attribute.BACKGROUND) { + ShorthandBackgroundParser.parseShorthandBackground + (this, value, attr); + } + else if (key == CSS.Attribute.MARGIN) { + ShorthandMarginParser.parseShorthandMargin(this, value, attr, + CSS.Attribute.ALL_MARGINS); + } + else if (key == CSS.Attribute.PADDING) { + ShorthandMarginParser.parseShorthandMargin(this, value, attr, + CSS.Attribute.ALL_PADDING); + } + else if (key == CSS.Attribute.BORDER_WIDTH) { + ShorthandMarginParser.parseShorthandMargin(this, value, attr, + CSS.Attribute.ALL_BORDER_WIDTHS); + } + else if (key == CSS.Attribute.BORDER_COLOR) { + ShorthandMarginParser.parseShorthandMargin(this, value, attr, + CSS.Attribute.ALL_BORDER_COLORS); + } + else if (key == CSS.Attribute.BORDER_STYLE) { + ShorthandMarginParser.parseShorthandMargin(this, value, attr, + CSS.Attribute.ALL_BORDER_STYLES); + } + else if ((key == CSS.Attribute.BORDER) || + (key == CSS.Attribute.BORDER_TOP) || + (key == CSS.Attribute.BORDER_RIGHT) || + (key == CSS.Attribute.BORDER_BOTTOM) || + (key == CSS.Attribute.BORDER_LEFT)) { + ShorthandBorderParser.parseShorthandBorder(attr, key, value); + } + else { + Object iValue = getInternalCSSValue(key, value); + if (iValue != null) { + attr.addAttribute(key, iValue); + } + } + } + + /** + * Gets the internal CSS representation of value which is + * a CSS value of the CSS attribute named key. The receiver + * should not modify value, and the first count + * strings are valid. + */ + Object getInternalCSSValue(CSS.Attribute key, String value) { + CssValue conv = (CssValue) valueConvertor.get(key); + Object r = conv.parseCssValue(value); + return r != null ? r : conv.parseCssValue(key.getDefaultValue()); + } + + /** + * Maps from a StyleConstants to a CSS Attribute. + */ + Attribute styleConstantsKeyToCSSKey(StyleConstants sc) { + return (Attribute)styleConstantToCssMap.get(sc); + } + + /** + * Maps from a StyleConstants value to a CSS value. + */ + Object styleConstantsValueToCSSValue(StyleConstants sc, + Object styleValue) { + Object cssKey = styleConstantsKeyToCSSKey(sc); + if (cssKey != null) { + CssValue conv = (CssValue)valueConvertor.get(cssKey); + return conv.fromStyleConstants(sc, styleValue); + } + return null; + } + + /** + * Converts the passed in CSS value to a StyleConstants value. + * key identifies the CSS attribute being mapped. + */ + Object cssValueToStyleConstantsValue(StyleConstants key, Object value) { + if (value instanceof CssValue) { + return ((CssValue)value).toStyleConstants((StyleConstants)key, + null); + } + return null; + } + + /** + * Returns the font for the values in the passed in AttributeSet. + * It is assumed the keys will be CSS.Attribute keys. + * sc is the StyleContext that will be messaged to get + * the font once the size, name and style have been determined. + */ + Font getFont(StyleContext sc, AttributeSet a, int defaultSize, StyleSheet ss) { + ss = getStyleSheet(ss); + int size = getFontSize(a, defaultSize, ss); + + /* + * If the vertical alignment is set to either superscirpt or + * subscript we reduce the font size by 2 points. + */ + StringValue vAlignV = (StringValue)a.getAttribute + (CSS.Attribute.VERTICAL_ALIGN); + if ((vAlignV != null)) { + String vAlign = vAlignV.toString(); + if ((vAlign.indexOf("sup") >= 0) || + (vAlign.indexOf("sub") >= 0)) { + size -= 2; + } + } + + FontFamily familyValue = (FontFamily)a.getAttribute + (CSS.Attribute.FONT_FAMILY); + String family = (familyValue != null) ? familyValue.getValue() : + Font.SANS_SERIF; + int style = Font.PLAIN; + FontWeight weightValue = (FontWeight) a.getAttribute + (CSS.Attribute.FONT_WEIGHT); + if ((weightValue != null) && (weightValue.getValue() > 400)) { + style |= Font.BOLD; + } + Object fs = a.getAttribute(CSS.Attribute.FONT_STYLE); + if ((fs != null) && (fs.toString().indexOf("italic") >= 0)) { + style |= Font.ITALIC; + } + if (family.equalsIgnoreCase("monospace")) { + family = Font.MONOSPACED; + } + Font f = sc.getFont(family, style, size); + if (f == null + || (f.getFamily().equals(Font.DIALOG) + && ! family.equalsIgnoreCase(Font.DIALOG))) { + family = Font.SANS_SERIF; + f = sc.getFont(family, style, size); + } + return f; + } + + static int getFontSize(AttributeSet attr, int defaultSize, StyleSheet ss) { + // PENDING(prinz) this is a 1.1 based implementation, need to also + // have a 1.2 version. + FontSize sizeValue = (FontSize)attr.getAttribute(CSS.Attribute. + FONT_SIZE); + + return (sizeValue != null) ? sizeValue.getValue(attr, ss) + : defaultSize; + } + + /** + * Takes a set of attributes and turn it into a color + * specification. This might be used to specify things + * like brighter, more hue, etc. + * This will return null if there is no value for key. + * + * @param key CSS.Attribute identifying where color is stored. + * @param a the set of attributes + * @return the color + */ + Color getColor(AttributeSet a, CSS.Attribute key) { + ColorValue cv = (ColorValue) a.getAttribute(key); + if (cv != null) { + return cv.getValue(); + } + return null; + } + + /** + * Returns the size of a font from the passed in string. + * + * @param size CSS string describing font size + * @param baseFontSize size to use for relative units. + */ + float getPointSize(String size, StyleSheet ss) { + int relSize, absSize, diff, index; + ss = getStyleSheet(ss); + if (size != null) { + if (size.startsWith("+")) { + relSize = Integer.valueOf(size.substring(1)).intValue(); + return getPointSize(baseFontSize + relSize, ss); + } else if (size.startsWith("-")) { + relSize = -Integer.valueOf(size.substring(1)).intValue(); + return getPointSize(baseFontSize + relSize, ss); + } else { + absSize = Integer.valueOf(size).intValue(); + return getPointSize(absSize, ss); + } + } + return 0; + } + + /** + * Returns the length of the attribute in a with + * key key. + */ + float getLength(AttributeSet a, CSS.Attribute key, StyleSheet ss) { + ss = getStyleSheet(ss); + LengthValue lv = (LengthValue) a.getAttribute(key); + boolean isW3CLengthUnits = (ss == null) ? false : ss.isW3CLengthUnits(); + float len = (lv != null) ? lv.getValue(isW3CLengthUnits) : 0; + return len; + } + + /** + * Convert a set of HTML attributes to an equivalent + * set of CSS attributes. + * + * @param AttributeSet containing the HTML attributes. + * @return AttributeSet containing the corresponding CSS attributes. + * The AttributeSet will be empty if there are no mapping + * CSS attributes. + */ + AttributeSet translateHTMLToCSS(AttributeSet htmlAttrSet) { + MutableAttributeSet cssAttrSet = new SimpleAttributeSet(); + Element elem = (Element)htmlAttrSet; + HTML.Tag tag = getHTMLTag(htmlAttrSet); + if ((tag == HTML.Tag.TD) || (tag == HTML.Tag.TH)) { + // translate border width into the cells, if it has non-zero value. + AttributeSet tableAttr = elem.getParentElement(). + getParentElement().getAttributes(); + int borderWidth; + try { + borderWidth = Integer.parseInt( + (String) tableAttr.getAttribute(HTML.Attribute.BORDER)); + } catch (NumberFormatException e) { + borderWidth = 0; + } + if (borderWidth > 0) { + translateAttribute(HTML.Attribute.BORDER, tableAttr, cssAttrSet); + } + String pad = (String)tableAttr.getAttribute(HTML.Attribute.CELLPADDING); + if (pad != null) { + LengthValue v = + (LengthValue)getInternalCSSValue(CSS.Attribute.PADDING_TOP, pad); + v.span = (v.span < 0) ? 0 : v.span; + cssAttrSet.addAttribute(CSS.Attribute.PADDING_TOP, v); + cssAttrSet.addAttribute(CSS.Attribute.PADDING_BOTTOM, v); + cssAttrSet.addAttribute(CSS.Attribute.PADDING_LEFT, v); + cssAttrSet.addAttribute(CSS.Attribute.PADDING_RIGHT, v); + } + } + if (elem.isLeaf()) { + translateEmbeddedAttributes(htmlAttrSet, cssAttrSet); + } else { + translateAttributes(tag, htmlAttrSet, cssAttrSet); + } + if (tag == HTML.Tag.CAPTION) { + /* + * Navigator uses ALIGN for caption placement and IE uses VALIGN. + */ + Object v = htmlAttrSet.getAttribute(HTML.Attribute.ALIGN); + if ((v != null) && (v.equals("top") || v.equals("bottom"))) { + cssAttrSet.addAttribute(CSS.Attribute.CAPTION_SIDE, v); + cssAttrSet.removeAttribute(CSS.Attribute.TEXT_ALIGN); + } else { + v = htmlAttrSet.getAttribute(HTML.Attribute.VALIGN); + if (v != null) { + cssAttrSet.addAttribute(CSS.Attribute.CAPTION_SIDE, v); + } + } + } + return cssAttrSet; + } + + private static final Hashtable attributeMap = new Hashtable(); + private static final Hashtable valueMap = new Hashtable(); + + /** + * The hashtable and the static initalization block below, + * set up a mapping from well-known HTML attributes to + * CSS attributes. For the most part, there is a 1-1 mapping + * between the two. However in the case of certain HTML + * attributes for example HTML.Attribute.VSPACE or + * HTML.Attribute.HSPACE, end up mapping to two CSS.Attribute's. + * Therefore, the value associated with each HTML.Attribute. + * key ends up being an array of CSS.Attribute.* objects. + */ + private static final Hashtable htmlAttrToCssAttrMap = new Hashtable(20); + + /** + * The hashtable and static initialization that follows sets + * up a translation from StyleConstants (i.e. the well known + * attributes) to the associated CSS attributes. + */ + private static final Hashtable styleConstantToCssMap = new Hashtable(17); + /** Maps from HTML value to a CSS value. Used in internal mapping. */ + private static final Hashtable htmlValueToCssValueMap = new Hashtable(8); + /** Maps from CSS value (string) to internal value. */ + private static final Hashtable cssValueToInternalValueMap = new Hashtable(13); + + static { + // load the attribute map + for (int i = 0; i < Attribute.allAttributes.length; i++ ) { + attributeMap.put(Attribute.allAttributes[i].toString(), + Attribute.allAttributes[i]); + } + // load the value map + for (int i = 0; i < Value.allValues.length; i++ ) { + valueMap.put(Value.allValues[i].toString(), + Value.allValues[i]); + } + + htmlAttrToCssAttrMap.put(HTML.Attribute.COLOR, + new CSS.Attribute[]{CSS.Attribute.COLOR}); + htmlAttrToCssAttrMap.put(HTML.Attribute.TEXT, + new CSS.Attribute[]{CSS.Attribute.COLOR}); + htmlAttrToCssAttrMap.put(HTML.Attribute.CLEAR, + new CSS.Attribute[]{CSS.Attribute.CLEAR}); + htmlAttrToCssAttrMap.put(HTML.Attribute.BACKGROUND, + new CSS.Attribute[]{CSS.Attribute.BACKGROUND_IMAGE}); + htmlAttrToCssAttrMap.put(HTML.Attribute.BGCOLOR, + new CSS.Attribute[]{CSS.Attribute.BACKGROUND_COLOR}); + htmlAttrToCssAttrMap.put(HTML.Attribute.WIDTH, + new CSS.Attribute[]{CSS.Attribute.WIDTH}); + htmlAttrToCssAttrMap.put(HTML.Attribute.HEIGHT, + new CSS.Attribute[]{CSS.Attribute.HEIGHT}); + htmlAttrToCssAttrMap.put(HTML.Attribute.BORDER, + new CSS.Attribute[]{CSS.Attribute.BORDER_TOP_WIDTH, CSS.Attribute.BORDER_RIGHT_WIDTH, CSS.Attribute.BORDER_BOTTOM_WIDTH, CSS.Attribute.BORDER_LEFT_WIDTH}); + htmlAttrToCssAttrMap.put(HTML.Attribute.CELLPADDING, + new CSS.Attribute[]{CSS.Attribute.PADDING}); + htmlAttrToCssAttrMap.put(HTML.Attribute.CELLSPACING, + new CSS.Attribute[]{CSS.Attribute.BORDER_SPACING}); + htmlAttrToCssAttrMap.put(HTML.Attribute.MARGINWIDTH, + new CSS.Attribute[]{CSS.Attribute.MARGIN_LEFT, + CSS.Attribute.MARGIN_RIGHT}); + htmlAttrToCssAttrMap.put(HTML.Attribute.MARGINHEIGHT, + new CSS.Attribute[]{CSS.Attribute.MARGIN_TOP, + CSS.Attribute.MARGIN_BOTTOM}); + htmlAttrToCssAttrMap.put(HTML.Attribute.HSPACE, + new CSS.Attribute[]{CSS.Attribute.PADDING_LEFT, + CSS.Attribute.PADDING_RIGHT}); + htmlAttrToCssAttrMap.put(HTML.Attribute.VSPACE, + new CSS.Attribute[]{CSS.Attribute.PADDING_BOTTOM, + CSS.Attribute.PADDING_TOP}); + htmlAttrToCssAttrMap.put(HTML.Attribute.FACE, + new CSS.Attribute[]{CSS.Attribute.FONT_FAMILY}); + htmlAttrToCssAttrMap.put(HTML.Attribute.SIZE, + new CSS.Attribute[]{CSS.Attribute.FONT_SIZE}); + htmlAttrToCssAttrMap.put(HTML.Attribute.VALIGN, + new CSS.Attribute[]{CSS.Attribute.VERTICAL_ALIGN}); + htmlAttrToCssAttrMap.put(HTML.Attribute.ALIGN, + new CSS.Attribute[]{CSS.Attribute.VERTICAL_ALIGN, + CSS.Attribute.TEXT_ALIGN, + CSS.Attribute.FLOAT}); + htmlAttrToCssAttrMap.put(HTML.Attribute.TYPE, + new CSS.Attribute[]{CSS.Attribute.LIST_STYLE_TYPE}); + htmlAttrToCssAttrMap.put(HTML.Attribute.NOWRAP, + new CSS.Attribute[]{CSS.Attribute.WHITE_SPACE}); + + // initialize StyleConstants mapping + styleConstantToCssMap.put(StyleConstants.FontFamily, + CSS.Attribute.FONT_FAMILY); + styleConstantToCssMap.put(StyleConstants.FontSize, + CSS.Attribute.FONT_SIZE); + styleConstantToCssMap.put(StyleConstants.Bold, + CSS.Attribute.FONT_WEIGHT); + styleConstantToCssMap.put(StyleConstants.Italic, + CSS.Attribute.FONT_STYLE); + styleConstantToCssMap.put(StyleConstants.Underline, + CSS.Attribute.TEXT_DECORATION); + styleConstantToCssMap.put(StyleConstants.StrikeThrough, + CSS.Attribute.TEXT_DECORATION); + styleConstantToCssMap.put(StyleConstants.Superscript, + CSS.Attribute.VERTICAL_ALIGN); + styleConstantToCssMap.put(StyleConstants.Subscript, + CSS.Attribute.VERTICAL_ALIGN); + styleConstantToCssMap.put(StyleConstants.Foreground, + CSS.Attribute.COLOR); + styleConstantToCssMap.put(StyleConstants.Background, + CSS.Attribute.BACKGROUND_COLOR); + styleConstantToCssMap.put(StyleConstants.FirstLineIndent, + CSS.Attribute.TEXT_INDENT); + styleConstantToCssMap.put(StyleConstants.LeftIndent, + CSS.Attribute.MARGIN_LEFT); + styleConstantToCssMap.put(StyleConstants.RightIndent, + CSS.Attribute.MARGIN_RIGHT); + styleConstantToCssMap.put(StyleConstants.SpaceAbove, + CSS.Attribute.MARGIN_TOP); + styleConstantToCssMap.put(StyleConstants.SpaceBelow, + CSS.Attribute.MARGIN_BOTTOM); + styleConstantToCssMap.put(StyleConstants.Alignment, + CSS.Attribute.TEXT_ALIGN); + + // HTML->CSS + htmlValueToCssValueMap.put("disc", CSS.Value.DISC); + htmlValueToCssValueMap.put("square", CSS.Value.SQUARE); + htmlValueToCssValueMap.put("circle", CSS.Value.CIRCLE); + htmlValueToCssValueMap.put("1", CSS.Value.DECIMAL); + htmlValueToCssValueMap.put("a", CSS.Value.LOWER_ALPHA); + htmlValueToCssValueMap.put("A", CSS.Value.UPPER_ALPHA); + htmlValueToCssValueMap.put("i", CSS.Value.LOWER_ROMAN); + htmlValueToCssValueMap.put("I", CSS.Value.UPPER_ROMAN); + + // CSS-> internal CSS + cssValueToInternalValueMap.put("none", CSS.Value.NONE); + cssValueToInternalValueMap.put("disc", CSS.Value.DISC); + cssValueToInternalValueMap.put("square", CSS.Value.SQUARE); + cssValueToInternalValueMap.put("circle", CSS.Value.CIRCLE); + cssValueToInternalValueMap.put("decimal", CSS.Value.DECIMAL); + cssValueToInternalValueMap.put("lower-roman", CSS.Value.LOWER_ROMAN); + cssValueToInternalValueMap.put("upper-roman", CSS.Value.UPPER_ROMAN); + cssValueToInternalValueMap.put("lower-alpha", CSS.Value.LOWER_ALPHA); + cssValueToInternalValueMap.put("upper-alpha", CSS.Value.UPPER_ALPHA); + cssValueToInternalValueMap.put("repeat", CSS.Value.BACKGROUND_REPEAT); + cssValueToInternalValueMap.put("no-repeat", + CSS.Value.BACKGROUND_NO_REPEAT); + cssValueToInternalValueMap.put("repeat-x", + CSS.Value.BACKGROUND_REPEAT_X); + cssValueToInternalValueMap.put("repeat-y", + CSS.Value.BACKGROUND_REPEAT_Y); + cssValueToInternalValueMap.put("scroll", + CSS.Value.BACKGROUND_SCROLL); + cssValueToInternalValueMap.put("fixed", + CSS.Value.BACKGROUND_FIXED); + + // Register all the CSS attribute keys for archival/unarchival + Object[] keys = CSS.Attribute.allAttributes; + try { + for (int i = 0; i < keys.length; i++) { + StyleContext.registerStaticAttributeKey(keys[i]); + } + } catch (Throwable e) { + e.printStackTrace(); + } + + // Register all the CSS Values for archival/unarchival + keys = CSS.Value.allValues; + try { + for (int i = 0; i < keys.length; i++) { + StyleContext.registerStaticAttributeKey(keys[i]); + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + + /** + * Return the set of all possible CSS attribute keys. + */ + public static Attribute[] getAllAttributeKeys() { + Attribute[] keys = new Attribute[Attribute.allAttributes.length]; + System.arraycopy(Attribute.allAttributes, 0, keys, 0, Attribute.allAttributes.length); + return keys; + } + + /** + * Translates a string to a CSS.Attribute object. + * This will return null if there is no attribute + * by the given name. + * + * @param name the name of the CSS attribute to fetch the + * typesafe enumeration for + * @return the CSS.Attribute object, + * or null if the string + * doesn't represent a valid attribute key + */ + public static final Attribute getAttribute(String name) { + return (Attribute) attributeMap.get(name); + } + + /** + * Translates a string to a CSS.Value object. + * This will return null if there is no value + * by the given name. + * + * @param name the name of the CSS value to fetch the + * typesafe enumeration for + * @return the CSS.Value object, + * or null if the string + * doesn't represent a valid CSS value name; this does + * not mean that it doesn't represent a valid CSS value + */ + static final Value getValue(String name) { + return (Value) valueMap.get(name); + } + + + // + // Conversion related methods/classes + // + + /** + * Returns a URL for the given CSS url string. If relative, + * base is used as the parent. If a valid URL can not + * be found, this will not throw a MalformedURLException, instead + * null will be returned. + */ + static URL getURL(URL base, String cssString) { + if (cssString == null) { + return null; + } + if (cssString.startsWith("url(") && + cssString.endsWith(")")) { + cssString = cssString.substring(4, cssString.length() - 1); + } + // Absolute first + try { + URL url = new URL(cssString); + if (url != null) { + return url; + } + } catch (MalformedURLException mue) { + } + // Then relative + if (base != null) { + // Relative URL, try from base + try { + URL url = new URL(base, cssString); + return url; + } + catch (MalformedURLException muee) { + } + } + return null; + } + + /** + * Converts a type Color to a hex string + * in the format "#RRGGBB" + */ + static String colorToHex(Color color) { + + String colorstr = new String("#"); + + // Red + String str = Integer.toHexString(color.getRed()); + if (str.length() > 2) + str = str.substring(0, 2); + else if (str.length() < 2) + colorstr += "0" + str; + else + colorstr += str; + + // Green + str = Integer.toHexString(color.getGreen()); + if (str.length() > 2) + str = str.substring(0, 2); + else if (str.length() < 2) + colorstr += "0" + str; + else + colorstr += str; + + // Blue + str = Integer.toHexString(color.getBlue()); + if (str.length() > 2) + str = str.substring(0, 2); + else if (str.length() < 2) + colorstr += "0" + str; + else + colorstr += str; + + return colorstr; + } + + /** + * Convert a "#FFFFFF" hex string to a Color. + * If the color specification is bad, an attempt + * will be made to fix it up. + */ + static final Color hexToColor(String value) { + String digits; + int n = value.length(); + if (value.startsWith("#")) { + digits = value.substring(1, Math.min(value.length(), 7)); + } else { + digits = value; + } + String hstr = "0x" + digits; + Color c; + try { + c = Color.decode(hstr); + } catch (NumberFormatException nfe) { + c = null; + } + return c; + } + + /** + * Convert a color string such as "RED" or "#NNNNNN" or "rgb(r, g, b)" + * to a Color. + */ + static Color stringToColor(String str) { + Color color = null; + + if (str == null) { + return null; + } + if (str.length() == 0) + color = Color.black; + else if (str.startsWith("rgb(")) { + color = parseRGB(str); + } + else if (str.charAt(0) == '#') + color = hexToColor(str); + else if (str.equalsIgnoreCase("Black")) + color = hexToColor("#000000"); + else if(str.equalsIgnoreCase("Silver")) + color = hexToColor("#C0C0C0"); + else if(str.equalsIgnoreCase("Gray")) + color = hexToColor("#808080"); + else if(str.equalsIgnoreCase("White")) + color = hexToColor("#FFFFFF"); + else if(str.equalsIgnoreCase("Maroon")) + color = hexToColor("#800000"); + else if(str.equalsIgnoreCase("Red")) + color = hexToColor("#FF0000"); + else if(str.equalsIgnoreCase("Purple")) + color = hexToColor("#800080"); + else if(str.equalsIgnoreCase("Fuchsia")) + color = hexToColor("#FF00FF"); + else if(str.equalsIgnoreCase("Green")) + color = hexToColor("#008000"); + else if(str.equalsIgnoreCase("Lime")) + color = hexToColor("#00FF00"); + else if(str.equalsIgnoreCase("Olive")) + color = hexToColor("#808000"); + else if(str.equalsIgnoreCase("Yellow")) + color = hexToColor("#FFFF00"); + else if(str.equalsIgnoreCase("Navy")) + color = hexToColor("#000080"); + else if(str.equalsIgnoreCase("Blue")) + color = hexToColor("#0000FF"); + else if(str.equalsIgnoreCase("Teal")) + color = hexToColor("#008080"); + else if(str.equalsIgnoreCase("Aqua")) + color = hexToColor("#00FFFF"); + else if(str.equalsIgnoreCase("Orange")) + color = hexToColor("#FF8000"); + else + color = hexToColor(str); // sometimes get specified without leading # + return color; + } + + /** + * Parses a String in the format rgb(r, g, b) where + * each of the Color components is either an integer, or a floating number + * with a % after indicating a percentage value of 255. Values are + * constrained to fit with 0-255. The resulting Color is returned. + */ + private static Color parseRGB(String string) { + // Find the next numeric char + int[] index = new int[1]; + + index[0] = 4; + int red = getColorComponent(string, index); + int green = getColorComponent(string, index); + int blue = getColorComponent(string, index); + + return new Color(red, green, blue); + } + + /** + * Returns the next integer value from string starting + * at index[0]. The value can either can an integer, or + * a percentage (floating number ending with %), in which case it is + * multiplied by 255. + */ + private static int getColorComponent(String string, int[] index) { + int length = string.length(); + char aChar; + + // Skip non-decimal chars + while(index[0] < length && (aChar = string.charAt(index[0])) != '-' && + !Character.isDigit(aChar) && aChar != '.') { + index[0]++; + } + + int start = index[0]; + + if (start < length && string.charAt(index[0]) == '-') { + index[0]++; + } + while(index[0] < length && + Character.isDigit(string.charAt(index[0]))) { + index[0]++; + } + if (index[0] < length && string.charAt(index[0]) == '.') { + // Decimal value + index[0]++; + while(index[0] < length && + Character.isDigit(string.charAt(index[0]))) { + index[0]++; + } + } + if (start != index[0]) { + try { + float value = Float.parseFloat(string.substring + (start, index[0])); + + if (index[0] < length && string.charAt(index[0]) == '%') { + index[0]++; + value = value * 255f / 100f; + } + return Math.min(255, Math.max(0, (int)value)); + } catch (NumberFormatException nfe) { + // Treat as 0 + } + } + return 0; + } + + static int getIndexOfSize(float pt, int[] sizeMap) { + for (int i = 0; i < sizeMap.length; i ++ ) + if (pt <= sizeMap[i]) + return i + 1; + return sizeMap.length; + } + + static int getIndexOfSize(float pt, StyleSheet ss) { + int[] sizeMap = (ss != null) ? ss.getSizeMap() : + StyleSheet.sizeMapDefault; + return getIndexOfSize(pt, sizeMap); + } + + + /** + * @return an array of all the strings in value + * that are separated by whitespace. + */ + static String[] parseStrings(String value) { + int current, last; + int length = (value == null) ? 0 : value.length(); + Vector temp = new Vector(4); + + current = 0; + while (current < length) { + // Skip ws + while (current < length && Character.isWhitespace + (value.charAt(current))) { + current++; + } + last = current; + while (current < length && !Character.isWhitespace + (value.charAt(current))) { + current++; + } + if (last != current) { + temp.addElement(value.substring(last, current)); + } + current++; + } + String[] retValue = new String[temp.size()]; + temp.copyInto(retValue); + return retValue; + } + + /** + * Return the point size, given a size index. Legal HTML index sizes + * are 1-7. + */ + float getPointSize(int index, StyleSheet ss) { + ss = getStyleSheet(ss); + int[] sizeMap = (ss != null) ? ss.getSizeMap() : + StyleSheet.sizeMapDefault; + --index; + if (index < 0) + return sizeMap[0]; + else if (index > sizeMap.length - 1) + return sizeMap[sizeMap.length - 1]; + else + return sizeMap[index]; + } + + + private void translateEmbeddedAttributes(AttributeSet htmlAttrSet, + MutableAttributeSet cssAttrSet) { + Enumeration keys = htmlAttrSet.getAttributeNames(); + if (htmlAttrSet.getAttribute(StyleConstants.NameAttribute) == + HTML.Tag.HR) { + // HR needs special handling due to us treating it as a leaf. + translateAttributes(HTML.Tag.HR, htmlAttrSet, cssAttrSet); + } + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key instanceof HTML.Tag) { + HTML.Tag tag = (HTML.Tag)key; + Object o = htmlAttrSet.getAttribute(tag); + if (o != null && o instanceof AttributeSet) { + translateAttributes(tag, (AttributeSet)o, cssAttrSet); + } + } else if (key instanceof CSS.Attribute) { + cssAttrSet.addAttribute(key, htmlAttrSet.getAttribute(key)); + } + } + } + + private void translateAttributes(HTML.Tag tag, + AttributeSet htmlAttrSet, + MutableAttributeSet cssAttrSet) { + Enumeration names = htmlAttrSet.getAttributeNames(); + while (names.hasMoreElements()) { + Object name = names.nextElement(); + + if (name instanceof HTML.Attribute) { + HTML.Attribute key = (HTML.Attribute)name; + + /* + * HTML.Attribute.ALIGN needs special processing. + * It can map to to 1 of many(3) possible CSS attributes + * depending on the nature of the tag the attribute is + * part off and depending on the value of the attribute. + */ + if (key == HTML.Attribute.ALIGN) { + String htmlAttrValue = (String)htmlAttrSet.getAttribute(HTML.Attribute.ALIGN); + if (htmlAttrValue != null) { + CSS.Attribute cssAttr = getCssAlignAttribute(tag, htmlAttrSet); + if (cssAttr != null) { + Object o = getCssValue(cssAttr, htmlAttrValue); + if (o != null) { + cssAttrSet.addAttribute(cssAttr, o); + } + } + } + } else { + + /* + * The html size attribute has a mapping in the CSS world only + * if it is par of a font or base font tag. + */ + + if (key == HTML.Attribute.SIZE && !isHTMLFontTag(tag)) { + continue; + } + + translateAttribute(key, htmlAttrSet, cssAttrSet); + } + } else if (name instanceof CSS.Attribute) { + cssAttrSet.addAttribute(name, htmlAttrSet.getAttribute(name)); + } + } + } + + private void translateAttribute(HTML.Attribute key, + AttributeSet htmlAttrSet, + MutableAttributeSet cssAttrSet) { + /* + * In the case of all remaining HTML.Attribute's they + * map to 1 or more CCS.Attribute. + */ + CSS.Attribute[] cssAttrList = getCssAttribute(key); + + String htmlAttrValue = (String)htmlAttrSet.getAttribute(key); + + if (cssAttrList == null || htmlAttrValue == null) { + return; + } + for (int i = 0; i < cssAttrList.length; i++) { + Object o = getCssValue(cssAttrList[i], htmlAttrValue); + if (o != null) { + cssAttrSet.addAttribute(cssAttrList[i], o); + } + } + } + + /** + * Given a CSS.Attribute object and its corresponding HTML.Attribute's + * value, this method returns a CssValue object to associate with the + * CSS attribute. + * + * @param the CSS.Attribute + * @param a String containing the value associated HTML.Attribtue. + */ + Object getCssValue(CSS.Attribute cssAttr, String htmlAttrValue) { + CssValue value = (CssValue)valueConvertor.get(cssAttr); + Object o = value.parseHtmlValue(htmlAttrValue); + return o; + } + + /** + * Maps an HTML.Attribute object to its appropriate CSS.Attributes. + * + * @param HTML.Attribute + * @return CSS.Attribute[] + */ + private CSS.Attribute[] getCssAttribute(HTML.Attribute hAttr) { + return (CSS.Attribute[])htmlAttrToCssAttrMap.get(hAttr); + } + + /** + * Maps HTML.Attribute.ALIGN to either: + * CSS.Attribute.TEXT_ALIGN + * CSS.Attribute.FLOAT + * CSS.Attribute.VERTICAL_ALIGN + * based on the tag associated with the attribute and the + * value of the attribute. + * + * @param AttributeSet containing HTML attributes. + * @return CSS.Attribute mapping for HTML.Attribute.ALIGN. + */ + private CSS.Attribute getCssAlignAttribute(HTML.Tag tag, + AttributeSet htmlAttrSet) { + return CSS.Attribute.TEXT_ALIGN; +/* + String htmlAttrValue = (String)htmlAttrSet.getAttribute(HTML.Attribute.ALIGN); + CSS.Attribute cssAttr = CSS.Attribute.TEXT_ALIGN; + if (htmlAttrValue != null && htmlAttrSet instanceof Element) { + Element elem = (Element)htmlAttrSet; + if (!elem.isLeaf() && tag.isBlock() && validTextAlignValue(htmlAttrValue)) { + return CSS.Attribute.TEXT_ALIGN; + } else if (isFloater(htmlAttrValue)) { + return CSS.Attribute.FLOAT; + } else if (elem.isLeaf()) { + return CSS.Attribute.VERTICAL_ALIGN; + } + } + return null; + */ + } + + /** + * Fetches the tag associated with the HTML AttributeSet. + * + * @param AttributeSet containing the HTML attributes. + * @return HTML.Tag + */ + private HTML.Tag getHTMLTag(AttributeSet htmlAttrSet) { + Object o = htmlAttrSet.getAttribute(StyleConstants.NameAttribute); + if (o instanceof HTML.Tag) { + HTML.Tag tag = (HTML.Tag) o; + return tag; + } + return null; + } + + + private boolean isHTMLFontTag(HTML.Tag tag) { + return (tag != null && ((tag == HTML.Tag.FONT) || (tag == HTML.Tag.BASEFONT))); + } + + + private boolean isFloater(String alignValue) { + return (alignValue.equals("left") || alignValue.equals("right")); + } + + private boolean validTextAlignValue(String alignValue) { + return (isFloater(alignValue) || alignValue.equals("center")); + } + + /** + * Base class to CSS values in the attribute sets. This + * is intended to act as a convertor to/from other attribute + * formats. + *

    + * The CSS parser uses the parseCssValue method to convert + * a string to whatever format is appropriate a given key + * (i.e. these convertors are stored in a map using the + * CSS.Attribute as a key and the CssValue as the value). + *

    + * The HTML to CSS conversion process first converts the + * HTML.Attribute to a CSS.Attribute, and then calls + * the parseHtmlValue method on the value of the HTML + * attribute to produce the corresponding CSS value. + *

    + * The StyleConstants to CSS conversion process first + * converts the StyleConstants attribute to a + * CSS.Attribute, and then calls the fromStyleConstants + * method to convert the StyleConstants value to a + * CSS value. + *

    + * The CSS to StyleConstants conversion process first + * converts the StyleConstants attribute to a + * CSS.Attribute, and then calls the toStyleConstants + * method to convert the CSS value to a StyleConstants + * value. + */ + static class CssValue implements Serializable { + + /** + * Convert a CSS value string to the internal format + * (for fast processing) used in the attribute sets. + * The fallback storage for any value that we don't + * have a special binary format for is a String. + */ + Object parseCssValue(String value) { + return value; + } + + /** + * Convert an HTML attribute value to a CSS attribute + * value. If there is no conversion, return null. + * This is implemented to simply forward to the CSS + * parsing by default (since some of the attribute + * values are the same). If the attribute value + * isn't recognized as a CSS value it is generally + * returned as null. + */ + Object parseHtmlValue(String value) { + return parseCssValue(value); + } + + /** + * Converts a StyleConstants attribute value to + * a CSS attribute value. If there is no conversion, + * returns null. By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @param value the value of a StyleConstants + * attribute to be converted + * @return the CSS value that represents the + * StyleConstants value + */ + Object fromStyleConstants(StyleConstants key, Object value) { + return null; + } + + /** + * Converts a CSS attribute value to a + * StyleConstants + * value. If there is no conversion, returns + * null. + * By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @param v the view containing AttributeSet + * @return the StyleConstants attribute value that + * represents the CSS attribute value + */ + Object toStyleConstants(StyleConstants key, View v) { + return null; + } + + /** + * Return the CSS format of the value + */ + public String toString() { + return svalue; + } + + /** + * The value as a string... before conversion to a + * binary format. + */ + String svalue; + } + + /** + * By default CSS attributes are represented as simple + * strings. They also have no conversion to/from + * StyleConstants by default. This class represents the + * value as a string (via the superclass), but + * provides StyleConstants conversion support for the + * CSS attributes that are held as strings. + */ + static class StringValue extends CssValue { + + /** + * Convert a CSS value string to the internal format + * (for fast processing) used in the attribute sets. + * This produces a StringValue, so that it can be + * used to convert from CSS to StyleConstants values. + */ + Object parseCssValue(String value) { + StringValue sv = new StringValue(); + sv.svalue = value; + return sv; + } + + /** + * Converts a StyleConstants attribute value to + * a CSS attribute value. If there is no conversion + * returns null. + * + * @param key the StyleConstants attribute + * @param value the value of a StyleConstants + * attribute to be converted + * @return the CSS value that represents the + * StyleConstants value + */ + Object fromStyleConstants(StyleConstants key, Object value) { + if (key == StyleConstants.Italic) { + if (value.equals(Boolean.TRUE)) { + return parseCssValue("italic"); + } + return parseCssValue(""); + } else if (key == StyleConstants.Underline) { + if (value.equals(Boolean.TRUE)) { + return parseCssValue("underline"); + } + return parseCssValue(""); + } else if (key == StyleConstants.Alignment) { + int align = ((Integer)value).intValue(); + String ta; + switch(align) { + case StyleConstants.ALIGN_LEFT: + ta = "left"; + break; + case StyleConstants.ALIGN_RIGHT: + ta = "right"; + break; + case StyleConstants.ALIGN_CENTER: + ta = "center"; + break; + case StyleConstants.ALIGN_JUSTIFIED: + ta = "justify"; + break; + default: + ta = "left"; + } + return parseCssValue(ta); + } else if (key == StyleConstants.StrikeThrough) { + if (value.equals(Boolean.TRUE)) { + return parseCssValue("line-through"); + } + return parseCssValue(""); + } else if (key == StyleConstants.Superscript) { + if (value.equals(Boolean.TRUE)) { + return parseCssValue("super"); + } + return parseCssValue(""); + } else if (key == StyleConstants.Subscript) { + if (value.equals(Boolean.TRUE)) { + return parseCssValue("sub"); + } + return parseCssValue(""); + } + return null; + } + + /** + * Converts a CSS attribute value to a + * StyleConstants value. + * If there is no conversion, returns null. + * By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @return the StyleConstants attribute value that + * represents the CSS attribute value + */ + Object toStyleConstants(StyleConstants key, View v) { + if (key == StyleConstants.Italic) { + if (svalue.indexOf("italic") >= 0) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } else if (key == StyleConstants.Underline) { + if (svalue.indexOf("underline") >= 0) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } else if (key == StyleConstants.Alignment) { + if (svalue.equals("right")) { + return new Integer(StyleConstants.ALIGN_RIGHT); + } else if (svalue.equals("center")) { + return new Integer(StyleConstants.ALIGN_CENTER); + } else if (svalue.equals("justify")) { + return new Integer(StyleConstants.ALIGN_JUSTIFIED); + } + return new Integer(StyleConstants.ALIGN_LEFT); + } else if (key == StyleConstants.StrikeThrough) { + if (svalue.indexOf("line-through") >= 0) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } else if (key == StyleConstants.Superscript) { + if (svalue.indexOf("super") >= 0) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } else if (key == StyleConstants.Subscript) { + if (svalue.indexOf("sub") >= 0) { + return Boolean.TRUE; + } + return Boolean.FALSE; + } + return null; + } + + // Used by ViewAttributeSet + boolean isItalic() { + return (svalue.indexOf("italic") != -1); + } + + boolean isStrike() { + return (svalue.indexOf("line-through") != -1); + } + + boolean isUnderline() { + return (svalue.indexOf("underline") != -1); + } + + boolean isSub() { + return (svalue.indexOf("sub") != -1); + } + + boolean isSup() { + return (svalue.indexOf("sup") != -1); + } + } + + /** + * Represents a value for the CSS.FONT_SIZE attribute. + * The binary format of the value can be one of several + * types. If the type is Float, + * the value is specified in terms of point or + * percentage, depending upon the ending of the + * associated string. + * If the type is Integer, the value is specified + * in terms of a size index. + */ + class FontSize extends CssValue { + + /** + * Returns the size in points. This is ultimately + * what we need for the purpose of creating/fetching + * a Font object. + * + * @param a the attribute set the value is being + * requested from. We may need to walk up the + * resolve hierarchy if it's relative. + */ + int getValue(AttributeSet a, StyleSheet ss) { + ss = getStyleSheet(ss); + if (index) { + // it's an index, translate from size table + return Math.round(getPointSize((int) value, ss)); + } + else if (lu == null) { + return Math.round(value); + } + else { + if (lu.type == 0) { + boolean isW3CLengthUnits = (ss == null) ? false : ss.isW3CLengthUnits(); + return Math.round(lu.getValue(isW3CLengthUnits)); + } + if (a != null) { + AttributeSet resolveParent = a.getResolveParent(); + + if (resolveParent != null) { + int pValue = StyleConstants.getFontSize(resolveParent); + + float retValue; + if (lu.type == 1 || lu.type == 3) { + retValue = lu.value * (float)pValue; + } + else { + retValue = lu.value + (float)pValue; + } + return Math.round(retValue); + } + } + // a is null, or no resolve parent. + return 12; + } + } + + Object parseCssValue(String value) { + FontSize fs = new FontSize(); + fs.svalue = value; + try { + if (value.equals("xx-small")) { + fs.value = 1; + fs.index = true; + } else if (value.equals("x-small")) { + fs.value = 2; + fs.index = true; + } else if (value.equals("small")) { + fs.value = 3; + fs.index = true; + } else if (value.equals("medium")) { + fs.value = 4; + fs.index = true; + } else if (value.equals("large")) { + fs.value = 5; + fs.index = true; + } else if (value.equals("x-large")) { + fs.value = 6; + fs.index = true; + } else if (value.equals("xx-large")) { + fs.value = 7; + fs.index = true; + } else { + fs.lu = new LengthUnit(value, (short)1, 1f); + } + // relative sizes, larger | smaller (adjust from parent by + // 1.5 pixels) + // em, ex refer to parent sizes + // lengths: pt, mm, cm, pc, in, px + // em (font height 3em would be 3 times font height) + // ex (height of X) + // lengths are (+/-) followed by a number and two letter + // unit identifier + } catch (NumberFormatException nfe) { + fs = null; + } + return fs; + } + + Object parseHtmlValue(String value) { + if ((value == null) || (value.length() == 0)) { + return null; + } + FontSize fs = new FontSize(); + fs.svalue = value; + + try { + /* + * relative sizes in the size attribute are relative + * to the 's size. + */ + int baseFontSize = getBaseFontSize(); + if (value.charAt(0) == '+') { + int relSize = Integer.valueOf(value.substring(1)).intValue(); + fs.value = baseFontSize + relSize; + fs.index = true; + } else if (value.charAt(0) == '-') { + int relSize = -Integer.valueOf(value.substring(1)).intValue(); + fs.value = baseFontSize + relSize; + fs.index = true; + } else { + fs.value = Integer.parseInt(value); + if (fs.value > 7) { + fs.value = 7; + } else if (fs.value < 0) { + fs.value = 0; + } + fs.index = true; + } + + } catch (NumberFormatException nfe) { + fs = null; + } + return fs; + } + + /** + * Converts a StyleConstants attribute value to + * a CSS attribute value. If there is no conversion + * returns null. By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @param value the value of a StyleConstants + * attribute to be converted + * @return the CSS value that represents the + * StyleConstants value + */ + Object fromStyleConstants(StyleConstants key, Object value) { + if (value instanceof Number) { + FontSize fs = new FontSize(); + + fs.value = getIndexOfSize(((Number)value).floatValue(), StyleSheet.sizeMapDefault); + fs.svalue = Integer.toString((int)fs.value); + fs.index = true; + return fs; + } + return parseCssValue(value.toString()); + } + + /** + * Converts a CSS attribute value to a StyleConstants + * value. If there is no conversion, returns null. + * By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @return the StyleConstants attribute value that + * represents the CSS attribute value + */ + Object toStyleConstants(StyleConstants key, View v) { + if (v != null) { + return Integer.valueOf(getValue(v.getAttributes(), null)); + } + return Integer.valueOf(getValue(null, null)); + } + + float value; + boolean index; + LengthUnit lu; + } + + static class FontFamily extends CssValue { + + /** + * Returns the font family to use. + */ + String getValue() { + return family; + } + + Object parseCssValue(String value) { + int cIndex = value.indexOf(','); + FontFamily ff = new FontFamily(); + ff.svalue = value; + ff.family = null; + + if (cIndex == -1) { + setFontName(ff, value); + } + else { + boolean done = false; + int lastIndex; + int length = value.length(); + cIndex = 0; + while (!done) { + // skip ws. + while (cIndex < length && + Character.isWhitespace(value.charAt(cIndex))) + cIndex++; + // Find next ',' + lastIndex = cIndex; + cIndex = value.indexOf(',', cIndex); + if (cIndex == -1) { + cIndex = length; + } + if (lastIndex < length) { + if (lastIndex != cIndex) { + int lastCharIndex = cIndex; + if (cIndex > 0 && value.charAt(cIndex - 1) == ' '){ + lastCharIndex--; + } + setFontName(ff, value.substring + (lastIndex, lastCharIndex)); + done = (ff.family != null); + } + cIndex++; + } + else { + done = true; + } + } + } + if (ff.family == null) { + ff.family = Font.SANS_SERIF; + } + return ff; + } + + private void setFontName(FontFamily ff, String fontName) { + ff.family = fontName; + } + + Object parseHtmlValue(String value) { + // TBD + return parseCssValue(value); + } + + /** + * Converts a StyleConstants attribute value to + * a CSS attribute value. If there is no conversion + * returns null. By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @param value the value of a StyleConstants + * attribute to be converted + * @return the CSS value that represents the + * StyleConstants value + */ + Object fromStyleConstants(StyleConstants key, Object value) { + return parseCssValue(value.toString()); + } + + /** + * Converts a CSS attribute value to a StyleConstants + * value. If there is no conversion, returns null. + * By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @return the StyleConstants attribute value that + * represents the CSS attribute value + */ + Object toStyleConstants(StyleConstants key, View v) { + return family; + } + + String family; + } + + static class FontWeight extends CssValue { + + int getValue() { + return weight; + } + + Object parseCssValue(String value) { + FontWeight fw = new FontWeight(); + fw.svalue = value; + if (value.equals("bold")) { + fw.weight = 700; + } else if (value.equals("normal")) { + fw.weight = 400; + } else { + // PENDING(prinz) add support for relative values + try { + fw.weight = Integer.parseInt(value); + } catch (NumberFormatException nfe) { + fw = null; + } + } + return fw; + } + + /** + * Converts a StyleConstants attribute value to + * a CSS attribute value. If there is no conversion + * returns null. By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @param value the value of a StyleConstants + * attribute to be converted + * @return the CSS value that represents the + * StyleConstants value + */ + Object fromStyleConstants(StyleConstants key, Object value) { + if (value.equals(Boolean.TRUE)) { + return parseCssValue("bold"); + } + return parseCssValue("normal"); + } + + /** + * Converts a CSS attribute value to a StyleConstants + * value. If there is no conversion, returns null. + * By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @return the StyleConstants attribute value that + * represents the CSS attribute value + */ + Object toStyleConstants(StyleConstants key, View v) { + return (weight > 500) ? Boolean.TRUE : Boolean.FALSE; + } + + boolean isBold() { + return (weight > 500); + } + + int weight; + } + + static class ColorValue extends CssValue { + + /** + * Returns the color to use. + */ + Color getValue() { + return c; + } + + Object parseCssValue(String value) { + + Color c = stringToColor(value); + if (c != null) { + ColorValue cv = new ColorValue(); + cv.svalue = value; + cv.c = c; + return cv; + } + return null; + } + + Object parseHtmlValue(String value) { + return parseCssValue(value); + } + + /** + * Converts a StyleConstants attribute value to + * a CSS attribute value. If there is no conversion + * returns null. By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @param value the value of a StyleConstants + * attribute to be converted + * @return the CSS value that represents the + * StyleConstants value + */ + Object fromStyleConstants(StyleConstants key, Object value) { + ColorValue colorValue = new ColorValue(); + colorValue.c = (Color)value; + colorValue.svalue = colorToHex(colorValue.c); + return colorValue; + } + + /** + * Converts a CSS attribute value to a StyleConstants + * value. If there is no conversion, returns null. + * By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @return the StyleConstants attribute value that + * represents the CSS attribute value + */ + Object toStyleConstants(StyleConstants key, View v) { + return c; + } + + Color c; + } + + static class BorderStyle extends CssValue { + + CSS.Value getValue() { + return style; + } + + Object parseCssValue(String value) { + CSS.Value cssv = CSS.getValue(value); + if (cssv != null) { + if ((cssv == CSS.Value.INSET) || + (cssv == CSS.Value.OUTSET) || + (cssv == CSS.Value.NONE) || + (cssv == CSS.Value.DOTTED) || + (cssv == CSS.Value.DASHED) || + (cssv == CSS.Value.SOLID) || + (cssv == CSS.Value.DOUBLE) || + (cssv == CSS.Value.GROOVE) || + (cssv == CSS.Value.RIDGE)) { + + BorderStyle bs = new BorderStyle(); + bs.svalue = value; + bs.style = cssv; + return bs; + } + } + return null; + } + + private void writeObject(java.io.ObjectOutputStream s) + throws IOException { + s.defaultWriteObject(); + if (style == null) { + s.writeObject(null); + } + else { + s.writeObject(style.toString()); + } + } + + private void readObject(ObjectInputStream s) + throws ClassNotFoundException, IOException { + s.defaultReadObject(); + Object value = s.readObject(); + if (value != null) { + style = CSS.getValue((String)value); + } + } + + // CSS.Values are static, don't archive it. + transient private CSS.Value style; + } + + static class LengthValue extends CssValue { + + /** + * if this length value may be negative. + */ + boolean mayBeNegative; + + LengthValue() { + this(false); + } + + LengthValue(boolean mayBeNegative) { + this.mayBeNegative = mayBeNegative; + } + + /** + * Returns the length (span) to use. + */ + float getValue() { + return getValue(false); + } + + float getValue(boolean isW3CLengthUnits) { + return getValue(0, isW3CLengthUnits); + } + + /** + * Returns the length (span) to use. If the value represents + * a percentage, it is scaled based on currentValue. + */ + float getValue(float currentValue) { + return getValue(currentValue, false); + } + float getValue(float currentValue, boolean isW3CLengthUnits) { + if (percentage) { + return span * currentValue; + } + return LengthUnit.getValue(span, units, isW3CLengthUnits); + } + + /** + * Returns true if the length represents a percentage of the + * containing box. + */ + boolean isPercentage() { + return percentage; + } + + Object parseCssValue(String value) { + LengthValue lv; + try { + // Assume pixels + float absolute = Float.valueOf(value).floatValue(); + lv = new LengthValue(); + lv.span = absolute; + } catch (NumberFormatException nfe) { + // Not pixels, use LengthUnit + LengthUnit lu = new LengthUnit(value, + LengthUnit.UNINITALIZED_LENGTH, + 0); + + // PENDING: currently, we only support absolute values and + // percentages. + switch (lu.type) { + case 0: + // Absolute + lv = new LengthValue(); + lv.span = + (mayBeNegative) ? lu.value : Math.max(0, lu.value); + lv.units = lu.units; + break; + case 1: + // % + lv = new LengthValue(); + lv.span = Math.max(0, Math.min(1, lu.value)); + lv.percentage = true; + break; + default: + return null; + } + } + lv.svalue = value; + return lv; + } + + Object parseHtmlValue(String value) { + if (value.equals(HTML.NULL_ATTRIBUTE_VALUE)) { + value = "1"; + } + return parseCssValue(value); + } + /** + * Converts a StyleConstants attribute value to + * a CSS attribute value. If there is no conversion, + * returns null. By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @param value the value of a StyleConstants + * attribute to be converted + * @return the CSS value that represents the + * StyleConstants value + */ + Object fromStyleConstants(StyleConstants key, Object value) { + LengthValue v = new LengthValue(); + v.svalue = value.toString(); + v.span = ((Float)value).floatValue(); + return v; + } + + /** + * Converts a CSS attribute value to a StyleConstants + * value. If there is no conversion, returns null. + * By default, there is no conversion. + * + * @param key the StyleConstants attribute + * @return the StyleConstants attribute value that + * represents the CSS attribute value + */ + Object toStyleConstants(StyleConstants key, View v) { + return new Float(getValue(false)); + } + + /** If true, span is a percentage value, and that to determine + * the length another value needs to be passed in. */ + boolean percentage; + /** Either the absolute value (percentage == false) or + * a percentage value. */ + float span; + + String units = null; + } + + + /** + * BorderWidthValue is used to model BORDER_XXX_WIDTH and adds support + * for the thin/medium/thick values. + */ + static class BorderWidthValue extends LengthValue { + BorderWidthValue(String svalue, int index) { + this.svalue = svalue; + span = values[index]; + percentage = false; + } + + Object parseCssValue(String value) { + if (value != null) { + if (value.equals("thick")) { + return new BorderWidthValue(value, 2); + } + else if (value.equals("medium")) { + return new BorderWidthValue(value, 1); + } + else if (value.equals("thin")) { + return new BorderWidthValue(value, 0); + } + } + // Assume its a length. + return super.parseCssValue(value); + } + + Object parseHtmlValue(String value) { + if (value == HTML.NULL_ATTRIBUTE_VALUE) { + return parseCssValue("medium"); + } + return parseCssValue(value); + } + + /** Values used to represent border width. */ + private static final float[] values = { 1, 2, 4 }; + } + + + /** + * Handles uniquing of CSS values, like lists, and background image + * repeating. + */ + static class CssValueMapper extends CssValue { + Object parseCssValue(String value) { + Object retValue = cssValueToInternalValueMap.get(value); + if (retValue == null) { + retValue = cssValueToInternalValueMap.get(value.toLowerCase()); + } + return retValue; + } + + + Object parseHtmlValue(String value) { + Object retValue = htmlValueToCssValueMap.get(value); + if (retValue == null) { + retValue = htmlValueToCssValueMap.get(value.toLowerCase()); + } + return retValue; + } + } + + + /** + * Used for background images, to represent the position. + */ + static class BackgroundPosition extends CssValue { + float horizontalPosition; + float verticalPosition; + // bitmask: bit 0, horizontal relative, bit 1 horizontal relative to + // font size, 2 vertical relative to size, 3 vertical relative to + // font size. + // + short relative; + + Object parseCssValue(String value) { + // 'top left' and 'left top' both mean the same as '0% 0%'. + // 'top', 'top center' and 'center top' mean the same as '50% 0%'. + // 'right top' and 'top right' mean the same as '100% 0%'. + // 'left', 'left center' and 'center left' mean the same as + // '0% 50%'. + // 'center' and 'center center' mean the same as '50% 50%'. + // 'right', 'right center' and 'center right' mean the same as + // '100% 50%'. + // 'bottom left' and 'left bottom' mean the same as '0% 100%'. + // 'bottom', 'bottom center' and 'center bottom' mean the same as + // '50% 100%'. + // 'bottom right' and 'right bottom' mean the same as '100% 100%'. + String[] strings = CSS.parseStrings(value); + int count = strings.length; + BackgroundPosition bp = new BackgroundPosition(); + bp.relative = 5; + bp.svalue = value; + + if (count > 0) { + // bit 0 for vert, 1 hor, 2 for center + short found = 0; + int index = 0; + while (index < count) { + // First, check for keywords + String string = strings[index++]; + if (string.equals("center")) { + found |= 4; + continue; + } + else { + if ((found & 1) == 0) { + if (string.equals("top")) { + found |= 1; + } + else if (string.equals("bottom")) { + found |= 1; + bp.verticalPosition = 1; + continue; + } + } + if ((found & 2) == 0) { + if (string.equals("left")) { + found |= 2; + bp.horizontalPosition = 0; + } + else if (string.equals("right")) { + found |= 2; + bp.horizontalPosition = 1; + } + } + } + } + if (found != 0) { + if ((found & 1) == 1) { + if ((found & 2) == 0) { + // vert and no horiz. + bp.horizontalPosition = .5f; + } + } + else if ((found & 2) == 2) { + // horiz and no vert. + bp.verticalPosition = .5f; + } + else { + // no horiz, no vert, but center + bp.horizontalPosition = bp.verticalPosition = .5f; + } + } + else { + // Assume lengths + LengthUnit lu = new LengthUnit(strings[0], (short)0, 0f); + + if (lu.type == 0) { + bp.horizontalPosition = lu.value; + bp.relative = (short)(1 ^ bp.relative); + } + else if (lu.type == 1) { + bp.horizontalPosition = lu.value; + } + else if (lu.type == 3) { + bp.horizontalPosition = lu.value; + bp.relative = (short)((1 ^ bp.relative) | 2); + } + if (count > 1) { + lu = new LengthUnit(strings[1], (short)0, 0f); + + if (lu.type == 0) { + bp.verticalPosition = lu.value; + bp.relative = (short)(4 ^ bp.relative); + } + else if (lu.type == 1) { + bp.verticalPosition = lu.value; + } + else if (lu.type == 3) { + bp.verticalPosition = lu.value; + bp.relative = (short)((4 ^ bp.relative) | 8); + } + } + else { + bp.verticalPosition = .5f; + } + } + } + return bp; + } + + boolean isHorizontalPositionRelativeToSize() { + return ((relative & 1) == 1); + } + + boolean isHorizontalPositionRelativeToFontSize() { + return ((relative & 2) == 2); + } + + float getHorizontalPosition() { + return horizontalPosition; + } + + boolean isVerticalPositionRelativeToSize() { + return ((relative & 4) == 4); + } + + boolean isVerticalPositionRelativeToFontSize() { + return ((relative & 8) == 8); + } + + float getVerticalPosition() { + return verticalPosition; + } + } + + + /** + * Used for BackgroundImages. + */ + static class BackgroundImage extends CssValue { + private boolean loadedImage; + private ImageIcon image; + + Object parseCssValue(String value) { + BackgroundImage retValue = new BackgroundImage(); + retValue.svalue = value; + return retValue; + } + + Object parseHtmlValue(String value) { + return parseCssValue(value); + } + + // PENDING: this base is wrong for linked style sheets. + ImageIcon getImage(URL base) { + if (!loadedImage) { + synchronized(this) { + if (!loadedImage) { + URL url = CSS.getURL(base, svalue); + loadedImage = true; + if (url != null) { + image = new ImageIcon(); + Image tmpImg = Toolkit.getDefaultToolkit().createImage(url); + if (tmpImg != null) { + image.setImage(tmpImg); + } + } + } + } + } + return image; + } + } + + /** + * Parses a length value, this is used internally, and never added + * to an AttributeSet or returned to the developer. + */ + static class LengthUnit implements Serializable { + static Hashtable lengthMapping = new Hashtable(6); + static Hashtable w3cLengthMapping = new Hashtable(6); + static { + lengthMapping.put("pt", new Float(1f)); + // Not sure about 1.3, determined by experiementation. + lengthMapping.put("px", new Float(1.3f)); + lengthMapping.put("mm", new Float(2.83464f)); + lengthMapping.put("cm", new Float(28.3464f)); + lengthMapping.put("pc", new Float(12f)); + lengthMapping.put("in", new Float(72f)); + int res = 72; + try { + res = Toolkit.getDefaultToolkit().getScreenResolution(); + } catch (HeadlessException e) { + } + // mapping according to the CSS2 spec + w3cLengthMapping.put("pt", new Float(res/72f)); + w3cLengthMapping.put("px", new Float(1f)); + w3cLengthMapping.put("mm", new Float(res/25.4f)); + w3cLengthMapping.put("cm", new Float(res/2.54f)); + w3cLengthMapping.put("pc", new Float(res/6f)); + w3cLengthMapping.put("in", new Float(res)); + } + + LengthUnit(String value, short defaultType, float defaultValue) { + parse(value, defaultType, defaultValue); + } + + void parse(String value, short defaultType, float defaultValue) { + type = defaultType; + this.value = defaultValue; + + int length = value.length(); + if (length > 0 && value.charAt(length - 1) == '%') { + try { + this.value = Float.valueOf(value.substring(0, length - 1)). + floatValue() / 100.0f; + type = 1; + } + catch (NumberFormatException nfe) { } + } + if (length >= 2) { + units = value.substring(length - 2, length); + Float scale = (Float)lengthMapping.get(units); + if (scale != null) { + try { + this.value = Float.valueOf(value.substring(0, + length - 2)).floatValue(); + type = 0; + } + catch (NumberFormatException nfe) { } + } + else if (units.equals("em") || + units.equals("ex")) { + try { + this.value = Float.valueOf(value.substring(0, + length - 2)).floatValue(); + type = 3; + } + catch (NumberFormatException nfe) { } + } + else if (value.equals("larger")) { + this.value = 2f; + type = 2; + } + else if (value.equals("smaller")) { + this.value = -2; + type = 2; + } + else { + // treat like points. + try { + this.value = Float.valueOf(value).floatValue(); + type = 0; + } catch (NumberFormatException nfe) {} + } + } + else if (length > 0) { + // treat like points. + try { + this.value = Float.valueOf(value).floatValue(); + type = 0; + } catch (NumberFormatException nfe) {} + } + } + + float getValue(boolean w3cLengthUnits) { + Hashtable mapping = (w3cLengthUnits) ? w3cLengthMapping : lengthMapping; + float scale = 1; + if (units != null) { + Float scaleFloat = (Float)mapping.get(units); + if (scaleFloat != null) { + scale = scaleFloat.floatValue(); + } + } + return this.value * scale; + + } + + static float getValue(float value, String units, Boolean w3cLengthUnits) { + Hashtable mapping = (w3cLengthUnits) ? w3cLengthMapping : lengthMapping; + float scale = 1; + if (units != null) { + Float scaleFloat = (Float)mapping.get(units); + if (scaleFloat != null) { + scale = scaleFloat.floatValue(); + } + } + return value * scale; + } + + public String toString() { + return type + " " + value; + } + + // 0 - value indicates real value + // 1 - % value, value relative to depends upon key. + // 50% will have a value = .5 + // 2 - add value to parent value. + // 3 - em/ex relative to font size of element (except for + // font-size, which is relative to parent). + short type; + float value; + String units = null; + + + static final short UNINITALIZED_LENGTH = (short)10; + } + + + /** + * Class used to parse font property. The font property is shorthand + * for the other font properties. This expands the properties, placing + * them in the attributeset. + */ + static class ShorthandFontParser { + /** + * Parses the shorthand font string value, placing the + * result in attr. + */ + static void parseShorthandFont(CSS css, String value, + MutableAttributeSet attr) { + // font is of the form: + // [ || || ]? + // [ / ]? + String[] strings = CSS.parseStrings(value); + int count = strings.length; + int index = 0; + // bitmask, 1 for style, 2 for variant, 3 for weight + short found = 0; + int maxC = Math.min(3, count); + + // Check for font-style font-variant font-weight + while (index < maxC) { + if ((found & 1) == 0 && isFontStyle(strings[index])) { + css.addInternalCSSValue(attr, CSS.Attribute.FONT_STYLE, + strings[index++]); + found |= 1; + } + else if ((found & 2) == 0 && isFontVariant(strings[index])) { + css.addInternalCSSValue(attr, CSS.Attribute.FONT_VARIANT, + strings[index++]); + found |= 2; + } + else if ((found & 4) == 0 && isFontWeight(strings[index])) { + css.addInternalCSSValue(attr, CSS.Attribute.FONT_WEIGHT, + strings[index++]); + found |= 4; + } + else if (strings[index].equals("normal")) { + index++; + } + else { + break; + } + } + if ((found & 1) == 0) { + css.addInternalCSSValue(attr, CSS.Attribute.FONT_STYLE, + "normal"); + } + if ((found & 2) == 0) { + css.addInternalCSSValue(attr, CSS.Attribute.FONT_VARIANT, + "normal"); + } + if ((found & 4) == 0) { + css.addInternalCSSValue(attr, CSS.Attribute.FONT_WEIGHT, + "normal"); + } + + // string at index should be the font-size + if (index < count) { + String fontSize = strings[index]; + int slashIndex = fontSize.indexOf('/'); + + if (slashIndex != -1) { + fontSize = fontSize.substring(0, slashIndex); + strings[index] = strings[index].substring(slashIndex); + } + else { + index++; + } + css.addInternalCSSValue(attr, CSS.Attribute.FONT_SIZE, + fontSize); + } + else { + css.addInternalCSSValue(attr, CSS.Attribute.FONT_SIZE, + "medium"); + } + + // Check for line height + if (index < count && strings[index].startsWith("/")) { + String lineHeight = null; + if (strings[index].equals("/")) { + if (++index < count) { + lineHeight = strings[index++]; + } + } + else { + lineHeight = strings[index++].substring(1); + } + // line height + if (lineHeight != null) { + css.addInternalCSSValue(attr, CSS.Attribute.LINE_HEIGHT, + lineHeight); + } + else { + css.addInternalCSSValue(attr, CSS.Attribute.LINE_HEIGHT, + "normal"); + } + } + else { + css.addInternalCSSValue(attr, CSS.Attribute.LINE_HEIGHT, + "normal"); + } + + // remainder of strings are font-family + if (index < count) { + String family = strings[index++]; + + while (index < count) { + family += " " + strings[index++]; + } + css.addInternalCSSValue(attr, CSS.Attribute.FONT_FAMILY, + family); + } + else { + css.addInternalCSSValue(attr, CSS.Attribute.FONT_FAMILY, + Font.SANS_SERIF); + } + } + + private static boolean isFontStyle(String string) { + return (string.equals("italic") || + string.equals("oblique")); + } + + private static boolean isFontVariant(String string) { + return (string.equals("small-caps")); + } + + private static boolean isFontWeight(String string) { + if (string.equals("bold") || string.equals("bolder") || + string.equals("italic") || string.equals("lighter")) { + return true; + } + // test for 100-900 + return (string.length() == 3 && + string.charAt(0) >= '1' && string.charAt(0) <= '9' && + string.charAt(1) == '0' && string.charAt(2) == '0'); + } + + } + + + /** + * Parses the background property into its intrinsic values. + */ + static class ShorthandBackgroundParser { + /** + * Parses the shorthand font string value, placing the + * result in attr. + */ + static void parseShorthandBackground(CSS css, String value, + MutableAttributeSet attr) { + String[] strings = parseStrings(value); + int count = strings.length; + int index = 0; + // bitmask: 0 for image, 1 repeat, 2 attachment, 3 position, + // 4 color + short found = 0; + + while (index < count) { + String string = strings[index++]; + if ((found & 1) == 0 && isImage(string)) { + css.addInternalCSSValue(attr, CSS.Attribute. + BACKGROUND_IMAGE, string); + found |= 1; + } + else if ((found & 2) == 0 && isRepeat(string)) { + css.addInternalCSSValue(attr, CSS.Attribute. + BACKGROUND_REPEAT, string); + found |= 2; + } + else if ((found & 4) == 0 && isAttachment(string)) { + css.addInternalCSSValue(attr, CSS.Attribute. + BACKGROUND_ATTACHMENT, string); + found |= 4; + } + else if ((found & 8) == 0 && isPosition(string)) { + if (index < count && isPosition(strings[index])) { + css.addInternalCSSValue(attr, CSS.Attribute. + BACKGROUND_POSITION, + string + " " + + strings[index++]); + } + else { + css.addInternalCSSValue(attr, CSS.Attribute. + BACKGROUND_POSITION, string); + } + found |= 8; + } + else if ((found & 16) == 0 && isColor(string)) { + css.addInternalCSSValue(attr, CSS.Attribute. + BACKGROUND_COLOR, string); + found |= 16; + } + } + if ((found & 1) == 0) { + css.addInternalCSSValue(attr, CSS.Attribute.BACKGROUND_IMAGE, + null); + } + if ((found & 2) == 0) { + css.addInternalCSSValue(attr, CSS.Attribute.BACKGROUND_REPEAT, + "repeat"); + } + if ((found & 4) == 0) { + css.addInternalCSSValue(attr, CSS.Attribute. + BACKGROUND_ATTACHMENT, "scroll"); + } + if ((found & 8) == 0) { + css.addInternalCSSValue(attr, CSS.Attribute. + BACKGROUND_POSITION, null); + } + // Currently, there is no good way to express this. + /* + if ((found & 16) == 0) { + css.addInternalCSSValue(attr, CSS.Attribute.BACKGROUND_COLOR, + null); + } + */ + } + + static boolean isImage(String string) { + return (string.startsWith("url(") && string.endsWith(")")); + } + + static boolean isRepeat(String string) { + return (string.equals("repeat-x") || string.equals("repeat-y") || + string.equals("repeat") || string.equals("no-repeat")); + } + + static boolean isAttachment(String string) { + return (string.equals("fixed") || string.equals("scroll")); + } + + static boolean isPosition(String string) { + return (string.equals("top") || string.equals("bottom") || + string.equals("left") || string.equals("right") || + string.equals("center") || + (string.length() > 0 && + Character.isDigit(string.charAt(0)))); + } + + static boolean isColor(String string) { + return (CSS.stringToColor(string) != null); + } + } + + + /** + * Used to parser margin and padding. + */ + static class ShorthandMarginParser { + /** + * Parses the shorthand margin/padding/border string + * value, placing the result in attr. + * names give the 4 instrinsic property names. + */ + static void parseShorthandMargin(CSS css, String value, + MutableAttributeSet attr, + CSS.Attribute[] names) { + String[] strings = parseStrings(value); + int count = strings.length; + int index = 0; + switch (count) { + case 0: + // empty string + return; + case 1: + // Identifies all values. + for (int counter = 0; counter < 4; counter++) { + css.addInternalCSSValue(attr, names[counter], strings[0]); + } + break; + case 2: + // 0 & 2 = strings[0], 1 & 3 = strings[1] + css.addInternalCSSValue(attr, names[0], strings[0]); + css.addInternalCSSValue(attr, names[2], strings[0]); + css.addInternalCSSValue(attr, names[1], strings[1]); + css.addInternalCSSValue(attr, names[3], strings[1]); + break; + case 3: + css.addInternalCSSValue(attr, names[0], strings[0]); + css.addInternalCSSValue(attr, names[1], strings[1]); + css.addInternalCSSValue(attr, names[2], strings[2]); + css.addInternalCSSValue(attr, names[3], strings[1]); + break; + default: + for (int counter = 0; counter < 4; counter++) { + css.addInternalCSSValue(attr, names[counter], + strings[counter]); + } + break; + } + } + } + + static class ShorthandBorderParser { + static Attribute[] keys = { + Attribute.BORDER_TOP, Attribute.BORDER_RIGHT, + Attribute.BORDER_BOTTOM, Attribute.BORDER_LEFT, + }; + + static void parseShorthandBorder(MutableAttributeSet attributes, + CSS.Attribute key, String value) { + Object[] parts = new Object[CSSBorder.PARSERS.length]; + String[] strings = parseStrings(value); + for (String s : strings) { + boolean valid = false; + for (int i = 0; i < parts.length; i++) { + Object v = CSSBorder.PARSERS[i].parseCssValue(s); + if (v != null) { + if (parts[i] == null) { + parts[i] = v; + valid = true; + } + break; + } + } + if (!valid) { + // Part is non-parseable or occured more than once. + return; + } + } + + // Unspecified parts get default values. + for (int i = 0; i < parts.length; i++) { + if (parts[i] == null) { + parts[i] = CSSBorder.DEFAULTS[i]; + } + } + + // Dispatch collected values to individual properties. + for (int i = 0; i < keys.length; i++) { + if ((key == Attribute.BORDER) || (key == keys[i])) { + for (int k = 0; k < parts.length; k++) { + attributes.addAttribute( + CSSBorder.ATTRIBUTES[k][i], parts[k]); + } + } + } + } + } + + /** + * Calculate the requirements needed to tile the requirements + * given by the iterator that would be tiled. The calculation + * takes into consideration margin and border spacing. + */ + static SizeRequirements calculateTiledRequirements(LayoutIterator iter, SizeRequirements r) { + long minimum = 0; + long maximum = 0; + long preferred = 0; + int lastMargin = 0; + int totalSpacing = 0; + int n = iter.getCount(); + for (int i = 0; i < n; i++) { + iter.setIndex(i); + int margin0 = lastMargin; + int margin1 = (int) iter.getLeadingCollapseSpan(); + totalSpacing += Math.max(margin0, margin1);; + preferred += (int) iter.getPreferredSpan(0); + minimum += iter.getMinimumSpan(0); + maximum += iter.getMaximumSpan(0); + + lastMargin = (int) iter.getTrailingCollapseSpan(); + } + totalSpacing += lastMargin; + totalSpacing += 2 * iter.getBorderWidth(); + + // adjust for the spacing area + minimum += totalSpacing; + preferred += totalSpacing; + maximum += totalSpacing; + + // set return value + if (r == null) { + r = new SizeRequirements(); + } + r.minimum = (minimum > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)minimum; + r.preferred = (preferred > Integer.MAX_VALUE) ? Integer.MAX_VALUE :(int) preferred; + r.maximum = (maximum > Integer.MAX_VALUE) ? Integer.MAX_VALUE :(int) maximum; + return r; + } + + /** + * Calculate a tiled layout for the given iterator. + * This should be done collapsing the neighboring + * margins to be a total of the maximum of the two + * neighboring margin areas as described in the CSS spec. + */ + static void calculateTiledLayout(LayoutIterator iter, int targetSpan) { + + /* + * first pass, calculate the preferred sizes, adjustments needed because + * of margin collapsing, and the flexibility to adjust the sizes. + */ + long preferred = 0; + long currentPreferred = 0; + int lastMargin = 0; + int totalSpacing = 0; + int n = iter.getCount(); + int adjustmentWeightsCount = LayoutIterator.WorstAdjustmentWeight + 1; + //max gain we can get adjusting elements with adjustmentWeight <= i + long gain[] = new long[adjustmentWeightsCount]; + //max loss we can get adjusting elements with adjustmentWeight <= i + long loss[] = new long[adjustmentWeightsCount]; + + for (int i = 0; i < adjustmentWeightsCount; i++) { + gain[i] = loss[i] = 0; + } + for (int i = 0; i < n; i++) { + iter.setIndex(i); + int margin0 = lastMargin; + int margin1 = (int) iter.getLeadingCollapseSpan(); + + iter.setOffset(Math.max(margin0, margin1)); + totalSpacing += iter.getOffset(); + + currentPreferred = (long)iter.getPreferredSpan(targetSpan); + iter.setSpan((int) currentPreferred); + preferred += currentPreferred; + gain[iter.getAdjustmentWeight()] += + (long)iter.getMaximumSpan(targetSpan) - currentPreferred; + loss[iter.getAdjustmentWeight()] += + currentPreferred - (long)iter.getMinimumSpan(targetSpan); + lastMargin = (int) iter.getTrailingCollapseSpan(); + } + totalSpacing += lastMargin; + totalSpacing += 2 * iter.getBorderWidth(); + + for (int i = 1; i < adjustmentWeightsCount; i++) { + gain[i] += gain[i - 1]; + loss[i] += loss[i - 1]; + } + + /* + * Second pass, expand or contract by as much as possible to reach + * the target span. This takes the margin collapsing into account + * prior to adjusting the span. + */ + + // determine the adjustment to be made + int allocated = targetSpan - totalSpacing; + long desiredAdjustment = allocated - preferred; + long adjustmentsArray[] = (desiredAdjustment > 0) ? gain : loss; + desiredAdjustment = Math.abs(desiredAdjustment); + int adjustmentLevel = 0; + for (;adjustmentLevel <= LayoutIterator.WorstAdjustmentWeight; + adjustmentLevel++) { + // adjustmentsArray[] is sorted. I do not bother about + // binary search though + if (adjustmentsArray[adjustmentLevel] >= desiredAdjustment) { + break; + } + } + float adjustmentFactor = 0.0f; + if (adjustmentLevel <= LayoutIterator.WorstAdjustmentWeight) { + desiredAdjustment -= (adjustmentLevel > 0) ? + adjustmentsArray[adjustmentLevel - 1] : 0; + if (desiredAdjustment != 0) { + float maximumAdjustment = + adjustmentsArray[adjustmentLevel] - + ((adjustmentLevel > 0) ? + adjustmentsArray[adjustmentLevel - 1] : 0 + ); + adjustmentFactor = desiredAdjustment / maximumAdjustment; + } + } + // make the adjustments + int totalOffset = (int)iter.getBorderWidth();; + for (int i = 0; i < n; i++) { + iter.setIndex(i); + iter.setOffset( iter.getOffset() + totalOffset); + if (iter.getAdjustmentWeight() < adjustmentLevel) { + iter.setSpan((int) + ((allocated > preferred) ? + Math.floor(iter.getMaximumSpan(targetSpan)) : + Math.ceil(iter.getMinimumSpan(targetSpan)) + ) + ); + } else if (iter.getAdjustmentWeight() == adjustmentLevel) { + int availableSpan = (allocated > preferred) ? + (int) iter.getMaximumSpan(targetSpan) - iter.getSpan() : + iter.getSpan() - (int) iter.getMinimumSpan(targetSpan); + int adj = (int)Math.floor(adjustmentFactor * availableSpan); + iter.setSpan(iter.getSpan() + + ((allocated > preferred) ? adj : -adj)); + } + totalOffset = (int) Math.min((long) iter.getOffset() + + (long) iter.getSpan(), + Integer.MAX_VALUE); + } + + // while rounding we could lose several pixels. + int roundError = targetSpan - totalOffset - + (int)iter.getTrailingCollapseSpan() - + (int)iter.getBorderWidth(); + int adj = (roundError > 0) ? 1 : -1; + roundError *= adj; + + boolean canAdjust = true; + while (roundError > 0 && canAdjust) { + // check for infinite loop + canAdjust = false; + int offsetAdjust = 0; + // try to distribute roundError. one pixel per cell + for (int i = 0; i < n; i++) { + iter.setIndex(i); + iter.setOffset(iter.getOffset() + offsetAdjust); + int curSpan = iter.getSpan(); + if (roundError > 0) { + int boundGap = (adj > 0) ? + (int)Math.floor(iter.getMaximumSpan(targetSpan)) - curSpan : + curSpan - (int)Math.ceil(iter.getMinimumSpan(targetSpan)); + if (boundGap >= 1) { + canAdjust = true; + iter.setSpan(curSpan + adj); + offsetAdjust += adj; + roundError--; + } + } + } + } + } + + /** + * An iterator to express the requirements to use when computing + * layout. + */ + interface LayoutIterator { + + void setOffset(int offs); + + int getOffset(); + + void setSpan(int span); + + int getSpan(); + + int getCount(); + + void setIndex(int i); + + float getMinimumSpan(float parentSpan); + + float getPreferredSpan(float parentSpan); + + float getMaximumSpan(float parentSpan); + + int getAdjustmentWeight(); //0 is the best weight WorstAdjustmentWeight is a worst one + + //float getAlignment(); + + float getBorderWidth(); + + float getLeadingCollapseSpan(); + + float getTrailingCollapseSpan(); + public static final int WorstAdjustmentWeight = 2; + } + + // + // Serialization support + // + + private void writeObject(java.io.ObjectOutputStream s) + throws IOException + { + s.defaultWriteObject(); + + // Determine what values in valueConvertor need to be written out. + Enumeration keys = valueConvertor.keys(); + s.writeInt(valueConvertor.size()); + if (keys != null) { + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + Object value = valueConvertor.get(key); + if (!(key instanceof Serializable) && + (key = StyleContext.getStaticAttributeKey(key)) == null) { + // Should we throw an exception here? + key = null; + value = null; + } + else if (!(value instanceof Serializable) && + (value = StyleContext.getStaticAttributeKey(value)) == null){ + // Should we throw an exception here? + key = null; + value = null; + } + s.writeObject(key); + s.writeObject(value); + } + } + } + + private void readObject(ObjectInputStream s) + throws ClassNotFoundException, IOException + { + s.defaultReadObject(); + // Reconstruct the hashtable. + int numValues = s.readInt(); + valueConvertor = new Hashtable(Math.max(1, numValues)); + while (numValues-- > 0) { + Object key = s.readObject(); + Object value = s.readObject(); + Object staticKey = StyleContext.getStaticAttribute(key); + if (staticKey != null) { + key = staticKey; + } + Object staticValue = StyleContext.getStaticAttribute(value); + if (staticValue != null) { + value = staticValue; + } + if (key != null && value != null) { + valueConvertor.put(key, value); + } + } + } + + + /* + * we need StyleSheet for resolving lenght units. (see + * isW3CLengthUnits) + * we can not pass stylesheet for handling relative sizes. (do not + * think changing public API is necessary) + * CSS is not likely to be accessed from more then one thread. + * Having local storage for StyleSheet for resolving relative + * sizes is safe + * + * idk 08/30/2004 + */ + private StyleSheet getStyleSheet(StyleSheet ss) { + if (ss != null) { + styleSheet = ss; + } + return styleSheet; + } + // + // Instance variables + // + + /** Maps from CSS key to CssValue. */ + private transient Hashtable valueConvertor; + + /** Size used for relative units. */ + private int baseFontSize; + + private transient StyleSheet styleSheet = null; + + static int baseFontSizeIndex = 3; +} diff --git a/src/share/classes/javax/swing/text/html/CSSBorder.java b/src/share/classes/javax/swing/text/html/CSSBorder.java new file mode 100644 index 000000000..a24afd7f6 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/CSSBorder.java @@ -0,0 +1,435 @@ +/* + * Copyright 2007 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.Shape; +import java.util.HashMap; +import java.util.Map; +import javax.swing.border.AbstractBorder; +import javax.swing.text.AttributeSet; +import javax.swing.text.View; +import javax.swing.text.html.CSS.Attribute; +import javax.swing.text.html.CSS.BorderStyle; +import javax.swing.text.html.CSS.BorderWidthValue; +import javax.swing.text.html.CSS.ColorValue; +import javax.swing.text.html.CSS.CssValue; +import javax.swing.text.html.CSS.LengthValue; +import javax.swing.text.html.CSS.Value; + +/** + * CSS-style borders for HTML elements. + * + * @author Sergey Groznyh + */ +class CSSBorder extends AbstractBorder { + + /** Indices for the attribute groups. */ + final static int COLOR = 0, STYLE = 1, WIDTH = 2; + + /** Indices for the box sides within the attribute group. */ + final static int TOP = 0, RIGHT = 1, BOTTOM = 2, LEFT = 3; + + /** The attribute groups. */ + final static Attribute[][] ATTRIBUTES = { + { Attribute.BORDER_TOP_COLOR, Attribute.BORDER_RIGHT_COLOR, + Attribute.BORDER_BOTTOM_COLOR, Attribute.BORDER_LEFT_COLOR, }, + { Attribute.BORDER_TOP_STYLE, Attribute.BORDER_RIGHT_STYLE, + Attribute.BORDER_BOTTOM_STYLE, Attribute.BORDER_LEFT_STYLE, }, + { Attribute.BORDER_TOP_WIDTH, Attribute.BORDER_RIGHT_WIDTH, + Attribute.BORDER_BOTTOM_WIDTH, Attribute.BORDER_LEFT_WIDTH, }, + }; + + /** Parsers for the border properties. */ + final static CssValue PARSERS[] = { + new ColorValue(), new BorderStyle(), new BorderWidthValue(null, 0), + }; + + /** Default values for the border properties. */ + final static Object[] DEFAULTS = { + Attribute.BORDER_COLOR, // marker: value will be computed on request + PARSERS[1].parseCssValue(Attribute.BORDER_STYLE.getDefaultValue()), + PARSERS[2].parseCssValue(Attribute.BORDER_WIDTH.getDefaultValue()), + }; + + /** Attribute set containing border properties. */ + final AttributeSet attrs; + + /** + * Initialize the attribute set. + */ + CSSBorder(AttributeSet attrs) { + this.attrs = attrs; + } + + /** + * Return the border color for the given side. + */ + private Color getBorderColor(int side) { + Object o = attrs.getAttribute(ATTRIBUTES[COLOR][side]); + ColorValue cv; + if (o instanceof ColorValue) { + cv = (ColorValue) o; + } else { + // Marker for the default value. Use 'color' property value as the + // computed value of the 'border-color' property (CSS2 8.5.2) + cv = (ColorValue) attrs.getAttribute(Attribute.COLOR); + if (cv == null) { + cv = (ColorValue) PARSERS[COLOR].parseCssValue( + Attribute.COLOR.getDefaultValue()); + } + } + return cv.getValue(); + } + + /** + * Return the border width for the given side. + */ + private int getBorderWidth(int side) { + int width = 0; + BorderStyle bs = (BorderStyle) attrs.getAttribute( + ATTRIBUTES[STYLE][side]); + if ((bs != null) && (bs.getValue() != Value.NONE)) { + // The 'border-style' value of "none" forces the computed value + // of 'border-width' to be 0 (CSS2 8.5.3) + LengthValue bw = (LengthValue) attrs.getAttribute( + ATTRIBUTES[WIDTH][side]); + if (bw == null) { + bw = (LengthValue) DEFAULTS[WIDTH]; + } + width = (int) bw.getValue(true); + } + return width; + } + + /** + * Return an array of border widths in the TOP, RIGHT, BOTTOM, LEFT order. + */ + private int[] getWidths() { + int[] widths = new int[4]; + for (int i = 0; i < widths.length; i++) { + widths[i] = getBorderWidth(i); + } + return widths; + } + + /** + * Return the border style for the given side. + */ + private Value getBorderStyle(int side) { + BorderStyle style = + (BorderStyle) attrs.getAttribute(ATTRIBUTES[STYLE][side]); + if (style == null) { + style = (BorderStyle) DEFAULTS[STYLE]; + } + return style.getValue(); + } + + /** + * Return border shape for {@code side} as if the border has zero interior + * length. Shape start is at (0,0); points are added clockwise. + */ + private Polygon getBorderShape(int side) { + Polygon shape = null; + int[] widths = getWidths(); + if (widths[side] != 0) { + shape = new Polygon(new int[4], new int[4], 0); + shape.addPoint(0, 0); + shape.addPoint(-widths[(side + 3) % 4], -widths[side]); + shape.addPoint(widths[(side + 1) % 4], -widths[side]); + shape.addPoint(0, 0); + } + return shape; + } + + /** + * Return the border painter appropriate for the given side. + */ + private BorderPainter getBorderPainter(int side) { + Value style = getBorderStyle(side); + return borderPainters.get(style); + } + + /** + * Return the color with brightness adjusted by the specified factor. + * + * The factor values are between 0.0 (no change) and 1.0 (turn into white). + * Negative factor values decrease brigthness (ie, 1.0 turns into black). + */ + static Color getAdjustedColor(Color c, double factor) { + double f = 1 - Math.min(Math.abs(factor), 1); + double inc = (factor > 0 ? 255 * (1 - f) : 0); + return new Color((int) (c.getRed() * f + inc), + (int) (c.getGreen() * f + inc), + (int) (c.getBlue() * f + inc)); + } + + + /* The javax.swing.border.Border methods. */ + + public Insets getBorderInsets(Component c, Insets insets) { + int[] widths = getWidths(); + insets.set(widths[TOP], widths[LEFT], widths[BOTTOM], widths[RIGHT]); + return insets; + } + + public void paintBorder(Component c, Graphics g, + int x, int y, int width, int height) { + assert (g instanceof Graphics2D) : "need Graphics2D instanse"; + Graphics2D g2 = (Graphics2D) g; + Color savedColor = g2.getColor(); + Shape savedClip = g2.getClip(); + + int[] widths = getWidths(); + + // Position and size of the border interior. + int intX = x + widths[LEFT]; + int intY = y + widths[TOP]; + int intWidth = width - (widths[RIGHT] + widths[LEFT]); + int intHeight = height - (widths[TOP] + widths[BOTTOM]); + + // Coordinates of the interior corners, from NW clockwise. + int[][] intCorners = { + { intX, intY }, + { intX + intWidth, intY }, + { intX + intWidth, intY + intHeight }, + { intX, intY + intHeight, }, + }; + + // Draw the borders for all sides. + for (int i = 0; i < 4; i++) { + Value style = getBorderStyle(i); + Polygon shape = getBorderShape(i); + if ((style != Value.NONE) && (shape != null)) { + int sideLength = (i % 2 == 0 ? intWidth : intHeight); + + // "stretch" the border shape by the interior area dimension + shape.xpoints[2] += sideLength; + shape.xpoints[3] += sideLength; + Color color = getBorderColor(i); + BorderPainter painter = getBorderPainter(i); + + double angle = i * Math.PI / 2; + g2.translate(intCorners[i][0], intCorners[i][1]); + g2.rotate(angle); + g2.setClip(shape); + painter.paint(shape, g, color, i); + g2.rotate(-angle); + g2.translate(-intCorners[i][0], -intCorners[i][1]); + } + } + g2.setColor(savedColor); + g2.setClip(savedClip); + } + + + /* Border painters. */ + + interface BorderPainter { + /** + * The painter should paint the border as if it were at the top and the + * coordinates of the NW corner of the interior area is (0, 0). The + * caller is responsible for the appropriate affine transformations. + * + * Clip is set by the caller to the exact border shape so it's safe to + * simply draw into the shape's bounding rectangle. + */ + void paint(Polygon shape, Graphics g, Color color, int side); + } + + /** + * Painter for the "none" and "hidden" CSS border styles. + */ + static class NullPainter implements BorderPainter { + public void paint(Polygon shape, Graphics g, Color color, int side) { + // Do nothing. + } + } + + /** + * Painter for the "solid" CSS border style. + */ + static class SolidPainter implements BorderPainter { + public void paint(Polygon shape, Graphics g, Color color, int side) { + g.setColor(color); + g.fillPolygon(shape); + } + } + + /** + * Defines a method for painting strokes in the specified direction using + * the given length and color patterns. + */ + abstract static class StrokePainter implements BorderPainter { + /** + * Paint strokes repeatedly using the given length and color patterns. + */ + void paintStrokes(Rectangle r, Graphics g, int axis, + int[] lengthPattern, Color[] colorPattern) { + boolean xAxis = (axis == View.X_AXIS); + int start = 0; + int end = (xAxis ? r.width : r.height); + while (start < end) { + for (int i = 0; i < lengthPattern.length; i++) { + if (start >= end) { + break; + } + int length = lengthPattern[i]; + Color c = colorPattern[i]; + if (c != null) { + int x = r.x + (xAxis ? start : 0); + int y = r.y + (xAxis ? 0 : start); + int width = xAxis ? length : r.width; + int height = xAxis ? r.height : length; + g.setColor(c); + g.fillRect(x, y, width, height); + } + start += length; + } + } + } + } + + /** + * Painter for the "double" CSS border style. + */ + static class DoublePainter extends StrokePainter { + public void paint(Polygon shape, Graphics g, Color color, int side) { + Rectangle r = shape.getBounds(); + int length = Math.max(r.height / 3, 1); + int[] lengthPattern = { length, length }; + Color[] colorPattern = { color, null }; + paintStrokes(r, g, View.Y_AXIS, lengthPattern, colorPattern); + } + } + + /** + * Painter for the "dotted" and "dashed" CSS border styles. + */ + static class DottedDashedPainter extends StrokePainter { + final int factor; + + DottedDashedPainter(int factor) { + this.factor = factor; + } + + public void paint(Polygon shape, Graphics g, Color color, int side) { + Rectangle r = shape.getBounds(); + int length = r.height * factor; + int[] lengthPattern = { length, length }; + Color[] colorPattern = { color, null }; + paintStrokes(r, g, View.X_AXIS, lengthPattern, colorPattern); + } + } + + /** + * Painter that defines colors for "shadow" and "light" border sides. + */ + abstract static class ShadowLightPainter extends StrokePainter { + /** + * Return the "shadow" border side color. + */ + static Color getShadowColor(Color c) { + return CSSBorder.getAdjustedColor(c, -0.3); + } + + /** + * Return the "light" border side color. + */ + static Color getLightColor(Color c) { + return CSSBorder.getAdjustedColor(c, 0.7); + } + } + + /** + * Painter for the "groove" and "ridge" CSS border styles. + */ + static class GrooveRidgePainter extends ShadowLightPainter { + final Value type; + + GrooveRidgePainter(Value type) { + this.type = type; + } + + public void paint(Polygon shape, Graphics g, Color color, int side) { + Rectangle r = shape.getBounds(); + int length = Math.max(r.height / 2, 1); + int[] lengthPattern = { length, length }; + Color[] colorPattern = + ((side + 1) % 4 < 2) == (type == Value.GROOVE) ? + new Color[] { getShadowColor(color), getLightColor(color) } : + new Color[] { getLightColor(color), getShadowColor(color) }; + paintStrokes(r, g, View.Y_AXIS, lengthPattern, colorPattern); + } + } + + /** + * Painter for the "inset" and "outset" CSS border styles. + */ + static class InsetOutsetPainter extends ShadowLightPainter { + Value type; + + InsetOutsetPainter(Value type) { + this.type = type; + } + + public void paint(Polygon shape, Graphics g, Color color, int side) { + g.setColor(((side + 1) % 4 < 2) == (type == Value.INSET) ? + getShadowColor(color) : getLightColor(color)); + g.fillPolygon(shape); + } + } + + /** + * Add the specified painter to the painters map. + */ + static void registerBorderPainter(Value style, BorderPainter painter) { + borderPainters.put(style, painter); + } + + /** Map the border style values to the border painter objects. */ + static Map borderPainters = + new HashMap(); + + /* Initialize the border painters map with the pre-defined values. */ + static { + registerBorderPainter(Value.NONE, new NullPainter()); + registerBorderPainter(Value.HIDDEN, new NullPainter()); + registerBorderPainter(Value.SOLID, new SolidPainter()); + registerBorderPainter(Value.DOUBLE, new DoublePainter()); + registerBorderPainter(Value.DOTTED, new DottedDashedPainter(1)); + registerBorderPainter(Value.DASHED, new DottedDashedPainter(3)); + registerBorderPainter(Value.GROOVE, new GrooveRidgePainter(Value.GROOVE)); + registerBorderPainter(Value.RIDGE, new GrooveRidgePainter(Value.RIDGE)); + registerBorderPainter(Value.INSET, new InsetOutsetPainter(Value.INSET)); + registerBorderPainter(Value.OUTSET, new InsetOutsetPainter(Value.OUTSET)); + } +} diff --git a/src/share/classes/javax/swing/text/html/CSSParser.java b/src/share/classes/javax/swing/text/html/CSSParser.java new file mode 100644 index 000000000..00251a740 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/CSSParser.java @@ -0,0 +1,854 @@ +/* + * Copyright 1999-2000 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.io.*; + +/** + * A CSS parser. This works by way of a delegate that implements the + * CSSParserCallback interface. The delegate is notified of the following + * events: + *

      + *
    • Import statement: handleImport + *
    • Selectors handleSelector. This is invoked for each + * string. For example if the Reader contained p, bar , a {}, the delegate + * would be notified 4 times, for 'p,' 'bar' ',' and 'a'. + *
    • When a rule starts, startRule + *
    • Properties in the rule via the handleProperty. This + * is invoked one per property/value key, eg font size: foo;, would + * cause the delegate to be notified once with a value of 'font size'. + *
    • Values in the rule via the handleValue, this is notified + * for the total value. + *
    • When a rule ends, endRule + *
    + * This will parse much more than CSS 1, and loosely implements the + * recommendation for Forward-compatible parsing in section + * 7.1 of the CSS spec found at: + * http://www.w3.org/TR/REC-CSS1. + * If an error results in parsing, a RuntimeException will be thrown. + *

    + * This will preserve case. If the callback wishes to treat certain poritions + * case insensitively (such as selectors), it should use toLowerCase, or + * something similar. + * + * @author Scott Violet + */ +class CSSParser { + // Parsing something like the following: + // (@rule | ruleset | block)* + // + // @rule (block | identifier)*; (block with {} ends @rule) + // block matching [] () {} (that is, [()] is a block, [(){}{[]}] + // is a block, ()[] is two blocks) + // identifier "*" | '*' | anything but a [](){} and whitespace + // + // ruleset selector decblock + // selector (identifier | (block, except block '{}') )* + // declblock declaration* block* + // declaration (identifier* stopping when identifier ends with :) + // (identifier* stopping when identifier ends with ;) + // + // comments /* */ can appear any where, and are stripped. + + + // identifier - letters, digits, dashes and escaped characters + // block starts with { ends with matching }, () [] and {} always occur + // in matching pairs, '' and "" also occur in pairs, except " may be + + + // Indicates the type of token being parsed. + private static final int IDENTIFIER = 1; + private static final int BRACKET_OPEN = 2; + private static final int BRACKET_CLOSE = 3; + private static final int BRACE_OPEN = 4; + private static final int BRACE_CLOSE = 5; + private static final int PAREN_OPEN = 6; + private static final int PAREN_CLOSE = 7; + private static final int END = -1; + + private static final char[] charMapping = { 0, 0, '[', ']', '{', '}', '(', + ')', 0}; + + + /** Set to true if one character has been read ahead. */ + private boolean didPushChar; + /** The read ahead character. */ + private int pushedChar; + /** Temporary place to hold identifiers. */ + private StringBuffer unitBuffer; + /** Used to indicate blocks. */ + private int[] unitStack; + /** Number of valid blocks. */ + private int stackCount; + /** Holds the incoming CSS rules. */ + private Reader reader; + /** Set to true when the first non @ rule is encountered. */ + private boolean encounteredRuleSet; + /** Notified of state. */ + private CSSParserCallback callback; + /** nextToken() inserts the string here. */ + private char[] tokenBuffer; + /** Current number of chars in tokenBufferLength. */ + private int tokenBufferLength; + /** Set to true if any whitespace is read. */ + private boolean readWS; + + + // The delegate interface. + static interface CSSParserCallback { + /** Called when an @import is encountered. */ + void handleImport(String importString); + // There is currently no way to distinguish between '"foo,"' and + // 'foo,'. But this generally isn't valid CSS. If it becomes + // a problem, handleSelector will have to be told if the string is + // quoted. + void handleSelector(String selector); + void startRule(); + // Property names are mapped to lower case before being passed to + // the delegate. + void handleProperty(String property); + void handleValue(String value); + void endRule(); + } + + CSSParser() { + unitStack = new int[2]; + tokenBuffer = new char[80]; + unitBuffer = new StringBuffer(); + } + + void parse(Reader reader, CSSParserCallback callback, + boolean inRule) throws IOException { + this.callback = callback; + stackCount = tokenBufferLength = 0; + this.reader = reader; + encounteredRuleSet = false; + try { + if (inRule) { + parseDeclarationBlock(); + } + else { + while (getNextStatement()); + } + } finally { + callback = null; + reader = null; + } + } + + /** + * Gets the next statement, returning false if the end is reached. A + * statement is either an @rule, or a ruleset. + */ + private boolean getNextStatement() throws IOException { + unitBuffer.setLength(0); + + int token = nextToken((char)0); + + switch (token) { + case IDENTIFIER: + if (tokenBufferLength > 0) { + if (tokenBuffer[0] == '@') { + parseAtRule(); + } + else { + encounteredRuleSet = true; + parseRuleSet(); + } + } + return true; + case BRACKET_OPEN: + case BRACE_OPEN: + case PAREN_OPEN: + parseTillClosed(token); + return true; + + case BRACKET_CLOSE: + case BRACE_CLOSE: + case PAREN_CLOSE: + // Shouldn't happen... + throw new RuntimeException("Unexpected top level block close"); + + case END: + return false; + } + return true; + } + + /** + * Parses an @ rule, stopping at a matching brace pair, or ;. + */ + private void parseAtRule() throws IOException { + // PENDING: make this more effecient. + boolean done = false; + boolean isImport = (tokenBufferLength == 7 && + tokenBuffer[0] == '@' && tokenBuffer[1] == 'i' && + tokenBuffer[2] == 'm' && tokenBuffer[3] == 'p' && + tokenBuffer[4] == 'o' && tokenBuffer[5] == 'r' && + tokenBuffer[6] == 't'); + + unitBuffer.setLength(0); + while (!done) { + int nextToken = nextToken(';'); + + switch (nextToken) { + case IDENTIFIER: + if (tokenBufferLength > 0 && + tokenBuffer[tokenBufferLength - 1] == ';') { + --tokenBufferLength; + done = true; + } + if (tokenBufferLength > 0) { + if (unitBuffer.length() > 0 && readWS) { + unitBuffer.append(' '); + } + unitBuffer.append(tokenBuffer, 0, tokenBufferLength); + } + break; + + case BRACE_OPEN: + if (unitBuffer.length() > 0 && readWS) { + unitBuffer.append(' '); + } + unitBuffer.append(charMapping[nextToken]); + parseTillClosed(nextToken); + done = true; + // Skip a tailing ';', not really to spec. + { + int nextChar = readWS(); + if (nextChar != -1 && nextChar != ';') { + pushChar(nextChar); + } + } + break; + + case BRACKET_OPEN: case PAREN_OPEN: + unitBuffer.append(charMapping[nextToken]); + parseTillClosed(nextToken); + break; + + case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE: + throw new RuntimeException("Unexpected close in @ rule"); + + case END: + done = true; + break; + } + } + if (isImport && !encounteredRuleSet) { + callback.handleImport(unitBuffer.toString()); + } + } + + /** + * Parses the next rule set, which is a selector followed by a + * declaration block. + */ + private void parseRuleSet() throws IOException { + if (parseSelectors()) { + callback.startRule(); + parseDeclarationBlock(); + callback.endRule(); + } + } + + /** + * Parses a set of selectors, returning false if the end of the stream + * is reached. + */ + private boolean parseSelectors() throws IOException { + // Parse the selectors + int nextToken; + + if (tokenBufferLength > 0) { + callback.handleSelector(new String(tokenBuffer, 0, + tokenBufferLength)); + } + + unitBuffer.setLength(0); + for (;;) { + while ((nextToken = nextToken((char)0)) == IDENTIFIER) { + if (tokenBufferLength > 0) { + callback.handleSelector(new String(tokenBuffer, 0, + tokenBufferLength)); + } + } + switch (nextToken) { + case BRACE_OPEN: + return true; + + case BRACKET_OPEN: case PAREN_OPEN: + parseTillClosed(nextToken); + // Not too sure about this, how we handle this isn't very + // well spec'd. + unitBuffer.setLength(0); + break; + + case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE: + throw new RuntimeException("Unexpected block close in selector"); + + case END: + // Prematurely hit end. + return false; + } + } + } + + /** + * Parses a declaration block. Which a number of declarations followed + * by a })]. + */ + private void parseDeclarationBlock() throws IOException { + for (;;) { + int token = parseDeclaration(); + switch (token) { + case END: case BRACE_CLOSE: + return; + + case BRACKET_CLOSE: case PAREN_CLOSE: + // Bail + throw new RuntimeException("Unexpected close in declaration block"); + case IDENTIFIER: + break; + } + } + } + + /** + * Parses a single declaration, which is an identifier a : and another + * identifier. This returns the last token seen. + */ + // identifier+: identifier* ;|} + private int parseDeclaration() throws IOException { + int token; + + if ((token = parseIdentifiers(':', false)) != IDENTIFIER) { + return token; + } + // Make the property name to lowercase + for (int counter = unitBuffer.length() - 1; counter >= 0; counter--) { + unitBuffer.setCharAt(counter, Character.toLowerCase + (unitBuffer.charAt(counter))); + } + callback.handleProperty(unitBuffer.toString()); + + token = parseIdentifiers(';', true); + callback.handleValue(unitBuffer.toString()); + return token; + } + + /** + * Parses identifiers until extraChar is encountered, + * returning the ending token, which will be IDENTIFIER if extraChar + * is found. + */ + private int parseIdentifiers(char extraChar, + boolean wantsBlocks) throws IOException { + int nextToken; + int ubl; + + unitBuffer.setLength(0); + for (;;) { + nextToken = nextToken(extraChar); + + switch (nextToken) { + case IDENTIFIER: + if (tokenBufferLength > 0) { + if (tokenBuffer[tokenBufferLength - 1] == extraChar) { + if (--tokenBufferLength > 0) { + if (readWS && unitBuffer.length() > 0) { + unitBuffer.append(' '); + } + unitBuffer.append(tokenBuffer, 0, + tokenBufferLength); + } + return IDENTIFIER; + } + if (readWS && unitBuffer.length() > 0) { + unitBuffer.append(' '); + } + unitBuffer.append(tokenBuffer, 0, tokenBufferLength); + } + break; + + case BRACKET_OPEN: + case BRACE_OPEN: + case PAREN_OPEN: + ubl = unitBuffer.length(); + if (wantsBlocks) { + unitBuffer.append(charMapping[nextToken]); + } + parseTillClosed(nextToken); + if (!wantsBlocks) { + unitBuffer.setLength(ubl); + } + break; + + case BRACE_CLOSE: + // No need to throw for these two, we return token and + // caller can do whatever. + case BRACKET_CLOSE: + case PAREN_CLOSE: + case END: + // Hit the end + return nextToken; + } + } + } + + /** + * Parses till a matching block close is encountered. This is only + * appropriate to be called at the top level (no nesting). + */ + private void parseTillClosed(int openToken) throws IOException { + int nextToken; + boolean done = false; + + startBlock(openToken); + while (!done) { + nextToken = nextToken((char)0); + switch (nextToken) { + case IDENTIFIER: + if (unitBuffer.length() > 0 && readWS) { + unitBuffer.append(' '); + } + if (tokenBufferLength > 0) { + unitBuffer.append(tokenBuffer, 0, tokenBufferLength); + } + break; + + case BRACKET_OPEN: case BRACE_OPEN: case PAREN_OPEN: + if (unitBuffer.length() > 0 && readWS) { + unitBuffer.append(' '); + } + unitBuffer.append(charMapping[nextToken]); + startBlock(nextToken); + break; + + case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE: + if (unitBuffer.length() > 0 && readWS) { + unitBuffer.append(' '); + } + unitBuffer.append(charMapping[nextToken]); + endBlock(nextToken); + if (!inBlock()) { + done = true; + } + break; + + case END: + // Prematurely hit end. + throw new RuntimeException("Unclosed block"); + } + } + } + + /** + * Fetches the next token. + */ + private int nextToken(char idChar) throws IOException { + readWS = false; + + int nextChar = readWS(); + + switch (nextChar) { + case '\'': + readTill('\''); + if (tokenBufferLength > 0) { + tokenBufferLength--; + } + return IDENTIFIER; + case '"': + readTill('"'); + if (tokenBufferLength > 0) { + tokenBufferLength--; + } + return IDENTIFIER; + case '[': + return BRACKET_OPEN; + case ']': + return BRACKET_CLOSE; + case '{': + return BRACE_OPEN; + case '}': + return BRACE_CLOSE; + case '(': + return PAREN_OPEN; + case ')': + return PAREN_CLOSE; + case -1: + return END; + default: + pushChar(nextChar); + getIdentifier(idChar); + return IDENTIFIER; + } + } + + /** + * Gets an identifier, returning true if the length of the string is greater than 0, + * stopping when stopChar, whitespace, or one of {}()[] is + * hit. + */ + // NOTE: this could be combined with readTill, as they contain somewhat + // similiar functionality. + private boolean getIdentifier(char stopChar) throws IOException { + boolean lastWasEscape = false; + boolean done = false; + int escapeCount = 0; + int escapeChar = 0; + int nextChar; + int intStopChar = (int)stopChar; + // 1 for '\', 2 for valid escape char [0-9a-fA-F], 3 for + // stop character (white space, ()[]{}) 0 otherwise + short type; + int escapeOffset = 0; + + tokenBufferLength = 0; + while (!done) { + nextChar = readChar(); + switch (nextChar) { + case '\\': + type = 1; + break; + + case '0': case '1': case '2': case '3': case '4': case '5': + case '6': case '7': case '8': case '9': + type = 2; + escapeOffset = nextChar - '0'; + break; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + type = 2; + escapeOffset = nextChar - 'a' + 10; + break; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + type = 2; + escapeOffset = nextChar - 'A' + 10; + break; + + case '\'': case '"': case '[': case ']': case '{': case '}': + case '(': case ')': + case ' ': case '\n': case '\t': case '\r': + type = 3; + break; + + case '/': + type = 4; + break; + + case -1: + // Reached the end + done = true; + type = 0; + break; + + default: + type = 0; + break; + } + if (lastWasEscape) { + if (type == 2) { + // Continue with escape. + escapeChar = escapeChar * 16 + escapeOffset; + if (++escapeCount == 4) { + lastWasEscape = false; + append((char)escapeChar); + } + } + else { + // no longer escaped + lastWasEscape = false; + if (escapeCount > 0) { + append((char)escapeChar); + // Make this simpler, reprocess the character. + pushChar(nextChar); + } + else if (!done) { + append((char)nextChar); + } + } + } + else if (!done) { + if (type == 1) { + lastWasEscape = true; + escapeChar = escapeCount = 0; + } + else if (type == 3) { + done = true; + pushChar(nextChar); + } + else if (type == 4) { + // Potential comment + nextChar = readChar(); + if (nextChar == '*') { + done = true; + readComment(); + readWS = true; + } + else { + append('/'); + if (nextChar == -1) { + done = true; + } + else { + pushChar(nextChar); + } + } + } + else { + append((char)nextChar); + if (nextChar == intStopChar) { + done = true; + } + } + } + } + return (tokenBufferLength > 0); + } + + /** + * Reads till a stopChar is encountered, escaping characters + * as necessary. + */ + private void readTill(char stopChar) throws IOException { + boolean lastWasEscape = false; + int escapeCount = 0; + int escapeChar = 0; + int nextChar; + boolean done = false; + int intStopChar = (int)stopChar; + // 1 for '\', 2 for valid escape char [0-9a-fA-F], 0 otherwise + short type; + int escapeOffset = 0; + + tokenBufferLength = 0; + while (!done) { + nextChar = readChar(); + switch (nextChar) { + case '\\': + type = 1; + break; + + case '0': case '1': case '2': case '3': case '4':case '5': + case '6': case '7': case '8': case '9': + type = 2; + escapeOffset = nextChar - '0'; + break; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + type = 2; + escapeOffset = nextChar - 'a' + 10; + break; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + type = 2; + escapeOffset = nextChar - 'A' + 10; + break; + + case -1: + // Prematurely reached the end! + throw new RuntimeException("Unclosed " + stopChar); + + default: + type = 0; + break; + } + if (lastWasEscape) { + if (type == 2) { + // Continue with escape. + escapeChar = escapeChar * 16 + escapeOffset; + if (++escapeCount == 4) { + lastWasEscape = false; + append((char)escapeChar); + } + } + else { + // no longer escaped + if (escapeCount > 0) { + append((char)escapeChar); + if (type == 1) { + lastWasEscape = true; + escapeChar = escapeCount = 0; + } + else { + if (nextChar == intStopChar) { + done = true; + } + append((char)nextChar); + lastWasEscape = false; + } + } + else { + append((char)nextChar); + lastWasEscape = false; + } + } + } + else if (type == 1) { + lastWasEscape = true; + escapeChar = escapeCount = 0; + } + else { + if (nextChar == intStopChar) { + done = true; + } + append((char)nextChar); + } + } + } + + private void append(char character) { + if (tokenBufferLength == tokenBuffer.length) { + char[] newBuffer = new char[tokenBuffer.length * 2]; + System.arraycopy(tokenBuffer, 0, newBuffer, 0, tokenBuffer.length); + tokenBuffer = newBuffer; + } + tokenBuffer[tokenBufferLength++] = character; + } + + /** + * Parses a comment block. + */ + private void readComment() throws IOException { + int nextChar; + + for(;;) { + nextChar = readChar(); + switch (nextChar) { + case -1: + throw new RuntimeException("Unclosed comment"); + case '*': + nextChar = readChar(); + if (nextChar == '/') { + return; + } + else if (nextChar == -1) { + throw new RuntimeException("Unclosed comment"); + } + else { + pushChar(nextChar); + } + break; + default: + break; + } + } + } + + /** + * Called when a block start is encountered ({[. + */ + private void startBlock(int startToken) { + if (stackCount == unitStack.length) { + int[] newUS = new int[stackCount * 2]; + + System.arraycopy(unitStack, 0, newUS, 0, stackCount); + unitStack = newUS; + } + unitStack[stackCount++] = startToken; + } + + /** + * Called when an end block is encountered )]} + */ + private void endBlock(int endToken) { + int startToken; + + switch (endToken) { + case BRACKET_CLOSE: + startToken = BRACKET_OPEN; + break; + case BRACE_CLOSE: + startToken = BRACE_OPEN; + break; + case PAREN_CLOSE: + startToken = PAREN_OPEN; + break; + default: + // Will never happen. + startToken = -1; + break; + } + if (stackCount > 0 && unitStack[stackCount - 1] == startToken) { + stackCount--; + } + else { + // Invalid state, should do something. + throw new RuntimeException("Unmatched block"); + } + } + + /** + * @return true if currently in a block. + */ + private boolean inBlock() { + return (stackCount > 0); + } + + /** + * Skips any white space, returning the character after the white space. + */ + private int readWS() throws IOException { + int nextChar; + while ((nextChar = readChar()) != -1 && + Character.isWhitespace((char)nextChar)) { + readWS = true; + } + return nextChar; + } + + /** + * Reads a character from the stream. + */ + private int readChar() throws IOException { + if (didPushChar) { + didPushChar = false; + return pushedChar; + } + return reader.read(); + // Uncomment the following to do case insensitive parsing. + /* + if (retValue != -1) { + return (int)Character.toLowerCase((char)retValue); + } + return retValue; + */ + } + + /** + * Supports one character look ahead, this will throw if called twice + * in a row. + */ + private void pushChar(int tempChar) { + if (didPushChar) { + // Should never happen. + throw new RuntimeException("Can not handle look ahead of more than one character"); + } + didPushChar = true; + pushedChar = tempChar; + } +} diff --git a/src/share/classes/javax/swing/text/html/CommentView.java b/src/share/classes/javax/swing/text/html/CommentView.java new file mode 100644 index 000000000..54e1c1c6b --- /dev/null +++ b/src/share/classes/javax/swing/text/html/CommentView.java @@ -0,0 +1,141 @@ +/* + * Copyright 1998-2004 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +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 java.util.*; + +/** + * CommentView subclasses HiddenTagView to contain a JTextArea showing + * a comment. When the textarea is edited the comment is + * reset. As this inherits from EditableView if the JTextComponent is + * not editable, the textarea will not be visible. + * + * @author Scott Violet + */ +class CommentView extends HiddenTagView { + CommentView(Element e) { + super(e); + } + + protected Component createComponent() { + Container host = getContainer(); + if (host != null && !((JTextComponent)host).isEditable()) { + return null; + } + JTextArea ta = new JTextArea(getRepresentedText()); + Document doc = getDocument(); + Font font; + if (doc instanceof StyledDocument) { + font = ((StyledDocument)doc).getFont(getAttributes()); + ta.setFont(font); + } + else { + font = ta.getFont(); + } + updateYAlign(font); + ta.setBorder(CBorder); + ta.getDocument().addDocumentListener(this); + ta.setFocusable(isVisible()); + return ta; + } + + void resetBorder() { + } + + /** + * This is subclassed to put the text on the Comment attribute of + * the Element's AttributeSet. + */ + void _updateModelFromText() { + JTextComponent textC = getTextComponent(); + Document doc = getDocument(); + if (textC != null && doc != null) { + String text = textC.getText(); + SimpleAttributeSet sas = new SimpleAttributeSet(); + isSettingAttributes = true; + try { + sas.addAttribute(HTML.Attribute.COMMENT, text); + ((StyledDocument)doc).setCharacterAttributes + (getStartOffset(), getEndOffset() - + getStartOffset(), sas, false); + } + finally { + isSettingAttributes = false; + } + } + } + + JTextComponent getTextComponent() { + return (JTextComponent)getComponent(); + } + + String getRepresentedText() { + AttributeSet as = getElement().getAttributes(); + if (as != null) { + Object comment = as.getAttribute(HTML.Attribute.COMMENT); + if (comment instanceof String) { + return (String)comment; + } + } + return ""; + } + + static final Border CBorder = new CommentBorder(); + static final int commentPadding = 3; + static final int commentPaddingD = commentPadding * 3; + + static class CommentBorder extends LineBorder { + CommentBorder() { + super(Color.black, 1); + } + + public void paintBorder(Component c, Graphics g, int x, int y, + int width, int height) { + super.paintBorder(c, g, x + commentPadding, y, + width - commentPaddingD, height); + } + + public Insets getBorderInsets(Component c, Insets insets) { + Insets retI = super.getBorderInsets(c, insets); + + retI.left += commentPadding; + retI.right += commentPadding; + return retI; + } + + public boolean isBorderOpaque() { + return false; + } + } // End of class CommentView.CommentBorder +} // End of CommentView diff --git a/src/share/classes/javax/swing/text/html/EditableView.java b/src/share/classes/javax/swing/text/html/EditableView.java new file mode 100644 index 000000000..b1958bafa --- /dev/null +++ b/src/share/classes/javax/swing/text/html/EditableView.java @@ -0,0 +1,128 @@ +/* + * Copyright 1998-2004 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +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 java.util.*; + +/** + * EditableView sets the view it contains to be visible only when the + * JTextComponent the view is contained in is editable. The min/pref/max + * size is 0 when not visible. + * + * @author Scott Violet + */ +class EditableView extends ComponentView { + + EditableView(Element e) { + super(e); + } + + public float getMinimumSpan(int axis) { + if (isVisible) { + return super.getMinimumSpan(axis); + } + return 0; + } + + public float getPreferredSpan(int axis) { + if (isVisible) { + return super.getPreferredSpan(axis); + } + return 0; + } + + public float getMaximumSpan(int axis) { + if (isVisible) { + return super.getMaximumSpan(axis); + } + return 0; + } + + public void paint(Graphics g, Shape allocation) { + Component c = getComponent(); + Container host = getContainer(); + + if (host != null && + isVisible != ((JTextComponent)host).isEditable()) { + isVisible = ((JTextComponent)host).isEditable(); + preferenceChanged(null, true, true); + host.repaint(); + } + /* + * Note: we cannot tweak the visible state of the + * component in createComponent() even though it + * gets called after the setParent() call where + * the value of the boolean is set. This + * because, the setComponentParent() in the + * superclass, always does a setVisible(false) + * after calling createComponent(). We therefore + * use this flag in the paint() method to + * setVisible() to true if required. + */ + if (isVisible) { + super.paint(g, allocation); + } + else { + setSize(0, 0); + } + if (c != null) { + c.setFocusable(isVisible); + } + } + + public void setParent(View parent) { + if (parent != null) { + Container host = parent.getContainer(); + if (host != null) { + if (host instanceof JTextComponent) { + isVisible = ((JTextComponent)host).isEditable(); + } else { + isVisible = false; + } + } + } + super.setParent(parent); + } + + /** + * @return true if the Component is visible. + */ + public boolean isVisible() { + return isVisible; + } + + /** Set to true if the component is visible. This is based off the + * editability of the container. */ + private boolean isVisible; +} // End of EditableView diff --git a/src/share/classes/javax/swing/text/html/FormSubmitEvent.java b/src/share/classes/javax/swing/text/html/FormSubmitEvent.java new file mode 100644 index 000000000..bc0925207 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/FormSubmitEvent.java @@ -0,0 +1,92 @@ +/* + * Copyright 2003-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import javax.swing.text.*; +import java.net.URL; + +/** + * FormSubmitEvent is used to notify interested + * parties that a form was submited. + * + * @since 1.5 + * @author Denis Sharypov + */ + +public class FormSubmitEvent extends HTMLFrameHyperlinkEvent { + + /** + * Represents an HTML form method type. + *

      + *
    • GET corresponds to the GET form method
    • + *
    • POST corresponds to the POST from method
    • + *
    + * @since 1.5 + */ + public enum MethodType { GET, POST }; + + /** + * Creates a new object representing an html form submit event. + * + * @param source the object responsible for the event + * @param type the event type + * @param actionURL the form action URL + * @param sourceElement the element that corresponds to the source + * of the event + * @param targetFrame the Frame to display the document in + * @param method the form method type + * @param data the form submission data + */ + FormSubmitEvent(Object source, EventType type, URL targetURL, + Element sourceElement, String targetFrame, + MethodType method, String data) { + super(source, type, targetURL, sourceElement, targetFrame); + this.method = method; + this.data = data; + } + + + /** + * Gets the form method type. + * + * @return the form method type, either + * Method.GET or Method.POST. + */ + public MethodType getMethod() { + return method; + } + + /** + * Gets the form submission data. + * + * @return the string representing the form submission data. + */ + public String getData() { + return data; + } + + private MethodType method; + private String data; +} diff --git a/src/share/classes/javax/swing/text/html/FormView.java b/src/share/classes/javax/swing/text/html/FormView.java new file mode 100644 index 000000000..d2a6d081f --- /dev/null +++ b/src/share/classes/javax/swing/text/html/FormView.java @@ -0,0 +1,932 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.net.*; +import java.io.*; +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.text.*; + +/** + * Component decorator that implements the view interface + * for form elements, <input>, <textarea>, + * and <select>. The model for the component is stored + * as an attribute of the the element (using StyleConstants.ModelAttribute), + * and is used to build the component of the view. The type + * of the model is assumed to of the type that would be set by + * HTMLDocument.HTMLReader.FormAction. If there are + * multiple views mapped over the document, they will share the + * embedded component models. + *

    + * The following table shows what components get built + * by this view. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Element TypeComponent built
    input, type buttonJButton
    input, type checkboxJCheckBox
    input, type imageJButton
    input, type passwordJPasswordField
    input, type radioJRadioButton
    input, type resetJButton
    input, type submitJButton
    input, type textJTextField
    select, size > 1 or multiple attribute definedJList in a JScrollPane
    select, size unspecified or 1JComboBox
    textareaJTextArea in a JScrollPane
    input, type fileJTextField
    + * + * @author Timothy Prinzing + * @author Sunita Mani + */ +public class FormView extends ComponentView implements ActionListener { + + /** + * If a value attribute is not specified for a FORM input element + * of type "submit", then this default string is used. + * + * @deprecated As of 1.3, value now comes from UIManager property + * FormView.submitButtonText + */ + @Deprecated + public static final String SUBMIT = new String("Submit Query"); + /** + * If a value attribute is not specified for a FORM input element + * of type "reset", then this default string is used. + * + * @deprecated As of 1.3, value comes from UIManager UIManager property + * FormView.resetButtonText + */ + @Deprecated + public static final String RESET = new String("Reset"); + + /** + * Document attribute name for storing POST data. JEditorPane.getPostData() + * uses the same name, should be kept in sync. + */ + final static String PostDataProperty = "javax.swing.JEditorPane.postdata"; + + /** + * Used to indicate if the maximum span should be the same as the + * preferred span. This is used so that the Component's size doesn't + * change if there is extra room on a line. The first bit is used for + * the X direction, and the second for the y direction. + */ + private short maxIsPreferred; + + /** + * Creates a new FormView object. + * + * @param elem the element to decorate + */ + public FormView(Element elem) { + super(elem); + } + + /** + * Create the component. This is basically a + * big switch statement based upon the tag type + * and html attributes of the associated element. + */ + protected Component createComponent() { + AttributeSet attr = getElement().getAttributes(); + HTML.Tag t = (HTML.Tag) + attr.getAttribute(StyleConstants.NameAttribute); + JComponent c = null; + Object model = attr.getAttribute(StyleConstants.ModelAttribute); + if (t == HTML.Tag.INPUT) { + c = createInputComponent(attr, model); + } else if (t == HTML.Tag.SELECT) { + + if (model instanceof OptionListModel) { + + JList list = new JList((ListModel) model); + int size = HTML.getIntegerAttributeValue(attr, + HTML.Attribute.SIZE, + 1); + list.setVisibleRowCount(size); + list.setSelectionModel((ListSelectionModel)model); + c = new JScrollPane(list); + } else { + c = new JComboBox((ComboBoxModel) model); + maxIsPreferred = 3; + } + } else if (t == HTML.Tag.TEXTAREA) { + JTextArea area = new JTextArea((Document) model); + int rows = HTML.getIntegerAttributeValue(attr, + HTML.Attribute.ROWS, + 1); + area.setRows(rows); + int cols = HTML.getIntegerAttributeValue(attr, + HTML.Attribute.COLS, + 20); + maxIsPreferred = 3; + area.setColumns(cols); + c = new JScrollPane(area, + JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } + + if (c != null) { + c.setAlignmentY(1.0f); + } + return c; + } + + + /** + * Creates a component for an <INPUT> element based on the + * value of the "type" attribute. + * + * @param set of attributes associated with the <INPUT> element. + * @param model the value of the StyleConstants.ModelAttribute + * @return the component. + */ + private JComponent createInputComponent(AttributeSet attr, Object model) { + JComponent c = null; + String type = (String) attr.getAttribute(HTML.Attribute.TYPE); + + if (type.equals("submit") || type.equals("reset")) { + String value = (String) + attr.getAttribute(HTML.Attribute.VALUE); + if (value == null) { + if (type.equals("submit")) { + value = UIManager.getString("FormView.submitButtonText"); + } else { + value = UIManager.getString("FormView.resetButtonText"); + } + } + JButton button = new JButton(value); + if (model != null) { + button.setModel((ButtonModel)model); + button.addActionListener(this); + } + c = button; + maxIsPreferred = 3; + } else if (type.equals("image")) { + String srcAtt = (String) attr.getAttribute(HTML.Attribute.SRC); + JButton button; + try { + URL base = ((HTMLDocument)getElement().getDocument()).getBase(); + URL srcURL = new URL(base, srcAtt); + Icon icon = new ImageIcon(srcURL); + button = new JButton(icon); + } catch (MalformedURLException e) { + button = new JButton(srcAtt); + } + if (model != null) { + button.setModel((ButtonModel)model); + button.addMouseListener(new MouseEventListener()); + } + c = button; + maxIsPreferred = 3; + } else if (type.equals("checkbox")) { + c = new JCheckBox(); + if (model != null) { + ((JCheckBox)c).setModel((JToggleButton.ToggleButtonModel) model); + } + maxIsPreferred = 3; + } else if (type.equals("radio")) { + c = new JRadioButton(); + if (model != null) { + ((JRadioButton)c).setModel((JToggleButton.ToggleButtonModel)model); + } + maxIsPreferred = 3; + } else if (type.equals("text")) { + int size = HTML.getIntegerAttributeValue(attr, + HTML.Attribute.SIZE, + -1); + JTextField field; + if (size > 0) { + field = new JTextField(); + field.setColumns(size); + } + else { + field = new JTextField(); + field.setColumns(20); + } + c = field; + if (model != null) { + field.setDocument((Document) model); + } + field.addActionListener(this); + maxIsPreferred = 3; + } else if (type.equals("password")) { + JPasswordField field = new JPasswordField(); + c = field; + if (model != null) { + field.setDocument((Document) model); + } + int size = HTML.getIntegerAttributeValue(attr, + HTML.Attribute.SIZE, + -1); + field.setColumns((size > 0) ? size : 20); + field.addActionListener(this); + maxIsPreferred = 3; + } else if (type.equals("file")) { + JTextField field = new JTextField(); + if (model != null) { + field.setDocument((Document)model); + } + int size = HTML.getIntegerAttributeValue(attr, HTML.Attribute.SIZE, + -1); + field.setColumns((size > 0) ? size : 20); + JButton browseButton = new JButton(UIManager.getString + ("FormView.browseFileButtonText")); + Box box = Box.createHorizontalBox(); + box.add(field); + box.add(Box.createHorizontalStrut(5)); + box.add(browseButton); + browseButton.addActionListener(new BrowseFileAction( + attr, (Document)model)); + c = box; + maxIsPreferred = 3; + } + return c; + } + + + /** + * Determines the maximum span for this view along an + * axis. For certain components, the maximum and preferred span are the + * same. For others this will return the value + * returned by Component.getMaximumSize along the + * axis of interest. + * + * @param axis may be either View.X_AXIS or View.Y_AXIS + * @return the span the view would like to be rendered into >= 0. + * Typically the view is told to render into the span + * that is returned, although there is no guarantee. + * The parent may choose to resize or break the view. + * @exception IllegalArgumentException for an invalid axis + */ + public float getMaximumSpan(int axis) { + switch (axis) { + case View.X_AXIS: + if ((maxIsPreferred & 1) == 1) { + super.getMaximumSpan(axis); + return getPreferredSpan(axis); + } + return super.getMaximumSpan(axis); + case View.Y_AXIS: + if ((maxIsPreferred & 2) == 2) { + super.getMaximumSpan(axis); + return getPreferredSpan(axis); + } + return super.getMaximumSpan(axis); + default: + break; + } + return super.getMaximumSpan(axis); + } + + + /** + * Responsible for processeing the ActionEvent. + * If the element associated with the FormView, + * has a type of "submit", "reset", "text" or "password" + * then the action is processed. In the case of a "submit" + * the form is submitted. In the case of a "reset" + * the form is reset to its original state. + * In the case of "text" or "password", if the + * element is the last one of type "text" or "password", + * the form is submitted. Otherwise, focus is transferred + * to the next component in the form. + * + * @param evt the ActionEvent. + */ + public void actionPerformed(ActionEvent evt) { + Element element = getElement(); + StringBuffer dataBuffer = new StringBuffer(); + HTMLDocument doc = (HTMLDocument)getDocument(); + AttributeSet attr = element.getAttributes(); + + String type = (String) attr.getAttribute(HTML.Attribute.TYPE); + + if (type.equals("submit")) { + getFormData(dataBuffer); + submitData(dataBuffer.toString()); + } else if (type.equals("reset")) { + resetForm(); + } else if (type.equals("text") || type.equals("password")) { + if (isLastTextOrPasswordField()) { + getFormData(dataBuffer); + submitData(dataBuffer.toString()); + } else { + getComponent().transferFocus(); + } + } + } + + + /** + * This method is responsible for submitting the form data. + * A thread is forked to undertake the submission. + */ + protected void submitData(String data) { + Element form = getFormElement(); + AttributeSet attrs = form.getAttributes(); + HTMLDocument doc = (HTMLDocument) form.getDocument(); + URL base = doc.getBase(); + + String target = (String) attrs.getAttribute(HTML.Attribute.TARGET); + if (target == null) { + target = "_self"; + } + + String method = (String) attrs.getAttribute(HTML.Attribute.METHOD); + if (method == null) { + method = "GET"; + } + method = method.toLowerCase(); + boolean isPostMethod = method.equals("post"); + if (isPostMethod) { + storePostData(doc, target, data); + } + + String action = (String) attrs.getAttribute(HTML.Attribute.ACTION); + URL actionURL; + try { + actionURL = (action == null) + ? new URL(base.getProtocol(), base.getHost(), + base.getPort(), base.getFile()) + : new URL(base, action); + if (!isPostMethod) { + String query = data.toString(); + actionURL = new URL(actionURL + "?" + query); + } + } catch (MalformedURLException e) { + actionURL = null; + } + final JEditorPane c = (JEditorPane) getContainer(); + HTMLEditorKit kit = (HTMLEditorKit) c.getEditorKit(); + + FormSubmitEvent formEvent = null; + if (!kit.isAutoFormSubmission() || doc.isFrameDocument()) { + FormSubmitEvent.MethodType methodType = isPostMethod + ? FormSubmitEvent.MethodType.POST + : FormSubmitEvent.MethodType.GET; + formEvent = new FormSubmitEvent( + FormView.this, HyperlinkEvent.EventType.ACTIVATED, + actionURL, form, target, methodType, data); + + } + // setPage() may take significant time so schedule it to run later. + final FormSubmitEvent fse = formEvent; + final URL url = actionURL; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (fse != null) { + c.fireHyperlinkUpdate(fse); + } else { + try { + c.setPage(url); + } catch (IOException e) { + UIManager.getLookAndFeel().provideErrorFeedback(c); + } + } + } + }); + } + + private void storePostData(HTMLDocument doc, String target, String data) { + + /* POST data is stored into the document property named by constant + * PostDataProperty from where it is later retrieved by method + * JEditorPane.getPostData(). If the current document is in a frame, + * the data is initially put into the toplevel (frameset) document + * property (named .). It is the + * responsibility of FrameView which updates the target frame + * to move data from the frameset document property into the frame + * document property. + */ + + Document propDoc = doc; + String propName = PostDataProperty; + + if (doc.isFrameDocument()) { + // find the top-most JEditorPane holding the frameset view. + FrameView.FrameEditorPane p = + (FrameView.FrameEditorPane) getContainer(); + FrameView v = p.getFrameView(); + JEditorPane c = v.getOutermostJEditorPane(); + if (c != null) { + propDoc = c.getDocument(); + propName += ("." + target); + } + } + + propDoc.putProperty(propName, data); + } + + /** + * MouseEventListener class to handle form submissions when + * an input with type equal to image is clicked on. + * A MouseListener is necessary since along with the image + * data the coordinates associated with the mouse click + * need to be submitted. + */ + protected class MouseEventListener extends MouseAdapter { + + public void mouseReleased(MouseEvent evt) { + String imageData = getImageData(evt.getPoint()); + imageSubmit(imageData); + } + } + + /** + * This method is called to submit a form in response + * to a click on an image -- an <INPUT> form + * element of type "image". + * + * @param imageData the mouse click coordinates. + */ + protected void imageSubmit(String imageData) { + + StringBuffer dataBuffer = new StringBuffer(); + Element elem = getElement(); + HTMLDocument hdoc = (HTMLDocument)elem.getDocument(); + getFormData(dataBuffer); + if (dataBuffer.length() > 0) { + dataBuffer.append('&'); + } + dataBuffer.append(imageData); + submitData(dataBuffer.toString()); + return; + } + + /** + * Extracts the value of the name attribute + * associated with the input element of type + * image. If name is defined it is encoded using + * the URLEncoder.encode() method and the + * image data is returned in the following format: + * name + ".x" +"="+ x +"&"+ name +".y"+"="+ y + * otherwise, + * "x="+ x +"&y="+ y + * + * @param point associated with the mouse click. + * @return the image data. + */ + private String getImageData(Point point) { + + String mouseCoords = point.x + ":" + point.y; + int sep = mouseCoords.indexOf(':'); + String x = mouseCoords.substring(0, sep); + String y = mouseCoords.substring(++sep); + String name = (String) getElement().getAttributes().getAttribute(HTML.Attribute.NAME); + + String data; + if (name == null || name.equals("")) { + data = "x="+ x +"&y="+ y; + } else { + name = URLEncoder.encode(name); + data = name + ".x" +"="+ x +"&"+ name +".y"+"="+ y; + } + return data; + } + + + /** + * The following methods provide functionality required to + * iterate over a the elements of the form and in the case + * of a form submission, extract the data from each model + * that is associated with each form element, and in the + * case of reset, reinitialize the each model to its + * initial state. + */ + + + /** + * Returns the Element representing the FORM. + */ + private Element getFormElement() { + Element elem = getElement(); + while (elem != null) { + if (elem.getAttributes().getAttribute + (StyleConstants.NameAttribute) == HTML.Tag.FORM) { + return elem; + } + elem = elem.getParentElement(); + } + return null; + } + + /** + * Iterates over the + * element hierarchy, extracting data from the + * models associated with the relevant form elements. + * "Relevant" means the form elements that are part + * of the same form whose element triggered the submit + * action. + * + * @param buffer the buffer that contains that data to submit + * @param targetElement the element that triggered the + * form submission + */ + void getFormData(StringBuffer buffer) { + Element formE = getFormElement(); + if (formE != null) { + ElementIterator it = new ElementIterator(formE); + Element next; + + while ((next = it.next()) != null) { + if (isControl(next)) { + String type = (String)next.getAttributes().getAttribute + (HTML.Attribute.TYPE); + + if (type != null && type.equals("submit") && + next != getElement()) { + // do nothing - this submit isnt the trigger + } else if (type == null || !type.equals("image")) { + // images only result in data if they triggered + // the submit and they require that the mouse click + // coords be appended to the data. Hence its + // processing is handled by the view. + loadElementDataIntoBuffer(next, buffer); + } + } + } + } + } + + /** + * Loads the data + * associated with the element into the buffer. + * The format in which data is appended depends + * on the type of the form element. Essentially + * data is loaded in name/value pairs. + * + */ + private void loadElementDataIntoBuffer(Element elem, StringBuffer buffer) { + + AttributeSet attr = elem.getAttributes(); + String name = (String)attr.getAttribute(HTML.Attribute.NAME); + if (name == null) { + return; + } + String value = null; + HTML.Tag tag = (HTML.Tag)elem.getAttributes().getAttribute + (StyleConstants.NameAttribute); + + if (tag == HTML.Tag.INPUT) { + value = getInputElementData(attr); + } else if (tag == HTML.Tag.TEXTAREA) { + value = getTextAreaData(attr); + } else if (tag == HTML.Tag.SELECT) { + loadSelectData(attr, buffer); + } + + if (name != null && value != null) { + appendBuffer(buffer, name, value); + } + } + + + /** + * Returns the data associated with an <INPUT> form + * element. The value of "type" attributes is + * used to determine the type of the model associated + * with the element and then the relevant data is + * extracted. + */ + private String getInputElementData(AttributeSet attr) { + + Object model = attr.getAttribute(StyleConstants.ModelAttribute); + String type = (String) attr.getAttribute(HTML.Attribute.TYPE); + String value = null; + + if (type.equals("text") || type.equals("password")) { + Document doc = (Document)model; + try { + value = doc.getText(0, doc.getLength()); + } catch (BadLocationException e) { + value = null; + } + } else if (type.equals("submit") || type.equals("hidden")) { + value = (String) attr.getAttribute(HTML.Attribute.VALUE); + if (value == null) { + value = ""; + } + } else if (type.equals("radio") || type.equals("checkbox")) { + ButtonModel m = (ButtonModel)model; + if (m.isSelected()) { + value = (String) attr.getAttribute(HTML.Attribute.VALUE); + if (value == null) { + value = "on"; + } + } + } else if (type.equals("file")) { + Document doc = (Document)model; + String path; + + try { + path = doc.getText(0, doc.getLength()); + } catch (BadLocationException e) { + path = null; + } + if (path != null && path.length() > 0) { + value = path; +/* + + try { + Reader reader = new BufferedReader(new FileReader(path)); + StringBuffer buffer = new StringBuffer(); + char[] cBuff = new char[1024]; + int read; + + try { + while ((read = reader.read(cBuff)) != -1) { + buffer.append(cBuff, 0, read); + } + } catch (IOException ioe) { + buffer = null; + } + try { + reader.close(); + } catch (IOException ioe) {} + if (buffer != null) { + value = buffer.toString(); + } + } catch (IOException ioe) {} +*/ + } + } + return value; + } + + /** + * Returns the data associated with the <TEXTAREA> form + * element. This is done by getting the text stored in the + * Document model. + */ + private String getTextAreaData(AttributeSet attr) { + Document doc = (Document)attr.getAttribute(StyleConstants.ModelAttribute); + try { + return doc.getText(0, doc.getLength()); + } catch (BadLocationException e) { + return null; + } + } + + + /** + * Loads the buffer with the data associated with the Select + * form element. Basically, only items that are selected + * and have their name attribute set are added to the buffer. + */ + private void loadSelectData(AttributeSet attr, StringBuffer buffer) { + + String name = (String)attr.getAttribute(HTML.Attribute.NAME); + if (name == null) { + return; + } + Object m = attr.getAttribute(StyleConstants.ModelAttribute); + if (m instanceof OptionListModel) { + OptionListModel model = (OptionListModel)m; + + for (int i = 0; i < model.getSize(); i++) { + if (model.isSelectedIndex(i)) { + Option option = (Option) model.getElementAt(i); + appendBuffer(buffer, name, option.getValue()); + } + } + } else if (m instanceof ComboBoxModel) { + ComboBoxModel model = (ComboBoxModel)m; + Option option = (Option)model.getSelectedItem(); + if (option != null) { + appendBuffer(buffer, name, option.getValue()); + } + } + } + + /** + * Appends name / value pairs into the + * buffer. Both names and values are encoded using the + * URLEncoder.encode() method before being added to the + * buffer. + */ + private void appendBuffer(StringBuffer buffer, String name, String value) { + if (buffer.length() > 0) { + buffer.append('&'); + } + String encodedName = URLEncoder.encode(name); + buffer.append(encodedName); + buffer.append('='); + String encodedValue = URLEncoder.encode(value); + buffer.append(encodedValue); + } + + /** + * Returns true if the Element elem represents a control. + */ + private boolean isControl(Element elem) { + return elem.isLeaf(); + } + + /** + * Iterates over the element hierarchy to determine if + * the element parameter, which is assumed to be an + * <INPUT> element of type password or text, is the last + * one of either kind, in the form to which it belongs. + */ + boolean isLastTextOrPasswordField() { + Element parent = getFormElement(); + Element elem = getElement(); + + if (parent != null) { + ElementIterator it = new ElementIterator(parent); + Element next; + boolean found = false; + + while ((next = it.next()) != null) { + if (next == elem) { + found = true; + } + else if (found && isControl(next)) { + AttributeSet elemAttr = next.getAttributes(); + + if (HTMLDocument.matchNameAttribute + (elemAttr, HTML.Tag.INPUT)) { + String type = (String)elemAttr.getAttribute + (HTML.Attribute.TYPE); + + if ("text".equals(type) || "password".equals(type)) { + return false; + } + } + } + } + } + return true; + } + + /** + * Resets the form + * to its initial state by reinitializing the models + * associated with each form element to their initial + * values. + * + * param elem the element that triggered the reset + */ + void resetForm() { + Element parent = getFormElement(); + + if (parent != null) { + ElementIterator it = new ElementIterator(parent); + Element next; + + while((next = it.next()) != null) { + if (isControl(next)) { + AttributeSet elemAttr = next.getAttributes(); + Object m = elemAttr.getAttribute(StyleConstants. + ModelAttribute); + if (m instanceof TextAreaDocument) { + TextAreaDocument doc = (TextAreaDocument)m; + doc.reset(); + } else if (m instanceof PlainDocument) { + try { + PlainDocument doc = (PlainDocument)m; + doc.remove(0, doc.getLength()); + if (HTMLDocument.matchNameAttribute + (elemAttr, HTML.Tag.INPUT)) { + String value = (String)elemAttr. + getAttribute(HTML.Attribute.VALUE); + if (value != null) { + doc.insertString(0, value, null); + } + } + } catch (BadLocationException e) { + } + } else if (m instanceof OptionListModel) { + OptionListModel model = (OptionListModel) m; + int size = model.getSize(); + for (int i = 0; i < size; i++) { + model.removeIndexInterval(i, i); + } + BitSet selectionRange = model.getInitialSelection(); + for (int i = 0; i < selectionRange.size(); i++) { + if (selectionRange.get(i)) { + model.addSelectionInterval(i, i); + } + } + } else if (m instanceof OptionComboBoxModel) { + OptionComboBoxModel model = (OptionComboBoxModel) m; + Option option = model.getInitialSelection(); + if (option != null) { + model.setSelectedItem(option); + } + } else if (m instanceof JToggleButton.ToggleButtonModel) { + boolean checked = ((String)elemAttr.getAttribute + (HTML.Attribute.CHECKED) != null); + JToggleButton.ToggleButtonModel model = + (JToggleButton.ToggleButtonModel)m; + model.setSelected(checked); + } + } + } + } + } + + + /** + * BrowseFileAction is used for input type == file. When the user + * clicks the button a JFileChooser is brought up allowing the user + * to select a file in the file system. The resulting path to the selected + * file is set in the text field (actually an instance of Document). + */ + private class BrowseFileAction implements ActionListener { + private AttributeSet attrs; + private Document model; + + BrowseFileAction(AttributeSet attrs, Document model) { + this.attrs = attrs; + this.model = model; + } + + public void actionPerformed(ActionEvent ae) { + // PENDING: When mime support is added to JFileChooser use the + // accept value of attrs. + JFileChooser fc = new JFileChooser(); + fc.setMultiSelectionEnabled(false); + if (fc.showOpenDialog(getContainer()) == + JFileChooser.APPROVE_OPTION) { + File selected = fc.getSelectedFile(); + + if (selected != null) { + try { + if (model.getLength() > 0) { + model.remove(0, model.getLength()); + } + model.insertString(0, selected.getPath(), null); + } catch (BadLocationException ble) {} + } + } + } + } +} diff --git a/src/share/classes/javax/swing/text/html/FrameSetView.java b/src/share/classes/javax/swing/text/html/FrameSetView.java new file mode 100644 index 000000000..b653d32ee --- /dev/null +++ b/src/share/classes/javax/swing/text/html/FrameSetView.java @@ -0,0 +1,322 @@ +/* + * Copyright 1998-2003 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import java.util.*; +import javax.swing.*; +import javax.swing.text.*; +import javax.swing.event.*; + +/** + * Implements a FrameSetView, intended to support the HTML + * <FRAMESET> tag. Supports the ROWS and COLS attributes. + * + * @author Sunita Mani + * + * Credit also to the hotjava browser engineers that + * worked on making the allocation of space algorithms + * conform to the HTML 4.0 standard and also be netscape + * compatible. + * + */ + +class FrameSetView extends javax.swing.text.BoxView { + + String[] children; + int[] percentChildren; + int[] absoluteChildren; + int[] relativeChildren; + int percentTotals; + int absoluteTotals; + int relativeTotals; + + /** + * Constructs a FrameSetView for the given element. + * + * @param elem the element that this view is responsible for + */ + public FrameSetView(Element elem, int axis) { + super(elem, axis); + children = null; + } + + /** + * Parses the ROW or COL attributes and returns + * an array of strings that represent the space + * distribution. + * + */ + private String[] parseRowColSpec(HTML.Attribute key) { + + AttributeSet attributes = getElement().getAttributes(); + String spec = "*"; + if (attributes != null) { + if (attributes.getAttribute(key) != null) { + spec = (String)attributes.getAttribute(key); + } + } + + StringTokenizer tokenizer = new StringTokenizer(spec, ","); + int nTokens = tokenizer.countTokens(); + int n = getViewCount(); + String[] items = new String[Math.max(nTokens, n)]; + int i = 0; + for (; i < nTokens; i++) { + items[i] = tokenizer.nextToken().trim(); + // As per the spec, 100% is the same as * + // hence the mapping. + // + if (items[i].equals("100%")) { + items[i] = "*"; + } + } + // extend spec if we have more children than specified + // in ROWS or COLS attribute + for (; i < items.length; i++) { + items[i] = "*"; + } + return items; + } + + + /** + * Initializes a number of internal state variables + * that store information about space allocation + * for the frames contained within the frameset. + */ + private void init() { + if (getAxis() == View.Y_AXIS) { + children = parseRowColSpec(HTML.Attribute.ROWS); + } else { + children = parseRowColSpec(HTML.Attribute.COLS); + } + percentChildren = new int[children.length]; + relativeChildren = new int[children.length]; + absoluteChildren = new int[children.length]; + + for (int i = 0; i < children.length; i++) { + percentChildren[i] = -1; + relativeChildren[i] = -1; + absoluteChildren[i] = -1; + + if (children[i].endsWith("*")) { + if (children[i].length() > 1) { + relativeChildren[i] = + Integer.parseInt(children[i].substring( + 0, children[i].length()-1)); + relativeTotals += relativeChildren[i]; + } else { + relativeChildren[i] = 1; + relativeTotals += 1; + } + } else if (children[i].indexOf('%') != -1) { + percentChildren[i] = parseDigits(children[i]); + percentTotals += percentChildren[i]; + } else { + absoluteChildren[i] = Integer.parseInt(children[i]); + } + } + if (percentTotals > 100) { + for (int i = 0; i < percentChildren.length; i++) { + if (percentChildren[i] > 0) { + percentChildren[i] = + (percentChildren[i] * 100) / percentTotals; + } + } + percentTotals = 100; + } + } + + /** + * Perform layout for the major axis of the box (i.e. the + * axis that it represents). The results of the layout should + * be placed in the given arrays which represent the allocations + * to the children along the major axis. + * + * @param targetSpan the total span given to the view, which + * whould be used to layout the children + * @param axis the axis being layed out + * @param offsets the offsets from the origin of the view for + * each of the child views; this is a return value and is + * filled in by the implementation of this method + * @param spans the span of each child view; this is a return + * value and is filled in by the implementation of this method + * @return the offset and span for each child view in the + * offsets and spans parameters + */ + protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, + int[] spans) { + if (children == null) { + init(); + } + SizeRequirements.calculateTiledPositions(targetSpan, null, + getChildRequests(targetSpan, + axis), + offsets, spans); + } + + protected SizeRequirements[] getChildRequests(int targetSpan, int axis) { + + int span[] = new int[children.length]; + + spread(targetSpan, span); + int n = getViewCount(); + SizeRequirements[] reqs = new SizeRequirements[n]; + for (int i = 0, sIndex = 0; i < n; i++) { + View v = getView(i); + if ((v instanceof FrameView) || (v instanceof FrameSetView)) { + reqs[i] = new SizeRequirements((int) v.getMinimumSpan(axis), + span[sIndex], + (int) v.getMaximumSpan(axis), + 0.5f); + sIndex++; + } else { + int min = (int) v.getMinimumSpan(axis); + int pref = (int) v.getPreferredSpan(axis); + int max = (int) v.getMaximumSpan(axis); + float a = v.getAlignment(axis); + reqs[i] = new SizeRequirements(min, pref, max, a); + } + } + return reqs; + } + + + /** + * This method is responsible for returning in span[] the + * span for each child view along the major axis. it + * computes this based on the information that extracted + * from the value of the ROW/COL attribute. + */ + private void spread(int targetSpan, int span[]) { + + if (targetSpan == 0) { + return; + } + + int tempSpace = 0; + int remainingSpace = targetSpan; + + // allocate the absolute's first, they have + // precedence + // + for (int i = 0; i < span.length; i++) { + if (absoluteChildren[i] > 0) { + span[i] = absoluteChildren[i]; + remainingSpace -= span[i]; + } + } + + // then deal with percents. + // + tempSpace = remainingSpace; + for (int i = 0; i < span.length; i++) { + if (percentChildren[i] > 0 && tempSpace > 0) { + span[i] = (percentChildren[i] * tempSpace) / 100; + remainingSpace -= span[i]; + } else if (percentChildren[i] > 0 && tempSpace <= 0) { + span[i] = targetSpan / span.length; + remainingSpace -= span[i]; + } + } + + // allocate remainingSpace to relative + if (remainingSpace > 0 && relativeTotals > 0) { + for (int i = 0; i < span.length; i++) { + if (relativeChildren[i] > 0) { + span[i] = (remainingSpace * + relativeChildren[i]) / relativeTotals; + } + } + } else if (remainingSpace > 0) { + // There are no relative columns and the space has been + // under- or overallocated. In this case, turn all the + // percentage and pixel specified columns to percentage + // columns based on the ratio of their pixel count to the + // total "virtual" size. (In the case of percentage columns, + // the pixel count would equal the specified percentage + // of the screen size. + + // This action is in accordance with the HTML + // 4.0 spec (see section 8.3, the end of the discussion of + // the FRAMESET tag). The precedence of percentage and pixel + // specified columns is unclear (spec seems to indicate that + // they share priority, however, unspecified what happens when + // overallocation occurs.) + + // addendum is that we behave similiar to netscape in that specified + // widths have precedance over percentage widths... + + float vTotal = (float)(targetSpan - remainingSpace); + float[] tempPercents = new float[span.length]; + remainingSpace = targetSpan; + for (int i = 0; i < span.length; i++) { + // ok we know what our total space is, and we know how large each + // column should be relative to each other... therefore we can use + // that relative information to deduce their percentages of a whole + // and then scale them appropriately for the correct size + tempPercents[i] = ((float)span[i] / vTotal) * 100.00f; + span[i] = (int) ( ((float)targetSpan * tempPercents[i]) / 100.00f); + remainingSpace -= span[i]; + } + + + // this is for just in case there is something left over.. if there is we just + // add it one pixel at a time to the frames in order.. We shouldn't really ever get + // here and if we do it shouldn't be with more than 1 pixel, maybe two. + int i = 0; + while (remainingSpace != 0) { + if (remainingSpace < 0) { + span[i++]--; + remainingSpace++; + } + else { + span[i++]++; + remainingSpace--; + } + + // just in case there are more pixels than frames...should never happen.. + if (i == span.length)i = 0; + } + } + } + + /* + * Users have been known to type things like "%25" and "25 %". Deal + * with it. + */ + private int parseDigits(String mixedStr) { + int result = 0; + for (int i = 0; i < mixedStr.length(); i++) { + char ch = mixedStr.charAt(i); + if (Character.isDigit(ch)) { + result = (result * 10) + Character.digit(ch, 10); + } + } + return result; + } + +} diff --git a/src/share/classes/javax/swing/text/html/FrameView.java b/src/share/classes/javax/swing/text/html/FrameView.java new file mode 100644 index 000000000..f66a5531c --- /dev/null +++ b/src/share/classes/javax/swing/text/html/FrameView.java @@ -0,0 +1,479 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import java.util.*; +import java.net.*; +import java.io.*; +import javax.swing.*; +import javax.swing.text.*; +import javax.swing.event.*; + +import sun.swing.text.html.FrameEditorPaneTag; + +/** + * Implements a FrameView, intended to support the HTML + * <FRAME> tag. Supports the frameborder, scrolling, + * marginwidth and marginheight attributes. + * + * @author Sunita Mani + */ + +class FrameView extends ComponentView implements HyperlinkListener { + + + JEditorPane htmlPane; + JScrollPane scroller; + boolean editable; + float width; + float height; + URL src; + /** Set to true when the component has been created. */ + private boolean createdComponent; + + /** + * Creates a new Frame. + * + * @param elem the element to represent. + */ + public FrameView(Element elem) { + super(elem); + } + + protected Component createComponent() { + + Element elem = getElement(); + AttributeSet attributes = elem.getAttributes(); + String srcAtt = (String)attributes.getAttribute(HTML.Attribute.SRC); + + if ((srcAtt != null) && (!srcAtt.equals(""))) { + try { + URL base = ((HTMLDocument)elem.getDocument()).getBase(); + src = new URL(base, srcAtt); + htmlPane = new FrameEditorPane(); + htmlPane.addHyperlinkListener(this); + JEditorPane host = getHostPane(); + boolean isAutoFormSubmission = true; + if (host != null) { + htmlPane.setEditable(host.isEditable()); + String charset = (String) host.getClientProperty("charset"); + if (charset != null) { + htmlPane.putClientProperty("charset", charset); + } + HTMLEditorKit hostKit = (HTMLEditorKit)host.getEditorKit(); + if (hostKit != null) { + isAutoFormSubmission = hostKit.isAutoFormSubmission(); + } + } + htmlPane.setPage(src); + HTMLEditorKit kit = (HTMLEditorKit)htmlPane.getEditorKit(); + if (kit != null) { + kit.setAutoFormSubmission(isAutoFormSubmission); + } + + Document doc = htmlPane.getDocument(); + if (doc instanceof HTMLDocument) { + ((HTMLDocument)doc).setFrameDocumentState(true); + } + setMargin(); + createScrollPane(); + setBorder(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + createdComponent = true; + return scroller; + } + + JEditorPane getHostPane() { + Container c = getContainer(); + while ((c != null) && ! (c instanceof JEditorPane)) { + c = c.getParent(); + } + return (JEditorPane) c; + } + + + /** + * Sets the parent view for the FrameView. + * Also determines if the FrameView should be editable + * or not based on whether the JTextComponent that + * contains it is editable. + * + * @param parent View + */ + public void setParent(View parent) { + if (parent != null) { + JTextComponent t = (JTextComponent)parent.getContainer(); + editable = t.isEditable(); + } + super.setParent(parent); + } + + + /** + * Also determines if the FrameView should be editable + * or not based on whether the JTextComponent that + * contains it is editable. And then proceeds to call + * the superclass to do the paint(). + * + * @param parent View + * @see text.ComponentView#paint + */ + public void paint(Graphics g, Shape allocation) { + + Container host = getContainer(); + if (host != null && htmlPane != null && + htmlPane.isEditable() != ((JTextComponent)host).isEditable()) { + editable = ((JTextComponent)host).isEditable(); + htmlPane.setEditable(editable); + } + super.paint(g, allocation); + } + + + /** + * If the marginwidth or marginheight attributes have been specified, + * then the JEditorPane's margin's are set to the new values. + */ + private void setMargin() { + int margin = 0; + Insets in = htmlPane.getMargin(); + Insets newInsets; + boolean modified = false; + AttributeSet attributes = getElement().getAttributes(); + String marginStr = (String)attributes.getAttribute(HTML.Attribute.MARGINWIDTH); + if ( in != null) { + newInsets = new Insets(in.top, in.left, in.right, in.bottom); + } else { + newInsets = new Insets(0,0,0,0); + } + if (marginStr != null) { + margin = Integer.parseInt(marginStr); + if (margin > 0) { + newInsets.left = margin; + newInsets.right = margin; + modified = true; + } + } + marginStr = (String)attributes.getAttribute(HTML.Attribute.MARGINHEIGHT); + if (marginStr != null) { + margin = Integer.parseInt(marginStr); + if (margin > 0) { + newInsets.top = margin; + newInsets.bottom = margin; + modified = true; + } + } + if (modified) { + htmlPane.setMargin(newInsets); + } + } + + /** + * If the frameborder attribute has been specified, either in the frame, + * or by the frames enclosing frameset, the JScrollPane's setBorder() + * method is invoked to achieve the desired look. + */ + private void setBorder() { + + AttributeSet attributes = getElement().getAttributes(); + String frameBorder = (String)attributes.getAttribute(HTML.Attribute.FRAMEBORDER); + if ((frameBorder != null) && + (frameBorder.equals("no") || frameBorder.equals("0"))) { + // make invisible borders. + scroller.setBorder(null); + } + } + + + /** + * This method creates the JScrollPane. The scrollbar policy is determined by + * the scrolling attribute. If not defined, the default is "auto" which + * maps to the scrollbar's being displayed as needed. + */ + private void createScrollPane() { + AttributeSet attributes = getElement().getAttributes(); + String scrolling = (String)attributes.getAttribute(HTML.Attribute.SCROLLING); + if (scrolling == null) { + scrolling = "auto"; + } + + if (!scrolling.equals("no")) { + if (scrolling.equals("yes")) { + scroller = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } else { + // scrollbars will be displayed if needed + // + scroller = new JScrollPane(); + } + } else { + scroller = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_NEVER, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + + JViewport vp = scroller.getViewport(); + vp.add(htmlPane); + vp.setBackingStoreEnabled(true); + scroller.setMinimumSize(new Dimension(5,5)); + scroller.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); + } + + + /** + * Finds the outermost FrameSetView. It then + * returns that FrameSetView's container. + */ + JEditorPane getOutermostJEditorPane() { + + View parent = getParent(); + FrameSetView frameSetView = null; + while (parent != null) { + if (parent instanceof FrameSetView) { + frameSetView = (FrameSetView)parent; + } + parent = parent.getParent(); + } + if (frameSetView != null) { + return (JEditorPane)frameSetView.getContainer(); + } + return null; + } + + + /** + * Returns true if this frame is contained within + * a nested frameset. + */ + private boolean inNestedFrameSet() { + FrameSetView parent = (FrameSetView)getParent(); + return (parent.getParent() instanceof FrameSetView); + } + + + /** + * Notification of a change relative to a + * hyperlink. This method searches for the outermost + * JEditorPane, and then fires an HTMLFrameHyperlinkEvent + * to that frame. In addition, if the target is _parent, + * and there is not nested framesets then the target is + * reset to _top. If the target is _top, in addition to + * firing the event to the outermost JEditorPane, this + * method also invokes the setPage() method and explicitly + * replaces the current document with the destination url. + * + * @param HyperlinkEvent + */ + public void hyperlinkUpdate(HyperlinkEvent evt) { + + JEditorPane c = getOutermostJEditorPane(); + if (c == null) { + return; + } + + if (!(evt instanceof HTMLFrameHyperlinkEvent)) { + c.fireHyperlinkUpdate(evt); + return; + } + + HTMLFrameHyperlinkEvent e = (HTMLFrameHyperlinkEvent)evt; + + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + String target = e.getTarget(); + String postTarget = target; + + if (target.equals("_parent") && !inNestedFrameSet()){ + target = "_top"; + } + + if (evt instanceof FormSubmitEvent) { + HTMLEditorKit kit = (HTMLEditorKit)c.getEditorKit(); + if (kit != null && kit.isAutoFormSubmission()) { + if (target.equals("_top")) { + try { + movePostData(c, postTarget); + c.setPage(e.getURL()); + } catch (IOException ex) { + // Need a way to handle exceptions + } + } else { + HTMLDocument doc = (HTMLDocument)c.getDocument(); + doc.processHTMLFrameHyperlinkEvent(e); + } + } else { + c.fireHyperlinkUpdate(evt); + } + return; + } + + if (target.equals("_top")) { + try { + c.setPage(e.getURL()); + } catch (IOException ex) { + // Need a way to handle exceptions + // ex.printStackTrace(); + } + } + if (!c.isEditable()) { + c.fireHyperlinkUpdate(new HTMLFrameHyperlinkEvent(c, + e.getEventType(), + e.getURL(), + e.getDescription(), + getElement(), + e.getInputEvent(), + target)); + } + } + } + + /** + * Gives notification from the document that attributes were changed + * in a location that this view is responsible for. Currently this view + * handles changes to its SRC attribute. + * + * @param e the change information from the associated document + * @param a the current allocation of the view + * @param f the factory to use to rebuild if the view has children + * + */ + public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { + + Element elem = getElement(); + AttributeSet attributes = elem.getAttributes(); + + URL oldPage = src; + + String srcAtt = (String)attributes.getAttribute(HTML.Attribute.SRC); + URL base = ((HTMLDocument)elem.getDocument()).getBase(); + try { + if (!createdComponent) { + return; + } + + Object postData = movePostData(htmlPane, null); + src = new URL(base, srcAtt); + if (oldPage.equals(src) && (src.getRef() == null) && (postData == null)) { + return; + } + + htmlPane.setPage(src); + Document newDoc = htmlPane.getDocument(); + if (newDoc instanceof HTMLDocument) { + ((HTMLDocument)newDoc).setFrameDocumentState(true); + } + } catch (MalformedURLException e1) { + // Need a way to handle exceptions + //e1.printStackTrace(); + } catch (IOException e2) { + // Need a way to handle exceptions + //e2.printStackTrace(); + } + } + + /** + * Move POST data from temporary storage into the target document property. + * + * @return the POST data or null if no data found + */ + private Object movePostData(JEditorPane targetPane, String frameName) { + Object postData = null; + JEditorPane p = getOutermostJEditorPane(); + if (p != null) { + if (frameName == null) { + frameName = (String) getElement().getAttributes().getAttribute( + HTML.Attribute.NAME); + } + if (frameName != null) { + String propName = FormView.PostDataProperty + "." + frameName; + Document d = p.getDocument(); + postData = d.getProperty(propName); + if (postData != null) { + targetPane.getDocument().putProperty( + FormView.PostDataProperty, postData); + d.putProperty(propName, null); + } + } + } + + return postData; + } + + /** + * Determines the minimum span for this view along an + * axis. + * + * @param axis may be either View.X_AXIS or + * View.Y_AXIS + * @return the preferred span; given that we do not + * support resizing of frames, the minimum span returned + * is the same as the preferred span + * + */ + public float getMinimumSpan(int axis) { + return 5; + } + + /** + * Determines the maximum span for this view along an + * axis. + * + * @param axis may be either View.X_AXIS or + * View.Y_AXIS + * @return the preferred span; given that we do not + * support resizing of frames, the maximum span returned + * is the same as the preferred span + * + */ + public float getMaximumSpan(int axis) { + return Integer.MAX_VALUE; + } + + /** Editor pane rendering frame of HTML document + * It uses the same editor kits classes as outermost JEditorPane + */ + class FrameEditorPane extends JEditorPane implements FrameEditorPaneTag { + public EditorKit getEditorKitForContentType(String type) { + EditorKit editorKit = super.getEditorKitForContentType(type); + JEditorPane outerMostJEditorPane = null; + if ((outerMostJEditorPane = getOutermostJEditorPane()) != null) { + EditorKit inheritedEditorKit = outerMostJEditorPane.getEditorKitForContentType(type); + if (! editorKit.getClass().equals(inheritedEditorKit.getClass())) { + editorKit = (EditorKit) inheritedEditorKit.clone(); + setEditorKitForContentType(type, editorKit); + } + } + return editorKit; + } + + FrameView getFrameView() { + return FrameView.this; + } + } +} diff --git a/src/share/classes/javax/swing/text/html/HRuleView.java b/src/share/classes/javax/swing/text/html/HRuleView.java new file mode 100644 index 000000000..d345b348d --- /dev/null +++ b/src/share/classes/javax/swing/text/html/HRuleView.java @@ -0,0 +1,316 @@ +/* + * Copyright 1997-2002 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import javax.swing.event.DocumentEvent; +import javax.swing.text.*; +import java.util.Enumeration; +import java.lang.Integer; + +/** + * A view implementation to display an html horizontal + * rule. + * + * @author Timothy Prinzing + * @author Sara Swanson + */ +class HRuleView extends View { + + /** + * Creates a new view that represents an <hr> element. + * + * @param elem the element to create a view for + */ + public HRuleView(Element elem) { + super(elem); + setPropertiesFromAttributes(); + } + + /** + * Update any cached values that come from attributes. + */ + protected void setPropertiesFromAttributes() { + StyleSheet sheet = ((HTMLDocument)getDocument()).getStyleSheet(); + AttributeSet eAttr = getElement().getAttributes(); + attr = sheet.getViewAttributes(this); + + alignment = StyleConstants.ALIGN_CENTER; + size = 0; + noshade = null; + widthValue = null; + + if (attr != null) { + // getAlignment() returns ALIGN_LEFT by default, and HR should + // use ALIGN_CENTER by default, so we check if the alignment + // attribute is actually defined + if (attr.getAttribute(StyleConstants.Alignment) != null) { + alignment = StyleConstants.getAlignment(attr); + } + + noshade = (String)eAttr.getAttribute(HTML.Attribute.NOSHADE); + Object value = eAttr.getAttribute(HTML.Attribute.SIZE); + if (value != null && (value instanceof String)) + size = Integer.parseInt((String)value); + value = attr.getAttribute(CSS.Attribute.WIDTH); + if (value != null && (value instanceof CSS.LengthValue)) { + widthValue = (CSS.LengthValue)value; + } + topMargin = getLength(CSS.Attribute.MARGIN_TOP, attr); + bottomMargin = getLength(CSS.Attribute.MARGIN_BOTTOM, attr); + leftMargin = getLength(CSS.Attribute.MARGIN_LEFT, attr); + rightMargin = getLength(CSS.Attribute.MARGIN_RIGHT, attr); + } + else { + topMargin = bottomMargin = leftMargin = rightMargin = 0; + } + size = Math.max(2, size); + } + + // This will be removed and centralized at some point, need to unify this + // and avoid private classes. + private float getLength(CSS.Attribute key, AttributeSet a) { + CSS.LengthValue lv = (CSS.LengthValue) a.getAttribute(key); + float len = (lv != null) ? lv.getValue() : 0; + return len; + } + + // --- View methods --------------------------------------------- + + /** + * Paints the view. + * + * @param g the graphics context + * @param a the allocation region for the view + * @see View#paint + */ + public void paint(Graphics g, Shape a) { + Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : + a.getBounds(); + int x = 0; + int y = alloc.y + SPACE_ABOVE + (int)topMargin; + int width = alloc.width - (int)(leftMargin + rightMargin); + if (widthValue != null) { + width = (int)widthValue.getValue((float)width); + } + int height = alloc.height - (SPACE_ABOVE + SPACE_BELOW + + (int)topMargin + (int)bottomMargin); + if (size > 0) + height = size; + + // Align the rule horizontally. + switch (alignment) { + case StyleConstants.ALIGN_CENTER: + x = alloc.x + (alloc.width / 2) - (width / 2); + break; + case StyleConstants.ALIGN_RIGHT: + x = alloc.x + alloc.width - width - (int)rightMargin; + break; + case StyleConstants.ALIGN_LEFT: + default: + x = alloc.x + (int)leftMargin; + break; + } + + // Paint either a shaded rule or a solid line. + if (noshade != null) { + g.setColor(Color.black); + g.fillRect(x, y, width, height); + } + else { + Color bg = getContainer().getBackground(); + Color bottom, top; + if (bg == null || bg.equals(Color.white)) { + top = Color.darkGray; + bottom = Color.lightGray; + } + else { + top = Color.darkGray; + bottom = Color.white; + } + g.setColor(bottom); + g.drawLine(x + width - 1, y, x + width - 1, y + height - 1); + g.drawLine(x, y + height - 1, x + width - 1, y + height - 1); + g.setColor(top); + g.drawLine(x, y, x + width - 1, y); + g.drawLine(x, y, x, y + height - 1); + } + + } + + + /** + * Calculates the desired shape of the rule... this is + * basically the preferred size of the border. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the desired span + * @see View#getPreferredSpan + */ + public float getPreferredSpan(int axis) { + switch (axis) { + case View.X_AXIS: + return 1; + case View.Y_AXIS: + if (size > 0) { + return size + SPACE_ABOVE + SPACE_BELOW + topMargin + + bottomMargin; + } else { + if (noshade != null) { + return 2 + SPACE_ABOVE + SPACE_BELOW + topMargin + + bottomMargin; + } else { + return SPACE_ABOVE + SPACE_BELOW + topMargin +bottomMargin; + } + } + default: + throw new IllegalArgumentException("Invalid axis: " + axis); + } + } + + /** + * Gets the resize weight for the axis. + * The rule is: rigid vertically and flexible horizontally. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the weight + */ + public int getResizeWeight(int axis) { + if (axis == View.X_AXIS) { + return 1; + } else if (axis == View.Y_AXIS) { + return 0; + } else { + return 0; + } + } + + /** + * Determines how attractive a break opportunity in + * this view is. This is implemented to request a forced break. + * + * @param axis may be either View.X_AXIS or View.Y_AXIS + * @param pos the potential location of the start of the + * broken view (greater than or equal to zero). + * This may be useful for calculating tab + * positions. + * @param len specifies the relative length from pos + * where a potential break is desired. The value must be greater + * than or equal to zero. + * @return the weight, which should be a value between + * ForcedBreakWeight and BadBreakWeight. + */ + public int getBreakWeight(int axis, float pos, float len) { + if (axis == X_AXIS) { + return ForcedBreakWeight; + } + return BadBreakWeight; + } + + public View breakView(int axis, int offset, float pos, float len) { + return null; + } + + /** + * Provides a mapping from the document model coordinate space + * to the coordinate space of the view mapped to it. + * + * @param pos the position to convert + * @param a the allocated region to render into + * @return the bounding box of the given position + * @exception BadLocationException if the given position does not + * represent a valid location in the associated document + * @see View#modelToView + */ + public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { + int p0 = getStartOffset(); + int p1 = getEndOffset(); + if ((pos >= p0) && (pos <= p1)) { + Rectangle r = a.getBounds(); + if (pos == p1) { + r.x += r.width; + } + r.width = 0; + return r; + } + return null; + } + + /** + * Provides a mapping from the view coordinate space to the logical + * coordinate space of the model. + * + * @param x the X coordinate + * @param y the Y coordinate + * @param a the allocated region to render into + * @return the location within the model that best represents the + * given point of view + * @see View#viewToModel + */ + public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) { + Rectangle alloc = (Rectangle) a; + if (x < alloc.x + (alloc.width / 2)) { + bias[0] = Position.Bias.Forward; + return getStartOffset(); + } + bias[0] = Position.Bias.Backward; + return getEndOffset(); + } + + /** + * Fetches the attributes to use when rendering. This is + * implemented to multiplex the attributes specified in the + * model with a StyleSheet. + */ + public AttributeSet getAttributes() { + return attr; + } + + public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) { + super.changedUpdate(changes, a, f); + int pos = changes.getOffset(); + if (pos <= getStartOffset() && (pos + changes.getLength()) >= + getEndOffset()) { + setPropertiesFromAttributes(); + } + } + + // --- variables ------------------------------------------------ + + private float topMargin; + private float bottomMargin; + private float leftMargin; + private float rightMargin; + private int alignment = StyleConstants.ALIGN_CENTER; + private String noshade = null; + private int size = 0; + private CSS.LengthValue widthValue; + + private static final int SPACE_ABOVE = 3; + private static final int SPACE_BELOW = 3; + + /** View Attributes. */ + private AttributeSet attr; +} diff --git a/src/share/classes/javax/swing/text/html/HTML.java b/src/share/classes/javax/swing/text/html/HTML.java new file mode 100644 index 000000000..1976c16fa --- /dev/null +++ b/src/share/classes/javax/swing/text/html/HTML.java @@ -0,0 +1,697 @@ +/* + * Copyright 1998-2005 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.io.*; +import java.util.Hashtable; +import javax.swing.text.AttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyleContext; + +/** + * Constants used in the HTMLDocument. These + * are basically tag and attribute definitions. + * + * @author Timothy Prinzing + * @author Sunita Mani + * + */ +public class HTML { + + /** + * Typesafe enumeration for an HTML tag. Although the + * set of HTML tags is a closed set, we have left the + * set open so that people can add their own tag types + * to their custom parser and still communicate to the + * reader. + */ + public static class Tag { + + /** @since 1.3 */ + public Tag() {} + + /** + * Creates a new Tag with the specified id, + * and with causesBreak and isBlock + * set to false. + * + * @param id the id of the new tag + */ + protected Tag(String id) { + this(id, false, false); + } + + /** + * Creates a new Tag with the specified id; + * causesBreak and isBlock are defined + * by the user. + * + * @param id the id of the new tag + * @param causesBreak true if this tag + * causes a break to the flow of data + * @param isBlock true if the tag is used + * to add structure to a document + */ + protected Tag(String id, boolean causesBreak, boolean isBlock) { + name = id; + this.breakTag = causesBreak; + this.blockTag = isBlock; + } + + /** + * Returns true if this tag is a block + * tag, which is a tag used to add structure to a + * document. + * + * @return true if this tag is a block + * tag, otherwise returns false + */ + public boolean isBlock() { + return blockTag; + } + + /** + * Returns true if this tag causes a + * line break to the flow of data, otherwise returns + * false. + * + * @return true if this tag causes a + * line break to the flow of data, otherwise returns + * false + */ + public boolean breaksFlow() { + return breakTag; + } + + /** + * Returns true if this tag is pre-formatted, + * which is true if the tag is either PRE or + * TEXTAREA. + * + * @return true if this tag is pre-formatted, + * otherwise returns false + */ + public boolean isPreformatted() { + return (this == PRE || this == TEXTAREA); + } + + /** + * Returns the string representation of the + * tag. + * + * @return the String representation of the tag + */ + public String toString() { + return name; + } + + /** + * Returns true if this tag is considered to be a paragraph + * in the internal HTML model. false - otherwise. + * + * @return true if this tag is considered to be a paragraph + * in the internal HTML model. false - otherwise. + * @see javax.swing.text.html.HTMLDocument#HTMLReader#ParagraphAction + */ + boolean isParagraph() { + return ( + this == P + || this == IMPLIED + || this == DT + || this == H1 + || this == H2 + || this == H3 + || this == H4 + || this == H5 + || this == H6 + ); + } + + boolean blockTag; + boolean breakTag; + String name; + boolean unknown; + + // --- Tag Names ----------------------------------- + + public static final Tag A = new Tag("a"); + public static final Tag ADDRESS = new Tag("address"); + public static final Tag APPLET = new Tag("applet"); + public static final Tag AREA = new Tag("area"); + public static final Tag B = new Tag("b"); + public static final Tag BASE = new Tag("base"); + public static final Tag BASEFONT = new Tag("basefont"); + public static final Tag BIG = new Tag("big"); + public static final Tag BLOCKQUOTE = new Tag("blockquote", true, true); + public static final Tag BODY = new Tag("body", true, true); + public static final Tag BR = new Tag("br", true, false); + public static final Tag CAPTION = new Tag("caption"); + public static final Tag CENTER = new Tag("center", true, false); + public static final Tag CITE = new Tag("cite"); + public static final Tag CODE = new Tag("code"); + public static final Tag DD = new Tag("dd", true, true); + public static final Tag DFN = new Tag("dfn"); + public static final Tag DIR = new Tag("dir", true, true); + public static final Tag DIV = new Tag("div", true, true); + public static final Tag DL = new Tag("dl", true, true); + public static final Tag DT = new Tag("dt", true, true); + public static final Tag EM = new Tag("em"); + public static final Tag FONT = new Tag("font"); + public static final Tag FORM = new Tag("form", true, false); + public static final Tag FRAME = new Tag("frame"); + public static final Tag FRAMESET = new Tag("frameset"); + public static final Tag H1 = new Tag("h1", true, true); + public static final Tag H2 = new Tag("h2", true, true); + public static final Tag H3 = new Tag("h3", true, true); + public static final Tag H4 = new Tag("h4", true, true); + public static final Tag H5 = new Tag("h5", true, true); + public static final Tag H6 = new Tag("h6", true, true); + public static final Tag HEAD = new Tag("head", true, true); + public static final Tag HR = new Tag("hr", true, false); + public static final Tag HTML = new Tag("html", true, false); + public static final Tag I = new Tag("i"); + public static final Tag IMG = new Tag("img"); + public static final Tag INPUT = new Tag("input"); + public static final Tag ISINDEX = new Tag("isindex", true, false); + public static final Tag KBD = new Tag("kbd"); + public static final Tag LI = new Tag("li", true, true); + public static final Tag LINK = new Tag("link"); + public static final Tag MAP = new Tag("map"); + public static final Tag MENU = new Tag("menu", true, true); + public static final Tag META = new Tag("meta"); + /*public*/ static final Tag NOBR = new Tag("nobr"); + public static final Tag NOFRAMES = new Tag("noframes", true, true); + public static final Tag OBJECT = new Tag("object"); + public static final Tag OL = new Tag("ol", true, true); + public static final Tag OPTION = new Tag("option"); + public static final Tag P = new Tag("p", true, true); + public static final Tag PARAM = new Tag("param"); + public static final Tag PRE = new Tag("pre", true, true); + public static final Tag SAMP = new Tag("samp"); + public static final Tag SCRIPT = new Tag("script"); + public static final Tag SELECT = new Tag("select"); + public static final Tag SMALL = new Tag("small"); + public static final Tag SPAN = new Tag("span"); + public static final Tag STRIKE = new Tag("strike"); + public static final Tag S = new Tag("s"); + public static final Tag STRONG = new Tag("strong"); + public static final Tag STYLE = new Tag("style"); + public static final Tag SUB = new Tag("sub"); + public static final Tag SUP = new Tag("sup"); + public static final Tag TABLE = new Tag("table", false, true); + public static final Tag TD = new Tag("td", true, true); + public static final Tag TEXTAREA = new Tag("textarea"); + public static final Tag TH = new Tag("th", true, true); + public static final Tag TITLE = new Tag("title", true, true); + public static final Tag TR = new Tag("tr", false, true); + public static final Tag TT = new Tag("tt"); + public static final Tag U = new Tag("u"); + public static final Tag UL = new Tag("ul", true, true); + public static final Tag VAR = new Tag("var"); + + /** + * All text content must be in a paragraph element. + * If a paragraph didn't exist when content was + * encountered, a paragraph is manufactured. + *

    + * This is a tag synthesized by the HTML reader. + * Since elements are identified by their tag type, + * we create a some fake tag types to mark the elements + * that were manufactured. + */ + public static final Tag IMPLIED = new Tag("p-implied"); + + /** + * All text content is labeled with this tag. + *

    + * This is a tag synthesized by the HTML reader. + * Since elements are identified by their tag type, + * we create a some fake tag types to mark the elements + * that were manufactured. + */ + public static final Tag CONTENT = new Tag("content"); + + /** + * All comments are labeled with this tag. + *

    + * This is a tag synthesized by the HTML reader. + * Since elements are identified by their tag type, + * we create a some fake tag types to mark the elements + * that were manufactured. + */ + public static final Tag COMMENT = new Tag("comment"); + + static final Tag allTags[] = { + A, ADDRESS, APPLET, AREA, B, BASE, BASEFONT, BIG, + BLOCKQUOTE, BODY, BR, CAPTION, CENTER, CITE, CODE, + DD, DFN, DIR, DIV, DL, DT, EM, FONT, FORM, FRAME, + FRAMESET, H1, H2, H3, H4, H5, H6, HEAD, HR, HTML, + I, IMG, INPUT, ISINDEX, KBD, LI, LINK, MAP, MENU, + META, NOBR, NOFRAMES, OBJECT, OL, OPTION, P, PARAM, + PRE, SAMP, SCRIPT, SELECT, SMALL, SPAN, STRIKE, S, + STRONG, STYLE, SUB, SUP, TABLE, TD, TEXTAREA, + TH, TITLE, TR, TT, U, UL, VAR + }; + + static { + // Force HTMLs static initialize to be loaded. + getTag("html"); + } + } + + // There is no unique instance of UnknownTag, so we allow it to be + // Serializable. + public static class UnknownTag extends Tag implements Serializable { + + /** + * Creates a new UnknownTag with the specified + * id. + * @param id the id of the new tag + */ + public UnknownTag(String id) { + super(id); + } + + /** + * Returns the hash code which corresponds to the string + * for this tag. + */ + public int hashCode() { + return toString().hashCode(); + } + + /** + * Compares this object to the specifed object. + * The result is true if and only if the argument is not + * null and is an UnknownTag object + * with the same name. + * + * @param obj the object to compare this tag with + * @return true if the objects are equal; + * false otherwise + */ + public boolean equals(Object obj) { + if (obj instanceof UnknownTag) { + return toString().equals(obj.toString()); + } + return false; + } + + private void writeObject(java.io.ObjectOutputStream s) + throws IOException { + s.defaultWriteObject(); + s.writeBoolean(blockTag); + s.writeBoolean(breakTag); + s.writeBoolean(unknown); + s.writeObject(name); + } + + private void readObject(ObjectInputStream s) + throws ClassNotFoundException, IOException { + s.defaultReadObject(); + blockTag = s.readBoolean(); + breakTag = s.readBoolean(); + unknown = s.readBoolean(); + name = (String)s.readObject(); + } + } + + /** + * Typesafe enumeration representing an HTML + * attribute. + */ + public static final class Attribute { + + /** + * Creates a new Attribute with the specified + * id. + * + * @param id the id of the new Attribute + */ + Attribute(String id) { + name = id; + } + + /** + * Returns the string representation of this attribute. + * @return the string representation of this attribute + */ + public String toString() { + return name; + } + + private String name; + + public static final Attribute SIZE = new Attribute("size"); + public static final Attribute COLOR = new Attribute("color"); + public static final Attribute CLEAR = new Attribute("clear"); + public static final Attribute BACKGROUND = new Attribute("background"); + public static final Attribute BGCOLOR = new Attribute("bgcolor"); + public static final Attribute TEXT = new Attribute("text"); + public static final Attribute LINK = new Attribute("link"); + public static final Attribute VLINK = new Attribute("vlink"); + public static final Attribute ALINK = new Attribute("alink"); + public static final Attribute WIDTH = new Attribute("width"); + public static final Attribute HEIGHT = new Attribute("height"); + public static final Attribute ALIGN = new Attribute("align"); + public static final Attribute NAME = new Attribute("name"); + public static final Attribute HREF = new Attribute("href"); + public static final Attribute REL = new Attribute("rel"); + public static final Attribute REV = new Attribute("rev"); + public static final Attribute TITLE = new Attribute("title"); + public static final Attribute TARGET = new Attribute("target"); + public static final Attribute SHAPE = new Attribute("shape"); + public static final Attribute COORDS = new Attribute("coords"); + public static final Attribute ISMAP = new Attribute("ismap"); + public static final Attribute NOHREF = new Attribute("nohref"); + public static final Attribute ALT = new Attribute("alt"); + public static final Attribute ID = new Attribute("id"); + public static final Attribute SRC = new Attribute("src"); + public static final Attribute HSPACE = new Attribute("hspace"); + public static final Attribute VSPACE = new Attribute("vspace"); + public static final Attribute USEMAP = new Attribute("usemap"); + public static final Attribute LOWSRC = new Attribute("lowsrc"); + public static final Attribute CODEBASE = new Attribute("codebase"); + public static final Attribute CODE = new Attribute("code"); + public static final Attribute ARCHIVE = new Attribute("archive"); + public static final Attribute VALUE = new Attribute("value"); + public static final Attribute VALUETYPE = new Attribute("valuetype"); + public static final Attribute TYPE = new Attribute("type"); + public static final Attribute CLASS = new Attribute("class"); + public static final Attribute STYLE = new Attribute("style"); + public static final Attribute LANG = new Attribute("lang"); + public static final Attribute FACE = new Attribute("face"); + public static final Attribute DIR = new Attribute("dir"); + public static final Attribute DECLARE = new Attribute("declare"); + public static final Attribute CLASSID = new Attribute("classid"); + public static final Attribute DATA = new Attribute("data"); + public static final Attribute CODETYPE = new Attribute("codetype"); + public static final Attribute STANDBY = new Attribute("standby"); + public static final Attribute BORDER = new Attribute("border"); + public static final Attribute SHAPES = new Attribute("shapes"); + public static final Attribute NOSHADE = new Attribute("noshade"); + public static final Attribute COMPACT = new Attribute("compact"); + public static final Attribute START = new Attribute("start"); + public static final Attribute ACTION = new Attribute("action"); + public static final Attribute METHOD = new Attribute("method"); + public static final Attribute ENCTYPE = new Attribute("enctype"); + public static final Attribute CHECKED = new Attribute("checked"); + public static final Attribute MAXLENGTH = new Attribute("maxlength"); + public static final Attribute MULTIPLE = new Attribute("multiple"); + public static final Attribute SELECTED = new Attribute("selected"); + public static final Attribute ROWS = new Attribute("rows"); + public static final Attribute COLS = new Attribute("cols"); + public static final Attribute DUMMY = new Attribute("dummy"); + public static final Attribute CELLSPACING = new Attribute("cellspacing"); + public static final Attribute CELLPADDING = new Attribute("cellpadding"); + public static final Attribute VALIGN = new Attribute("valign"); + public static final Attribute HALIGN = new Attribute("halign"); + public static final Attribute NOWRAP = new Attribute("nowrap"); + public static final Attribute ROWSPAN = new Attribute("rowspan"); + public static final Attribute COLSPAN = new Attribute("colspan"); + public static final Attribute PROMPT = new Attribute("prompt"); + public static final Attribute HTTPEQUIV = new Attribute("http-equiv"); + public static final Attribute CONTENT = new Attribute("content"); + public static final Attribute LANGUAGE = new Attribute("language"); + public static final Attribute VERSION = new Attribute("version"); + public static final Attribute N = new Attribute("n"); + public static final Attribute FRAMEBORDER = new Attribute("frameborder"); + public static final Attribute MARGINWIDTH = new Attribute("marginwidth"); + public static final Attribute MARGINHEIGHT = new Attribute("marginheight"); + public static final Attribute SCROLLING = new Attribute("scrolling"); + public static final Attribute NORESIZE = new Attribute("noresize"); + public static final Attribute ENDTAG = new Attribute("endtag"); + public static final Attribute COMMENT = new Attribute("comment"); + static final Attribute MEDIA = new Attribute("media"); + + static final Attribute allAttributes[] = { + FACE, + COMMENT, + SIZE, + COLOR, + CLEAR, + BACKGROUND, + BGCOLOR, + TEXT, + LINK, + VLINK, + ALINK, + WIDTH, + HEIGHT, + ALIGN, + NAME, + HREF, + REL, + REV, + TITLE, + TARGET, + SHAPE, + COORDS, + ISMAP, + NOHREF, + ALT, + ID, + SRC, + HSPACE, + VSPACE, + USEMAP, + LOWSRC, + CODEBASE, + CODE, + ARCHIVE, + VALUE, + VALUETYPE, + TYPE, + CLASS, + STYLE, + LANG, + DIR, + DECLARE, + CLASSID, + DATA, + CODETYPE, + STANDBY, + BORDER, + SHAPES, + NOSHADE, + COMPACT, + START, + ACTION, + METHOD, + ENCTYPE, + CHECKED, + MAXLENGTH, + MULTIPLE, + SELECTED, + ROWS, + COLS, + DUMMY, + CELLSPACING, + CELLPADDING, + VALIGN, + HALIGN, + NOWRAP, + ROWSPAN, + COLSPAN, + PROMPT, + HTTPEQUIV, + CONTENT, + LANGUAGE, + VERSION, + N, + FRAMEBORDER, + MARGINWIDTH, + MARGINHEIGHT, + SCROLLING, + NORESIZE, + MEDIA, + ENDTAG + }; + } + + // The secret to 73, is that, given that the Hashtable contents + // never change once the static initialization happens, the initial size + // that the hashtable grew to was determined, and then that very size + // is used. + // + private static final Hashtable tagHashtable = new Hashtable(73); + + /** Maps from StyleConstant key to HTML.Tag. */ + private static final Hashtable scMapping = new Hashtable(8); + + static { + + for (int i = 0; i < Tag.allTags.length; i++ ) { + tagHashtable.put(Tag.allTags[i].toString(), Tag.allTags[i]); + StyleContext.registerStaticAttributeKey(Tag.allTags[i]); + } + StyleContext.registerStaticAttributeKey(Tag.IMPLIED); + StyleContext.registerStaticAttributeKey(Tag.CONTENT); + StyleContext.registerStaticAttributeKey(Tag.COMMENT); + for (int i = 0; i < Attribute.allAttributes.length; i++) { + StyleContext.registerStaticAttributeKey(Attribute. + allAttributes[i]); + } + StyleContext.registerStaticAttributeKey(HTML.NULL_ATTRIBUTE_VALUE); + scMapping.put(StyleConstants.Bold, Tag.B); + scMapping.put(StyleConstants.Italic, Tag.I); + scMapping.put(StyleConstants.Underline, Tag.U); + scMapping.put(StyleConstants.StrikeThrough, Tag.STRIKE); + scMapping.put(StyleConstants.Superscript, Tag.SUP); + scMapping.put(StyleConstants.Subscript, Tag.SUB); + scMapping.put(StyleConstants.FontFamily, Tag.FONT); + scMapping.put(StyleConstants.FontSize, Tag.FONT); + } + + /** + * Returns the set of actual HTML tags that + * are recognized by the default HTML reader. + * This set does not include tags that are + * manufactured by the reader. + */ + public static Tag[] getAllTags() { + Tag[] tags = new Tag[Tag.allTags.length]; + System.arraycopy(Tag.allTags, 0, tags, 0, Tag.allTags.length); + return tags; + } + + /** + * Fetches a tag constant for a well-known tag name (i.e. one of + * the tags in the set {A, ADDRESS, APPLET, AREA, B, + * BASE, BASEFONT, BIG, + * BLOCKQUOTE, BODY, BR, CAPTION, CENTER, CITE, CODE, + * DD, DFN, DIR, DIV, DL, DT, EM, FONT, FORM, FRAME, + * FRAMESET, H1, H2, H3, H4, H5, H6, HEAD, HR, HTML, + * I, IMG, INPUT, ISINDEX, KBD, LI, LINK, MAP, MENU, + * META, NOBR, NOFRAMES, OBJECT, OL, OPTION, P, PARAM, + * PRE, SAMP, SCRIPT, SELECT, SMALL, SPAN, STRIKE, S, + * STRONG, STYLE, SUB, SUP, TABLE, TD, TEXTAREA, + * TH, TITLE, TR, TT, U, UL, VAR}. If the given + * name does not represent one of the well-known tags, then + * null will be returned. + * + * @param tagName the String name requested + * @return a tag constant corresponding to the tagName, + * or null if not found + */ + public static Tag getTag(String tagName) { + + Object t = tagHashtable.get(tagName); + return (t == null ? null : (Tag)t); + } + + /** + * Returns the HTML Tag associated with the + * StyleConstants key sc. + * If no matching Tag is found, returns + * null. + * + * @param sc the StyleConstants key + * @return tag which corresponds to sc, or + * null if not found + */ + static Tag getTagForStyleConstantsKey(StyleConstants sc) { + return (Tag)scMapping.get(sc); + } + + /** + * Fetches an integer attribute value. Attribute values + * are stored as a string, and this is a convenience method + * to convert to an actual integer. + * + * @param attr the set of attributes to use to try to fetch a value + * @param key the key to use to fetch the value + * @param def the default value to use if the attribute isn't + * defined or there is an error converting to an integer + */ + public static int getIntegerAttributeValue(AttributeSet attr, + Attribute key, int def) { + int value = def; + String istr = (String) attr.getAttribute(key); + if (istr != null) { + try { + value = Integer.valueOf(istr).intValue(); + } catch (NumberFormatException e) { + value = def; + } + } + return value; + } + + // This is used in cases where the value for the attribute has not + // been specified. + // + public static final String NULL_ATTRIBUTE_VALUE = "#DEFAULT"; + + // size determined similar to size of tagHashtable + private static final Hashtable attHashtable = new Hashtable(77); + + static { + + for (int i = 0; i < Attribute.allAttributes.length; i++ ) { + attHashtable.put(Attribute.allAttributes[i].toString(), Attribute.allAttributes[i]); + } + } + + /** + * Returns the set of HTML attributes recognized. + * @return the set of HTML attributes recognized + */ + public static Attribute[] getAllAttributeKeys() { + Attribute[] attributes = new Attribute[Attribute.allAttributes.length]; + System.arraycopy(Attribute.allAttributes, 0, + attributes, 0, Attribute.allAttributes.length); + return attributes; + } + + /** + * Fetches an attribute constant for a well-known attribute name + * (i.e. one of the attributes in the set {FACE, COMMENT, SIZE, + * COLOR, CLEAR, BACKGROUND, BGCOLOR, TEXT, LINK, VLINK, ALINK, + * WIDTH, HEIGHT, ALIGN, NAME, HREF, REL, REV, TITLE, TARGET, + * SHAPE, COORDS, ISMAP, NOHREF, ALT, ID, SRC, HSPACE, VSPACE, + * USEMAP, LOWSRC, CODEBASE, CODE, ARCHIVE, VALUE, VALUETYPE, + * TYPE, CLASS, STYLE, LANG, DIR, DECLARE, CLASSID, DATA, CODETYPE, + * STANDBY, BORDER, SHAPES, NOSHADE, COMPACT, START, ACTION, METHOD, + * ENCTYPE, CHECKED, MAXLENGTH, MULTIPLE, SELECTED, ROWS, COLS, + * DUMMY, CELLSPACING, CELLPADDING, VALIGN, HALIGN, NOWRAP, ROWSPAN, + * COLSPAN, PROMPT, HTTPEQUIV, CONTENT, LANGUAGE, VERSION, N, + * FRAMEBORDER, MARGINWIDTH, MARGINHEIGHT, SCROLLING, NORESIZE, + * MEDIA, ENDTAG}). + * If the given name does not represent one of the well-known attributes, + * then null will be returned. + * + * @param attName the String requested + * @return the Attribute corresponding to attName + */ + public static Attribute getAttributeKey(String attName) { + Object a = attHashtable.get(attName); + if (a == null) { + return null; + } + return (Attribute)a; + } + +} diff --git a/src/share/classes/javax/swing/text/html/HTMLDocument.java b/src/share/classes/javax/swing/text/html/HTMLDocument.java new file mode 100644 index 000000000..871372cb8 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/HTMLDocument.java @@ -0,0 +1,4185 @@ +/* + * Copyright 1997-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.Color; +import java.awt.Component; +import java.awt.font.TextAttribute; +import java.util.*; +import java.net.URL; +import java.net.URLEncoder; +import java.net.MalformedURLException; +import java.io.*; +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.text.*; +import javax.swing.undo.*; +import java.text.Bidi; +import sun.swing.SwingUtilities2; + +/** + * A document that models HTML. The purpose of this model is to + * support both browsing and editing. As a result, the structure + * described by an HTML document is not exactly replicated by default. + * The element structure that is modeled by default, is built by the + * class HTMLDocument.HTMLReader, which implements the + * HTMLEditorKit.ParserCallback protocol that the parser + * expects. To change the structure one can subclass + * HTMLReader, and reimplement the method {@link + * #getReader(int)} to return the new reader implementation. The + * documentation for HTMLReader should be consulted for + * the details of the default structure created. The intent is that + * the document be non-lossy (although reproducing the HTML format may + * result in a different format). + * + *

    The document models only HTML, and makes no attempt to store + * view attributes in it. The elements are identified by the + * StyleContext.NameAttribute attribute, which should + * always have a value of type HTML.Tag that identifies + * the kind of element. Some of the elements (such as comments) are + * synthesized. The HTMLFactory uses this attribute to + * determine what kind of view to build.

    + * + *

    This document supports incremental loading. The + * TokenThreshold property controls how much of the parse + * is buffered before trying to update the element structure of the + * document. This property is set by the EditorKit so + * that subclasses can disable it.

    + * + *

    The Base property determines the URL against which + * relative URLs are resolved. By default, this will be the + * Document.StreamDescriptionProperty if the value of the + * property is a URL. If a <BASE> tag is encountered, the base + * will become the URL specified by that tag. Because the base URL is + * a property, it can of course be set directly.

    + * + *

    The default content storage mechanism for this document is a gap + * buffer (GapContent). Alternatives can be supplied by + * using the constructor that takes a Content + * implementation.

    + * + *

    Modifying HTMLDocument

    + * + *

    In addition to the methods provided by Document and + * StyledDocument for mutating an HTMLDocument, HTMLDocument provides + * a number of convenience methods. The following methods can be used + * to insert HTML content into an existing document.

    + * + *
      + *
    • {@link #setInnerHTML(Element, String)}
    • + *
    • {@link #setOuterHTML(Element, String)}
    • + *
    • {@link #insertBeforeStart(Element, String)}
    • + *
    • {@link #insertAfterStart(Element, String)}
    • + *
    • {@link #insertBeforeEnd(Element, String)}
    • + *
    • {@link #insertAfterEnd(Element, String)}
    • + *
    + * + *

    The following examples illustrate using these methods. Each + * example assumes the HTML document is initialized in the following + * way:

    + * + *
    + * JEditorPane p = new JEditorPane();
    + * p.setContentType("text/html");
    + * p.setText("..."); // Document text is provided below.
    + * HTMLDocument d = (HTMLDocument) p.getDocument();
    + * 
    + * + *

    With the following HTML content:

    + * + *
    + * <html>
    + *   <head>
    + *     <title>An example HTMLDocument</title>
    + *     <style type="text/css">
    + *       div { background-color: silver; }
    + *       ul { color: red; }
    + *     </style>
    + *   </head>
    + *   <body>
    + *     <div id="BOX">
    + *       <p>Paragraph 1</p>
    + *       <p>Paragraph 2</p>
    + *     </div>
    + *   </body>
    + * </html>
    + * 
    + * + *

    All the methods for modifying an HTML document require an {@link + * Element}. Elements can be obtained from an HTML document by using + * the method {@link #getElement(Element e, Object attribute, Object + * value)}. It returns the first descendant element that contains the + * specified attribute with the given value, in depth-first order. + * For example, d.getElement(d.getDefaultRootElement(), + * StyleConstants.NameAttribute, HTML.Tag.P) returns the first + * paragraph element.

    + * + *

    A convenient shortcut for locating elements is the method {@link + * #getElement(String)}; returns an element whose ID + * attribute matches the specified value. For example, + * d.getElement("BOX") returns the DIV + * element.

    + * + *

    The {@link #getIterator(HTML.Tag t)} method can also be used for + * finding all occurrences of the specified HTML tag in the + * document.

    + * + *

    Inserting elements

    + * + *

    Elements can be inserted before or after the existing children + * of any non-leaf element by using the methods + * insertAfterStart and insertBeforeEnd. + * For example, if e is the DIV element, + * d.insertAfterStart(e, "<ul><li>List + * Item</li></ul>") inserts the list before the first + * paragraph, and d.insertBeforeEnd(e, "<ul><li>List + * Item</li></ul>") inserts the list after the last + * paragraph. The DIV block becomes the parent of the + * newly inserted elements.

    + * + *

    Sibling elements can be inserted before or after any element by + * using the methods insertBeforeStart and + * insertAfterEnd. For example, if e is the + * DIV element, d.insertBeforeStart(e, + * "<ul><li>List Item</li></ul>") inserts the list + * before the DIV element, and d.insertAfterEnd(e, + * "<ul><li>List Item</li></ul>") inserts the list + * after the DIV element. The newly inserted elements + * become siblings of the DIV element.

    + * + *

    Replacing elements

    + * + *

    Elements and all their descendants can be replaced by using the + * methods setInnerHTML and setOuterHTML. + * For example, if e is the DIV element, + * d.setInnerHTML(e, "<ul><li>List + * Item</li></ul>") replaces all children paragraphs with + * the list, and d.setOuterHTML(e, "<ul><li>List + * Item</li></ul>") replaces the DIV element + * itself. In latter case the parent of the list is the + * BODY element. + * + *

    Summary

    + * + *

    The following table shows the example document and the results + * of various methods described above.

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    ExampleinsertAfterStartinsertBeforeEndinsertBeforeStartinsertAfterEndsetInnerHTMLsetOuterHTML
    + *
    + *

    Paragraph 1

    + *

    Paragraph 2

    + *
    + *
    + *
    + *
      + *
    • List Item
    • + *
    + *

    Paragraph 1

    + *

    Paragraph 2

    + *
    + *
    + *
    + *

    Paragraph 1

    + *

    Paragraph 2

    + *
      + *
    • List Item
    • + *
    + *
    + *
    + *
      + *
    • List Item
    • + *
    + *
    + *

    Paragraph 1

    + *

    Paragraph 2

    + *
    + *
    + *
    + *

    Paragraph 1

    + *

    Paragraph 2

    + *
    + *
      + *
    • List Item
    • + *
    + *
    + *
    + *
      + *
    • List Item
    • + *
    + *
    + *
    + *
      + *
    • List Item
    • + *
    + *
    + * + *

    Warning: Serialized objects of this class will + * not be compatible with future Swing releases. The current + * serialization support is appropriate for short term storage or RMI + * between applications running the same version of Swing. As of 1.4, + * support for long term storage of all JavaBeansTM has been added to the + * java.beans package. Please see {@link + * java.beans.XMLEncoder}.

    + * + * @author Timothy Prinzing + * @author Scott Violet + * @author Sunita Mani + */ +public class HTMLDocument extends DefaultStyledDocument { + /** + * Constructs an HTML document using the default buffer size + * and a default StyleSheet. This is a convenience + * method for the constructor + * HTMLDocument(Content, StyleSheet). + */ + public HTMLDocument() { + this(new GapContent(BUFFER_SIZE_DEFAULT), new StyleSheet()); + } + + /** + * Constructs an HTML document with the default content + * storage implementation and the specified style/attribute + * storage mechanism. This is a convenience method for the + * constructor + * HTMLDocument(Content, StyleSheet). + * + * @param styles the styles + */ + public HTMLDocument(StyleSheet styles) { + this(new GapContent(BUFFER_SIZE_DEFAULT), styles); + } + + /** + * Constructs an HTML document with the given content + * storage implementation and the given style/attribute + * storage mechanism. + * + * @param c the container for the content + * @param styles the styles + */ + public HTMLDocument(Content c, StyleSheet styles) { + super(c, styles); + } + + /** + * Fetches the reader for the parser to use when loading the document + * with HTML. This is implemented to return an instance of + * HTMLDocument.HTMLReader. + * Subclasses can reimplement this + * method to change how the document gets structured if desired. + * (For example, to handle custom tags, or structurally represent character + * style elements.) + * + * @param pos the starting position + * @return the reader used by the parser to load the document + */ + public HTMLEditorKit.ParserCallback getReader(int pos) { + Object desc = getProperty(Document.StreamDescriptionProperty); + if (desc instanceof URL) { + setBase((URL)desc); + } + HTMLReader reader = new HTMLReader(pos); + return reader; + } + + /** + * Returns the reader for the parser to use to load the document + * with HTML. This is implemented to return an instance of + * HTMLDocument.HTMLReader. + * Subclasses can reimplement this + * method to change how the document gets structured if desired. + * (For example, to handle custom tags, or structurally represent character + * style elements.) + *

    This is a convenience method for + * getReader(int, int, int, HTML.Tag, TRUE). + * + * @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 + * @return the reader used by the parser to load the document + */ + public HTMLEditorKit.ParserCallback getReader(int pos, int popDepth, + int pushDepth, + HTML.Tag insertTag) { + return getReader(pos, popDepth, pushDepth, insertTag, true); + } + + /** + * Fetches the reader for the parser to use to load the document + * with HTML. This is implemented to return an instance of + * HTMLDocument.HTMLReader. Subclasses can reimplement this + * method to change how the document get structured if desired + * (e.g. to handle custom tags, structurally represent character + * style elements, etc.). + * + * @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 + * @param insertInsertTag false if all the Elements after insertTag should + * be inserted; otherwise insertTag will be inserted + * @return the reader used by the parser to load the document + */ + HTMLEditorKit.ParserCallback getReader(int pos, int popDepth, + int pushDepth, + HTML.Tag insertTag, + boolean insertInsertTag) { + Object desc = getProperty(Document.StreamDescriptionProperty); + if (desc instanceof URL) { + setBase((URL)desc); + } + HTMLReader reader = new HTMLReader(pos, popDepth, pushDepth, + insertTag, insertInsertTag, false, + true); + return reader; + } + + /** + * Returns the location to resolve relative URLs against. By + * default this will be the document's URL if the document + * was loaded from a URL. If a base tag is found and + * can be parsed, it will be used as the base location. + * + * @return the base location + */ + public URL getBase() { + return base; + } + + /** + * Sets the location to resolve relative URLs against. By + * default this will be the document's URL if the document + * was loaded from a URL. If a base tag is found and + * can be parsed, it will be used as the base location. + *

    This also sets the base of the StyleSheet + * to be u as well as the base of the document. + * + * @param u the desired base URL + */ + public void setBase(URL u) { + base = u; + getStyleSheet().setBase(u); + } + + /** + * Inserts new elements in bulk. This is how elements get created + * in the document. The parsing determines what structure is needed + * and creates the specification as a set of tokens that describe the + * edit while leaving the document free of a write-lock. This method + * can then be called in bursts by the reader to acquire a write-lock + * for a shorter duration (i.e. while the document is actually being + * altered). + * + * @param offset the starting offset + * @param data the element data + * @exception BadLocationException if the given position does not + * represent a valid location in the associated document. + */ + protected void insert(int offset, ElementSpec[] data) throws BadLocationException { + super.insert(offset, data); + } + + /** + * Updates document structure as a result of text insertion. This + * will happen within a write lock. This implementation simply + * parses the inserted content for line breaks and builds up a set + * of instructions for the element buffer. + * + * @param chng a description of the document change + * @param attr the attributes + */ + protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) { + if(attr == null) { + attr = contentAttributeSet; + } + + // If this is the composed text element, merge the content attribute to it + else if (attr.isDefined(StyleConstants.ComposedTextAttribute)) { + ((MutableAttributeSet)attr).addAttributes(contentAttributeSet); + } + + if (attr.isDefined(IMPLIED_CR)) { + ((MutableAttributeSet)attr).removeAttribute(IMPLIED_CR); + } + + super.insertUpdate(chng, attr); + } + + /** + * Replaces the contents of the document with the given + * element specifications. This is called before insert if + * the loading is done in bursts. This is the only method called + * if loading the document entirely in one burst. + * + * @param data the new contents of the document + */ + protected void create(ElementSpec[] data) { + super.create(data); + } + + /** + * Sets attributes for a paragraph. + *

    + * This method is thread safe, although most Swing methods + * are not. Please see + * How + * to Use Threads for more information. + * + * @param offset the offset into the paragraph (must be at least 0) + * @param length the number of characters affected (must be at least 0) + * @param s the attributes + * @param replace whether to replace existing attributes, or merge them + */ + public void setParagraphAttributes(int offset, int length, AttributeSet s, + boolean replace) { + try { + writeLock(); + // Make sure we send out a change for the length of the paragraph. + int end = Math.min(offset + length, getLength()); + Element e = getParagraphElement(offset); + offset = e.getStartOffset(); + e = getParagraphElement(end); + length = Math.max(0, e.getEndOffset() - offset); + DefaultDocumentEvent changes = + new DefaultDocumentEvent(offset, length, + DocumentEvent.EventType.CHANGE); + AttributeSet sCopy = s.copyAttributes(); + int lastEnd = Integer.MAX_VALUE; + for (int pos = offset; pos <= end; pos = lastEnd) { + Element paragraph = getParagraphElement(pos); + if (lastEnd == paragraph.getEndOffset()) { + lastEnd++; + } + else { + lastEnd = paragraph.getEndOffset(); + } + MutableAttributeSet attr = + (MutableAttributeSet) paragraph.getAttributes(); + changes.addEdit(new AttributeUndoableEdit(paragraph, sCopy, replace)); + if (replace) { + attr.removeAttributes(attr); + } + attr.addAttributes(s); + } + changes.end(); + fireChangedUpdate(changes); + fireUndoableEditUpdate(new UndoableEditEvent(this, changes)); + } finally { + writeUnlock(); + } + } + + /** + * Fetches the StyleSheet with the document-specific display + * rules (CSS) that were specified in the HTML document itself. + * + * @return the StyleSheet + */ + public StyleSheet getStyleSheet() { + return (StyleSheet) getAttributeContext(); + } + + /** + * Fetches an iterator for the specified HTML tag. + * This can be used for things like iterating over the + * set of anchors contained, or iterating over the input + * elements. + * + * @param t the requested HTML.Tag + * @return the Iterator for the given HTML tag + * @see javax.swing.text.html.HTML.Tag + */ + public Iterator getIterator(HTML.Tag t) { + if (t.isBlock()) { + // TBD + return null; + } + return new LeafIterator(t, this); + } + + /** + * Creates a document leaf element that directly represents + * text (doesn't have any children). This is implemented + * to return an element of type + * HTMLDocument.RunElement. + * + * @param parent the parent element + * @param a the attributes for the element + * @param p0 the beginning of the range (must be at least 0) + * @param p1 the end of the range (must be at least p0) + * @return the new element + */ + protected Element createLeafElement(Element parent, AttributeSet a, int p0, int p1) { + return new RunElement(parent, a, p0, p1); + } + + /** + * Creates a document branch element, that can contain other elements. + * This is implemented to return an element of type + * HTMLDocument.BlockElement. + * + * @param parent the parent element + * @param a the attributes + * @return the element + */ + protected Element createBranchElement(Element parent, AttributeSet a) { + return new BlockElement(parent, a); + } + + /** + * Creates the root element to be used to represent the + * default document structure. + * + * @return the element base + */ + protected AbstractElement createDefaultRoot() { + // grabs a write-lock for this initialization and + // abandon it during initialization so in normal + // operation we can detect an illegitimate attempt + // to mutate attributes. + writeLock(); + MutableAttributeSet a = new SimpleAttributeSet(); + a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.HTML); + BlockElement html = new BlockElement(null, a.copyAttributes()); + a.removeAttributes(a); + a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.BODY); + BlockElement body = new BlockElement(html, a.copyAttributes()); + a.removeAttributes(a); + a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.P); + getStyleSheet().addCSSAttributeFromHTML(a, CSS.Attribute.MARGIN_TOP, "0"); + BlockElement paragraph = new BlockElement(body, a.copyAttributes()); + a.removeAttributes(a); + a.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT); + RunElement brk = new RunElement(paragraph, a, 0, 1); + Element[] buff = new Element[1]; + buff[0] = brk; + paragraph.replace(0, 0, buff); + buff[0] = paragraph; + body.replace(0, 0, buff); + buff[0] = body; + html.replace(0, 0, buff); + writeUnlock(); + return html; + } + + /** + * Sets the number of tokens to buffer before trying to update + * the documents element structure. + * + * @param n the number of tokens to buffer + */ + public void setTokenThreshold(int n) { + putProperty(TokenThreshold, new Integer(n)); + } + + /** + * Gets the number of tokens to buffer before trying to update + * the documents element structure. The default value is + * Integer.MAX_VALUE. + * + * @return the number of tokens to buffer + */ + public int getTokenThreshold() { + Integer i = (Integer) getProperty(TokenThreshold); + if (i != null) { + return i.intValue(); + } + return Integer.MAX_VALUE; + } + + /** + * Determines how unknown tags are handled by the parser. + * If set to true, unknown + * tags are put in the model, otherwise they are dropped. + * + * @param preservesTags true if unknown tags should be + * saved in the model, otherwise tags are dropped + * @see javax.swing.text.html.HTML.Tag + */ + public void setPreservesUnknownTags(boolean preservesTags) { + preservesUnknownTags = preservesTags; + } + + /** + * Returns the behavior the parser observes when encountering + * unknown tags. + * + * @see javax.swing.text.html.HTML.Tag + * @return true if unknown tags are to be preserved when parsing + */ + public boolean getPreservesUnknownTags() { + return preservesUnknownTags; + } + + /** + * Processes HyperlinkEvents that + * are generated by documents in an HTML frame. + * The HyperlinkEvent type, as the parameter suggests, + * is HTMLFrameHyperlinkEvent. + * In addition to the typical information contained in a + * HyperlinkEvent, + * this event contains the element that corresponds to the frame in + * which the click happened (the source element) and the + * target name. The target name has 4 possible values: + *

      + *
    • _self + *
    • _parent + *
    • _top + *
    • a named frame + *
    + * + * If target is _self, the action is to change the value of the + * HTML.Attribute.SRC attribute and fires a + * ChangedUpdate event. + *

    + * If the target is _parent, then it deletes the parent element, + * which is a <FRAMESET> element, and inserts a new <FRAME> + * element, and sets its HTML.Attribute.SRC attribute + * to have a value equal to the destination URL and fire a + * RemovedUpdate and InsertUpdate. + *

    + * If the target is _top, this method does nothing. In the implementation + * of the view for a frame, namely the FrameView, + * the processing of _top is handled. Given that _top implies + * replacing the entire document, it made sense to handle this outside + * of the document that it will replace. + *

    + * If the target is a named frame, then the element hierarchy is searched + * for an element with a name equal to the target, its + * HTML.Attribute.SRC attribute is updated and a + * ChangedUpdate event is fired. + * + * @param e the event + */ + public void processHTMLFrameHyperlinkEvent(HTMLFrameHyperlinkEvent e) { + String frameName = e.getTarget(); + Element element = e.getSourceElement(); + String urlStr = e.getURL().toString(); + + if (frameName.equals("_self")) { + /* + The source and destination elements + are the same. + */ + updateFrame(element, urlStr); + } else if (frameName.equals("_parent")) { + /* + The destination is the parent of the frame. + */ + updateFrameSet(element.getParentElement(), urlStr); + } else { + /* + locate a named frame + */ + Element targetElement = findFrame(frameName); + if (targetElement != null) { + updateFrame(targetElement, urlStr); + } + } + } + + + /** + * Searches the element hierarchy for an FRAME element + * that has its name attribute equal to the frameName. + * + * @param frameName + * @return the element whose NAME attribute has a value of + * frameName; returns null + * if not found + */ + private Element findFrame(String frameName) { + ElementIterator it = new ElementIterator(this); + Element next = null; + + while ((next = it.next()) != null) { + AttributeSet attr = next.getAttributes(); + if (matchNameAttribute(attr, HTML.Tag.FRAME)) { + String frameTarget = (String)attr.getAttribute(HTML.Attribute.NAME); + if (frameTarget != null && frameTarget.equals(frameName)) { + break; + } + } + } + return next; + } + + /** + * Returns true if StyleConstants.NameAttribute is + * equal to the tag that is passed in as a parameter. + * + * @param attr the attributes to be matched + * @param tag the value to be matched + * @return true if there is a match, false otherwise + * @see javax.swing.text.html.HTML.Attribute + */ + static boolean matchNameAttribute(AttributeSet attr, HTML.Tag tag) { + Object o = attr.getAttribute(StyleConstants.NameAttribute); + if (o instanceof HTML.Tag) { + HTML.Tag name = (HTML.Tag) o; + if (name == tag) { + return true; + } + } + return false; + } + + /** + * Replaces a frameset branch Element with a frame leaf element. + * + * @param element the frameset element to remove + * @param url the value for the SRC attribute for the + * new frame that will replace the frameset + */ + private void updateFrameSet(Element element, String url) { + try { + int startOffset = element.getStartOffset(); + int endOffset = Math.min(getLength(), element.getEndOffset()); + String html = "HTML.Attribute.SRC attribute + * and fires a ChangedUpdate event. + * + * @param element a FRAME element whose SRC attribute will be updated + * @param url a string specifying the new value for the SRC attribute + */ + private void updateFrame(Element element, String url) { + + try { + writeLock(); + DefaultDocumentEvent changes = new DefaultDocumentEvent(element.getStartOffset(), + 1, + DocumentEvent.EventType.CHANGE); + AttributeSet sCopy = element.getAttributes().copyAttributes(); + MutableAttributeSet attr = (MutableAttributeSet) element.getAttributes(); + changes.addEdit(new AttributeUndoableEdit(element, sCopy, false)); + attr.removeAttribute(HTML.Attribute.SRC); + attr.addAttribute(HTML.Attribute.SRC, url); + changes.end(); + fireChangedUpdate(changes); + fireUndoableEditUpdate(new UndoableEditEvent(this, changes)); + } finally { + writeUnlock(); + } + } + + + /** + * Returns true if the document will be viewed in a frame. + * @return true if document will be viewed in a frame, otherwise false + */ + boolean isFrameDocument() { + return frameDocument; + } + + /** + * Sets a boolean state about whether the document will be + * viewed in a frame. + * @param frameDoc true if the document will be viewed in a frame, + * otherwise false + */ + void setFrameDocumentState(boolean frameDoc) { + this.frameDocument = frameDoc; + } + + /** + * Adds the specified map, this will remove a Map that has been + * previously registered with the same name. + * + * @param map the Map to be registered + */ + void addMap(Map map) { + String name = map.getName(); + + if (name != null) { + Object maps = getProperty(MAP_PROPERTY); + + if (maps == null) { + maps = new Hashtable(11); + putProperty(MAP_PROPERTY, maps); + } + if (maps instanceof Hashtable) { + ((Hashtable)maps).put("#" + name, map); + } + } + } + + /** + * Removes a previously registered map. + * @param map the Map to be removed + */ + void removeMap(Map map) { + String name = map.getName(); + + if (name != null) { + Object maps = getProperty(MAP_PROPERTY); + + if (maps instanceof Hashtable) { + ((Hashtable)maps).remove("#" + name); + } + } + } + + /** + * Returns the Map associated with the given name. + * @param the name of the desired Map + * @return the Map or null if it can't + * be found, or if name is null + */ + Map getMap(String name) { + if (name != null) { + Object maps = getProperty(MAP_PROPERTY); + + if (maps != null && (maps instanceof Hashtable)) { + return (Map)((Hashtable)maps).get(name); + } + } + return null; + } + + /** + * Returns an Enumeration of the possible Maps. + * @return the enumerated list of maps, or null + * if the maps are not an instance of Hashtable + */ + Enumeration getMaps() { + Object maps = getProperty(MAP_PROPERTY); + + if (maps instanceof Hashtable) { + return ((Hashtable)maps).elements(); + } + return null; + } + + /** + * Sets the content type language used for style sheets that do not + * explicitly specify the type. The default is text/css. + * @param contentType the content type language for the style sheets + */ + /* public */ + void setDefaultStyleSheetType(String contentType) { + putProperty(StyleType, contentType); + } + + /** + * Returns the content type language used for style sheets. The default + * is text/css. + * @return the content type language used for the style sheets + */ + /* public */ + String getDefaultStyleSheetType() { + String retValue = (String)getProperty(StyleType); + if (retValue == null) { + return "text/css"; + } + return retValue; + } + + /** + * Sets the parser that is used by the methods that insert html + * into the existing document, such as setInnerHTML, + * and setOuterHTML. + *

    + * HTMLEditorKit.createDefaultDocument will set the parser + * for you. If you create an HTMLDocument by hand, + * be sure and set the parser accordingly. + * @param parser the parser to be used for text insertion + * + * @since 1.3 + */ + public void setParser(HTMLEditorKit.Parser parser) { + this.parser = parser; + putProperty("__PARSER__", null); + } + + /** + * Returns the parser that is used when inserting HTML into the existing + * document. + * @return the parser used for text insertion + * + * @since 1.3 + */ + public HTMLEditorKit.Parser getParser() { + Object p = getProperty("__PARSER__"); + + if (p instanceof HTMLEditorKit.Parser) { + return (HTMLEditorKit.Parser)p; + } + return parser; + } + + /** + * Replaces the children of the given element with the contents + * specified as an HTML string. + * + *

    This will be seen as at least two events, n inserts followed by + * a remove.

    + * + *

    Consider the following structure (the elem + * parameter is in bold).

    + * + *
    +     *     <body>
    +     *       |
    +     *     <div>
    +     *      /  \
    +     *    <p>   <p>
    +     * 
    + * + *

    Invoking setInnerHTML(elem, "<ul><li>") + * results in the following structure (new elements are in red).

    + * + *
    +     *     <body>
    +     *       |
    +     *     <div>
    +     *         \
    +     *         <ul>
    +     *           \
    +     *           <li>
    +     * 
    + * + *

    Parameter elem must not be a leaf element, + * otherwise an IllegalArgumentException is thrown. + * If either elem or htmlText parameter + * is null, no changes are made to the document.

    + * + *

    For this to work correcty, the document must have an + * HTMLEditorKit.Parser set. This will be the case + * if the document was created from an HTMLEditorKit via the + * createDefaultDocument method.

    + * + * @param elem the branch element whose children will be replaced + * @param htmlText the string to be parsed and assigned to elem + * @throws IllegalArgumentException if elem is a leaf + * @throws IllegalStateException if an HTMLEditorKit.Parser + * has not been defined + * @since 1.3 + */ + public void setInnerHTML(Element elem, String htmlText) throws + BadLocationException, IOException { + verifyParser(); + if (elem != null && elem.isLeaf()) { + throw new IllegalArgumentException + ("Can not set inner HTML of a leaf"); + } + if (elem != null && htmlText != null) { + int oldCount = elem.getElementCount(); + int insertPosition = elem.getStartOffset(); + insertHTML(elem, elem.getStartOffset(), htmlText, true); + if (elem.getElementCount() > oldCount) { + // Elements were inserted, do the cleanup. + removeElements(elem, elem.getElementCount() - oldCount, + oldCount); + } + } + } + + /** + * Replaces the given element in the parent with the contents + * specified as an HTML string. + * + *

    This will be seen as at least two events, n inserts followed by + * a remove.

    + * + *

    When replacing a leaf this will attempt to make sure there is + * a newline present if one is needed. This may result in an additional + * element being inserted. Consider, if you were to replace a character + * element that contained a newline with <img> this would create + * two elements, one for the image, ane one for the newline.

    + * + *

    If you try to replace the element at length you will most + * likely end up with two elements, eg + * setOuterHTML(getCharacterElement (getLength()), + * "blah") will result in two leaf elements at the end, one + * representing 'blah', and the other representing the end + * element.

    + * + *

    Consider the following structure (the elem + * parameter is in bold).

    + * + *
    +     *     <body>
    +     *       |
    +     *     <div>
    +     *      /  \
    +     *    <p>   <p>
    +     * 
    + * + *

    Invoking setOuterHTML(elem, "<ul><li>") + * results in the following structure (new elements are in red).

    + * + *
    +     *    <body>
    +     *      |
    +     *     <ul>
    +     *       \
    +     *       <li>
    +     * 
    + * + *

    If either elem or htmlText + * parameter is null, no changes are made to the + * document.

    + * + *

    For this to work correcty, the document must have an + * HTMLEditorKit.Parser set. This will be the case if the document + * was created from an HTMLEditorKit via the + * createDefaultDocument method.

    + * + * @param elem the element to replace + * @param htmlText the string to be parsed and inserted in place of elem + * @throws IllegalStateException if an HTMLEditorKit.Parser has not + * been set + * @since 1.3 + */ + public void setOuterHTML(Element elem, String htmlText) throws + BadLocationException, IOException { + verifyParser(); + if (elem != null && elem.getParentElement() != null && + htmlText != null) { + int start = elem.getStartOffset(); + int end = elem.getEndOffset(); + int startLength = getLength(); + // We don't want a newline if elem is a leaf, and doesn't contain + // a newline. + boolean wantsNewline = !elem.isLeaf(); + if (!wantsNewline && (end > startLength || + getText(end - 1, 1).charAt(0) == NEWLINE[0])){ + wantsNewline = true; + } + Element parent = elem.getParentElement(); + int oldCount = parent.getElementCount(); + insertHTML(parent, start, htmlText, wantsNewline); + // Remove old. + int newLength = getLength(); + if (oldCount != parent.getElementCount()) { + int removeIndex = parent.getElementIndex(start + newLength - + startLength); + removeElements(parent, removeIndex, 1); + } + } + } + + /** + * Inserts the HTML specified as a string at the start + * of the element. + * + *

    Consider the following structure (the elem + * parameter is in bold).

    + * + *
    +     *     <body>
    +     *       |
    +     *     <div>
    +     *      /  \
    +     *    <p>   <p>
    +     * 
    + * + *

    Invoking insertAfterStart(elem, + * "<ul><li>") results in the following structure + * (new elements are in red).

    + * + *
    +     *        <body>
    +     *          |
    +     *        <div>
    +     *       /  |  \
    +     *    <ul> <p> <p>
    +     *     /
    +     *  <li>
    +     * 
    + * + *

    Unlike the insertBeforeStart method, new + * elements become children of the specified element, + * not siblings.

    + * + *

    Parameter elem must not be a leaf element, + * otherwise an IllegalArgumentException is thrown. + * If either elem or htmlText parameter + * is null, no changes are made to the document.

    + * + *

    For this to work correcty, the document must have an + * HTMLEditorKit.Parser set. This will be the case + * if the document was created from an HTMLEditorKit via the + * createDefaultDocument method.

    + * + * @param elem the branch element to be the root for the new text + * @param htmlText the string to be parsed and assigned to elem + * @throws IllegalArgumentException if elem is a leaf + * @throws IllegalStateException if an HTMLEditorKit.Parser has not + * been set on the document + * @since 1.3 + */ + public void insertAfterStart(Element elem, String htmlText) throws + BadLocationException, IOException { + verifyParser(); + if (elem != null && elem.isLeaf()) { + throw new IllegalArgumentException + ("Can not insert HTML after start of a leaf"); + } + insertHTML(elem, elem.getStartOffset(), htmlText, false); + } + + /** + * Inserts the HTML specified as a string at the end of + * the element. + * + *

    If elem's children are leaves, and the + * character at a elem.getEndOffset() - 1 is a newline, + * this will insert before the newline so that there isn't text after + * the newline.

    + * + *

    Consider the following structure (the elem + * parameter is in bold).

    + * + *
    +     *     <body>
    +     *       |
    +     *     <div>
    +     *      /  \
    +     *    <p>   <p>
    +     * 
    + * + *

    Invoking insertBeforeEnd(elem, "<ul><li>") + * results in the following structure (new elements are in red).

    + * + *
    +     *        <body>
    +     *          |
    +     *        <div>
    +     *       /  |  \
    +     *     <p> <p> <ul>
    +     *               \
    +     *               <li>
    +     * 
    + * + *

    Unlike the insertAfterEnd method, new elements + * become children of the specified element, not + * siblings.

    + * + *

    Parameter elem must not be a leaf element, + * otherwise an IllegalArgumentException is thrown. + * If either elem or htmlText parameter + * is null, no changes are made to the document.

    + * + *

    For this to work correcty, the document must have an + * HTMLEditorKit.Parser set. This will be the case + * if the document was created from an HTMLEditorKit via the + * createDefaultDocument method.

    + * + * @param elem the element to be the root for the new text + * @param htmlText the string to be parsed and assigned to elem + * @throws IllegalArgumentException if elem is a leaf + * @throws IllegalStateException if an HTMLEditorKit.Parser has not + * been set on the document + * @since 1.3 + */ + public void insertBeforeEnd(Element elem, String htmlText) throws + BadLocationException, IOException { + verifyParser(); + if (elem != null && elem.isLeaf()) { + throw new IllegalArgumentException + ("Can not set inner HTML before end of leaf"); + } + if (elem != null) { + int offset = elem.getEndOffset(); + if (elem.getElement(elem.getElementIndex(offset - 1)).isLeaf() && + getText(offset - 1, 1).charAt(0) == NEWLINE[0]) { + offset--; + } + insertHTML(elem, offset, htmlText, false); + } + } + + /** + * Inserts the HTML specified as a string before the start of + * the given element. + * + *

    Consider the following structure (the elem + * parameter is in bold).

    + * + *
    +     *     <body>
    +     *       |
    +     *     <div>
    +     *      /  \
    +     *    <p>   <p>
    +     * 
    + * + *

    Invoking insertBeforeStart(elem, + * "<ul><li>") results in the following structure + * (new elements are in red).

    + * + *
    +     *        <body>
    +     *         /  \
    +     *      <ul> <div>
    +     *       /    /  \
    +     *     <li> <p>  <p>
    +     * 
    + * + *

    Unlike the insertAfterStart method, new + * elements become siblings of the specified element, not + * children.

    + * + *

    If either elem or htmlText + * parameter is null, no changes are made to the + * document.

    + * + *

    For this to work correcty, the document must have an + * HTMLEditorKit.Parser set. This will be the case + * if the document was created from an HTMLEditorKit via the + * createDefaultDocument method.

    + * + * @param elem the element the content is inserted before + * @param htmlText the string to be parsed and inserted before elem + * @throws IllegalStateException if an HTMLEditorKit.Parser has not + * been set on the document + * @since 1.3 + */ + public void insertBeforeStart(Element elem, String htmlText) throws + BadLocationException, IOException { + verifyParser(); + if (elem != null) { + Element parent = elem.getParentElement(); + + if (parent != null) { + insertHTML(parent, elem.getStartOffset(), htmlText, false); + } + } + } + + /** + * Inserts the HTML specified as a string after the the end of the + * given element. + * + *

    Consider the following structure (the elem + * parameter is in bold).

    + * + *
    +     *     <body>
    +     *       |
    +     *     <div>
    +     *      /  \
    +     *    <p>   <p>
    +     * 
    + * + *

    Invoking insertAfterEnd(elem, "<ul><li>") + * results in the following structure (new elements are in red).

    + * + *
    +     *        <body>
    +     *         /  \
    +     *      <div> <ul>
    +     *       / \    \
    +     *     <p> <p>  <li>
    +     * 
    + * + *

    Unlike the insertBeforeEnd method, new elements + * become siblings of the specified element, not + * children.

    + * + *

    If either elem or htmlText + * parameter is null, no changes are made to the + * document.

    + * + *

    For this to work correcty, the document must have an + * HTMLEditorKit.Parser set. This will be the case + * if the document was created from an HTMLEditorKit via the + * createDefaultDocument method.

    + * + * @param elem the element the content is inserted after + * @param htmlText the string to be parsed and inserted after elem + * @throws IllegalStateException if an HTMLEditorKit.Parser has not + * been set on the document + * @since 1.3 + */ + public void insertAfterEnd(Element elem, String htmlText) throws + BadLocationException, IOException { + verifyParser(); + if (elem != null) { + Element parent = elem.getParentElement(); + + if (parent != null) { + int offset = elem.getEndOffset(); + if (offset > getLength()) { + offset--; + } + else if (elem.isLeaf() && getText(offset - 1, 1). + charAt(0) == NEWLINE[0]) { + offset--; + } + insertHTML(parent, offset, htmlText, false); + } + } + } + + /** + * Returns the element that has the given id Attribute. + * If the element can't be found, null is returned. + * Note that this method works on an Attribute, + * not a character tag. In the following HTML snippet: + * <a id="HelloThere"> the attribute is + * 'id' and the character tag is 'a'. + * This is a convenience method for + * getElement(RootElement, HTML.Attribute.id, id). + * This is not thread-safe. + * + * @param id the string representing the desired Attribute + * @return the element with the specified Attribute + * or null if it can't be found, + * or null if id is null + * @see javax.swing.text.html.HTML.Attribute + * @since 1.3 + */ + public Element getElement(String id) { + if (id == null) { + return null; + } + return getElement(getDefaultRootElement(), HTML.Attribute.ID, id, + true); + } + + /** + * Returns the child element of e that contains the + * attribute, attribute with value value, or + * null if one isn't found. This is not thread-safe. + * + * @param e the root element where the search begins + * @param attribute the desired Attribute + * @param value the values for the specified Attribute + * @return the element with the specified Attribute + * and the specified value, or null + * if it can't be found + * @see javax.swing.text.html.HTML.Attribute + * @since 1.3 + */ + public Element getElement(Element e, Object attribute, Object value) { + return getElement(e, attribute, value, true); + } + + /** + * Returns the child element of e that contains the + * attribute, attribute with value value, or + * null if one isn't found. This is not thread-safe. + *

    + * If searchLeafAttributes is true, and e is + * a leaf, any attributes that are instances of HTML.Tag + * with a value that is an AttributeSet will also be checked. + * + * @param e the root element where the search begins + * @param attribute the desired Attribute + * @param value the values for the specified Attribute + * @return the element with the specified Attribute + * and the specified value, or null + * if it can't be found + * @see javax.swing.text.html.HTML.Attribute + */ + private Element getElement(Element e, Object attribute, Object value, + boolean searchLeafAttributes) { + AttributeSet attr = e.getAttributes(); + + if (attr != null && attr.isDefined(attribute)) { + if (value.equals(attr.getAttribute(attribute))) { + return e; + } + } + if (!e.isLeaf()) { + for (int counter = 0, maxCounter = e.getElementCount(); + counter < maxCounter; counter++) { + Element retValue = getElement(e.getElement(counter), attribute, + value, searchLeafAttributes); + + if (retValue != null) { + return retValue; + } + } + } + else if (searchLeafAttributes && attr != null) { + // For some leaf elements we store the actual attributes inside + // the AttributeSet of the Element (such as anchors). + Enumeration names = attr.getAttributeNames(); + if (names != null) { + while (names.hasMoreElements()) { + Object name = names.nextElement(); + if ((name instanceof HTML.Tag) && + (attr.getAttribute(name) instanceof AttributeSet)) { + + AttributeSet check = (AttributeSet)attr. + getAttribute(name); + if (check.isDefined(attribute) && + value.equals(check.getAttribute(attribute))) { + return e; + } + } + } + } + } + return null; + } + + /** + * Verifies the document has an HTMLEditorKit.Parser set. + * If getParser returns null, this will throw an + * IllegalStateException. + * + * @throws IllegalStateException if the document does not have a Parser + */ + private void verifyParser() { + if (getParser() == null) { + throw new IllegalStateException("No HTMLEditorKit.Parser"); + } + } + + /** + * Installs a default Parser if one has not been installed yet. + */ + private void installParserIfNecessary() { + if (getParser() == null) { + setParser(new HTMLEditorKit().getParser()); + } + } + + /** + * Inserts a string of HTML into the document at the given position. + * parent is used to identify the location to insert the + * html. If parent is a leaf this can have + * unexpected results. + */ + private void insertHTML(Element parent, int offset, String html, + boolean wantsTrailingNewline) + throws BadLocationException, IOException { + if (parent != null && html != null) { + HTMLEditorKit.Parser parser = getParser(); + if (parser != null) { + int lastOffset = Math.max(0, offset - 1); + Element charElement = getCharacterElement(lastOffset); + Element commonParent = parent; + int pop = 0; + int push = 0; + + if (parent.getStartOffset() > lastOffset) { + while (commonParent != null && + commonParent.getStartOffset() > lastOffset) { + commonParent = commonParent.getParentElement(); + push++; + } + if (commonParent == null) { + throw new BadLocationException("No common parent", + offset); + } + } + while (charElement != null && charElement != commonParent) { + pop++; + charElement = charElement.getParentElement(); + } + if (charElement != null) { + // Found it, do the insert. + HTMLReader reader = new HTMLReader(offset, pop - 1, push, + null, false, true, + wantsTrailingNewline); + + parser.parse(new StringReader(html), reader, true); + reader.flush(); + } + } + } + } + + /** + * Removes child Elements of the passed in Element e. This + * will do the necessary cleanup to ensure the element representing the + * end character is correctly created. + *

    This is not a general purpose method, it assumes that e + * will still have at least one child after the remove, and it assumes + * the character at e.getStartOffset() - 1 is a newline and + * is of length 1. + */ + private void removeElements(Element e, int index, int count) throws BadLocationException { + writeLock(); + try { + int start = e.getElement(index).getStartOffset(); + int end = e.getElement(index + count - 1).getEndOffset(); + if (end > getLength()) { + removeElementsAtEnd(e, index, count, start, end); + } + else { + removeElements(e, index, count, start, end); + } + } finally { + writeUnlock(); + } + } + + /** + * Called to remove child elements of e when one of the + * elements to remove is representing the end character. + *

    Since the Content will not allow a removal to the end character + * this will do a remove from start - 1 to end. + * The end Element(s) will be removed, and the element representing + * start - 1 to start will be recreated. This + * Element has to be recreated as after the content removal its offsets + * become start - 1 to start - 1. + */ + private void removeElementsAtEnd(Element e, int index, int count, + int start, int end) throws BadLocationException { + // index must be > 0 otherwise no insert would have happened. + boolean isLeaf = (e.getElement(index - 1).isLeaf()); + DefaultDocumentEvent dde = new DefaultDocumentEvent( + start - 1, end - start + 1, DocumentEvent. + EventType.REMOVE); + + if (isLeaf) { + Element endE = getCharacterElement(getLength()); + // e.getElement(index - 1) should represent the newline. + index--; + if (endE.getParentElement() != e) { + // The hiearchies don't match, we'll have to manually + // recreate the leaf at e.getElement(index - 1) + replace(dde, e, index, ++count, start, end, true, true); + } + else { + // The hierarchies for the end Element and + // e.getElement(index - 1), match, we can safely remove + // the Elements and the end content will be aligned + // appropriately. + replace(dde, e, index, count, start, end, true, false); + } + } + else { + // Not a leaf, descend until we find the leaf representing + // start - 1 and remove it. + Element newLineE = e.getElement(index - 1); + while (!newLineE.isLeaf()) { + newLineE = newLineE.getElement(newLineE.getElementCount() - 1); + } + newLineE = newLineE.getParentElement(); + replace(dde, e, index, count, start, end, false, false); + replace(dde, newLineE, newLineE.getElementCount() - 1, 1, start, + end, true, true); + } + postRemoveUpdate(dde); + dde.end(); + fireRemoveUpdate(dde); + fireUndoableEditUpdate(new UndoableEditEvent(this, dde)); + } + + /** + * This is used by removeElementsAtEnd, it removes + * count elements starting at start from + * e. If remove is true text of length + * start - 1 to end - 1 is removed. If + * create is true a new leaf is created of length 1. + */ + private void replace(DefaultDocumentEvent dde, Element e, int index, + int count, int start, int end, boolean remove, + boolean create) throws BadLocationException { + Element[] added; + AttributeSet attrs = e.getElement(index).getAttributes(); + Element[] removed = new Element[count]; + + for (int counter = 0; counter < count; counter++) { + removed[counter] = e.getElement(counter + index); + } + if (remove) { + UndoableEdit u = getContent().remove(start - 1, end - start); + if (u != null) { + dde.addEdit(u); + } + } + if (create) { + added = new Element[1]; + added[0] = createLeafElement(e, attrs, start - 1, start); + } + else { + added = new Element[0]; + } + dde.addEdit(new ElementEdit(e, index, removed, added)); + ((AbstractDocument.BranchElement)e).replace( + index, removed.length, added); + } + + /** + * Called to remove child Elements when the end is not touched. + */ + private void removeElements(Element e, int index, int count, + int start, int end) throws BadLocationException { + Element[] removed = new Element[count]; + Element[] added = new Element[0]; + for (int counter = 0; counter < count; counter++) { + removed[counter] = e.getElement(counter + index); + } + DefaultDocumentEvent dde = new DefaultDocumentEvent + (start, end - start, DocumentEvent.EventType.REMOVE); + ((AbstractDocument.BranchElement)e).replace(index, removed.length, + added); + dde.addEdit(new ElementEdit(e, index, removed, added)); + UndoableEdit u = getContent().remove(start, end - start); + if (u != null) { + dde.addEdit(u); + } + postRemoveUpdate(dde); + dde.end(); + fireRemoveUpdate(dde); + if (u != null) { + fireUndoableEditUpdate(new UndoableEditEvent(this, dde)); + } + } + + + // These two are provided for inner class access. The are named different + // than the super class as the super class implementations are final. + void obtainLock() { + writeLock(); + } + + void releaseLock() { + writeUnlock(); + } + + // + // Provided for inner class access. + // + + /** + * Notifies all listeners that have registered interest for + * notification on this event type. The event instance + * is lazily created using the parameters passed into + * the fire method. + * + * @param e the event + * @see EventListenerList + */ + protected void fireChangedUpdate(DocumentEvent e) { + super.fireChangedUpdate(e); + } + + /** + * Notifies all listeners that have registered interest for + * notification on this event type. The event instance + * is lazily created using the parameters passed into + * the fire method. + * + * @param e the event + * @see EventListenerList + */ + protected void fireUndoableEditUpdate(UndoableEditEvent e) { + super.fireUndoableEditUpdate(e); + } + + boolean hasBaseTag() { + return hasBaseTag; + } + + String getBaseTarget() { + return baseTarget; + } + + /* + * state defines whether the document is a frame document + * or not. + */ + private boolean frameDocument = false; + private boolean preservesUnknownTags = true; + + /* + * Used to store button groups for radio buttons in + * a form. + */ + private HashMap radioButtonGroupsMap; + + /** + * Document property for the number of tokens to buffer + * before building an element subtree to represent them. + */ + static final String TokenThreshold = "token threshold"; + + private static final int MaxThreshold = 10000; + + private static final int StepThreshold = 5; + + + /** + * Document property key value. The value for the key will be a Vector + * of Strings that are comments not found in the body. + */ + public static final String AdditionalComments = "AdditionalComments"; + + /** + * Document property key value. The value for the key will be a + * String indicating the default type of stylesheet links. + */ + /* public */ static final String StyleType = "StyleType"; + + /** + * The location to resolve relative URLs against. By + * default this will be the document's URL if the document + * was loaded from a URL. If a base tag is found and + * can be parsed, it will be used as the base location. + */ + URL base; + + /** + * does the document have base tag + */ + boolean hasBaseTag = false; + + /** + * BASE tag's TARGET attribute value + */ + private String baseTarget = null; + + /** + * The parser that is used when inserting html into the existing + * document. + */ + private HTMLEditorKit.Parser parser; + + /** + * Used for inserts when a null AttributeSet is supplied. + */ + private static AttributeSet contentAttributeSet; + + /** + * Property Maps are registered under, will be a Hashtable. + */ + static String MAP_PROPERTY = "__MAP__"; + + private static char[] NEWLINE; + private static final String IMPLIED_CR = "CR"; + + /** + * I18N property key. + * + * @see AbstractDocument.I18NProperty + */ + private static final String I18NProperty = "i18n"; + + static { + contentAttributeSet = new SimpleAttributeSet(); + ((MutableAttributeSet)contentAttributeSet). + addAttribute(StyleConstants.NameAttribute, + HTML.Tag.CONTENT); + NEWLINE = new char[1]; + NEWLINE[0] = '\n'; + } + + + /** + * An iterator to iterate over a particular type of + * tag. The iterator is not thread safe. If reliable + * access to the document is not already ensured by + * the context under which the iterator is being used, + * its use should be performed under the protection of + * Document.render. + */ + public static abstract class Iterator { + + /** + * Return the attributes for this tag. + * @return the AttributeSet for this tag, or + * null if none can be found + */ + public abstract AttributeSet getAttributes(); + + /** + * Returns the start of the range for which the current occurrence of + * the tag is defined and has the same attributes. + * + * @return the start of the range, or -1 if it can't be found + */ + public abstract int getStartOffset(); + + /** + * Returns the end of the range for which the current occurrence of + * the tag is defined and has the same attributes. + * + * @return the end of the range + */ + public abstract int getEndOffset(); + + /** + * Move the iterator forward to the next occurrence + * of the tag it represents. + */ + public abstract void next(); + + /** + * Indicates if the iterator is currently + * representing an occurrence of a tag. If + * false there are no more tags for this iterator. + * @return true if the iterator is currently representing an + * occurrence of a tag, otherwise returns false + */ + public abstract boolean isValid(); + + /** + * Type of tag this iterator represents. + */ + public abstract HTML.Tag getTag(); + } + + /** + * An iterator to iterate over a particular type of tag. + */ + static class LeafIterator extends Iterator { + + LeafIterator(HTML.Tag t, Document doc) { + tag = t; + pos = new ElementIterator(doc); + endOffset = 0; + next(); + } + + /** + * Returns the attributes for this tag. + * @return the AttributeSet for this tag, + * or null if none can be found + */ + public AttributeSet getAttributes() { + Element elem = pos.current(); + if (elem != null) { + AttributeSet a = (AttributeSet) + elem.getAttributes().getAttribute(tag); + if (a == null) { + a = (AttributeSet)elem.getAttributes(); + } + return a; + } + return null; + } + + /** + * Returns the start of the range for which the current occurrence of + * the tag is defined and has the same attributes. + * + * @return the start of the range, or -1 if it can't be found + */ + public int getStartOffset() { + Element elem = pos.current(); + if (elem != null) { + return elem.getStartOffset(); + } + return -1; + } + + /** + * Returns the end of the range for which the current occurrence of + * the tag is defined and has the same attributes. + * + * @return the end of the range + */ + public int getEndOffset() { + return endOffset; + } + + /** + * Moves the iterator forward to the next occurrence + * of the tag it represents. + */ + public void next() { + for (nextLeaf(pos); isValid(); nextLeaf(pos)) { + Element elem = pos.current(); + if (elem.getStartOffset() >= endOffset) { + AttributeSet a = pos.current().getAttributes(); + + if (a.isDefined(tag) || + a.getAttribute(StyleConstants.NameAttribute) == tag) { + + // we found the next one + setEndOffset(); + break; + } + } + } + } + + /** + * Returns the type of tag this iterator represents. + * + * @return the HTML.Tag that this iterator represents. + * @see javax.swing.text.html.HTML.Tag + */ + public HTML.Tag getTag() { + return tag; + } + + /** + * Returns true if the current position is not null. + * @return true if current position is not null, + * otherwise returns false + */ + public boolean isValid() { + return (pos.current() != null); + } + + /** + * Moves the given iterator to the next leaf element. + * @param iter the iterator to be scanned + */ + void nextLeaf(ElementIterator iter) { + for (iter.next(); iter.current() != null; iter.next()) { + Element e = iter.current(); + if (e.isLeaf()) { + break; + } + } + } + + /** + * Marches a cloned iterator forward to locate the end + * of the run. This sets the value of endOffset. + */ + void setEndOffset() { + AttributeSet a0 = getAttributes(); + endOffset = pos.current().getEndOffset(); + ElementIterator fwd = (ElementIterator) pos.clone(); + for (nextLeaf(fwd); fwd.current() != null; nextLeaf(fwd)) { + Element e = fwd.current(); + AttributeSet a1 = (AttributeSet) e.getAttributes().getAttribute(tag); + if ((a1 == null) || (! a1.equals(a0))) { + break; + } + endOffset = e.getEndOffset(); + } + } + + private int endOffset; + private HTML.Tag tag; + private ElementIterator pos; + + } + + /** + * An HTML reader to load an HTML document with an HTML + * element structure. This is a set of callbacks from + * the parser, implemented to create a set of elements + * tagged with attributes. The parse builds up tokens + * (ElementSpec) that describe the element subtree desired, + * and burst it into the document under the protection of + * a write lock using the insert method on the document + * outer class. + *

    + * The reader can be configured by registering actions + * (of type HTMLDocument.HTMLReader.TagAction) + * that describe how to handle the action. The idea behind + * the actions provided is that the most natural text editing + * operations can be provided if the element structure boils + * down to paragraphs with runs of some kind of style + * in them. Some things are more naturally specified + * structurally, so arbitrary structure should be allowed + * above the paragraphs, but will need to be edited with structural + * actions. The implication of this is that some of the + * HTML elements specified in the stream being parsed will + * be collapsed into attributes, and in some cases paragraphs + * will be synthesized. When HTML elements have been + * converted to attributes, the attribute key will be of + * type HTML.Tag, and the value will be of type AttributeSet + * so that no information is lost. This enables many of the + * existing actions to work so that the user can type input, + * hit the return key, backspace, delete, etc and have a + * reasonable result. Selections can be created, and attributes + * applied or removed, etc. With this in mind, the work done + * by the reader can be categorized into the following kinds + * of tasks: + *

    + *
    Block + *
    Build the structure like it's specified in the stream. + * This produces elements that contain other elements. + *
    Paragraph + *
    Like block except that it's expected that the element + * will be used with a paragraph view so a paragraph element + * won't need to be synthesized. + *
    Character + *
    Contribute the element as an attribute that will start + * and stop at arbitrary text locations. This will ultimately + * be mixed into a run of text, with all of the currently + * flattened HTML character elements. + *
    Special + *
    Produce an embedded graphical element. + *
    Form + *
    Produce an element that is like the embedded graphical + * element, except that it also has a component model associated + * with it. + *
    Hidden + *
    Create an element that is hidden from view when the + * document is being viewed read-only, and visible when the + * document is being edited. This is useful to keep the + * model from losing information, and used to store things + * like comments and unrecognized tags. + * + *
    + *

    + * Currently, <APPLET>, <PARAM>, <MAP>, <AREA>, <LINK>, + * <SCRIPT> and <STYLE> are unsupported. + * + *

    + * The assignment of the actions described is shown in the + * following table for the tags defined in HTML.Tag.

    + * + * + *
    TagAction
    HTML.Tag.A CharacterAction + *
    HTML.Tag.ADDRESS CharacterAction + *
    HTML.Tag.APPLET HiddenAction + *
    HTML.Tag.AREA AreaAction + *
    HTML.Tag.B CharacterAction + *
    HTML.Tag.BASE BaseAction + *
    HTML.Tag.BASEFONT CharacterAction + *
    HTML.Tag.BIG CharacterAction + *
    HTML.Tag.BLOCKQUOTEBlockAction + *
    HTML.Tag.BODY BlockAction + *
    HTML.Tag.BR SpecialAction + *
    HTML.Tag.CAPTION BlockAction + *
    HTML.Tag.CENTER BlockAction + *
    HTML.Tag.CITE CharacterAction + *
    HTML.Tag.CODE CharacterAction + *
    HTML.Tag.DD BlockAction + *
    HTML.Tag.DFN CharacterAction + *
    HTML.Tag.DIR BlockAction + *
    HTML.Tag.DIV BlockAction + *
    HTML.Tag.DL BlockAction + *
    HTML.Tag.DT ParagraphAction + *
    HTML.Tag.EM CharacterAction + *
    HTML.Tag.FONT CharacterAction + *
    HTML.Tag.FORM As of 1.4 a BlockAction + *
    HTML.Tag.FRAME SpecialAction + *
    HTML.Tag.FRAMESET BlockAction + *
    HTML.Tag.H1 ParagraphAction + *
    HTML.Tag.H2 ParagraphAction + *
    HTML.Tag.H3 ParagraphAction + *
    HTML.Tag.H4 ParagraphAction + *
    HTML.Tag.H5 ParagraphAction + *
    HTML.Tag.H6 ParagraphAction + *
    HTML.Tag.HEAD HeadAction + *
    HTML.Tag.HR SpecialAction + *
    HTML.Tag.HTML BlockAction + *
    HTML.Tag.I CharacterAction + *
    HTML.Tag.IMG SpecialAction + *
    HTML.Tag.INPUT FormAction + *
    HTML.Tag.ISINDEX IsndexAction + *
    HTML.Tag.KBD CharacterAction + *
    HTML.Tag.LI BlockAction + *
    HTML.Tag.LINK LinkAction + *
    HTML.Tag.MAP MapAction + *
    HTML.Tag.MENU BlockAction + *
    HTML.Tag.META MetaAction + *
    HTML.Tag.NOFRAMES BlockAction + *
    HTML.Tag.OBJECT SpecialAction + *
    HTML.Tag.OL BlockAction + *
    HTML.Tag.OPTION FormAction + *
    HTML.Tag.P ParagraphAction + *
    HTML.Tag.PARAM HiddenAction + *
    HTML.Tag.PRE PreAction + *
    HTML.Tag.SAMP CharacterAction + *
    HTML.Tag.SCRIPT HiddenAction + *
    HTML.Tag.SELECT FormAction + *
    HTML.Tag.SMALL CharacterAction + *
    HTML.Tag.STRIKE CharacterAction + *
    HTML.Tag.S CharacterAction + *
    HTML.Tag.STRONG CharacterAction + *
    HTML.Tag.STYLE StyleAction + *
    HTML.Tag.SUB CharacterAction + *
    HTML.Tag.SUP CharacterAction + *
    HTML.Tag.TABLE BlockAction + *
    HTML.Tag.TD BlockAction + *
    HTML.Tag.TEXTAREA FormAction + *
    HTML.Tag.TH BlockAction + *
    HTML.Tag.TITLE TitleAction + *
    HTML.Tag.TR BlockAction + *
    HTML.Tag.TT CharacterAction + *
    HTML.Tag.U CharacterAction + *
    HTML.Tag.UL BlockAction + *
    HTML.Tag.VAR CharacterAction + *
    + *

    + * Once </html> is encountered, the Actions are no longer notified. + */ + public class HTMLReader extends HTMLEditorKit.ParserCallback { + + public HTMLReader(int offset) { + this(offset, 0, 0, null); + } + + public HTMLReader(int offset, int popDepth, int pushDepth, + HTML.Tag insertTag) { + this(offset, popDepth, pushDepth, insertTag, true, false, true); + } + + /** + * Generates a RuntimeException (will eventually generate + * a BadLocationException when API changes are alloced) if inserting + * into non empty document, insertTag is + * non-null, and offset is not in the body. + */ + // PENDING(sky): Add throws BadLocationException and remove + // RuntimeException + HTMLReader(int offset, int popDepth, int pushDepth, + HTML.Tag insertTag, boolean insertInsertTag, + boolean insertAfterImplied, boolean wantsTrailingNewline) { + emptyDocument = (getLength() == 0); + isStyleCSS = "text/css".equals(getDefaultStyleSheetType()); + this.offset = offset; + threshold = HTMLDocument.this.getTokenThreshold(); + tagMap = new Hashtable(57); + TagAction na = new TagAction(); + TagAction ba = new BlockAction(); + TagAction pa = new ParagraphAction(); + TagAction ca = new CharacterAction(); + TagAction sa = new SpecialAction(); + TagAction fa = new FormAction(); + TagAction ha = new HiddenAction(); + TagAction conv = new ConvertAction(); + + // register handlers for the well known tags + tagMap.put(HTML.Tag.A, new AnchorAction()); + tagMap.put(HTML.Tag.ADDRESS, ca); + tagMap.put(HTML.Tag.APPLET, ha); + tagMap.put(HTML.Tag.AREA, new AreaAction()); + tagMap.put(HTML.Tag.B, conv); + tagMap.put(HTML.Tag.BASE, new BaseAction()); + tagMap.put(HTML.Tag.BASEFONT, ca); + tagMap.put(HTML.Tag.BIG, ca); + tagMap.put(HTML.Tag.BLOCKQUOTE, ba); + tagMap.put(HTML.Tag.BODY, ba); + tagMap.put(HTML.Tag.BR, sa); + tagMap.put(HTML.Tag.CAPTION, ba); + tagMap.put(HTML.Tag.CENTER, ba); + tagMap.put(HTML.Tag.CITE, ca); + tagMap.put(HTML.Tag.CODE, ca); + tagMap.put(HTML.Tag.DD, ba); + tagMap.put(HTML.Tag.DFN, ca); + tagMap.put(HTML.Tag.DIR, ba); + tagMap.put(HTML.Tag.DIV, ba); + tagMap.put(HTML.Tag.DL, ba); + tagMap.put(HTML.Tag.DT, pa); + tagMap.put(HTML.Tag.EM, ca); + tagMap.put(HTML.Tag.FONT, conv); + tagMap.put(HTML.Tag.FORM, new FormTagAction()); + tagMap.put(HTML.Tag.FRAME, sa); + tagMap.put(HTML.Tag.FRAMESET, ba); + tagMap.put(HTML.Tag.H1, pa); + tagMap.put(HTML.Tag.H2, pa); + tagMap.put(HTML.Tag.H3, pa); + tagMap.put(HTML.Tag.H4, pa); + tagMap.put(HTML.Tag.H5, pa); + tagMap.put(HTML.Tag.H6, pa); + tagMap.put(HTML.Tag.HEAD, new HeadAction()); + tagMap.put(HTML.Tag.HR, sa); + tagMap.put(HTML.Tag.HTML, ba); + tagMap.put(HTML.Tag.I, conv); + tagMap.put(HTML.Tag.IMG, sa); + tagMap.put(HTML.Tag.INPUT, fa); + tagMap.put(HTML.Tag.ISINDEX, new IsindexAction()); + tagMap.put(HTML.Tag.KBD, ca); + tagMap.put(HTML.Tag.LI, ba); + tagMap.put(HTML.Tag.LINK, new LinkAction()); + tagMap.put(HTML.Tag.MAP, new MapAction()); + tagMap.put(HTML.Tag.MENU, ba); + tagMap.put(HTML.Tag.META, new MetaAction()); + tagMap.put(HTML.Tag.NOBR, ca); + tagMap.put(HTML.Tag.NOFRAMES, ba); + tagMap.put(HTML.Tag.OBJECT, sa); + tagMap.put(HTML.Tag.OL, ba); + tagMap.put(HTML.Tag.OPTION, fa); + tagMap.put(HTML.Tag.P, pa); + tagMap.put(HTML.Tag.PARAM, new ObjectAction()); + tagMap.put(HTML.Tag.PRE, new PreAction()); + tagMap.put(HTML.Tag.SAMP, ca); + tagMap.put(HTML.Tag.SCRIPT, ha); + tagMap.put(HTML.Tag.SELECT, fa); + tagMap.put(HTML.Tag.SMALL, ca); + tagMap.put(HTML.Tag.SPAN, ca); + tagMap.put(HTML.Tag.STRIKE, conv); + tagMap.put(HTML.Tag.S, ca); + tagMap.put(HTML.Tag.STRONG, ca); + tagMap.put(HTML.Tag.STYLE, new StyleAction()); + tagMap.put(HTML.Tag.SUB, conv); + tagMap.put(HTML.Tag.SUP, conv); + tagMap.put(HTML.Tag.TABLE, ba); + tagMap.put(HTML.Tag.TD, ba); + tagMap.put(HTML.Tag.TEXTAREA, fa); + tagMap.put(HTML.Tag.TH, ba); + tagMap.put(HTML.Tag.TITLE, new TitleAction()); + tagMap.put(HTML.Tag.TR, ba); + tagMap.put(HTML.Tag.TT, ca); + tagMap.put(HTML.Tag.U, conv); + tagMap.put(HTML.Tag.UL, ba); + tagMap.put(HTML.Tag.VAR, ca); + + if (insertTag != null) { + this.insertTag = insertTag; + this.popDepth = popDepth; + this.pushDepth = pushDepth; + this.insertInsertTag = insertInsertTag; + foundInsertTag = false; + } + else { + foundInsertTag = true; + } + if (insertAfterImplied) { + this.popDepth = popDepth; + this.pushDepth = pushDepth; + this.insertAfterImplied = true; + foundInsertTag = false; + midInsert = false; + this.insertInsertTag = true; + this.wantsTrailingNewline = wantsTrailingNewline; + } + else { + midInsert = (!emptyDocument && insertTag == null); + if (midInsert) { + generateEndsSpecsForMidInsert(); + } + } + + /** + * This block initializes the inParagraph flag. + * It is left in false value automatically + * if the target document is empty or future inserts + * were positioned into the 'body' tag. + */ + if (!emptyDocument && !midInsert) { + int targetOffset = Math.max(this.offset - 1, 0); + Element elem = + HTMLDocument.this.getCharacterElement(targetOffset); + /* Going up by the left document structure path */ + for (int i = 0; i <= this.popDepth; i++) { + elem = elem.getParentElement(); + } + /* Going down by the right document structure path */ + for (int i = 0; i < this.pushDepth; i++) { + int index = elem.getElementIndex(this.offset); + elem = elem.getElement(index); + } + AttributeSet attrs = elem.getAttributes(); + if (attrs != null) { + HTML.Tag tagToInsertInto = + (HTML.Tag) attrs.getAttribute(StyleConstants.NameAttribute); + if (tagToInsertInto != null) { + this.inParagraph = tagToInsertInto.isParagraph(); + } + } + } + } + + /** + * Generates an initial batch of end ElementSpecs + * in parseBuffer to position future inserts into the body. + */ + private void generateEndsSpecsForMidInsert() { + int count = heightToElementWithName(HTML.Tag.BODY, + Math.max(0, offset - 1)); + boolean joinNext = false; + + if (count == -1 && offset > 0) { + count = heightToElementWithName(HTML.Tag.BODY, offset); + if (count != -1) { + // Previous isn't in body, but current is. Have to + // do some end specs, followed by join next. + count = depthTo(offset - 1) - 1; + joinNext = true; + } + } + if (count == -1) { + throw new RuntimeException("Must insert new content into body element-"); + } + if (count != -1) { + // Insert a newline, if necessary. + try { + if (!joinNext && offset > 0 && + !getText(offset - 1, 1).equals("\n")) { + SimpleAttributeSet newAttrs = new SimpleAttributeSet(); + newAttrs.addAttribute(StyleConstants.NameAttribute, + HTML.Tag.CONTENT); + ElementSpec spec = new ElementSpec(newAttrs, + ElementSpec.ContentType, NEWLINE, 0, 1); + parseBuffer.addElement(spec); + } + // Should never throw, but will catch anyway. + } catch (BadLocationException ble) {} + while (count-- > 0) { + parseBuffer.addElement(new ElementSpec + (null, ElementSpec.EndTagType)); + } + if (joinNext) { + ElementSpec spec = new ElementSpec(null, ElementSpec. + StartTagType); + + spec.setDirection(ElementSpec.JoinNextDirection); + parseBuffer.addElement(spec); + } + } + // We should probably throw an exception if (count == -1) + // Or look for the body and reset the offset. + } + + /** + * @return number of parents to reach the child at offset. + */ + private int depthTo(int offset) { + Element e = getDefaultRootElement(); + int count = 0; + + while (!e.isLeaf()) { + count++; + e = e.getElement(e.getElementIndex(offset)); + } + return count; + } + + /** + * @return number of parents of the leaf at offset + * until a parent with name, name has been + * found. -1 indicates no matching parent with + * name. + */ + private int heightToElementWithName(Object name, int offset) { + Element e = getCharacterElement(offset).getParentElement(); + int count = 0; + + while (e != null && e.getAttributes().getAttribute + (StyleConstants.NameAttribute) != name) { + count++; + e = e.getParentElement(); + } + return (e == null) ? -1 : count; + } + + /** + * This will make sure there aren't two BODYs (the second is + * typically created when you do a remove all, and then an insert). + */ + private void adjustEndElement() { + int length = getLength(); + if (length == 0) { + return; + } + obtainLock(); + try { + Element[] pPath = getPathTo(length - 1); + int pLength = pPath.length; + if (pLength > 1 && pPath[1].getAttributes().getAttribute + (StyleConstants.NameAttribute) == HTML.Tag.BODY && + pPath[1].getEndOffset() == length) { + String lastText = getText(length - 1, 1); + DefaultDocumentEvent event = null; + Element[] added; + Element[] removed; + int index; + // Remove the fake second body. + added = new Element[0]; + removed = new Element[1]; + index = pPath[0].getElementIndex(length); + removed[0] = pPath[0].getElement(index); + ((BranchElement)pPath[0]).replace(index, 1, added); + ElementEdit firstEdit = new ElementEdit(pPath[0], index, + removed, added); + + // Insert a new element to represent the end that the + // second body was representing. + SimpleAttributeSet sas = new SimpleAttributeSet(); + sas.addAttribute(StyleConstants.NameAttribute, + HTML.Tag.CONTENT); + sas.addAttribute(IMPLIED_CR, Boolean.TRUE); + added = new Element[1]; + added[0] = createLeafElement(pPath[pLength - 1], + sas, length, length + 1); + index = pPath[pLength - 1].getElementCount(); + ((BranchElement)pPath[pLength - 1]).replace(index, 0, + added); + event = new DefaultDocumentEvent(length, 1, + DocumentEvent.EventType.CHANGE); + event.addEdit(new ElementEdit(pPath[pLength - 1], + index, new Element[0], added)); + event.addEdit(firstEdit); + event.end(); + fireChangedUpdate(event); + fireUndoableEditUpdate(new UndoableEditEvent(this, event)); + + if (lastText.equals("\n")) { + // We now have two \n's, one part of the Document. + // We need to remove one + event = new DefaultDocumentEvent(length - 1, 1, + DocumentEvent.EventType.REMOVE); + removeUpdate(event); + UndoableEdit u = getContent().remove(length - 1, 1); + if (u != null) { + event.addEdit(u); + } + postRemoveUpdate(event); + // Mark the edit as done. + event.end(); + fireRemoveUpdate(event); + fireUndoableEditUpdate(new UndoableEditEvent( + this, event)); + } + } + } + catch (BadLocationException ble) { + } + finally { + releaseLock(); + } + } + + private Element[] getPathTo(int offset) { + Stack elements = new Stack(); + Element e = getDefaultRootElement(); + int index; + while (!e.isLeaf()) { + elements.push(e); + e = e.getElement(e.getElementIndex(offset)); + } + Element[] retValue = new Element[elements.size()]; + elements.copyInto(retValue); + return retValue; + } + + // -- HTMLEditorKit.ParserCallback methods -------------------- + + /** + * 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. + */ + public void flush() throws BadLocationException { + if (emptyDocument && !insertAfterImplied) { + if (HTMLDocument.this.getLength() > 0 || + parseBuffer.size() > 0) { + flushBuffer(true); + adjustEndElement(); + } + // We won't insert when + } + else { + flushBuffer(true); + } + } + + /** + * Called by the parser to indicate a block of text was + * encountered. + */ + public void handleText(char[] data, int pos) { + if (receivedEndHTML || (midInsert && !inBody)) { + return; + } + + // see if complex glyph layout support is needed + if(HTMLDocument.this.getProperty(I18NProperty).equals( Boolean.FALSE ) ) { + // if a default direction of right-to-left has been specified, + // we want complex layout even if the text is all left to right. + Object d = getProperty(TextAttribute.RUN_DIRECTION); + if ((d != null) && (d.equals(TextAttribute.RUN_DIRECTION_RTL))) { + HTMLDocument.this.putProperty( I18NProperty, Boolean.TRUE); + } else { + if (SwingUtilities2.isComplexLayout(data, 0, data.length)) { + HTMLDocument.this.putProperty( I18NProperty, Boolean.TRUE); + } + } + } + + if (inTextArea) { + textAreaContent(data); + } else if (inPre) { + preContent(data); + } else if (inTitle) { + putProperty(Document.TitleProperty, new String(data)); + } else if (option != null) { + option.setLabel(new String(data)); + } else if (inStyle) { + if (styles != null) { + styles.addElement(new String(data)); + } + } else if (inBlock > 0) { + if (!foundInsertTag && insertAfterImplied) { + // Assume content should be added. + foundInsertTag(false); + foundInsertTag = true; + inParagraph = impliedP = true; + } + if (data.length >= 1) { + addContent(data, 0, data.length); + } + } + } + + /** + * Callback from the parser. Route to the appropriate + * handler for the tag. + */ + public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) { + if (receivedEndHTML) { + return; + } + if (midInsert && !inBody) { + if (t == HTML.Tag.BODY) { + inBody = true; + // Increment inBlock since we know we are in the body, + // this is needed incase an implied-p is needed. If + // inBlock isn't incremented, and an implied-p is + // encountered, addContent won't be called! + inBlock++; + } + return; + } + if (!inBody && t == HTML.Tag.BODY) { + inBody = true; + } + if (isStyleCSS && a.isDefined(HTML.Attribute.STYLE)) { + // Map the style attributes. + String decl = (String)a.getAttribute(HTML.Attribute.STYLE); + a.removeAttribute(HTML.Attribute.STYLE); + styleAttributes = getStyleSheet().getDeclaration(decl); + a.addAttributes(styleAttributes); + } + else { + styleAttributes = null; + } + TagAction action = (TagAction) tagMap.get(t); + + if (action != null) { + action.start(t, a); + } + } + + public void handleComment(char[] data, int pos) { + if (receivedEndHTML) { + addExternalComment(new String(data)); + return; + } + if (inStyle) { + if (styles != null) { + styles.addElement(new String(data)); + } + } + else if (getPreservesUnknownTags()) { + if (inBlock == 0 && (foundInsertTag || + insertTag != HTML.Tag.COMMENT)) { + // Comment outside of body, will not be able to show it, + // but can add it as a property on the Document. + addExternalComment(new String(data)); + return; + } + SimpleAttributeSet sas = new SimpleAttributeSet(); + sas.addAttribute(HTML.Attribute.COMMENT, new String(data)); + addSpecialElement(HTML.Tag.COMMENT, sas); + } + + TagAction action = (TagAction)tagMap.get(HTML.Tag.COMMENT); + if (action != null) { + action.start(HTML.Tag.COMMENT, new SimpleAttributeSet()); + action.end(HTML.Tag.COMMENT); + } + } + + /** + * Adds the comment comment to the set of comments + * maintained outside of the scope of elements. + */ + private void addExternalComment(String comment) { + Object comments = getProperty(AdditionalComments); + if (comments != null && !(comments instanceof Vector)) { + // No place to put comment. + return; + } + if (comments == null) { + comments = new Vector(); + putProperty(AdditionalComments, comments); + } + ((Vector)comments).addElement(comment); + } + + /** + * Callback from the parser. Route to the appropriate + * handler for the tag. + */ + public void handleEndTag(HTML.Tag t, int pos) { + if (receivedEndHTML || (midInsert && !inBody)) { + return; + } + if (t == HTML.Tag.HTML) { + receivedEndHTML = true; + } + if (t == HTML.Tag.BODY) { + inBody = false; + if (midInsert) { + inBlock--; + } + } + TagAction action = (TagAction) tagMap.get(t); + if (action != null) { + action.end(t); + } + } + + /** + * Callback from the parser. Route to the appropriate + * handler for the tag. + */ + public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) { + if (receivedEndHTML || (midInsert && !inBody)) { + return; + } + + if (isStyleCSS && a.isDefined(HTML.Attribute.STYLE)) { + // Map the style attributes. + String decl = (String)a.getAttribute(HTML.Attribute.STYLE); + a.removeAttribute(HTML.Attribute.STYLE); + styleAttributes = getStyleSheet().getDeclaration(decl); + a.addAttributes(styleAttributes); + } + else { + styleAttributes = null; + } + + TagAction action = (TagAction) tagMap.get(t); + if (action != null) { + action.start(t, a); + action.end(t); + } + else if (getPreservesUnknownTags()) { + // unknown tag, only add if should preserve it. + addSpecialElement(t, a); + } + } + + /** + * 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) { + if (emptyDocument && eol != null) { + putProperty(DefaultEditorKit.EndOfLineStringProperty, + eol); + } + } + + // ---- tag handling support ------------------------------ + + /** + * Registers a handler for the given tag. By default + * all of the well-known tags will have been registered. + * This can be used to change the handling of a particular + * tag or to add support for custom tags. + */ + protected void registerTag(HTML.Tag t, TagAction a) { + tagMap.put(t, a); + } + + /** + * An action to be performed in response + * to parsing a tag. This allows customization + * of how each tag is handled and avoids a large + * switch statement. + */ + public class TagAction { + + /** + * Called when a start tag is seen for the + * type of tag this action was registered + * to. The tag argument indicates the actual + * tag for those actions that are shared across + * many tags. By default this does nothing and + * completely ignores the tag. + */ + public void start(HTML.Tag t, MutableAttributeSet a) { + } + + /** + * Called when an end tag is seen for the + * type of tag this action was registered + * to. The tag argument indicates the actual + * tag for those actions that are shared across + * many tags. By default this does nothing and + * completely ignores the tag. + */ + public void end(HTML.Tag t) { + } + + } + + public class BlockAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet attr) { + blockOpen(t, attr); + } + + public void end(HTML.Tag t) { + blockClose(t); + } + } + + + /** + * Action used for the actual element form tag. This is named such + * as there was already a public class named FormAction. + */ + private class FormTagAction extends BlockAction { + public void start(HTML.Tag t, MutableAttributeSet attr) { + super.start(t, attr); + // initialize a ButtonGroupsMap when + // FORM tag is encountered. This will + // be used for any radio buttons that + // might be defined in the FORM. + // for new group new ButtonGroup will be created (fix for 4529702) + // group name is a key in radioButtonGroupsMap + radioButtonGroupsMap = new HashMap(); + } + + public void end(HTML.Tag t) { + super.end(t); + // reset the button group to null since + // the form has ended. + radioButtonGroupsMap = null; + } + } + + + public class ParagraphAction extends BlockAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + super.start(t, a); + inParagraph = true; + } + + public void end(HTML.Tag t) { + super.end(t); + inParagraph = false; + } + } + + public class SpecialAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + addSpecialElement(t, a); + } + + } + + public class IsindexAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + blockOpen(HTML.Tag.IMPLIED, new SimpleAttributeSet()); + addSpecialElement(t, a); + blockClose(HTML.Tag.IMPLIED); + } + + } + + + public class HiddenAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + addSpecialElement(t, a); + } + + public void end(HTML.Tag t) { + if (!isEmpty(t)) { + MutableAttributeSet a = new SimpleAttributeSet(); + a.addAttribute(HTML.Attribute.ENDTAG, "true"); + addSpecialElement(t, a); + } + } + + boolean isEmpty(HTML.Tag t) { + if (t == HTML.Tag.APPLET || + t == HTML.Tag.SCRIPT) { + return false; + } + return true; + } + } + + + /** + * Subclass of HiddenAction to set the content type for style sheets, + * and to set the name of the default style sheet. + */ + class MetaAction extends HiddenAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + Object equiv = a.getAttribute(HTML.Attribute.HTTPEQUIV); + if (equiv != null) { + equiv = ((String)equiv).toLowerCase(); + if (equiv.equals("content-style-type")) { + String value = (String)a.getAttribute + (HTML.Attribute.CONTENT); + setDefaultStyleSheetType(value); + isStyleCSS = "text/css".equals + (getDefaultStyleSheetType()); + } + else if (equiv.equals("default-style")) { + defaultStyle = (String)a.getAttribute + (HTML.Attribute.CONTENT); + } + } + super.start(t, a); + } + + boolean isEmpty(HTML.Tag t) { + return true; + } + } + + + /** + * End if overridden to create the necessary stylesheets that + * are referenced via the link tag. It is done in this manner + * as the meta tag can be used to specify an alternate style sheet, + * and is not guaranteed to come before the link tags. + */ + class HeadAction extends BlockAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + inHead = true; + // This check of the insertTag is put in to avoid considering + // the implied-p that is generated for the head. This allows + // inserts for HR to work correctly. + if ((insertTag == null && !insertAfterImplied) || + (insertTag == HTML.Tag.HEAD) || + (insertAfterImplied && + (foundInsertTag || !a.isDefined(IMPLIED)))) { + super.start(t, a); + } + } + + public void end(HTML.Tag t) { + inHead = inStyle = false; + // See if there is a StyleSheet to link to. + if (styles != null) { + boolean isDefaultCSS = isStyleCSS; + for (int counter = 0, maxCounter = styles.size(); + counter < maxCounter;) { + Object value = styles.elementAt(counter); + if (value == HTML.Tag.LINK) { + handleLink((AttributeSet)styles. + elementAt(++counter)); + counter++; + } + else { + // Rule. + // First element gives type. + String type = (String)styles.elementAt(++counter); + boolean isCSS = (type == null) ? isDefaultCSS : + type.equals("text/css"); + while (++counter < maxCounter && + (styles.elementAt(counter) + instanceof String)) { + if (isCSS) { + addCSSRules((String)styles.elementAt + (counter)); + } + } + } + } + } + if ((insertTag == null && !insertAfterImplied) || + insertTag == HTML.Tag.HEAD || + (insertAfterImplied && foundInsertTag)) { + super.end(t); + } + } + + boolean isEmpty(HTML.Tag t) { + return false; + } + + private void handleLink(AttributeSet attr) { + // Link. + String type = (String)attr.getAttribute(HTML.Attribute.TYPE); + if (type == null) { + type = getDefaultStyleSheetType(); + } + // Only choose if type==text/css + // Select link if rel==stylesheet. + // Otherwise if rel==alternate stylesheet and + // title matches default style. + if (type.equals("text/css")) { + String rel = (String)attr.getAttribute(HTML.Attribute.REL); + String title = (String)attr.getAttribute + (HTML.Attribute.TITLE); + String media = (String)attr.getAttribute + (HTML.Attribute.MEDIA); + if (media == null) { + media = "all"; + } + else { + media = media.toLowerCase(); + } + if (rel != null) { + rel = rel.toLowerCase(); + if ((media.indexOf("all") != -1 || + media.indexOf("screen") != -1) && + (rel.equals("stylesheet") || + (rel.equals("alternate stylesheet") && + title.equals(defaultStyle)))) { + linkCSSStyleSheet((String)attr.getAttribute + (HTML.Attribute.HREF)); + } + } + } + } + } + + + /** + * A subclass to add the AttributeSet to styles if the + * attributes contains an attribute for 'rel' with value + * 'stylesheet' or 'alternate stylesheet'. + */ + class LinkAction extends HiddenAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + String rel = (String)a.getAttribute(HTML.Attribute.REL); + if (rel != null) { + rel = rel.toLowerCase(); + if (rel.equals("stylesheet") || + rel.equals("alternate stylesheet")) { + if (styles == null) { + styles = new Vector(3); + } + styles.addElement(t); + styles.addElement(a.copyAttributes()); + } + } + super.start(t, a); + } + } + + class MapAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + lastMap = new Map((String)a.getAttribute(HTML.Attribute.NAME)); + addMap(lastMap); + } + + public void end(HTML.Tag t) { + } + } + + + class AreaAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + if (lastMap != null) { + lastMap.addArea(a.copyAttributes()); + } + } + + public void end(HTML.Tag t) { + } + } + + + class StyleAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + if (inHead) { + if (styles == null) { + styles = new Vector(3); + } + styles.addElement(t); + styles.addElement(a.getAttribute(HTML.Attribute.TYPE)); + inStyle = true; + } + } + + public void end(HTML.Tag t) { + inStyle = false; + } + + boolean isEmpty(HTML.Tag t) { + return false; + } + } + + + public class PreAction extends BlockAction { + + public void start(HTML.Tag t, MutableAttributeSet attr) { + inPre = true; + blockOpen(t, attr); + attr.addAttribute(CSS.Attribute.WHITE_SPACE, "pre"); + blockOpen(HTML.Tag.IMPLIED, attr); + } + + public void end(HTML.Tag t) { + blockClose(HTML.Tag.IMPLIED); + // set inPre to false after closing, so that if a newline + // is added it won't generate a blockOpen. + inPre = false; + blockClose(t); + } + } + + public class CharacterAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet attr) { + pushCharacterStyle(); + if (!foundInsertTag) { + // Note that the third argument should really be based off + // inParagraph and impliedP. If we're wrong (that is + // insertTagDepthDelta shouldn't be changed), we'll end up + // removing an extra EndSpec, which won't matter anyway. + boolean insert = canInsertTag(t, attr, false); + if (foundInsertTag) { + if (!inParagraph) { + inParagraph = impliedP = true; + } + } + if (!insert) { + return; + } + } + if (attr.isDefined(IMPLIED)) { + attr.removeAttribute(IMPLIED); + } + charAttr.addAttribute(t, attr.copyAttributes()); + if (styleAttributes != null) { + charAttr.addAttributes(styleAttributes); + } + } + + public void end(HTML.Tag t) { + popCharacterStyle(); + } + } + + /** + * Provides conversion of HTML tag/attribute + * mappings that have a corresponding StyleConstants + * and CSS mapping. The conversion is to CSS attributes. + */ + class ConvertAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet attr) { + pushCharacterStyle(); + if (!foundInsertTag) { + // Note that the third argument should really be based off + // inParagraph and impliedP. If we're wrong (that is + // insertTagDepthDelta shouldn't be changed), we'll end up + // removing an extra EndSpec, which won't matter anyway. + boolean insert = canInsertTag(t, attr, false); + if (foundInsertTag) { + if (!inParagraph) { + inParagraph = impliedP = true; + } + } + if (!insert) { + return; + } + } + if (attr.isDefined(IMPLIED)) { + attr.removeAttribute(IMPLIED); + } + if (styleAttributes != null) { + charAttr.addAttributes(styleAttributes); + } + // We also need to add attr, otherwise we lose custom + // attributes, including class/id for style lookups, and + // further confuse style lookup (doesn't have tag). + charAttr.addAttribute(t, attr.copyAttributes()); + StyleSheet sheet = getStyleSheet(); + if (t == HTML.Tag.B) { + sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_WEIGHT, "bold"); + } else if (t == HTML.Tag.I) { + sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_STYLE, "italic"); + } else if (t == HTML.Tag.U) { + Object v = charAttr.getAttribute(CSS.Attribute.TEXT_DECORATION); + String value = "underline"; + value = (v != null) ? value + "," + v.toString() : value; + sheet.addCSSAttribute(charAttr, CSS.Attribute.TEXT_DECORATION, value); + } else if (t == HTML.Tag.STRIKE) { + Object v = charAttr.getAttribute(CSS.Attribute.TEXT_DECORATION); + String value = "line-through"; + value = (v != null) ? value + "," + v.toString() : value; + sheet.addCSSAttribute(charAttr, CSS.Attribute.TEXT_DECORATION, value); + } else if (t == HTML.Tag.SUP) { + Object v = charAttr.getAttribute(CSS.Attribute.VERTICAL_ALIGN); + String value = "sup"; + value = (v != null) ? value + "," + v.toString() : value; + sheet.addCSSAttribute(charAttr, CSS.Attribute.VERTICAL_ALIGN, value); + } else if (t == HTML.Tag.SUB) { + Object v = charAttr.getAttribute(CSS.Attribute.VERTICAL_ALIGN); + String value = "sub"; + value = (v != null) ? value + "," + v.toString() : value; + sheet.addCSSAttribute(charAttr, CSS.Attribute.VERTICAL_ALIGN, value); + } else if (t == HTML.Tag.FONT) { + String color = (String) attr.getAttribute(HTML.Attribute.COLOR); + if (color != null) { + sheet.addCSSAttribute(charAttr, CSS.Attribute.COLOR, color); + } + String face = (String) attr.getAttribute(HTML.Attribute.FACE); + if (face != null) { + sheet.addCSSAttribute(charAttr, CSS.Attribute.FONT_FAMILY, face); + } + String size = (String) attr.getAttribute(HTML.Attribute.SIZE); + if (size != null) { + sheet.addCSSAttributeFromHTML(charAttr, CSS.Attribute.FONT_SIZE, size); + } + } + } + + public void end(HTML.Tag t) { + popCharacterStyle(); + } + + } + + class AnchorAction extends CharacterAction { + + public void start(HTML.Tag t, MutableAttributeSet attr) { + // set flag to catch empty anchors + emptyAnchor = true; + super.start(t, attr); + } + + public void end(HTML.Tag t) { + if (emptyAnchor) { + // if the anchor was empty it was probably a + // named anchor point and we don't want to throw + // it away. + char[] one = new char[1]; + one[0] = '\n'; + addContent(one, 0, 1); + } + super.end(t); + } + } + + class TitleAction extends HiddenAction { + + public void start(HTML.Tag t, MutableAttributeSet attr) { + inTitle = true; + super.start(t, attr); + } + + public void end(HTML.Tag t) { + inTitle = false; + super.end(t); + } + + boolean isEmpty(HTML.Tag t) { + return false; + } + } + + + class BaseAction extends TagAction { + + public void start(HTML.Tag t, MutableAttributeSet attr) { + String href = (String) attr.getAttribute(HTML.Attribute.HREF); + if (href != null) { + try { + URL newBase = new URL(base, href); + setBase(newBase); + hasBaseTag = true; + } catch (MalformedURLException ex) { + } + } + baseTarget = (String) attr.getAttribute(HTML.Attribute.TARGET); + } + } + + class ObjectAction extends SpecialAction { + + public void start(HTML.Tag t, MutableAttributeSet a) { + if (t == HTML.Tag.PARAM) { + addParameter(a); + } else { + super.start(t, a); + } + } + + public void end(HTML.Tag t) { + if (t != HTML.Tag.PARAM) { + super.end(t); + } + } + + void addParameter(AttributeSet a) { + String name = (String) a.getAttribute(HTML.Attribute.NAME); + String value = (String) a.getAttribute(HTML.Attribute.VALUE); + if ((name != null) && (value != null)) { + ElementSpec objSpec = (ElementSpec) parseBuffer.lastElement(); + MutableAttributeSet objAttr = (MutableAttributeSet) objSpec.getAttributes(); + objAttr.addAttribute(name, value); + } + } + } + + /** + * Action to support forms by building all of the elements + * used to represent form controls. This will process + * the <INPUT>, <TEXTAREA>, <SELECT>, + * and <OPTION> tags. The element created by + * this action is expected to have the attribute + * StyleConstants.ModelAttribute set to + * the model that holds the state for the form control. + * This enables multiple views, and allows document to + * be iterated over picking up the data of the form. + * The following are the model assignments for the + * various type of form elements. + * + * + * + * + * + * + * + * + * + * + * + * + *
    Element Type + * Model Type + *
    input, type button + * {@link DefaultButtonModel} + *
    input, type checkbox + * {@link javax.swing.JToggleButton.ToggleButtonModel} + *
    input, type image + * {@link DefaultButtonModel} + *
    input, type password + * {@link PlainDocument} + *
    input, type radio + * {@link javax.swing.JToggleButton.ToggleButtonModel} + *
    input, type reset + * {@link DefaultButtonModel} + *
    input, type submit + * {@link DefaultButtonModel} + *
    input, type text or type is null. + * {@link PlainDocument} + *
    select + * {@link DefaultComboBoxModel} or an {@link DefaultListModel}, with an item type of Option + *
    textarea + * {@link PlainDocument} + *
    + * + */ + public class FormAction extends SpecialAction { + + public void start(HTML.Tag t, MutableAttributeSet attr) { + if (t == HTML.Tag.INPUT) { + String type = (String) + attr.getAttribute(HTML.Attribute.TYPE); + /* + * if type is not defined teh default is + * assumed to be text. + */ + if (type == null) { + type = "text"; + attr.addAttribute(HTML.Attribute.TYPE, "text"); + } + setModel(type, attr); + } else if (t == HTML.Tag.TEXTAREA) { + inTextArea = true; + textAreaDocument = new TextAreaDocument(); + attr.addAttribute(StyleConstants.ModelAttribute, + textAreaDocument); + } else if (t == HTML.Tag.SELECT) { + int size = HTML.getIntegerAttributeValue(attr, + HTML.Attribute.SIZE, + 1); + boolean multiple = ((String)attr.getAttribute(HTML.Attribute.MULTIPLE) != null); + if ((size > 1) || multiple) { + OptionListModel m = new OptionListModel(); + if (multiple) { + m.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } + selectModel = m; + } else { + selectModel = new OptionComboBoxModel(); + } + attr.addAttribute(StyleConstants.ModelAttribute, + selectModel); + + } + + // build the element, unless this is an option. + if (t == HTML.Tag.OPTION) { + option = new Option(attr); + + if (selectModel instanceof OptionListModel) { + OptionListModel m = (OptionListModel)selectModel; + m.addElement(option); + if (option.isSelected()) { + m.addSelectionInterval(optionCount, optionCount); + m.setInitialSelection(optionCount); + } + } else if (selectModel instanceof OptionComboBoxModel) { + OptionComboBoxModel m = (OptionComboBoxModel)selectModel; + m.addElement(option); + if (option.isSelected()) { + m.setSelectedItem(option); + m.setInitialSelection(option); + } + } + optionCount++; + } else { + super.start(t, attr); + } + } + + public void end(HTML.Tag t) { + if (t == HTML.Tag.OPTION) { + option = null; + } else { + if (t == HTML.Tag.SELECT) { + selectModel = null; + optionCount = 0; + } else if (t == HTML.Tag.TEXTAREA) { + inTextArea = false; + + /* Now that the textarea has ended, + * store the entire initial text + * of the text area. This will + * enable us to restore the initial + * state if a reset is requested. + */ + textAreaDocument.storeInitialText(); + } + super.end(t); + } + } + + void setModel(String type, MutableAttributeSet attr) { + if (type.equals("submit") || + type.equals("reset") || + type.equals("image")) { + + // button model + attr.addAttribute(StyleConstants.ModelAttribute, + new DefaultButtonModel()); + } else if (type.equals("text") || + type.equals("password")) { + // plain text model + int maxLength = HTML.getIntegerAttributeValue( + attr, HTML.Attribute.MAXLENGTH, -1); + Document doc; + + if (maxLength > 0) { + doc = new FixedLengthDocument(maxLength); + } + else { + doc = new PlainDocument(); + } + String value = (String) + attr.getAttribute(HTML.Attribute.VALUE); + try { + doc.insertString(0, value, null); + } catch (BadLocationException e) { + } + attr.addAttribute(StyleConstants.ModelAttribute, doc); + } else if (type.equals("file")) { + // plain text model + attr.addAttribute(StyleConstants.ModelAttribute, + new PlainDocument()); + } else if (type.equals("checkbox") || + type.equals("radio")) { + JToggleButton.ToggleButtonModel model = new JToggleButton.ToggleButtonModel(); + if (type.equals("radio")) { + String name = (String) attr.getAttribute(HTML.Attribute.NAME); + if ( radioButtonGroupsMap == null ) { //fix for 4772743 + radioButtonGroupsMap = new HashMap(); + } + ButtonGroup radioButtonGroup = (ButtonGroup)radioButtonGroupsMap.get(name); + if (radioButtonGroup == null) { + radioButtonGroup = new ButtonGroup(); + radioButtonGroupsMap.put(name,radioButtonGroup); + } + model.setGroup(radioButtonGroup); + } + boolean checked = (attr.getAttribute(HTML.Attribute.CHECKED) != null); + model.setSelected(checked); + attr.addAttribute(StyleConstants.ModelAttribute, model); + } + } + + /** + * If a <SELECT> tag is being processed, this + * model will be a reference to the model being filled + * with the <OPTION> elements (which produce + * objects of type Option. + */ + Object selectModel; + int optionCount; + } + + + // --- utility methods used by the reader ------------------ + + /** + * Pushes the current character style on a stack in preparation + * for forming a new nested character style. + */ + protected void pushCharacterStyle() { + charAttrStack.push(charAttr.copyAttributes()); + } + + /** + * Pops a previously pushed character style off the stack + * to return to a previous style. + */ + protected void popCharacterStyle() { + if (!charAttrStack.empty()) { + charAttr = (MutableAttributeSet) charAttrStack.peek(); + charAttrStack.pop(); + } + } + + /** + * Adds the given content to the textarea document. + * This method gets called when we are in a textarea + * context. Therefore all text that is seen belongs + * to the text area and is hence added to the + * TextAreaDocument associated with the text area. + */ + protected void textAreaContent(char[] data) { + try { + textAreaDocument.insertString(textAreaDocument.getLength(), new String(data), null); + } catch (BadLocationException e) { + // Should do something reasonable + } + } + + /** + * Adds the given content that was encountered in a + * PRE element. This synthesizes lines to hold the + * runs of text, and makes calls to addContent to + * actually add the text. + */ + protected void preContent(char[] data) { + int last = 0; + for (int i = 0; i < data.length; i++) { + if (data[i] == '\n') { + addContent(data, last, i - last + 1); + blockClose(HTML.Tag.IMPLIED); + MutableAttributeSet a = new SimpleAttributeSet(); + a.addAttribute(CSS.Attribute.WHITE_SPACE, "pre"); + blockOpen(HTML.Tag.IMPLIED, a); + last = i + 1; + } + } + if (last < data.length) { + addContent(data, last, data.length - last); + } + } + + /** + * Adds an instruction to the parse buffer to create a + * block element with the given attributes. + */ + protected void blockOpen(HTML.Tag t, MutableAttributeSet attr) { + if (impliedP) { + blockClose(HTML.Tag.IMPLIED); + } + + inBlock++; + + if (!canInsertTag(t, attr, true)) { + return; + } + if (attr.isDefined(IMPLIED)) { + attr.removeAttribute(IMPLIED); + } + lastWasNewline = false; + attr.addAttribute(StyleConstants.NameAttribute, t); + ElementSpec es = new ElementSpec( + attr.copyAttributes(), ElementSpec.StartTagType); + parseBuffer.addElement(es); + } + + /** + * Adds an instruction to the parse buffer to close out + * a block element of the given type. + */ + protected void blockClose(HTML.Tag t) { + inBlock--; + + if (!foundInsertTag) { + return; + } + + // Add a new line, if the last character wasn't one. This is + // needed for proper positioning of the cursor. addContent + // with true will force an implied paragraph to be generated if + // there isn't one. This may result in a rather bogus structure + // (perhaps a table with a child pargraph), but the paragraph + // is needed for proper positioning and display. + if(!lastWasNewline) { + pushCharacterStyle(); + charAttr.addAttribute(IMPLIED_CR, Boolean.TRUE); + addContent(NEWLINE, 0, 1, true); + popCharacterStyle(); + lastWasNewline = true; + } + + if (impliedP) { + impliedP = false; + inParagraph = false; + if (t != HTML.Tag.IMPLIED) { + blockClose(HTML.Tag.IMPLIED); + } + } + // an open/close with no content will be removed, so we + // add a space of content to keep the element being formed. + ElementSpec prev = (parseBuffer.size() > 0) ? + (ElementSpec) parseBuffer.lastElement() : null; + if (prev != null && prev.getType() == ElementSpec.StartTagType) { + char[] one = new char[1]; + one[0] = ' '; + addContent(one, 0, 1); + } + ElementSpec es = new ElementSpec( + null, ElementSpec.EndTagType); + parseBuffer.addElement(es); + } + + /** + * Adds some text with the current character attributes. + * + * @param data the content to add + * @param offs the initial offset + * @param length the length + */ + protected void addContent(char[] data, int offs, int length) { + addContent(data, offs, length, true); + } + + /** + * Adds some text with the current character attributes. + * + * @param data the content to add + * @param offs the initial offset + * @param length the length + * @param generateImpliedPIfNecessary whether to generate implied + * paragraphs + */ + protected void addContent(char[] data, int offs, int length, + boolean generateImpliedPIfNecessary) { + if (!foundInsertTag) { + return; + } + + if (generateImpliedPIfNecessary && (! inParagraph) && (! inPre)) { + blockOpen(HTML.Tag.IMPLIED, new SimpleAttributeSet()); + inParagraph = true; + impliedP = true; + } + emptyAnchor = false; + charAttr.addAttribute(StyleConstants.NameAttribute, HTML.Tag.CONTENT); + AttributeSet a = charAttr.copyAttributes(); + ElementSpec es = new ElementSpec( + a, ElementSpec.ContentType, data, offs, length); + parseBuffer.addElement(es); + + if (parseBuffer.size() > threshold) { + if ( threshold <= MaxThreshold ) { + threshold *= StepThreshold; + } + try { + flushBuffer(false); + } catch (BadLocationException ble) { + } + } + if(length > 0) { + lastWasNewline = (data[offs + length - 1] == '\n'); + } + } + + /** + * Adds content that is basically specified entirely + * in the attribute set. + */ + protected void addSpecialElement(HTML.Tag t, MutableAttributeSet a) { + if ((t != HTML.Tag.FRAME) && (! inParagraph) && (! inPre)) { + nextTagAfterPImplied = t; + blockOpen(HTML.Tag.IMPLIED, new SimpleAttributeSet()); + nextTagAfterPImplied = null; + inParagraph = true; + impliedP = true; + } + if (!canInsertTag(t, a, t.isBlock())) { + return; + } + if (a.isDefined(IMPLIED)) { + a.removeAttribute(IMPLIED); + } + emptyAnchor = false; + a.addAttributes(charAttr); + a.addAttribute(StyleConstants.NameAttribute, t); + char[] one = new char[1]; + one[0] = ' '; + ElementSpec es = new ElementSpec( + a.copyAttributes(), ElementSpec.ContentType, one, 0, 1); + parseBuffer.addElement(es); + // Set this to avoid generating a newline for frames, frames + // shouldn't have any content, and shouldn't need a newline. + if (t == HTML.Tag.FRAME) { + lastWasNewline = true; + } + } + + /** + * Flushes the current parse buffer into the document. + * @param endOfStream true if there is no more content to parser + */ + void flushBuffer(boolean endOfStream) throws BadLocationException { + int oldLength = HTMLDocument.this.getLength(); + int size = parseBuffer.size(); + if (endOfStream && (insertTag != null || insertAfterImplied) && + size > 0) { + adjustEndSpecsForPartialInsert(); + size = parseBuffer.size(); + } + ElementSpec[] spec = new ElementSpec[size]; + parseBuffer.copyInto(spec); + + if (oldLength == 0 && (insertTag == null && !insertAfterImplied)) { + create(spec); + } else { + insert(offset, spec); + } + parseBuffer.removeAllElements(); + offset += HTMLDocument.this.getLength() - oldLength; + flushCount++; + } + + /** + * This will be invoked for the last flush, if insertTag + * is non null. + */ + private void adjustEndSpecsForPartialInsert() { + int size = parseBuffer.size(); + if (insertTagDepthDelta < 0) { + // When inserting via an insertTag, the depths (of the tree + // being read in, and existing hiearchy) may not match up. + // This attemps to clean it up. + int removeCounter = insertTagDepthDelta; + while (removeCounter < 0 && size >= 0 && + ((ElementSpec)parseBuffer.elementAt(size - 1)). + getType() == ElementSpec.EndTagType) { + parseBuffer.removeElementAt(--size); + removeCounter++; + } + } + if (flushCount == 0 && (!insertAfterImplied || + !wantsTrailingNewline)) { + // If this starts with content (or popDepth > 0 && + // pushDepth > 0) and ends with EndTagTypes, make sure + // the last content isn't a \n, otherwise will end up with + // an extra \n in the middle of content. + int index = 0; + if (pushDepth > 0) { + if (((ElementSpec)parseBuffer.elementAt(0)).getType() == + ElementSpec.ContentType) { + index++; + } + } + index += (popDepth + pushDepth); + int cCount = 0; + int cStart = index; + while (index < size && ((ElementSpec)parseBuffer.elementAt + (index)).getType() == ElementSpec.ContentType) { + index++; + cCount++; + } + if (cCount > 1) { + while (index < size && ((ElementSpec)parseBuffer.elementAt + (index)).getType() == ElementSpec.EndTagType) { + index++; + } + if (index == size) { + char[] lastText = ((ElementSpec)parseBuffer.elementAt + (cStart + cCount - 1)).getArray(); + if (lastText.length == 1 && lastText[0] == NEWLINE[0]){ + index = cStart + cCount - 1; + while (size > index) { + parseBuffer.removeElementAt(--size); + } + } + } + } + } + if (wantsTrailingNewline) { + // Make sure there is in fact a newline + for (int counter = parseBuffer.size() - 1; counter >= 0; + counter--) { + ElementSpec spec = (ElementSpec)parseBuffer. + elementAt(counter); + if (spec.getType() == ElementSpec.ContentType) { + if (spec.getArray()[spec.getLength() - 1] != '\n') { + SimpleAttributeSet attrs =new SimpleAttributeSet(); + + attrs.addAttribute(StyleConstants.NameAttribute, + HTML.Tag.CONTENT); + parseBuffer.insertElementAt(new ElementSpec( + attrs, + ElementSpec.ContentType, NEWLINE, 0, 1), + counter + 1); + } + break; + } + } + } + } + + /** + * Adds the CSS rules in rules. + */ + void addCSSRules(String rules) { + StyleSheet ss = getStyleSheet(); + ss.addRule(rules); + } + + /** + * Adds the CSS stylesheet at href to the known list + * of stylesheets. + */ + void linkCSSStyleSheet(String href) { + URL url = null; + try { + url = new URL(base, href); + } catch (MalformedURLException mfe) { + try { + url = new URL(href); + } catch (MalformedURLException mfe2) { + url = null; + } + } + if (url != null) { + getStyleSheet().importStyleSheet(url); + } + } + + /** + * Returns true if can insert starting at t. This + * will return false if the insert tag is set, and hasn't been found + * yet. + */ + private boolean canInsertTag(HTML.Tag t, AttributeSet attr, + boolean isBlockTag) { + if (!foundInsertTag) { + boolean needPImplied = ((t == HTML.Tag.IMPLIED) + && (!inParagraph) + && (!inPre)); + if (needPImplied && (nextTagAfterPImplied != null)) { + + /* + * If insertTag == null then just proceed to + * foundInsertTag() call below and return true. + */ + if (insertTag != null) { + boolean nextTagIsInsertTag = + isInsertTag(nextTagAfterPImplied); + if ( (! nextTagIsInsertTag) || (! insertInsertTag) ) { + return false; + } + } + /* + * Proceed to foundInsertTag() call... + */ + } else if ((insertTag != null && !isInsertTag(t)) + || (insertAfterImplied + && (attr == null + || attr.isDefined(IMPLIED) + || t == HTML.Tag.IMPLIED + ) + ) + ) { + return false; + } + + // Allow the insert if t matches the insert tag, or + // insertAfterImplied is true and the element is implied. + foundInsertTag(isBlockTag); + if (!insertInsertTag) { + return false; + } + } + return true; + } + + private boolean isInsertTag(HTML.Tag tag) { + return (insertTag == tag); + } + + private void foundInsertTag(boolean isBlockTag) { + foundInsertTag = true; + if (!insertAfterImplied && (popDepth > 0 || pushDepth > 0)) { + try { + if (offset == 0 || !getText(offset - 1, 1).equals("\n")) { + // Need to insert a newline. + AttributeSet newAttrs = null; + boolean joinP = true; + + if (offset != 0) { + // Determine if we can use JoinPrevious, we can't + // if the Element has some attributes that are + // not meant to be duplicated. + Element charElement = getCharacterElement + (offset - 1); + AttributeSet attrs = charElement.getAttributes(); + + if (attrs.isDefined(StyleConstants. + ComposedTextAttribute)) { + joinP = false; + } + else { + Object name = attrs.getAttribute + (StyleConstants.NameAttribute); + if (name instanceof HTML.Tag) { + HTML.Tag tag = (HTML.Tag)name; + if (tag == HTML.Tag.IMG || + tag == HTML.Tag.HR || + tag == HTML.Tag.COMMENT || + (tag instanceof HTML.UnknownTag)) { + joinP = false; + } + } + } + } + if (!joinP) { + // If not joining with the previous element, be + // sure and set the name (otherwise it will be + // inherited). + newAttrs = new SimpleAttributeSet(); + ((SimpleAttributeSet)newAttrs).addAttribute + (StyleConstants.NameAttribute, + HTML.Tag.CONTENT); + } + ElementSpec es = new ElementSpec(newAttrs, + ElementSpec.ContentType, NEWLINE, 0, + NEWLINE.length); + if (joinP) { + es.setDirection(ElementSpec. + JoinPreviousDirection); + } + parseBuffer.addElement(es); + } + } catch (BadLocationException ble) {} + } + // pops + for (int counter = 0; counter < popDepth; counter++) { + parseBuffer.addElement(new ElementSpec(null, ElementSpec. + EndTagType)); + } + // pushes + for (int counter = 0; counter < pushDepth; counter++) { + ElementSpec es = new ElementSpec(null, ElementSpec. + StartTagType); + es.setDirection(ElementSpec.JoinNextDirection); + parseBuffer.addElement(es); + } + insertTagDepthDelta = depthTo(Math.max(0, offset - 1)) - + popDepth + pushDepth - inBlock; + if (isBlockTag) { + // A start spec will be added (for this tag), so we account + // for it here. + insertTagDepthDelta++; + } + else { + // An implied paragraph close (end spec) is going to be added, + // so we account for it here. + insertTagDepthDelta--; + inParagraph = true; + lastWasNewline = false; + } + } + + /** + * This is set to true when and end is invoked for . + */ + private boolean receivedEndHTML; + /** Number of times flushBuffer has been invoked. */ + private int flushCount; + /** If true, behavior is similiar to insertTag, but instead of + * waiting for insertTag will wait for first Element without + * an 'implied' attribute and begin inserting then. */ + private boolean insertAfterImplied; + /** This is only used if insertAfterImplied is true. If false, only + * inserting content, and there is a trailing newline it is removed. */ + private boolean wantsTrailingNewline; + int threshold; + int offset; + boolean inParagraph = false; + boolean impliedP = false; + boolean inPre = false; + boolean inTextArea = false; + TextAreaDocument textAreaDocument = null; + boolean inTitle = false; + boolean lastWasNewline = true; + boolean emptyAnchor; + /** True if (!emptyDocument && insertTag == null), this is used so + * much it is cached. */ + boolean midInsert; + /** True when the body has been encountered. */ + boolean inBody; + /** If non null, gives parent Tag that insert is to happen at. */ + HTML.Tag insertTag; + /** If true, the insertTag is inserted, otherwise elements after + * the insertTag is found are inserted. */ + boolean insertInsertTag; + /** Set to true when insertTag has been found. */ + boolean foundInsertTag; + /** When foundInsertTag is set to true, this will be updated to + * reflect the delta between the two structures. That is, it + * will be the depth the inserts are happening at minus the + * depth of the tags being passed in. A value of 0 (the common + * case) indicates the structures match, a value greater than 0 indicates + * the insert is happening at a deeper depth than the stream is + * parsing, and a value less than 0 indicates the insert is happening earlier + * in the tree that the parser thinks and that we will need to remove + * EndTagType specs in the flushBuffer method. + */ + int insertTagDepthDelta; + /** How many parents to ascend before insert new elements. */ + int popDepth; + /** How many parents to descend (relative to popDepth) before + * inserting. */ + int pushDepth; + /** Last Map that was encountered. */ + Map lastMap; + /** Set to true when a style element is encountered. */ + boolean inStyle = false; + /** Name of style to use. Obtained from Meta tag. */ + String defaultStyle; + /** Vector describing styles that should be include. Will consist + * of a bunch of HTML.Tags, which will either be: + *

    LINK: in which case it is followed by an AttributeSet + *

    STYLE: in which case the following element is a String + * indicating the type (may be null), and the elements following + * it until the next HTML.Tag are the rules as Strings. + */ + Vector styles; + /** True if inside the head tag. */ + boolean inHead = false; + /** Set to true if the style language is text/css. Since this is + * used alot, it is cached. */ + boolean isStyleCSS; + /** True if inserting into an empty document. */ + boolean emptyDocument; + /** Attributes from a style Attribute. */ + AttributeSet styleAttributes; + + /** + * Current option, if in an option element (needed to + * load the label. + */ + Option option; + + protected Vector parseBuffer = new Vector(); // Vector + protected MutableAttributeSet charAttr = new TaggedAttributeSet(); + Stack charAttrStack = new Stack(); + Hashtable tagMap; + int inBlock = 0; + + /** + * This attribute is sometimes used to refer to next tag + * to be handled after p-implied when the latter is + * the current tag which is being handled. + */ + private HTML.Tag nextTagAfterPImplied = null; + } + + + /** + * Used by StyleSheet to determine when to avoid removing HTML.Tags + * matching StyleConstants. + */ + static class TaggedAttributeSet extends SimpleAttributeSet { + TaggedAttributeSet() { + super(); + } + } + + + /** + * An element that represents a chunk of text that has + * a set of HTML character level attributes assigned to + * it. + */ + public class RunElement extends LeafElement { + + /** + * Constructs an element that represents content within the + * document (has no children). + * + * @param parent the parent element + * @param a the element attributes + * @param offs0 the start offset (must be at least 0) + * @param offs1 the end offset (must be at least offs0) + * @since 1.4 + */ + public RunElement(Element parent, AttributeSet a, int offs0, int offs1) { + super(parent, a, offs0, offs1); + } + + /** + * Gets the name of the element. + * + * @return the name, null if none + */ + public String getName() { + Object o = getAttribute(StyleConstants.NameAttribute); + if (o != null) { + return o.toString(); + } + return super.getName(); + } + + /** + * Gets the resolving parent. HTML attributes are not inherited + * at the model level so we override this to return null. + * + * @return null, there are none + * @see AttributeSet#getResolveParent + */ + public AttributeSet getResolveParent() { + return null; + } + } + + /** + * An element that represents a structural block of + * HTML. + */ + public class BlockElement extends BranchElement { + + /** + * Constructs a composite element that initially contains + * no children. + * + * @param parent the parent element + * @param a the attributes for the element + * @since 1.4 + */ + public BlockElement(Element parent, AttributeSet a) { + super(parent, a); + } + + /** + * Gets the name of the element. + * + * @return the name, null if none + */ + public String getName() { + Object o = getAttribute(StyleConstants.NameAttribute); + if (o != null) { + return o.toString(); + } + return super.getName(); + } + + /** + * Gets the resolving parent. HTML attributes are not inherited + * at the model level so we override this to return null. + * + * @return null, there are none + * @see AttributeSet#getResolveParent + */ + public AttributeSet getResolveParent() { + return null; + } + + } + + + /** + * Document that allows you to set the maximum length of the text. + */ + private static class FixedLengthDocument extends PlainDocument { + private int maxLength; + + public FixedLengthDocument(int maxLength) { + this.maxLength = maxLength; + } + + public void insertString(int offset, String str, AttributeSet a) + throws BadLocationException { + if (str != null && str.length() + getLength() <= maxLength) { + super.insertString(offset, str, a); + } + } + } +} diff --git a/src/share/classes/javax/swing/text/html/HTMLEditorKit.java b/src/share/classes/javax/swing/text/html/HTMLEditorKit.java new file mode 100644 index 000000000..980817d96 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/HTMLEditorKit.java @@ -0,0 +1,2279 @@ +/* + * Copyright 1997-2007 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +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 + */ +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; + } + + /** + * 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 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; + 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 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 { + 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 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. + * @since 1.3 + */ + 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 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 + */ + 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. + 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. + */ + 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, null); + } + + /** + * 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, 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 + * hdoc 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 = null; + + 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 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) { + 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; + 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 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.
    +     * 

    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 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, + new Integer(kit.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(); + 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 = 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) { + + 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; + return; + } + } + + /* + * 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. + */ + 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; + } + } + } + } + } + } + + 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 + */ + + 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; + } +} diff --git a/src/share/classes/javax/swing/text/html/HTMLFrameHyperlinkEvent.java b/src/share/classes/javax/swing/text/html/HTMLFrameHyperlinkEvent.java new file mode 100644 index 000000000..b1c97b9e2 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/HTMLFrameHyperlinkEvent.java @@ -0,0 +1,134 @@ +/* + * Copyright 1998-2000 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.event.InputEvent; +import javax.swing.text.*; +import javax.swing.event.HyperlinkEvent; +import java.net.URL; + +/** + * HTMLFrameHyperlinkEvent is used to notify interested + * parties that link was activated in a frame. + * + * @author Sunita Mani + */ + +public class HTMLFrameHyperlinkEvent extends HyperlinkEvent { + + /** + * Creates a new object representing a html frame + * hypertext link event. + * + * @param source the object responsible for the event + * @param type the event type + * @param targetURL the affected URL + * @param targetFrame the Frame to display the document in + */ + public HTMLFrameHyperlinkEvent(Object source, EventType type, URL targetURL, + String targetFrame) { + super(source, type, targetURL); + this.targetFrame = targetFrame; + } + + + /** + * Creates a new object representing a hypertext link event. + * + * @param source the object responsible for the event + * @param type the event type + * @param targetURL the affected URL + * @param desc a description + * @param targetFrame the Frame to display the document in + */ + public HTMLFrameHyperlinkEvent(Object source, EventType type, URL targetURL, String desc, + String targetFrame) { + super(source, type, targetURL, desc); + this.targetFrame = targetFrame; + } + + /** + * Creates a new object representing a hypertext link event. + * + * @param source the object responsible for the event + * @param type the event type + * @param targetURL the affected URL + * @param sourceElement the element that corresponds to the source + * of the event + * @param targetFrame the Frame to display the document in + */ + public HTMLFrameHyperlinkEvent(Object source, EventType type, URL targetURL, + Element sourceElement, String targetFrame) { + super(source, type, targetURL, null, sourceElement); + this.targetFrame = targetFrame; + } + + + /** + * Creates a new object representing a hypertext link event. + * + * @param source the object responsible for the event + * @param type the event type + * @param targetURL the affected URL + * @param desc a desription + * @param sourceElement the element that corresponds to the source + * of the event + * @param targetFrame the Frame to display the document in + */ + public HTMLFrameHyperlinkEvent(Object source, EventType type, URL targetURL, String desc, + Element sourceElement, String targetFrame) { + super(source, type, targetURL, desc, sourceElement); + this.targetFrame = targetFrame; + } + + /** + * Creates a new object representing a hypertext link event. + * + * @param source the object responsible for the event + * @param type the event type + * @param targetURL the affected URL + * @param desc a desription + * @param sourceElement the element that corresponds to the source + * of the event + * @param inputEvent InputEvent that triggered the hyperlink event + * @param targetFrame the Frame to display the document in + * @since 1.7 + */ + public HTMLFrameHyperlinkEvent(Object source, EventType type, URL targetURL, + String desc, Element sourceElement, + InputEvent inputEvent, String targetFrame) { + super(source, type, targetURL, desc, sourceElement, inputEvent); + this.targetFrame = targetFrame; + } + + /** + * returns the target for the link. + */ + public String getTarget() { + return targetFrame; + } + + private String targetFrame; +} diff --git a/src/share/classes/javax/swing/text/html/HTMLWriter.java b/src/share/classes/javax/swing/text/html/HTMLWriter.java new file mode 100644 index 000000000..f7fa0e89f --- /dev/null +++ b/src/share/classes/javax/swing/text/html/HTMLWriter.java @@ -0,0 +1,1276 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import javax.swing.text.*; +import java.io.Writer; +import java.util.Stack; +import java.util.Enumeration; +import java.util.Vector; +import java.io.IOException; +import java.util.StringTokenizer; +import java.util.NoSuchElementException; +import java.net.URL; + +/** + * This is a writer for HTMLDocuments. + * + * @author Sunita Mani + */ + + +public class HTMLWriter extends AbstractWriter { + /* + * Stores all elements for which end tags have to + * be emitted. + */ + private Stack blockElementStack = new Stack(); + private boolean inContent = false; + private boolean inPre = false; + /** When inPre is true, this will indicate the end offset of the pre + * element. */ + private int preEndOffset; + private boolean inTextArea = false; + private boolean newlineOutputed = false; + private boolean completeDoc; + + /* + * Stores all embedded tags. Embedded tags are tags that are + * stored as attributes in other tags. Generally they're + * character level attributes. Examples include + * <b>, <i>, <font>, and <a>. + */ + private Vector tags = new Vector(10); + + /** + * Values for the tags. + */ + private Vector tagValues = new Vector(10); + + /** + * Used when writing out content. + */ + private Segment segment; + + /* + * This is used in closeOutUnwantedEmbeddedTags. + */ + private Vector tagsToRemove = new Vector(10); + + /** + * Set to true after the head has been output. + */ + private boolean wroteHead; + + /** + * Set to true when entities (such as <) should be replaced. + */ + private boolean replaceEntities; + + /** + * Temporary buffer. + */ + private char[] tempChars; + + + /** + * Creates a new HTMLWriter. + * + * @param w a Writer + * @param doc an HTMLDocument + * + */ + public HTMLWriter(Writer w, HTMLDocument doc) { + this(w, doc, 0, doc.getLength()); + } + + /** + * Creates a new HTMLWriter. + * + * @param w a Writer + * @param doc an HTMLDocument + * @param pos the document location from which to fetch the content + * @param len the amount to write out + */ + public HTMLWriter(Writer w, HTMLDocument doc, int pos, int len) { + super(w, doc, pos, len); + completeDoc = (pos == 0 && len == doc.getLength()); + setLineLength(80); + } + + /** + * Iterates over the + * Element tree and controls the writing out of + * all the tags and its attributes. + * + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + * + */ + public void write() throws IOException, BadLocationException { + ElementIterator it = getElementIterator(); + Element current = null; + Element next = null; + + wroteHead = false; + setCurrentLineLength(0); + replaceEntities = false; + setCanWrapLines(false); + if (segment == null) { + segment = new Segment(); + } + inPre = false; + boolean forcedBody = false; + while ((next = it.next()) != null) { + if (!inRange(next)) { + if (completeDoc && next.getAttributes().getAttribute( + StyleConstants.NameAttribute) == HTML.Tag.BODY) { + forcedBody = true; + } + else { + continue; + } + } + if (current != null) { + + /* + if next is child of current increment indent + */ + + if (indentNeedsIncrementing(current, next)) { + incrIndent(); + } else if (current.getParentElement() != next.getParentElement()) { + /* + next and current are not siblings + so emit end tags for items on the stack until the + item on top of the stack, is the parent of the + next. + */ + Element top = (Element)blockElementStack.peek(); + while (top != next.getParentElement()) { + /* + pop() will return top. + */ + blockElementStack.pop(); + if (!synthesizedElement(top)) { + AttributeSet attrs = top.getAttributes(); + if (!matchNameAttribute(attrs, HTML.Tag.PRE) && + !isFormElementWithContent(attrs)) { + decrIndent(); + } + endTag(top); + } + top = (Element)blockElementStack.peek(); + } + } else if (current.getParentElement() == next.getParentElement()) { + /* + if next and current are siblings the indent level + is correct. But, we need to make sure that if current is + on the stack, we pop it off, and put out its end tag. + */ + Element top = (Element)blockElementStack.peek(); + if (top == current) { + blockElementStack.pop(); + endTag(top); + } + } + } + if (!next.isLeaf() || isFormElementWithContent(next.getAttributes())) { + blockElementStack.push(next); + startTag(next); + } else { + emptyTag(next); + } + current = next; + } + /* Emit all remaining end tags */ + + /* A null parameter ensures that all embedded tags + currently in the tags vector have their + corresponding end tags written out. + */ + closeOutUnwantedEmbeddedTags(null); + + if (forcedBody) { + blockElementStack.pop(); + endTag(current); + } + while (!blockElementStack.empty()) { + current = (Element)blockElementStack.pop(); + if (!synthesizedElement(current)) { + AttributeSet attrs = current.getAttributes(); + if (!matchNameAttribute(attrs, HTML.Tag.PRE) && + !isFormElementWithContent(attrs)) { + decrIndent(); + } + endTag(current); + } + } + + if (completeDoc) { + writeAdditionalComments(); + } + + segment.array = null; + } + + + /** + * Writes out the attribute set. Ignores all + * attributes with a key of type HTML.Tag, + * attributes with a key of type StyleConstants, + * and attributes with a key of type + * HTML.Attribute.ENDTAG. + * + * @param attr an AttributeSet + * @exception IOException on any I/O error + * + */ + protected void writeAttributes(AttributeSet attr) throws IOException { + // translate css attributes to html + convAttr.removeAttributes(convAttr); + convertToHTML32(attr, convAttr); + + Enumeration names = convAttr.getAttributeNames(); + while (names.hasMoreElements()) { + Object name = names.nextElement(); + if (name instanceof HTML.Tag || + name instanceof StyleConstants || + name == HTML.Attribute.ENDTAG) { + continue; + } + write(" " + name + "=\"" + convAttr.getAttribute(name) + "\""); + } + } + + /** + * Writes out all empty elements (all tags that have no + * corresponding end tag). + * + * @param elem an Element + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + */ + protected void emptyTag(Element elem) throws BadLocationException, IOException { + + if (!inContent && !inPre) { + indentSmart(); + } + + AttributeSet attr = elem.getAttributes(); + closeOutUnwantedEmbeddedTags(attr); + writeEmbeddedTags(attr); + + if (matchNameAttribute(attr, HTML.Tag.CONTENT)) { + inContent = true; + text(elem); + } else if (matchNameAttribute(attr, HTML.Tag.COMMENT)) { + comment(elem); + } else { + boolean isBlock = isBlockTag(elem.getAttributes()); + if (inContent && isBlock ) { + writeLineSeparator(); + indentSmart(); + } + + Object nameTag = (attr != null) ? attr.getAttribute + (StyleConstants.NameAttribute) : null; + Object endTag = (attr != null) ? attr.getAttribute + (HTML.Attribute.ENDTAG) : null; + + boolean outputEndTag = false; + // If an instance of an UNKNOWN Tag, or an instance of a + // tag that is only visible during editing + // + if (nameTag != null && endTag != null && + (endTag instanceof String) && + ((String)endTag).equals("true")) { + outputEndTag = true; + } + + if (completeDoc && matchNameAttribute(attr, HTML.Tag.HEAD)) { + if (outputEndTag) { + // Write out any styles. + writeStyles(((HTMLDocument)getDocument()).getStyleSheet()); + } + wroteHead = true; + } + + write('<'); + if (outputEndTag) { + write('/'); + } + write(elem.getName()); + writeAttributes(attr); + write('>'); + if (matchNameAttribute(attr, HTML.Tag.TITLE) && !outputEndTag) { + Document doc = elem.getDocument(); + String title = (String)doc.getProperty(Document.TitleProperty); + write(title); + } else if (!inContent || isBlock) { + writeLineSeparator(); + if (isBlock && inContent) { + indentSmart(); + } + } + } + } + + /** + * Determines if the HTML.Tag associated with the + * element is a block tag. + * + * @param attr an AttributeSet + * @return true if tag is block tag, false otherwise. + */ + protected boolean isBlockTag(AttributeSet attr) { + Object o = attr.getAttribute(StyleConstants.NameAttribute); + if (o instanceof HTML.Tag) { + HTML.Tag name = (HTML.Tag) o; + return name.isBlock(); + } + return false; + } + + + /** + * Writes out a start tag for the element. + * Ignores all synthesized elements. + * + * @param elem an Element + * @exception IOException on any I/O error + */ + protected void startTag(Element elem) throws IOException, BadLocationException { + + if (synthesizedElement(elem)) { + return; + } + + // Determine the name, as an HTML.Tag. + AttributeSet attr = elem.getAttributes(); + Object nameAttribute = attr.getAttribute(StyleConstants.NameAttribute); + HTML.Tag name; + if (nameAttribute instanceof HTML.Tag) { + name = (HTML.Tag)nameAttribute; + } + else { + name = null; + } + + if (name == HTML.Tag.PRE) { + inPre = true; + preEndOffset = elem.getEndOffset(); + } + + // write out end tags for item on stack + closeOutUnwantedEmbeddedTags(attr); + + if (inContent) { + writeLineSeparator(); + inContent = false; + newlineOutputed = false; + } + + if (completeDoc && name == HTML.Tag.BODY && !wroteHead) { + // If the head has not been output, output it and the styles. + wroteHead = true; + indentSmart(); + write(""); + writeLineSeparator(); + incrIndent(); + writeStyles(((HTMLDocument)getDocument()).getStyleSheet()); + decrIndent(); + writeLineSeparator(); + indentSmart(); + write(""); + writeLineSeparator(); + } + + indentSmart(); + write('<'); + write(elem.getName()); + writeAttributes(attr); + write('>'); + if (name != HTML.Tag.PRE) { + writeLineSeparator(); + } + + if (name == HTML.Tag.TEXTAREA) { + textAreaContent(elem.getAttributes()); + } else if (name == HTML.Tag.SELECT) { + selectContent(elem.getAttributes()); + } else if (completeDoc && name == HTML.Tag.BODY) { + // Write out the maps, which is not stored as Elements in + // the Document. + writeMaps(((HTMLDocument)getDocument()).getMaps()); + } + else if (name == HTML.Tag.HEAD) { + HTMLDocument document = (HTMLDocument)getDocument(); + wroteHead = true; + incrIndent(); + writeStyles(document.getStyleSheet()); + if (document.hasBaseTag()) { + indentSmart(); + write(""); + writeLineSeparator(); + } + decrIndent(); + } + + } + + + /** + * Writes out text that is contained in a TEXTAREA form + * element. + * + * @param attr an AttributeSet + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + */ + protected void textAreaContent(AttributeSet attr) throws BadLocationException, IOException { + Document doc = (Document)attr.getAttribute(StyleConstants.ModelAttribute); + if (doc != null && doc.getLength() > 0) { + if (segment == null) { + segment = new Segment(); + } + doc.getText(0, doc.getLength(), segment); + if (segment.count > 0) { + inTextArea = true; + incrIndent(); + indentSmart(); + setCanWrapLines(true); + replaceEntities = true; + write(segment.array, segment.offset, segment.count); + replaceEntities = false; + setCanWrapLines(false); + writeLineSeparator(); + inTextArea = false; + decrIndent(); + } + } + } + + + /** + * Writes out text. If a range is specified when the constructor + * is invoked, then only the appropriate range of text is written + * out. + * + * @param elem an Element + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + */ + protected void text(Element elem) throws BadLocationException, IOException { + int start = Math.max(getStartOffset(), elem.getStartOffset()); + int end = Math.min(getEndOffset(), elem.getEndOffset()); + if (start < end) { + if (segment == null) { + segment = new Segment(); + } + getDocument().getText(start, end - start, segment); + newlineOutputed = false; + if (segment.count > 0) { + if (segment.array[segment.offset + segment.count - 1] == '\n'){ + newlineOutputed = true; + } + if (inPre && end == preEndOffset) { + if (segment.count > 1) { + segment.count--; + } + else { + return; + } + } + replaceEntities = true; + setCanWrapLines(!inPre); + write(segment.array, segment.offset, segment.count); + setCanWrapLines(false); + replaceEntities = false; + } + } + } + + /** + * Writes out the content of the SELECT form element. + * + * @param attr the AttributeSet associated with the form element + * @exception IOException on any I/O error + */ + protected void selectContent(AttributeSet attr) throws IOException { + Object model = attr.getAttribute(StyleConstants.ModelAttribute); + incrIndent(); + if (model instanceof OptionListModel) { + OptionListModel listModel = (OptionListModel)model; + int size = listModel.getSize(); + for (int i = 0; i < size; i++) { + Option option = (Option)listModel.getElementAt(i); + writeOption(option); + } + } else if (model instanceof OptionComboBoxModel) { + OptionComboBoxModel comboBoxModel = (OptionComboBoxModel)model; + int size = comboBoxModel.getSize(); + for (int i = 0; i < size; i++) { + Option option = (Option)comboBoxModel.getElementAt(i); + writeOption(option); + } + } + decrIndent(); + } + + + /** + * Writes out the content of the Option form element. + * @param option an Option + * @exception IOException on any I/O error + * + */ + protected void writeOption(Option option) throws IOException { + + indentSmart(); + write('<'); + write("option"); + // PENDING: should this be changed to check for null first? + Object value = option.getAttributes().getAttribute + (HTML.Attribute.VALUE); + if (value != null) { + write(" value="+ value); + } + if (option.isSelected()) { + write(" selected"); + } + write('>'); + if (option.getLabel() != null) { + write(option.getLabel()); + } + writeLineSeparator(); + } + + /** + * Writes out an end tag for the element. + * + * @param elem an Element + * @exception IOException on any I/O error + */ + protected void endTag(Element elem) throws IOException { + if (synthesizedElement(elem)) { + return; + } + + // write out end tags for item on stack + closeOutUnwantedEmbeddedTags(elem.getAttributes()); + if (inContent) { + if (!newlineOutputed && !inPre) { + writeLineSeparator(); + } + newlineOutputed = false; + inContent = false; + } + if (!inPre) { + indentSmart(); + } + if (matchNameAttribute(elem.getAttributes(), HTML.Tag.PRE)) { + inPre = false; + } + write('<'); + write('/'); + write(elem.getName()); + write('>'); + writeLineSeparator(); + } + + + + /** + * Writes out comments. + * + * @param elem an Element + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + */ + protected void comment(Element elem) throws BadLocationException, IOException { + AttributeSet as = elem.getAttributes(); + if (matchNameAttribute(as, HTML.Tag.COMMENT)) { + Object comment = as.getAttribute(HTML.Attribute.COMMENT); + if (comment instanceof String) { + writeComment((String)comment); + } + else { + writeComment(null); + } + } + } + + + /** + * Writes out comment string. + * + * @param string the comment + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + */ + void writeComment(String string) throws IOException { + write(""); + writeLineSeparator(); + indentSmart(); + } + + + /** + * Writes out any additional comments (comments outside of the body) + * stored under the property HTMLDocument.AdditionalComments. + */ + void writeAdditionalComments() throws IOException { + Object comments = getDocument().getProperty + (HTMLDocument.AdditionalComments); + + if (comments instanceof Vector) { + Vector v = (Vector)comments; + for (int counter = 0, maxCounter = v.size(); counter < maxCounter; + counter++) { + writeComment(v.elementAt(counter).toString()); + } + } + } + + + /** + * Returns true if the element is a + * synthesized element. Currently we are only testing + * for the p-implied tag. + */ + protected boolean synthesizedElement(Element elem) { + if (matchNameAttribute(elem.getAttributes(), HTML.Tag.IMPLIED)) { + return true; + } + return false; + } + + + /** + * Returns true if the StyleConstants.NameAttribute is + * equal to the tag that is passed in as a parameter. + */ + protected boolean matchNameAttribute(AttributeSet attr, HTML.Tag tag) { + Object o = attr.getAttribute(StyleConstants.NameAttribute); + if (o instanceof HTML.Tag) { + HTML.Tag name = (HTML.Tag) o; + if (name == tag) { + return true; + } + } + return false; + } + + /** + * Searches for embedded tags in the AttributeSet + * and writes them out. It also stores these tags in a vector + * so that when appropriate the corresponding end tags can be + * written out. + * + * @exception IOException on any I/O error + */ + protected void writeEmbeddedTags(AttributeSet attr) throws IOException { + + // translate css attributes to html + attr = convertToHTML(attr, oConvAttr); + + Enumeration names = attr.getAttributeNames(); + while (names.hasMoreElements()) { + Object name = names.nextElement(); + if (name instanceof HTML.Tag) { + HTML.Tag tag = (HTML.Tag)name; + if (tag == HTML.Tag.FORM || tags.contains(tag)) { + continue; + } + write('<'); + write(tag.toString()); + Object o = attr.getAttribute(tag); + if (o != null && o instanceof AttributeSet) { + writeAttributes((AttributeSet)o); + } + write('>'); + tags.addElement(tag); + tagValues.addElement(o); + } + } + } + + + /** + * Searches the attribute set for a tag, both of which + * are passed in as a parameter. Returns true if no match is found + * and false otherwise. + */ + private boolean noMatchForTagInAttributes(AttributeSet attr, HTML.Tag t, + Object tagValue) { + if (attr != null && attr.isDefined(t)) { + Object newValue = attr.getAttribute(t); + + if ((tagValue == null) ? (newValue == null) : + (newValue != null && tagValue.equals(newValue))) { + return false; + } + } + return true; + } + + + /** + * Searches the attribute set and for each tag + * that is stored in the tag vector. If the tag isnt found, + * then the tag is removed from the vector and a corresponding + * end tag is written out. + * + * @exception IOException on any I/O error + */ + protected void closeOutUnwantedEmbeddedTags(AttributeSet attr) throws IOException { + + tagsToRemove.removeAllElements(); + + // translate css attributes to html + attr = convertToHTML(attr, null); + + HTML.Tag t; + Object tValue; + int firstIndex = -1; + int size = tags.size(); + // First, find all the tags that need to be removed. + for (int i = size - 1; i >= 0; i--) { + t = (HTML.Tag)tags.elementAt(i); + tValue = tagValues.elementAt(i); + if ((attr == null) || noMatchForTagInAttributes(attr, t, tValue)) { + firstIndex = i; + tagsToRemove.addElement(t); + } + } + if (firstIndex != -1) { + // Then close them out. + boolean removeAll = ((size - firstIndex) == tagsToRemove.size()); + for (int i = size - 1; i >= firstIndex; i--) { + t = (HTML.Tag)tags.elementAt(i); + if (removeAll || tagsToRemove.contains(t)) { + tags.removeElementAt(i); + tagValues.removeElementAt(i); + } + write('<'); + write('/'); + write(t.toString()); + write('>'); + } + // Have to output any tags after firstIndex that still remaing, + // as we closed them out, but they should remain open. + size = tags.size(); + for (int i = firstIndex; i < size; i++) { + t = (HTML.Tag)tags.elementAt(i); + write('<'); + write(t.toString()); + Object o = tagValues.elementAt(i); + if (o != null && o instanceof AttributeSet) { + writeAttributes((AttributeSet)o); + } + write('>'); + } + } + } + + + /** + * Determines if the element associated with the attributeset + * is a TEXTAREA or SELECT. If true, returns true else + * false + */ + private boolean isFormElementWithContent(AttributeSet attr) { + if (matchNameAttribute(attr, HTML.Tag.TEXTAREA) || + matchNameAttribute(attr, HTML.Tag.SELECT)) { + return true; + } + return false; + } + + + /** + * Determines whether a the indentation needs to be + * incremented. Basically, if next is a child of current, and + * next is NOT a synthesized element, the indent level will be + * incremented. If there is a parent-child relationship and "next" + * is a synthesized element, then its children must be indented. + * This state is maintained by the indentNext boolean. + * + * @return boolean that's true if indent level + * needs incrementing. + */ + private boolean indentNext = false; + private boolean indentNeedsIncrementing(Element current, Element next) { + if ((next.getParentElement() == current) && !inPre) { + if (indentNext) { + indentNext = false; + return true; + } else if (synthesizedElement(next)) { + indentNext = true; + } else if (!synthesizedElement(current)){ + return true; + } + } + return false; + } + + /** + * Outputs the maps as elements. Maps are not stored as elements in + * the document, and as such this is used to output them. + */ + void writeMaps(Enumeration maps) throws IOException { + if (maps != null) { + while(maps.hasMoreElements()) { + Map map = (Map)maps.nextElement(); + String name = map.getName(); + + incrIndent(); + indentSmart(); + write(""); + } + else { + write('>'); + } + writeLineSeparator(); + incrIndent(); + + // Output the areas + AttributeSet[] areas = map.getAreas(); + if (areas != null) { + for (int counter = 0, maxCounter = areas.length; + counter < maxCounter; counter++) { + indentSmart(); + write(""); + writeLineSeparator(); + } + } + decrIndent(); + indentSmart(); + write(""); + writeLineSeparator(); + decrIndent(); + } + } + } + + /** + * Outputs the styles as a single element. Styles are not stored as + * elements, but part of the document. For the time being styles are + * written out as a comment, inside a style tag. + */ + void writeStyles(StyleSheet sheet) throws IOException { + if (sheet != null) { + Enumeration styles = sheet.getStyleNames(); + if (styles != null) { + boolean outputStyle = false; + while (styles.hasMoreElements()) { + String name = (String)styles.nextElement(); + // Don't write out the default style. + if (!StyleContext.DEFAULT_STYLE.equals(name) && + writeStyle(name, sheet.getStyle(name), outputStyle)) { + outputStyle = true; + } + } + if (outputStyle) { + writeStyleEndTag(); + } + } + } + } + + /** + * Outputs the named style. outputStyle indicates + * whether or not a style has been output yet. This will return + * true if a style is written. + */ + boolean writeStyle(String name, Style style, boolean outputStyle) + throws IOException{ + boolean didOutputStyle = false; + Enumeration attributes = style.getAttributeNames(); + if (attributes != null) { + while (attributes.hasMoreElements()) { + Object attribute = attributes.nextElement(); + if (attribute instanceof CSS.Attribute) { + String value = style.getAttribute(attribute).toString(); + if (value != null) { + if (!outputStyle) { + writeStyleStartTag(); + outputStyle = true; + } + if (!didOutputStyle) { + didOutputStyle = true; + indentSmart(); + write(name); + write(" {"); + } + else { + write(";"); + } + write(' '); + write(attribute.toString()); + write(": "); + write(value); + } + } + } + } + if (didOutputStyle) { + write(" }"); + writeLineSeparator(); + } + return didOutputStyle; + } + + void writeStyleStartTag() throws IOException { + indentSmart(); + write(""); + writeLineSeparator(); + indentSmart(); + } + + // --- conversion support --------------------------- + + /** + * Convert the give set of attributes to be html for + * the purpose of writing them out. Any keys that + * have been converted will not appear in the resultant + * set. Any keys not converted will appear in the + * resultant set the same as the received set.

    + * This will put the converted values into to, unless + * it is null in which case a temporary AttributeSet will be returned. + */ + AttributeSet convertToHTML(AttributeSet from, MutableAttributeSet to) { + if (to == null) { + to = convAttr; + } + to.removeAttributes(to); + if (writeCSS) { + convertToHTML40(from, to); + } else { + convertToHTML32(from, to); + } + return to; + } + + /** + * If true, the writer will emit CSS attributes in preference + * to HTML tags/attributes (i.e. It will emit an HTML 4.0 + * style). + */ + private boolean writeCSS = false; + + /** + * Buffer for the purpose of attribute conversion + */ + private MutableAttributeSet convAttr = new SimpleAttributeSet(); + + /** + * Buffer for the purpose of attribute conversion. This can be + * used if convAttr is being used. + */ + private MutableAttributeSet oConvAttr = new SimpleAttributeSet(); + + /** + * Create an older style of HTML attributes. This will + * convert character level attributes that have a StyleConstants + * mapping over to an HTML tag/attribute. Other CSS attributes + * will be placed in an HTML style attribute. + */ + private static void convertToHTML32(AttributeSet from, MutableAttributeSet to) { + if (from == null) { + return; + } + Enumeration keys = from.getAttributeNames(); + String value = ""; + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key instanceof CSS.Attribute) { + if ((key == CSS.Attribute.FONT_FAMILY) || + (key == CSS.Attribute.FONT_SIZE) || + (key == CSS.Attribute.COLOR)) { + + createFontAttribute((CSS.Attribute)key, from, to); + } else if (key == CSS.Attribute.FONT_WEIGHT) { + // add a bold tag is weight is bold + CSS.FontWeight weightValue = (CSS.FontWeight) + from.getAttribute(CSS.Attribute.FONT_WEIGHT); + if ((weightValue != null) && (weightValue.getValue() > 400)) { + addAttribute(to, HTML.Tag.B, SimpleAttributeSet.EMPTY); + } + } else if (key == CSS.Attribute.FONT_STYLE) { + String s = from.getAttribute(key).toString(); + if (s.indexOf("italic") >= 0) { + addAttribute(to, HTML.Tag.I, SimpleAttributeSet.EMPTY); + } + } else if (key == CSS.Attribute.TEXT_DECORATION) { + String decor = from.getAttribute(key).toString(); + if (decor.indexOf("underline") >= 0) { + addAttribute(to, HTML.Tag.U, SimpleAttributeSet.EMPTY); + } + if (decor.indexOf("line-through") >= 0) { + addAttribute(to, HTML.Tag.STRIKE, SimpleAttributeSet.EMPTY); + } + } else if (key == CSS.Attribute.VERTICAL_ALIGN) { + String vAlign = from.getAttribute(key).toString(); + if (vAlign.indexOf("sup") >= 0) { + addAttribute(to, HTML.Tag.SUP, SimpleAttributeSet.EMPTY); + } + if (vAlign.indexOf("sub") >= 0) { + addAttribute(to, HTML.Tag.SUB, SimpleAttributeSet.EMPTY); + } + } else if (key == CSS.Attribute.TEXT_ALIGN) { + addAttribute(to, HTML.Attribute.ALIGN, + from.getAttribute(key).toString()); + } else { + // default is to store in a HTML style attribute + if (value.length() > 0) { + value = value + "; "; + } + value = value + key + ": " + from.getAttribute(key); + } + } else { + Object attr = from.getAttribute(key); + if (attr instanceof AttributeSet) { + attr = ((AttributeSet)attr).copyAttributes(); + } + addAttribute(to, key, attr); + } + } + if (value.length() > 0) { + to.addAttribute(HTML.Attribute.STYLE, value); + } + } + + /** + * Add an attribute only if it doesn't exist so that we don't + * loose information replacing it with SimpleAttributeSet.EMPTY + */ + private static void addAttribute(MutableAttributeSet to, Object key, Object value) { + Object attr = to.getAttribute(key); + if (attr == null || attr == SimpleAttributeSet.EMPTY) { + to.addAttribute(key, value); + } else { + if (attr instanceof MutableAttributeSet && + value instanceof AttributeSet) { + ((MutableAttributeSet)attr).addAttributes((AttributeSet)value); + } + } + } + + /** + * Create/update an HTML <font> tag attribute. The + * value of the attribute should be a MutableAttributeSet so + * that the attributes can be updated as they are discovered. + */ + private static void createFontAttribute(CSS.Attribute a, AttributeSet from, + MutableAttributeSet to) { + MutableAttributeSet fontAttr = (MutableAttributeSet) + to.getAttribute(HTML.Tag.FONT); + if (fontAttr == null) { + fontAttr = new SimpleAttributeSet(); + to.addAttribute(HTML.Tag.FONT, fontAttr); + } + // edit the parameters to the font tag + String htmlValue = from.getAttribute(a).toString(); + if (a == CSS.Attribute.FONT_FAMILY) { + fontAttr.addAttribute(HTML.Attribute.FACE, htmlValue); + } else if (a == CSS.Attribute.FONT_SIZE) { + fontAttr.addAttribute(HTML.Attribute.SIZE, htmlValue); + } else if (a == CSS.Attribute.COLOR) { + fontAttr.addAttribute(HTML.Attribute.COLOR, htmlValue); + } + } + + /** + * Copies the given AttributeSet to a new set, converting + * any CSS attributes found to arguments of an HTML style + * attribute. + */ + private static void convertToHTML40(AttributeSet from, MutableAttributeSet to) { + Enumeration keys = from.getAttributeNames(); + String value = ""; + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key instanceof CSS.Attribute) { + value = value + " " + key + "=" + from.getAttribute(key) + ";"; + } else { + to.addAttribute(key, from.getAttribute(key)); + } + } + if (value.length() > 0) { + to.addAttribute(HTML.Attribute.STYLE, value); + } + } + + // + // Overrides the writing methods to only break a string when + // canBreakString is true. + // In a future release it is likely AbstractWriter will get this + // functionality. + // + + /** + * Writes the line separator. This is overriden to make sure we don't + * replace the newline content in case it is outside normal ascii. + * @since 1.3 + */ + protected void writeLineSeparator() throws IOException { + boolean oldReplace = replaceEntities; + replaceEntities = false; + super.writeLineSeparator(); + replaceEntities = oldReplace; + indented = false; + } + + /** + * This method is overriden to map any character entities, such as + * < to &lt;. super.output will be invoked to + * write the content. + * @since 1.3 + */ + protected void output(char[] chars, int start, int length) + throws IOException { + if (!replaceEntities) { + super.output(chars, start, length); + return; + } + int last = start; + length += start; + for (int counter = start; counter < length; counter++) { + // This will change, we need better support character level + // entities. + switch(chars[counter]) { + // Character level entities. + case '<': + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + output("<"); + break; + case '>': + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + output(">"); + break; + case '&': + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + output("&"); + break; + case '"': + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + output("""); + break; + // Special characters + case '\n': + case '\t': + case '\r': + break; + default: + if (chars[counter] < ' ' || chars[counter] > 127) { + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + // If the character is outside of ascii, write the + // numeric value. + output("&#"); + output(String.valueOf((int)chars[counter])); + output(";"); + } + break; + } + } + if (last < length) { + super.output(chars, last, length - last); + } + } + + /** + * This directly invokes super's output after converting + * string to a char[]. + */ + private void output(String string) throws IOException { + int length = string.length(); + if (tempChars == null || tempChars.length < length) { + tempChars = new char[length]; + } + string.getChars(0, length, tempChars, 0); + super.output(tempChars, 0, length); + } + + private boolean indented = false; + + /** + * Writes indent only once per line. + */ + private void indentSmart() throws IOException { + if (!indented) { + indent(); + indented = true; + } + } +} diff --git a/src/share/classes/javax/swing/text/html/HiddenTagView.java b/src/share/classes/javax/swing/text/html/HiddenTagView.java new file mode 100644 index 000000000..7d46b2836 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/HiddenTagView.java @@ -0,0 +1,361 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +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 java.util.*; + +/** + * HiddenTagView subclasses EditableView to contain a JTextField showing + * the element name. When the textfield is edited the element name is + * reset. As this inherits from EditableView if the JTextComponent is + * not editable, the textfield will not be visible. + * + * @author Scott Violet + */ +class HiddenTagView extends EditableView implements DocumentListener { + HiddenTagView(Element e) { + super(e); + yAlign = 1; + } + + protected Component createComponent() { + JTextField tf = new JTextField(getElement().getName()); + Document doc = getDocument(); + Font font; + if (doc instanceof StyledDocument) { + font = ((StyledDocument)doc).getFont(getAttributes()); + tf.setFont(font); + } + else { + font = tf.getFont(); + } + tf.getDocument().addDocumentListener(this); + updateYAlign(font); + + // Create a panel to wrap the textfield so that the textfields + // laf border shows through. + JPanel panel = new JPanel(new BorderLayout()); + panel.setBackground(null); + if (isEndTag()) { + panel.setBorder(EndBorder); + } + else { + panel.setBorder(StartBorder); + } + panel.add(tf); + return panel; + } + + public float getAlignment(int axis) { + if (axis == View.Y_AXIS) { + return yAlign; + } + return 0.5f; + } + + public float getMinimumSpan(int axis) { + if (axis == View.X_AXIS && isVisible()) { + // Default to preferred. + return Math.max(30, super.getPreferredSpan(axis)); + } + return super.getMinimumSpan(axis); + } + + public float getPreferredSpan(int axis) { + if (axis == View.X_AXIS && isVisible()) { + return Math.max(30, super.getPreferredSpan(axis)); + } + return super.getPreferredSpan(axis); + } + + public float getMaximumSpan(int axis) { + if (axis == View.X_AXIS && isVisible()) { + // Default to preferred. + return Math.max(30, super.getMaximumSpan(axis)); + } + return super.getMaximumSpan(axis); + } + + // DocumentListener methods + public void insertUpdate(DocumentEvent e) { + updateModelFromText(); + } + + public void removeUpdate(DocumentEvent e) { + updateModelFromText(); + } + + public void changedUpdate(DocumentEvent e) { + updateModelFromText(); + } + + // View method + public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { + if (!isSettingAttributes) { + setTextFromModel(); + } + } + + // local methods + + void updateYAlign(Font font) { + Container c = getContainer(); + FontMetrics fm = (c != null) ? c.getFontMetrics(font) : + Toolkit.getDefaultToolkit().getFontMetrics(font); + float h = fm.getHeight(); + float d = fm.getDescent(); + yAlign = (h > 0) ? (h - d) / h : 0; + } + + void resetBorder() { + Component comp = getComponent(); + + if (comp != null) { + if (isEndTag()) { + ((JPanel)comp).setBorder(EndBorder); + } + else { + ((JPanel)comp).setBorder(StartBorder); + } + } + } + + /** + * This resets the text on the text component we created to match + * that of the AttributeSet for the Element we represent. + *

    If this is invoked on the event dispatching thread, this + * directly invokes _setTextFromModel, otherwise + * SwingUtilities.invokeLater is used to schedule execution + * of _setTextFromModel. + */ + void setTextFromModel() { + if (SwingUtilities.isEventDispatchThread()) { + _setTextFromModel(); + } + else { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + _setTextFromModel(); + } + }); + } + } + + /** + * This resets the text on the text component we created to match + * that of the AttributeSet for the Element we represent. + */ + void _setTextFromModel() { + Document doc = getDocument(); + try { + isSettingAttributes = true; + if (doc instanceof AbstractDocument) { + ((AbstractDocument)doc).readLock(); + } + JTextComponent text = getTextComponent(); + if (text != null) { + text.setText(getRepresentedText()); + resetBorder(); + Container host = getContainer(); + if (host != null) { + preferenceChanged(this, true, true); + host.repaint(); + } + } + } + finally { + isSettingAttributes = false; + if (doc instanceof AbstractDocument) { + ((AbstractDocument)doc).readUnlock(); + } + } + } + + /** + * This copies the text from the text component we've created + * to the Element's AttributeSet we represent. + *

    If this is invoked on the event dispatching thread, this + * directly invokes _updateModelFromText, otherwise + * SwingUtilities.invokeLater is used to schedule execution + * of _updateModelFromText. + */ + void updateModelFromText() { + if (!isSettingAttributes) { + if (SwingUtilities.isEventDispatchThread()) { + _updateModelFromText(); + } + else { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + _updateModelFromText(); + } + }); + } + } + } + + /** + * This copies the text from the text component we've created + * to the Element's AttributeSet we represent. + */ + void _updateModelFromText() { + Document doc = getDocument(); + Object name = getElement().getAttributes().getAttribute + (StyleConstants.NameAttribute); + if ((name instanceof HTML.UnknownTag) && + (doc instanceof StyledDocument)) { + SimpleAttributeSet sas = new SimpleAttributeSet(); + JTextComponent textComponent = getTextComponent(); + if (textComponent != null) { + String text = textComponent.getText(); + isSettingAttributes = true; + try { + sas.addAttribute(StyleConstants.NameAttribute, + new HTML.UnknownTag(text)); + ((StyledDocument)doc).setCharacterAttributes + (getStartOffset(), getEndOffset() - + getStartOffset(), sas, false); + } + finally { + isSettingAttributes = false; + } + } + } + } + + JTextComponent getTextComponent() { + Component comp = getComponent(); + + return (comp == null) ? null : (JTextComponent)((Container)comp). + getComponent(0); + } + + String getRepresentedText() { + String retValue = getElement().getName(); + return (retValue == null) ? "" : retValue; + } + + boolean isEndTag() { + AttributeSet as = getElement().getAttributes(); + if (as != null) { + Object end = as.getAttribute(HTML.Attribute.ENDTAG); + if (end != null && (end instanceof String) && + ((String)end).equals("true")) { + return true; + } + } + return false; + } + + /** Alignment along the y axis, based on the font of the textfield. */ + float yAlign; + /** Set to true when setting attributes. */ + boolean isSettingAttributes; + + + // Following are for Borders that used for Unknown tags and comments. + // + // Border defines + static final int circleR = 3; + static final int circleD = circleR * 2; + static final int tagSize = 6; + static final int padding = 3; + static final Color UnknownTagBorderColor = Color.black; + static final Border StartBorder = new StartTagBorder(); + static final Border EndBorder = new EndTagBorder(); + + + static class StartTagBorder implements Border, Serializable { + public void paintBorder(Component c, Graphics g, int x, int y, + int width, int height) { + g.setColor(UnknownTagBorderColor); + x += padding; + width -= (padding * 2); + g.drawLine(x, y + circleR, + x, y + height - circleR); + g.drawArc(x, y + height - circleD - 1, + circleD, circleD, 180, 90); + g.drawArc(x, y, circleD, circleD, 90, 90); + g.drawLine(x + circleR, y, x + width - tagSize, y); + g.drawLine(x + circleR, y + height - 1, + x + width - tagSize, y + height - 1); + + g.drawLine(x + width - tagSize, y, + x + width - 1, y + height / 2); + g.drawLine(x + width - tagSize, y + height, + x + width - 1, y + height / 2); + } + + public Insets getBorderInsets(Component c) { + return new Insets(2, 2 + padding, 2, tagSize + 2 + padding); + } + + public boolean isBorderOpaque() { + return false; + } + } // End of class HiddenTagView.StartTagBorder + + + static class EndTagBorder implements Border, Serializable { + public void paintBorder(Component c, Graphics g, int x, int y, + int width, int height) { + g.setColor(UnknownTagBorderColor); + x += padding; + width -= (padding * 2); + g.drawLine(x + width - 1, y + circleR, + x + width - 1, y + height - circleR); + g.drawArc(x + width - circleD - 1, y + height - circleD - 1, + circleD, circleD, 270, 90); + g.drawArc(x + width - circleD - 1, y, circleD, circleD, 0, 90); + g.drawLine(x + tagSize, y, x + width - circleR, y); + g.drawLine(x + tagSize, y + height - 1, + x + width - circleR, y + height - 1); + + g.drawLine(x + tagSize, y, + x, y + height / 2); + g.drawLine(x + tagSize, y + height, + x, y + height / 2); + } + + public Insets getBorderInsets(Component c) { + return new Insets(2, tagSize + 2 + padding, 2, 2 + padding); + } + + public boolean isBorderOpaque() { + return false; + } + } // End of class HiddenTagView.EndTagBorder + + +} // End of HiddenTagView diff --git a/src/share/classes/javax/swing/text/html/ImageView.java b/src/share/classes/javax/swing/text/html/ImageView.java new file mode 100644 index 000000000..4b645b6a7 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/ImageView.java @@ -0,0 +1,997 @@ +/* + * Copyright 1997-2005 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.ImageObserver; +import java.io.*; +import java.net.*; +import java.util.Dictionary; +import javax.swing.*; +import javax.swing.text.*; +import javax.swing.event.*; + +/** + * View of an Image, intended to support the HTML <IMG> tag. + * Supports scaling via the HEIGHT and WIDTH attributes of the tag. + * If the image is unable to be loaded any text specified via the + * ALT attribute will be rendered. + *

    + * While this class has been part of swing for a while now, it is public + * as of 1.4. + * + * @author Scott Violet + * @see IconView + * @since 1.4 + */ +public class ImageView extends View { + /** + * If true, when some of the bits are available a repaint is done. + *

    + * This is set to false as swing does not offer a repaint that takes a + * delay. If this were true, a bunch of immediate repaints would get + * generated that end up significantly delaying the loading of the image + * (or anything else going on for that matter). + */ + private static boolean sIsInc = false; + /** + * Repaint delay when some of the bits are available. + */ + private static int sIncRate = 100; + /** + * Property name for pending image icon + */ + private static final String PENDING_IMAGE = "html.pendingImage"; + /** + * Property name for missing image icon + */ + private static final String MISSING_IMAGE = "html.missingImage"; + + /** + * Document property for image cache. + */ + private static final String IMAGE_CACHE_PROPERTY = "imageCache"; + + // Height/width to use before we know the real size, these should at least + // the size of sMissingImageIcon and + // sPendingImageIcon + private static final int DEFAULT_WIDTH = 38; + private static final int DEFAULT_HEIGHT= 38; + + /** + * Default border to use if one is not specified. + */ + private static final int DEFAULT_BORDER = 2; + + // Bitmask values + private static final int LOADING_FLAG = 1; + private static final int LINK_FLAG = 2; + private static final int WIDTH_FLAG = 4; + private static final int HEIGHT_FLAG = 8; + private static final int RELOAD_FLAG = 16; + private static final int RELOAD_IMAGE_FLAG = 32; + private static final int SYNC_LOAD_FLAG = 64; + + private AttributeSet attr; + private Image image; + private int width; + private int height; + /** Bitmask containing some of the above bitmask values. Because the + * image loading notification can happen on another thread access to + * this is synchronized (at least for modifying it). */ + private int state; + private Container container; + private Rectangle fBounds; + private Color borderColor; + // Size of the border, the insets contains this valid. For example, if + // the HSPACE attribute was 4 and BORDER 2, leftInset would be 6. + private short borderSize; + // Insets, obtained from the painter. + private short leftInset; + private short rightInset; + private short topInset; + private short bottomInset; + /** + * We don't directly implement ImageObserver, instead we use an instance + * that calls back to us. + */ + private ImageObserver imageObserver; + /** + * Used for alt text. Will be non-null if the image couldn't be found, + * and there is valid alt text. + */ + private View altView; + /** Alignment along the vertical (Y) axis. */ + private float vAlign; + + + + /** + * Creates a new view that represents an IMG element. + * + * @param elem the element to create a view for + */ + public ImageView(Element elem) { + super(elem); + fBounds = new Rectangle(); + imageObserver = new ImageHandler(); + state = RELOAD_FLAG | RELOAD_IMAGE_FLAG; + } + + /** + * Returns the text to display if the image can't be loaded. This is + * obtained from the Elements attribute set with the attribute name + * HTML.Attribute.ALT. + */ + public String getAltText() { + return (String)getElement().getAttributes().getAttribute + (HTML.Attribute.ALT); + } + + /** + * Return a URL for the image source, + * or null if it could not be determined. + */ + public URL getImageURL() { + String src = (String)getElement().getAttributes(). + getAttribute(HTML.Attribute.SRC); + if (src == null) { + return null; + } + + URL reference = ((HTMLDocument)getDocument()).getBase(); + try { + URL u = new URL(reference,src); + return u; + } catch (MalformedURLException e) { + return null; + } + } + + /** + * Returns the icon to use if the image couldn't be found. + */ + public Icon getNoImageIcon() { + return (Icon) UIManager.getLookAndFeelDefaults().get(MISSING_IMAGE); + } + + /** + * Returns the icon to use while in the process of loading the image. + */ + public Icon getLoadingImageIcon() { + return (Icon) UIManager.getLookAndFeelDefaults().get(PENDING_IMAGE); + } + + /** + * Returns the image to render. + */ + public Image getImage() { + sync(); + return image; + } + + /** + * Sets how the image is loaded. If newValue is true, + * the image we be loaded when first asked for, otherwise it will + * be loaded asynchronously. The default is to not load synchronously, + * that is to load the image asynchronously. + */ + public void setLoadsSynchronously(boolean newValue) { + synchronized(this) { + if (newValue) { + state |= SYNC_LOAD_FLAG; + } + else { + state = (state | SYNC_LOAD_FLAG) ^ SYNC_LOAD_FLAG; + } + } + } + + /** + * Returns true if the image should be loaded when first asked for. + */ + public boolean getLoadsSynchronously() { + return ((state & SYNC_LOAD_FLAG) != 0); + } + + /** + * Convenience method to get the StyleSheet. + */ + protected StyleSheet getStyleSheet() { + HTMLDocument doc = (HTMLDocument) getDocument(); + return doc.getStyleSheet(); + } + + /** + * Fetches the attributes to use when rendering. This is + * implemented to multiplex the attributes specified in the + * model with a StyleSheet. + */ + public AttributeSet getAttributes() { + sync(); + return attr; + } + + /** + * For images the tooltip text comes from text specified with the + * ALT attribute. This is overriden to return + * getAltText. + * + * @see JTextComponent#getToolTipText + */ + public String getToolTipText(float x, float y, Shape allocation) { + return getAltText(); + } + + /** + * Update any cached values that come from attributes. + */ + protected void setPropertiesFromAttributes() { + StyleSheet sheet = getStyleSheet(); + this.attr = sheet.getViewAttributes(this); + + // Gutters + borderSize = (short)getIntAttr(HTML.Attribute.BORDER, isLink() ? + DEFAULT_BORDER : 0); + + leftInset = rightInset = (short)(getIntAttr(HTML.Attribute.HSPACE, + 0) + borderSize); + topInset = bottomInset = (short)(getIntAttr(HTML.Attribute.VSPACE, + 0) + borderSize); + + borderColor = ((StyledDocument)getDocument()).getForeground + (getAttributes()); + + AttributeSet attr = getElement().getAttributes(); + + // Alignment. + // PENDING: This needs to be changed to support the CSS versions + // when conversion from ALIGN to VERTICAL_ALIGN is complete. + Object alignment = attr.getAttribute(HTML.Attribute.ALIGN); + + vAlign = 1.0f; + if (alignment != null) { + alignment = alignment.toString(); + if ("top".equals(alignment)) { + vAlign = 0f; + } + else if ("middle".equals(alignment)) { + vAlign = .5f; + } + } + + AttributeSet anchorAttr = (AttributeSet)attr.getAttribute(HTML.Tag.A); + if (anchorAttr != null && anchorAttr.isDefined + (HTML.Attribute.HREF)) { + synchronized(this) { + state |= LINK_FLAG; + } + } + else { + synchronized(this) { + state = (state | LINK_FLAG) ^ LINK_FLAG; + } + } + } + + /** + * Establishes the parent view for this view. + * Seize this moment to cache the AWT Container I'm in. + */ + public void setParent(View parent) { + View oldParent = getParent(); + super.setParent(parent); + container = (parent != null) ? getContainer() : null; + if (oldParent != parent) { + synchronized(this) { + state |= RELOAD_FLAG; + } + } + } + + /** + * Invoked when the Elements attributes have changed. Recreates the image. + */ + public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { + super.changedUpdate(e,a,f); + + synchronized(this) { + state |= RELOAD_FLAG | RELOAD_IMAGE_FLAG; + } + + // Assume the worst. + preferenceChanged(null, true, true); + } + + /** + * Paints the View. + * + * @param g the rendering surface to use + * @param a the allocated region to render into + * @see View#paint + */ + public void paint(Graphics g, Shape a) { + sync(); + + Rectangle rect = (a instanceof Rectangle) ? (Rectangle)a : + a.getBounds(); + + Image image = getImage(); + Rectangle clip = g.getClipBounds(); + + fBounds.setBounds(rect); + paintHighlights(g, a); + paintBorder(g, rect); + if (clip != null) { + g.clipRect(rect.x + leftInset, rect.y + topInset, + rect.width - leftInset - rightInset, + rect.height - topInset - bottomInset); + } + if (image != null) { + if (!hasPixels(image)) { + // No pixels yet, use the default + Icon icon = (image == null) ? getNoImageIcon() : + getLoadingImageIcon(); + + if (icon != null) { + icon.paintIcon(getContainer(), g, rect.x + leftInset, + rect.y + topInset); + } + } + else { + // Draw the image + g.drawImage(image, rect.x + leftInset, rect.y + topInset, + width, height, imageObserver); + } + } + else { + Icon icon = getNoImageIcon(); + + if (icon != null) { + icon.paintIcon(getContainer(), g, rect.x + leftInset, + rect.y + topInset); + } + View view = getAltView(); + // Paint the view representing the alt text, if its non-null + if (view != null && ((state & WIDTH_FLAG) == 0 || + width > DEFAULT_WIDTH)) { + // Assume layout along the y direction + Rectangle altRect = new Rectangle + (rect.x + leftInset + DEFAULT_WIDTH, rect.y + topInset, + rect.width - leftInset - rightInset - DEFAULT_WIDTH, + rect.height - topInset - bottomInset); + + view.paint(g, altRect); + } + } + if (clip != null) { + // Reset clip. + g.setClip(clip.x, clip.y, clip.width, clip.height); + } + } + + private void paintHighlights(Graphics g, Shape shape) { + if (container instanceof JTextComponent) { + JTextComponent tc = (JTextComponent)container; + Highlighter h = tc.getHighlighter(); + if (h instanceof LayeredHighlighter) { + ((LayeredHighlighter)h).paintLayeredHighlights + (g, getStartOffset(), getEndOffset(), shape, tc, this); + } + } + } + + private void paintBorder(Graphics g, Rectangle rect) { + Color color = borderColor; + + if ((borderSize > 0 || image == null) && color != null) { + int xOffset = leftInset - borderSize; + int yOffset = topInset - borderSize; + g.setColor(color); + int n = (image == null) ? 1 : borderSize; + for (int counter = 0; counter < n; counter++) { + g.drawRect(rect.x + xOffset + counter, + rect.y + yOffset + counter, + rect.width - counter - counter - xOffset -xOffset-1, + rect.height - counter - counter -yOffset-yOffset-1); + } + } + } + + /** + * Determines the preferred span for this view along an + * axis. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the span the view would like to be rendered into; + * typically the view is told to render into the span + * that is returned, although there is no guarantee; + * the parent may choose to resize or break the view + */ + public float getPreferredSpan(int axis) { + sync(); + + // If the attributes specified a width/height, always use it! + if (axis == View.X_AXIS && (state & WIDTH_FLAG) == WIDTH_FLAG) { + getPreferredSpanFromAltView(axis); + return width + leftInset + rightInset; + } + if (axis == View.Y_AXIS && (state & HEIGHT_FLAG) == HEIGHT_FLAG) { + getPreferredSpanFromAltView(axis); + return height + topInset + bottomInset; + } + + Image image = getImage(); + + if (image != null) { + switch (axis) { + case View.X_AXIS: + return width + leftInset + rightInset; + case View.Y_AXIS: + return height + topInset + bottomInset; + default: + throw new IllegalArgumentException("Invalid axis: " + axis); + } + } + else { + View view = getAltView(); + float retValue = 0f; + + if (view != null) { + retValue = view.getPreferredSpan(axis); + } + switch (axis) { + case View.X_AXIS: + return retValue + (float)(width + leftInset + rightInset); + case View.Y_AXIS: + return retValue + (float)(height + topInset + bottomInset); + default: + throw new IllegalArgumentException("Invalid axis: " + axis); + } + } + } + + /** + * Determines the desired alignment for this view along an + * axis. This is implemented to give the alignment to the + * bottom of the icon along the y axis, and the default + * along the x axis. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the desired alignment; this should be a value + * between 0.0 and 1.0 where 0 indicates alignment at the + * origin and 1.0 indicates alignment to the full span + * away from the origin; an alignment of 0.5 would be the + * center of the view + */ + public float getAlignment(int axis) { + switch (axis) { + case View.Y_AXIS: + return vAlign; + default: + return super.getAlignment(axis); + } + } + + /** + * Provides a mapping from the document model coordinate space + * to the coordinate space of the view mapped to it. + * + * @param pos the position to convert + * @param a the allocated region to render into + * @return the bounding box of the given position + * @exception BadLocationException if the given position does not represent a + * valid location in the associated document + * @see View#modelToView + */ + public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { + int p0 = getStartOffset(); + int p1 = getEndOffset(); + if ((pos >= p0) && (pos <= p1)) { + Rectangle r = a.getBounds(); + if (pos == p1) { + r.x += r.width; + } + r.width = 0; + return r; + } + return null; + } + + /** + * Provides a mapping from the view coordinate space to the logical + * coordinate space of the model. + * + * @param x the X coordinate + * @param y the Y coordinate + * @param a the allocated region to render into + * @return the location within the model that best represents the + * given point of view + * @see View#viewToModel + */ + public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) { + Rectangle alloc = (Rectangle) a; + if (x < alloc.x + alloc.width) { + bias[0] = Position.Bias.Forward; + return getStartOffset(); + } + bias[0] = Position.Bias.Backward; + return getEndOffset(); + } + + /** + * Sets the size of the view. This should cause + * layout of the view if it has any layout duties. + * + * @param width the width >= 0 + * @param height the height >= 0 + */ + public void setSize(float width, float height) { + sync(); + + if (getImage() == null) { + View view = getAltView(); + + if (view != null) { + view.setSize(Math.max(0f, width - (float)(DEFAULT_WIDTH + leftInset + rightInset)), + Math.max(0f, height - (float)(topInset + bottomInset))); + } + } + } + + /** + * Returns true if this image within a link? + */ + private boolean isLink() { + return ((state & LINK_FLAG) == LINK_FLAG); + } + + /** + * Returns true if the passed in image has a non-zero width and height. + */ + private boolean hasPixels(Image image) { + return image != null && + (image.getHeight(imageObserver) > 0) && + (image.getWidth(imageObserver) > 0); + } + + /** + * Returns the preferred span of the View used to display the alt text, + * or 0 if the view does not exist. + */ + private float getPreferredSpanFromAltView(int axis) { + if (getImage() == null) { + View view = getAltView(); + + if (view != null) { + return view.getPreferredSpan(axis); + } + } + return 0f; + } + + /** + * Request that this view be repainted. + * Assumes the view is still at its last-drawn location. + */ + private void repaint(long delay) { + if (container != null && fBounds != null) { + container.repaint(delay, fBounds.x, fBounds.y, fBounds.width, + fBounds.height); + } + } + + /** + * Convenience method for getting an integer attribute from the elements + * AttributeSet. + */ + private int getIntAttr(HTML.Attribute name, int deflt) { + AttributeSet attr = getElement().getAttributes(); + if (attr.isDefined(name)) { // does not check parents! + int i; + String val = (String)attr.getAttribute(name); + if (val == null) { + i = deflt; + } + else { + try{ + i = Math.max(0, Integer.parseInt(val)); + }catch( NumberFormatException x ) { + i = deflt; + } + } + return i; + } else + return deflt; + } + + /** + * Makes sure the necessary properties and image is loaded. + */ + private void sync() { + int s = state; + if ((s & RELOAD_IMAGE_FLAG) != 0) { + refreshImage(); + } + s = state; + if ((s & RELOAD_FLAG) != 0) { + synchronized(this) { + state = (state | RELOAD_FLAG) ^ RELOAD_FLAG; + } + setPropertiesFromAttributes(); + } + } + + /** + * Loads the image and updates the size accordingly. This should be + * invoked instead of invoking loadImage or + * updateImageSize directly. + */ + private void refreshImage() { + synchronized(this) { + // clear out width/height/realoadimage flag and set loading flag + state = (state | LOADING_FLAG | RELOAD_IMAGE_FLAG | WIDTH_FLAG | + HEIGHT_FLAG) ^ (WIDTH_FLAG | HEIGHT_FLAG | + RELOAD_IMAGE_FLAG); + image = null; + width = height = 0; + } + + try { + // Load the image + loadImage(); + + // And update the size params + updateImageSize(); + } + finally { + synchronized(this) { + // Clear out state in case someone threw an exception. + state = (state | LOADING_FLAG) ^ LOADING_FLAG; + } + } + } + + /** + * Loads the image from the URL getImageURL. This should + * only be invoked from refreshImage. + */ + private void loadImage() { + URL src = getImageURL(); + Image newImage = null; + if (src != null) { + Dictionary cache = (Dictionary)getDocument(). + getProperty(IMAGE_CACHE_PROPERTY); + if (cache != null) { + newImage = (Image)cache.get(src); + } + else { + newImage = Toolkit.getDefaultToolkit().createImage(src); + if (newImage != null && getLoadsSynchronously()) { + // Force the image to be loaded by using an ImageIcon. + ImageIcon ii = new ImageIcon(); + ii.setImage(newImage); + } + } + } + image = newImage; + } + + /** + * Recreates and reloads the image. This should + * only be invoked from refreshImage. + */ + private void updateImageSize() { + int newWidth = 0; + int newHeight = 0; + int newState = 0; + Image newImage = getImage(); + + if (newImage != null) { + Element elem = getElement(); + AttributeSet attr = elem.getAttributes(); + + // Get the width/height and set the state ivar before calling + // anything that might cause the image to be loaded, and thus the + // ImageHandler to be called. + newWidth = getIntAttr(HTML.Attribute.WIDTH, -1); + if (newWidth > 0) { + newState |= WIDTH_FLAG; + } + newHeight = getIntAttr(HTML.Attribute.HEIGHT, -1); + if (newHeight > 0) { + newState |= HEIGHT_FLAG; + } + + if (newWidth <= 0) { + newWidth = newImage.getWidth(imageObserver); + if (newWidth <= 0) { + newWidth = DEFAULT_WIDTH; + } + } + + if (newHeight <= 0) { + newHeight = newImage.getHeight(imageObserver); + if (newHeight <= 0) { + newHeight = DEFAULT_HEIGHT; + } + } + + // Make sure the image starts loading: + if ((newState & (WIDTH_FLAG | HEIGHT_FLAG)) != 0) { + Toolkit.getDefaultToolkit().prepareImage(newImage, newWidth, + newHeight, + imageObserver); + } + else { + Toolkit.getDefaultToolkit().prepareImage(newImage, -1, -1, + imageObserver); + } + + boolean createText = false; + synchronized(this) { + // If imageloading failed, other thread may have called + // ImageLoader which will null out image, hence we check + // for it. + if (image != null) { + if ((newState & WIDTH_FLAG) == WIDTH_FLAG || width == 0) { + width = newWidth; + } + if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG || + height == 0) { + height = newHeight; + } + } + else { + createText = true; + if ((newState & WIDTH_FLAG) == WIDTH_FLAG) { + width = newWidth; + } + if ((newState & HEIGHT_FLAG) == HEIGHT_FLAG) { + height = newHeight; + } + } + state = state | newState; + state = (state | LOADING_FLAG) ^ LOADING_FLAG; + } + if (createText) { + // Only reset if this thread determined image is null + updateAltTextView(); + } + } + else { + width = height = DEFAULT_HEIGHT; + updateAltTextView(); + } + } + + /** + * Updates the view representing the alt text. + */ + private void updateAltTextView() { + String text = getAltText(); + + if (text != null) { + ImageLabelView newView; + + newView = new ImageLabelView(getElement(), text); + synchronized(this) { + altView = newView; + } + } + } + + /** + * Returns the view to use for alternate text. This may be null. + */ + private View getAltView() { + View view; + + synchronized(this) { + view = altView; + } + if (view != null && view.getParent() == null) { + view.setParent(getParent()); + } + return view; + } + + /** + * Invokes preferenceChanged on the event displatching + * thread. + */ + private void safePreferenceChanged() { + if (SwingUtilities.isEventDispatchThread()) { + Document doc = getDocument(); + if (doc instanceof AbstractDocument) { + ((AbstractDocument)doc).readLock(); + } + preferenceChanged(null, true, true); + if (doc instanceof AbstractDocument) { + ((AbstractDocument)doc).readUnlock(); + } + } + else { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + safePreferenceChanged(); + } + }); + } + } + + /** + * ImageHandler implements the ImageObserver to correctly update the + * display as new parts of the image become available. + */ + private class ImageHandler implements ImageObserver { + // This can come on any thread. If we are in the process of reloading + // the image and determining our state (loading == true) we don't fire + // preference changed, or repaint, we just reset the fWidth/fHeight as + // necessary and return. This is ok as we know when loading finishes + // it will pick up the new height/width, if necessary. + public boolean imageUpdate(Image img, int flags, int x, int y, + int newWidth, int newHeight ) { + if (image == null || image != img || getParent() == null) { + return false; + } + + // Bail out if there was an error: + if ((flags & (ABORT|ERROR)) != 0) { + repaint(0); + synchronized(ImageView.this) { + if (image == img) { + // Be sure image hasn't changed since we don't + // initialy synchronize + image = null; + if ((state & WIDTH_FLAG) != WIDTH_FLAG) { + width = DEFAULT_WIDTH; + } + if ((state & HEIGHT_FLAG) != HEIGHT_FLAG) { + height = DEFAULT_HEIGHT; + } + } + if ((state & LOADING_FLAG) == LOADING_FLAG) { + // No need to resize or repaint, still in the process + // of loading. + return false; + } + } + updateAltTextView(); + safePreferenceChanged(); + return false; + } + + // Resize image if necessary: + short changed = 0; + if ((flags & ImageObserver.HEIGHT) != 0 && !getElement(). + getAttributes().isDefined(HTML.Attribute.HEIGHT)) { + changed |= 1; + } + if ((flags & ImageObserver.WIDTH) != 0 && !getElement(). + getAttributes().isDefined(HTML.Attribute.WIDTH)) { + changed |= 2; + } + + synchronized(ImageView.this) { + if (image != img) { + return false; + } + if ((changed & 1) == 1 && (state & WIDTH_FLAG) == 0) { + width = newWidth; + } + if ((changed & 2) == 2 && (state & HEIGHT_FLAG) == 0) { + height = newHeight; + } + if ((state & LOADING_FLAG) == LOADING_FLAG) { + // No need to resize or repaint, still in the process of + // loading. + return true; + } + } + if (changed != 0) { + // May need to resize myself, asynchronously: + safePreferenceChanged(); + return true; + } + + // Repaint when done or when new pixels arrive: + if ((flags & (FRAMEBITS|ALLBITS)) != 0) { + repaint(0); + } + else if ((flags & SOMEBITS) != 0 && sIsInc) { + repaint(sIncRate); + } + return ((flags & ALLBITS) == 0); + } + } + + + /** + * ImageLabelView is used if the image can't be loaded, and + * the attribute specified an alt attribute. It overriden a handle of + * methods as the text is hardcoded and does not come from the document. + */ + private class ImageLabelView extends InlineView { + private Segment segment; + private Color fg; + + ImageLabelView(Element e, String text) { + super(e); + reset(text); + } + + public void reset(String text) { + segment = new Segment(text.toCharArray(), 0, text.length()); + } + + public void paint(Graphics g, Shape a) { + // Don't use supers paint, otherwise selection will be wrong + // as our start/end offsets are fake. + GlyphPainter painter = getGlyphPainter(); + + if (painter != null) { + g.setColor(getForeground()); + painter.paint(this, g, a, getStartOffset(), getEndOffset()); + } + } + + public Segment getText(int p0, int p1) { + if (p0 < 0 || p1 > segment.array.length) { + throw new RuntimeException("ImageLabelView: Stale view"); + } + segment.offset = p0; + segment.count = p1 - p0; + return segment; + } + + public int getStartOffset() { + return 0; + } + + public int getEndOffset() { + return segment.array.length; + } + + public View breakView(int axis, int p0, float pos, float len) { + // Don't allow a break + return this; + } + + public Color getForeground() { + View parent; + if (fg == null && (parent = getParent()) != null) { + Document doc = getDocument(); + AttributeSet attr = parent.getAttributes(); + + if (attr != null && (doc instanceof StyledDocument)) { + fg = ((StyledDocument)doc).getForeground(attr); + } + } + return fg; + } + } +} diff --git a/src/share/classes/javax/swing/text/html/InlineView.java b/src/share/classes/javax/swing/text/html/InlineView.java new file mode 100644 index 000000000..4a9f0be9b --- /dev/null +++ b/src/share/classes/javax/swing/text/html/InlineView.java @@ -0,0 +1,225 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import java.text.BreakIterator; +import javax.swing.event.DocumentEvent; +import javax.swing.text.*; + +/** + * Displays the inline element styles + * based upon css attributes. + * + * @author Timothy Prinzing + */ +public class InlineView extends LabelView { + + /** + * Constructs a new view wrapped on an element. + * + * @param elem the element + */ + public InlineView(Element elem) { + super(elem); + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + } + + /** + * Gives notification that something was inserted into + * the document in a location that this view is responsible for. + * If either parameter is null, behavior of this method is + * implementation dependent. + * + * @param e the change information from the associated document + * @param a the current allocation of the view + * @param f the factory to use to rebuild if the view has children + * @since 1.5 + * @see View#insertUpdate + */ + public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { + super.insertUpdate(e, a, f); + } + + /** + * Gives notification that something was removed from the document + * in a location that this view is responsible for. + * If either parameter is null, behavior of this method is + * implementation dependent. + * + * @param e the change information from the associated document + * @param a the current allocation of the view + * @param f the factory to use to rebuild if the view has children + * @since 1.5 + * @see View#removeUpdate + */ + public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { + super.removeUpdate(e, a, f); + } + + /** + * Gives notification from the document that attributes were changed + * in a location that this view is responsible for. + * + * @param e the change information from the associated document + * @param a the current allocation of the view + * @param f the factory to use to rebuild if the view has children + * @see View#changedUpdate + */ + public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { + super.changedUpdate(e, a, f); + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + preferenceChanged(null, true, true); + } + + /** + * Fetches the attributes to use when rendering. This is + * implemented to multiplex the attributes specified in the + * model with a StyleSheet. + */ + public AttributeSet getAttributes() { + return attr; + } + + /** + * Determines how attractive a break opportunity in + * this view is. This can be used for determining which + * view is the most attractive to call breakView + * on in the process of formatting. A view that represents + * text that has whitespace in it might be more attractive + * than a view that has no whitespace, for example. The + * higher the weight, the more attractive the break. A + * value equal to or lower than BadBreakWeight + * should not be considered for a break. A value greater + * than or equal to ForcedBreakWeight should + * be broken. + *

    + * This is implemented to provide the default behavior + * of returning BadBreakWeight unless the length + * is greater than the length of the view in which case the + * entire view represents the fragment. Unless a view has + * been written to support breaking behavior, it is not + * attractive to try and break the view. An example of + * a view that does support breaking is LabelView. + * An example of a view that uses break weight is + * ParagraphView. + * + * @param axis may be either View.X_AXIS or View.Y_AXIS + * @param pos the potential location of the start of the + * broken view >= 0. This may be useful for calculating tab + * positions. + * @param len specifies the relative length from pos + * where a potential break is desired >= 0. + * @return the weight, which should be a value between + * ForcedBreakWeight and BadBreakWeight. + * @see LabelView + * @see ParagraphView + * @see javax.swing.text.View#BadBreakWeight + * @see javax.swing.text.View#GoodBreakWeight + * @see javax.swing.text.View#ExcellentBreakWeight + * @see javax.swing.text.View#ForcedBreakWeight + */ + public int getBreakWeight(int axis, float pos, float len) { + if (nowrap) { + return BadBreakWeight; + } + return super.getBreakWeight(axis, pos, len); + } + + /** + * Tries to break this view on the given axis. Refer to + * {@link javax.swing.text.View#breakView} for a complete + * description of this method. + *

    Behavior of this method is unspecified in case axis + * is neither View.X_AXIS nor View.Y_AXIS, and + * in case offset, pos, or len + * is null. + * + * @param axis may be either View.X_AXIS or + * View.Y_AXIS + * @param offset the location in the document model + * that a broken fragment would occupy >= 0. This + * would be the starting offset of the fragment + * returned + * @param pos the position along the axis that the + * broken view would occupy >= 0. This may be useful for + * things like tab calculations + * @param len specifies the distance along the axis + * where a potential break is desired >= 0 + * @return the fragment of the view that represents the + * given span. + * @since 1.5 + * @see javax.swing.text.View#breakView + */ + public View breakView(int axis, int offset, float pos, float len) { + return super.breakView(axis, offset, pos, len); + } + + + /** + * Set the cached properties from the attributes. + */ + protected void setPropertiesFromAttributes() { + super.setPropertiesFromAttributes(); + AttributeSet a = getAttributes(); + Object decor = a.getAttribute(CSS.Attribute.TEXT_DECORATION); + boolean u = (decor != null) ? + (decor.toString().indexOf("underline") >= 0) : false; + setUnderline(u); + boolean s = (decor != null) ? + (decor.toString().indexOf("line-through") >= 0) : false; + setStrikeThrough(s); + Object vAlign = a.getAttribute(CSS.Attribute.VERTICAL_ALIGN); + s = (vAlign != null) ? (vAlign.toString().indexOf("sup") >= 0) : false; + setSuperscript(s); + s = (vAlign != null) ? (vAlign.toString().indexOf("sub") >= 0) : false; + setSubscript(s); + + Object whitespace = a.getAttribute(CSS.Attribute.WHITE_SPACE); + if ((whitespace != null) && whitespace.equals("nowrap")) { + nowrap = true; + } else { + nowrap = false; + } + + HTMLDocument doc = (HTMLDocument)getDocument(); + // fetches background color from stylesheet if specified + Color bg = doc.getBackground(a); + if (bg != null) { + setBackground(bg); + } + } + + + protected StyleSheet getStyleSheet() { + HTMLDocument doc = (HTMLDocument) getDocument(); + return doc.getStyleSheet(); + } + + private boolean nowrap; + private AttributeSet attr; +} diff --git a/src/share/classes/javax/swing/text/html/IsindexView.java b/src/share/classes/javax/swing/text/html/IsindexView.java new file mode 100644 index 000000000..b75a8312c --- /dev/null +++ b/src/share/classes/javax/swing/text/html/IsindexView.java @@ -0,0 +1,114 @@ +/* + * Copyright 1998-2000 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import java.awt.event.*; +import java.net.URLEncoder; +import java.net.MalformedURLException; +import java.io.IOException; +import java.net.URL; +import javax.swing.text.*; +import javax.swing.*; + + +/** + * A view that supports the <ISINDEX< tag. This is implemented + * as a JPanel that contains + * + * @author Sunita Mani + */ + +class IsindexView extends ComponentView implements ActionListener { + + JTextField textField; + + /** + * Creates an IsindexView + */ + public IsindexView(Element elem) { + super(elem); + } + + /** + * Creates the components necessary to to implement + * this view. The component returned is a JPanel, + * that contains the PROMPT to the left and JTextField + * to the right. + */ + public Component createComponent() { + AttributeSet attr = getElement().getAttributes(); + + JPanel panel = new JPanel(new BorderLayout()); + panel.setBackground(null); + + String prompt = (String)attr.getAttribute(HTML.Attribute.PROMPT); + if (prompt == null) { + prompt = UIManager.getString("IsindexView.prompt"); + } + JLabel label = new JLabel(prompt); + + textField = new JTextField(); + textField.addActionListener(this); + panel.add(label, BorderLayout.WEST); + panel.add(textField, BorderLayout.CENTER); + panel.setAlignmentY(1.0f); + panel.setOpaque(false); + return panel; + } + + /** + * Responsible for processing the ActionEvent. + * In this case this is hitting enter/return + * in the text field. This will construct the + * URL from the base URL of the document. + * To the URL is appended a '?' followed by the + * contents of the JTextField. The search + * contents are URLEncoded. + */ + public void actionPerformed(ActionEvent evt) { + + String data = textField.getText(); + if (data != null) { + data = URLEncoder.encode(data); + } + + + AttributeSet attr = getElement().getAttributes(); + HTMLDocument hdoc = (HTMLDocument)getElement().getDocument(); + + String action = (String) attr.getAttribute(HTML.Attribute.ACTION); + if (action == null) { + action = hdoc.getBase().toString(); + } + try { + URL url = new URL(action+"?"+data); + JEditorPane pane = (JEditorPane)getContainer(); + pane.setPage(url); + } catch (MalformedURLException e1) { + } catch (IOException e2) { + } + } +} diff --git a/src/share/classes/javax/swing/text/html/LineView.java b/src/share/classes/javax/swing/text/html/LineView.java new file mode 100644 index 000000000..b982cd342 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/LineView.java @@ -0,0 +1,186 @@ +/* + * Copyright 1997-2003 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.util.Enumeration; +import java.awt.*; +import javax.swing.*; +import javax.swing.border.*; +import javax.swing.event.*; +import javax.swing.text.*; + +/** + * A view implementation to display an unwrapped + * preformatted line.

    + * This subclasses ParagraphView, but this really only contains one + * Row of text. + * + * @author Timothy Prinzing + */ +class LineView extends ParagraphView { + /** Last place painted at. */ + int tabBase; + + /** + * Creates a LineView object. + * + * @param elem the element to wrap in a view + */ + public LineView(Element elem) { + super(elem); + } + + /** + * Preformatted lines are not suppressed if they + * have only whitespace, so they are always visible. + */ + public boolean isVisible() { + return true; + } + + /** + * Determines the minimum span for this view along an + * axis. The preformatted line should refuse to be + * sized less than the preferred size. + * + * @param axis may be either View.X_AXIS or + * View.Y_AXIS + * @return the minimum span the view can be rendered into + * @see View#getPreferredSpan + */ + public float getMinimumSpan(int axis) { + return getPreferredSpan(axis); + } + + /** + * Gets the resize weight for the specified axis. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the weight + */ + public int getResizeWeight(int axis) { + switch (axis) { + case View.X_AXIS: + return 1; + case View.Y_AXIS: + return 0; + default: + throw new IllegalArgumentException("Invalid axis: " + axis); + } + } + + /** + * Gets the alignment for an axis. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the alignment + */ + public float getAlignment(int axis) { + if (axis == View.X_AXIS) { + return 0; + } + return super.getAlignment(axis); + } + + /** + * Lays out the children. If the layout span has changed, + * the rows are rebuilt. The superclass functionality + * is called after checking and possibly rebuilding the + * rows. If the height has changed, the + * preferenceChanged method is called + * on the parent since the vertical preference is + * rigid. + * + * @param width the width to lay out against >= 0. This is + * the width inside of the inset area. + * @param height the height to lay out against >= 0 (not used + * by paragraph, but used by the superclass). This + * is the height inside of the inset area. + */ + protected void layout(int width, int height) { + super.layout(Integer.MAX_VALUE - 1, height); + } + + /** + * Returns the next tab stop position given a reference position. + * This view implements the tab coordinate system, and calls + * getTabbedSpan on the logical children in the process + * of layout to determine the desired span of the children. The + * logical children can delegate their tab expansion upward to + * the paragraph which knows how to expand tabs. + * LabelView is an example of a view that delegates + * its tab expansion needs upward to the paragraph. + *

    + * This is implemented to try and locate a TabSet + * in the paragraph element's attribute set. If one can be + * found, its settings will be used, otherwise a default expansion + * will be provided. The base location for for tab expansion + * is the left inset from the paragraphs most recent allocation + * (which is what the layout of the children is based upon). + * + * @param x the X reference position + * @param tabOffset the position within the text stream + * that the tab occurred at >= 0. + * @return the trailing end of the tab expansion >= 0 + * @see TabSet + * @see TabStop + * @see LabelView + */ + public float nextTabStop(float x, int tabOffset) { + // If the text isn't left justified, offset by 10 pixels! + if (getTabSet() == null && + StyleConstants.getAlignment(getAttributes()) == + StyleConstants.ALIGN_LEFT) { + return getPreTab(x, tabOffset); + } + return super.nextTabStop(x, tabOffset); + } + + /** + * Returns the location for the tab. + */ + protected float getPreTab(float x, int tabOffset) { + Document d = getDocument(); + View v = getViewAtPosition(tabOffset, null); + if ((d instanceof StyledDocument) && v != null) { + // Assume f is fixed point. + Font f = ((StyledDocument)d).getFont(v.getAttributes()); + Container c = getContainer(); + FontMetrics fm = (c != null) ? c.getFontMetrics(f) : + Toolkit.getDefaultToolkit().getFontMetrics(f); + int width = getCharactersPerTab() * fm.charWidth('W'); + int tb = (int)getTabBase(); + return (float)((((int)x - tb) / width + 1) * width + tb); + } + return 10.0f + x; + } + + /** + * @return number of characters per tab, 8. + */ + protected int getCharactersPerTab() { + return 8; + } +} diff --git a/src/share/classes/javax/swing/text/html/ListView.java b/src/share/classes/javax/swing/text/html/ListView.java new file mode 100644 index 000000000..775f2935c --- /dev/null +++ b/src/share/classes/javax/swing/text/html/ListView.java @@ -0,0 +1,122 @@ +/* + * Copyright 1997-1999 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.util.Enumeration; +import java.awt.*; +import javax.swing.text.*; + +/** + * A view implementation to display an html list + * + * @author Timothy Prinzing + */ +public class ListView extends BlockView { + + /** + * Creates a new view that represents a list element. + * + * @param elem the element to create a view for + */ + public ListView(Element elem) { + super(elem, View.Y_AXIS); + } + + /** + * Calculates the desired shape of the list. + * + * @return the desired span + * @see View#getPreferredSpan + */ + public float getAlignment(int axis) { + switch (axis) { + case View.X_AXIS: + return 0.5f; + case View.Y_AXIS: + return 0.5f; + default: + throw new IllegalArgumentException("Invalid axis: " + axis); + } + } + + /** + * Renders using the given rendering surface and area on that + * surface. + * + * @param g the rendering surface to use + * @param allocation the allocated region to render into + * @see View#paint + */ + public void paint(Graphics g, Shape allocation) { + super.paint(g, allocation); + Rectangle alloc = allocation.getBounds(); + Rectangle clip = g.getClipBounds(); + // Since listPainter paints in the insets we have to check for the + // case where the child is not painted because the paint region is + // to the left of the child. This assumes the ListPainter paints in + // the left margin. + if ((clip.x + clip.width) < (alloc.x + getLeftInset())) { + Rectangle childRect = alloc; + alloc = getInsideAllocation(allocation); + int n = getViewCount(); + int endY = clip.y + clip.height; + for (int i = 0; i < n; i++) { + childRect.setBounds(alloc); + childAllocation(i, childRect); + if (childRect.y < endY) { + if ((childRect.y + childRect.height) >= clip.y) { + listPainter.paint(g, childRect.x, childRect.y, + childRect.width, childRect.height, + this, i); + } + } + else { + break; + } + } + } + } + + /** + * Paints one of the children; called by paint(). By default + * that is all it does, but a subclass can use this to paint + * things relative to the child. + * + * @param g the graphics context + * @param alloc the allocated region to render the child into + * @param index the index of the child + */ + protected void paintChild(Graphics g, Rectangle alloc, int index) { + listPainter.paint(g, alloc.x, alloc.y, alloc.width, alloc.height, this, index); + super.paintChild(g, alloc, index); + } + + protected void setPropertiesFromAttributes() { + super.setPropertiesFromAttributes(); + listPainter = getStyleSheet().getListPainter(getAttributes()); + } + + private StyleSheet.ListPainter listPainter; +} diff --git a/src/share/classes/javax/swing/text/html/Map.java b/src/share/classes/javax/swing/text/html/Map.java new file mode 100644 index 000000000..0180df14f --- /dev/null +++ b/src/share/classes/javax/swing/text/html/Map.java @@ -0,0 +1,505 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.Polygon; +import java.io.Serializable; +import java.util.StringTokenizer; +import java.util.Vector; +import javax.swing.text.AttributeSet; + +/** + * Map is used to represent a map element that is part of an HTML document. + * Once a Map has been created, and any number of areas have been added, + * you can test if a point falls inside the map via the contains method. + * + * @author Scott Violet + */ +class Map implements Serializable { + /** Name of the Map. */ + private String name; + /** An array of AttributeSets. */ + private Vector areaAttributes; + /** An array of RegionContainments, will slowly grow to match the + * length of areaAttributes as needed. */ + private Vector areas; + + public Map() { + } + + public Map(String name) { + this.name = name; + } + + /** + * Returns the name of the Map. + */ + public String getName() { + return name; + } + + /** + * Defines a region of the Map, based on the passed in AttributeSet. + */ + public void addArea(AttributeSet as) { + if (as == null) { + return; + } + if (areaAttributes == null) { + areaAttributes = new Vector(2); + } + areaAttributes.addElement(as.copyAttributes()); + } + + /** + * Removes the previously created area. + */ + public void removeArea(AttributeSet as) { + if (as != null && areaAttributes != null) { + int numAreas = (areas != null) ? areas.size() : 0; + for (int counter = areaAttributes.size() - 1; counter >= 0; + counter--) { + if (((AttributeSet)areaAttributes.elementAt(counter)). + isEqual(as)){ + areaAttributes.removeElementAt(counter); + if (counter < numAreas) { + areas.removeElementAt(counter); + } + } + } + } + } + + /** + * Returns the AttributeSets representing the differet areas of the Map. + */ + public AttributeSet[] getAreas() { + int numAttributes = (areaAttributes != null) ? areaAttributes.size() : + 0; + if (numAttributes != 0) { + AttributeSet[] retValue = new AttributeSet[numAttributes]; + + areaAttributes.copyInto(retValue); + return retValue; + } + return null; + } + + /** + * Returns the AttributeSet that contains the passed in location, + * x, y. width, height + * gives the size of the region the map is defined over. If a matching + * area is found, the AttribueSet for it is returned. + */ + public AttributeSet getArea(int x, int y, int width, int height) { + int numAttributes = (areaAttributes != null) ? + areaAttributes.size() : 0; + + if (numAttributes > 0) { + int numAreas = (areas != null) ? areas.size() : 0; + + if (areas == null) { + areas = new Vector(numAttributes); + } + for (int counter = 0; counter < numAttributes; counter++) { + if (counter >= numAreas) { + areas.addElement(createRegionContainment + ((AttributeSet)areaAttributes.elementAt(counter))); + } + RegionContainment rc = (RegionContainment)areas. + elementAt(counter); + if (rc != null && rc.contains(x, y, width, height)) { + return (AttributeSet)areaAttributes.elementAt(counter); + } + } + } + return null; + } + + /** + * Creates and returns an instance of RegionContainment that can be + * used to test if a particular point lies inside a region. + */ + protected RegionContainment createRegionContainment + (AttributeSet attributes) { + Object shape = attributes.getAttribute(HTML.Attribute.SHAPE); + + if (shape == null) { + shape = "rect"; + } + if (shape instanceof String) { + String shapeString = ((String)shape).toLowerCase(); + RegionContainment rc = null; + + try { + if (shapeString.equals("rect")) { + rc = new RectangleRegionContainment(attributes); + } + else if (shapeString.equals("circle")) { + rc = new CircleRegionContainment(attributes); + } + else if (shapeString.equals("poly")) { + rc = new PolygonRegionContainment(attributes); + } + else if (shapeString.equals("default")) { + rc = DefaultRegionContainment.sharedInstance(); + } + } catch (RuntimeException re) { + // Something wrong with attributes. + rc = null; + } + return rc; + } + return null; + } + + /** + * Creates and returns an array of integers from the String + * stringCoords. If one of the values represents a + * % the returned value with be negative. If a parse error results + * from trying to parse one of the numbers null is returned. + */ + static protected int[] extractCoords(Object stringCoords) { + if (stringCoords == null || !(stringCoords instanceof String)) { + return null; + } + + StringTokenizer st = new StringTokenizer((String)stringCoords, + ", \t\n\r"); + int[] retValue = null; + int numCoords = 0; + + while(st.hasMoreElements()) { + String token = st.nextToken(); + int scale; + + if (token.endsWith("%")) { + scale = -1; + token = token.substring(0, token.length() - 1); + } + else { + scale = 1; + } + try { + int intValue = Integer.parseInt(token); + + if (retValue == null) { + retValue = new int[4]; + } + else if(numCoords == retValue.length) { + int[] temp = new int[retValue.length * 2]; + + System.arraycopy(retValue, 0, temp, 0, retValue.length); + retValue = temp; + } + retValue[numCoords++] = intValue * scale; + } catch (NumberFormatException nfe) { + return null; + } + } + if (numCoords > 0 && numCoords != retValue.length) { + int[] temp = new int[numCoords]; + + System.arraycopy(retValue, 0, temp, 0, numCoords); + retValue = temp; + } + return retValue; + } + + + /** + * Defines the interface used for to check if a point is inside a + * region. + */ + interface RegionContainment { + /** + * Returns true if the location x, y + * falls inside the region defined in the receiver. + * width, height is the size of + * the enclosing region. + */ + public boolean contains(int x, int y, int width, int height); + } + + + /** + * Used to test for containment in a rectangular region. + */ + static class RectangleRegionContainment implements RegionContainment { + /** Will be non-null if one of the values is a percent, and any value + * that is non null indicates it is a percent + * (order is x, y, width, height). */ + float[] percents; + /** Last value of width passed in. */ + int lastWidth; + /** Last value of height passed in. */ + int lastHeight; + /** Top left. */ + int x0; + int y0; + /** Bottom right. */ + int x1; + int y1; + + public RectangleRegionContainment(AttributeSet as) { + int[] coords = Map.extractCoords(as.getAttribute(HTML. + Attribute.COORDS)); + + percents = null; + if (coords == null || coords.length != 4) { + throw new RuntimeException("Unable to parse rectangular area"); + } + else { + x0 = coords[0]; + y0 = coords[1]; + x1 = coords[2]; + y1 = coords[3]; + if (x0 < 0 || y0 < 0 || x1 < 0 || y1 < 0) { + percents = new float[4]; + lastWidth = lastHeight = -1; + for (int counter = 0; counter < 4; counter++) { + if (coords[counter] < 0) { + percents[counter] = Math.abs + (coords[counter]) / 100.0f; + } + else { + percents[counter] = -1.0f; + } + } + } + } + } + + public boolean contains(int x, int y, int width, int height) { + if (percents == null) { + return contains(x, y); + } + if (lastWidth != width || lastHeight != height) { + lastWidth = width; + lastHeight = height; + if (percents[0] != -1.0f) { + x0 = (int)(percents[0] * width); + } + if (percents[1] != -1.0f) { + y0 = (int)(percents[1] * height); + } + if (percents[2] != -1.0f) { + x1 = (int)(percents[2] * width); + } + if (percents[3] != -1.0f) { + y1 = (int)(percents[3] * height); + } + } + return contains(x, y); + } + + public boolean contains(int x, int y) { + return ((x >= x0 && x <= x1) && + (y >= y0 && y <= y1)); + } + } + + + /** + * Used to test for containment in a polygon region. + */ + static class PolygonRegionContainment extends Polygon implements + RegionContainment { + /** If any value is a percent there will be an entry here for the + * percent value. Use percentIndex to find out the index for it. */ + float[] percentValues; + int[] percentIndexs; + /** Last value of width passed in. */ + int lastWidth; + /** Last value of height passed in. */ + int lastHeight; + + public PolygonRegionContainment(AttributeSet as) { + int[] coords = Map.extractCoords(as.getAttribute(HTML.Attribute. + COORDS)); + + if (coords == null || coords.length == 0 || + coords.length % 2 != 0) { + throw new RuntimeException("Unable to parse polygon area"); + } + else { + int numPercents = 0; + + lastWidth = lastHeight = -1; + for (int counter = coords.length - 1; counter >= 0; + counter--) { + if (coords[counter] < 0) { + numPercents++; + } + } + + if (numPercents > 0) { + percentIndexs = new int[numPercents]; + percentValues = new float[numPercents]; + for (int counter = coords.length - 1, pCounter = 0; + counter >= 0; counter--) { + if (coords[counter] < 0) { + percentValues[pCounter] = coords[counter] / + -100.0f; + percentIndexs[pCounter] = counter; + pCounter++; + } + } + } + else { + percentIndexs = null; + percentValues = null; + } + npoints = coords.length / 2; + xpoints = new int[npoints]; + ypoints = new int[npoints]; + + for (int counter = 0; counter < npoints; counter++) { + xpoints[counter] = coords[counter + counter]; + ypoints[counter] = coords[counter + counter + 1]; + } + } + } + + public boolean contains(int x, int y, int width, int height) { + if (percentValues == null || (lastWidth == width && + lastHeight == height)) { + return contains(x, y); + } + // Force the bounding box to be recalced. + bounds = null; + lastWidth = width; + lastHeight = height; + float fWidth = (float)width; + float fHeight = (float)height; + for (int counter = percentValues.length - 1; counter >= 0; + counter--) { + if (percentIndexs[counter] % 2 == 0) { + // x + xpoints[percentIndexs[counter] / 2] = + (int)(percentValues[counter] * fWidth); + } + else { + // y + ypoints[percentIndexs[counter] / 2] = + (int)(percentValues[counter] * fHeight); + } + } + return contains(x, y); + } + } + + + /** + * Used to test for containment in a circular region. + */ + static class CircleRegionContainment implements RegionContainment { + /** X origin of the circle. */ + int x; + /** Y origin of the circle. */ + int y; + /** Radius of the circle. */ + int radiusSquared; + /** Non-null indicates one of the values represents a percent. */ + float[] percentValues; + /** Last value of width passed in. */ + int lastWidth; + /** Last value of height passed in. */ + int lastHeight; + + public CircleRegionContainment(AttributeSet as) { + int[] coords = Map.extractCoords(as.getAttribute(HTML.Attribute. + COORDS)); + + if (coords == null || coords.length != 3) { + throw new RuntimeException("Unable to parse circular area"); + } + x = coords[0]; + y = coords[1]; + radiusSquared = coords[2] * coords[2]; + if (coords[0] < 0 || coords[1] < 0 || coords[2] < 0) { + lastWidth = lastHeight = -1; + percentValues = new float[3]; + for (int counter = 0; counter < 3; counter++) { + if (coords[counter] < 0) { + percentValues[counter] = coords[counter] / + -100.0f; + } + else { + percentValues[counter] = -1.0f; + } + } + } + else { + percentValues = null; + } + } + + public boolean contains(int x, int y, int width, int height) { + if (percentValues != null && (lastWidth != width || + lastHeight != height)) { + int newRad = Math.min(width, height) / 2; + + lastWidth = width; + lastHeight = height; + if (percentValues[0] != -1.0f) { + this.x = (int)(percentValues[0] * width); + } + if (percentValues[1] != -1.0f) { + this.y = (int)(percentValues[1] * height); + } + if (percentValues[2] != -1.0f) { + radiusSquared = (int)(percentValues[2] * + Math.min(width, height)); + radiusSquared *= radiusSquared; + } + } + return (((x - this.x) * (x - this.x) + + (y - this.y) * (y - this.y)) <= radiusSquared); + } + } + + + /** + * An implementation that will return true if the x, y location is + * inside a rectangle defined by origin 0, 0, and width equal to + * width passed in, and height equal to height passed in. + */ + static class DefaultRegionContainment implements RegionContainment { + /** A global shared instance. */ + static DefaultRegionContainment si = null; + + public static DefaultRegionContainment sharedInstance() { + if (si == null) { + si = new DefaultRegionContainment(); + } + return si; + } + + public boolean contains(int x, int y, int width, int height) { + return (x <= width && x >= 0 && y >= 0 && y <= width); + } + } +} diff --git a/src/share/classes/javax/swing/text/html/MinimalHTMLWriter.java b/src/share/classes/javax/swing/text/html/MinimalHTMLWriter.java new file mode 100644 index 000000000..d7637ae79 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/MinimalHTMLWriter.java @@ -0,0 +1,726 @@ +/* + * Copyright 1998-1999 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html; + +import java.io.Writer; +import java.io.IOException; +import java.util.*; +import java.awt.Color; +import javax.swing.text.*; + +/** + * MinimalHTMLWriter is a fallback writer used by the + * HTMLEditorKit to write out HTML for a document that + * is a not produced by the EditorKit. + * + * The format for the document is: + *

    + * <html>
    + *   <head>
    + *     <style>
    + *        <!-- list of named styles
    + *         p.normal {
    + *            font-family: SansSerif;
    + *            margin-height: 0;
    + *            font-size: 14
    + *         }
    + *        -->
    + *      </style>
    + *   </head>
    + *   <body>
    + *    <p style=normal>
    + *        Bold, italic, and underline attributes
    + *        of the run are emitted as HTML tags.
    + *        The remaining attributes are emitted as
    + *        part of the style attribute of a <span> tag.
    + *        The syntax is similar to inline styles.
    + *    </p>
    + *   </body>
    + * </html>
    + * 
    + * + * @author Sunita Mani + */ + +public class MinimalHTMLWriter extends AbstractWriter { + + /** + * These static finals are used to + * tweak and query the fontMask about which + * of these tags need to be generated or + * terminated. + */ + private static final int BOLD = 0x01; + private static final int ITALIC = 0x02; + private static final int UNDERLINE = 0x04; + + // Used to map StyleConstants to CSS. + private static final CSS css = new CSS(); + + private int fontMask = 0; + + int startOffset = 0; + int endOffset = 0; + + /** + * Stores the attributes of the previous run. + * Used to compare with the current run's + * attributeset. If identical, then a + * <span> tag is not emitted. + */ + private AttributeSet fontAttributes; + + /** + * Maps from style name as held by the Document, to the archived + * style name (style name written out). These may differ. + */ + private Hashtable styleNameMapping; + + /** + * Creates a new MinimalHTMLWriter. + * + * @param w Writer + * @param doc StyledDocument + * + */ + public MinimalHTMLWriter(Writer w, StyledDocument doc) { + super(w, doc); + } + + /** + * Creates a new MinimalHTMLWriter. + * + * @param w Writer + * @param doc StyledDocument + * @param pos The location in the document to fetch the + * content. + * @param len The amount to write out. + * + */ + public MinimalHTMLWriter(Writer w, StyledDocument doc, int pos, int len) { + super(w, doc, pos, len); + } + + /** + * Generates HTML output + * from a StyledDocument. + * + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + * + */ + public void write() throws IOException, BadLocationException { + styleNameMapping = new Hashtable(); + writeStartTag(""); + writeHeader(); + writeBody(); + writeEndTag(""); + } + + + /** + * Writes out all the attributes for the + * following types: + * StyleConstants.ParagraphConstants, + * StyleConstants.CharacterConstants, + * StyleConstants.FontConstants, + * StyleConstants.ColorConstants. + * The attribute name and value are separated by a colon. + * Each pair is separated by a semicolon. + * + * @exception IOException on any I/O error + */ + protected void writeAttributes(AttributeSet attr) throws IOException { + Enumeration attributeNames = attr.getAttributeNames(); + while (attributeNames.hasMoreElements()) { + Object name = attributeNames.nextElement(); + if ((name instanceof StyleConstants.ParagraphConstants) || + (name instanceof StyleConstants.CharacterConstants) || + (name instanceof StyleConstants.FontConstants) || + (name instanceof StyleConstants.ColorConstants)) { + indent(); + write(name.toString()); + write(':'); + write(css.styleConstantsValueToCSSValue + ((StyleConstants)name, attr.getAttribute(name)). + toString()); + write(';'); + write(NEWLINE); + } + } + } + + + /** + * Writes out text. + * + * @exception IOException on any I/O error + */ + protected void text(Element elem) throws IOException, BadLocationException { + String contentStr = getText(elem); + if ((contentStr.length() > 0) && + (contentStr.charAt(contentStr.length()-1) == NEWLINE)) { + contentStr = contentStr.substring(0, contentStr.length()-1); + } + if (contentStr.length() > 0) { + write(contentStr); + } + } + + /** + * Writes out a start tag appropriately + * indented. Also increments the indent level. + * + * @exception IOException on any I/O error + */ + protected void writeStartTag(String tag) throws IOException { + indent(); + write(tag); + write(NEWLINE); + incrIndent(); + } + + + /** + * Writes out an end tag appropriately + * indented. Also decrements the indent level. + * + * @exception IOException on any I/O error + */ + protected void writeEndTag(String endTag) throws IOException { + decrIndent(); + indent(); + write(endTag); + write(NEWLINE); + } + + + /** + * Writes out the <head> and <style> + * tags, and then invokes writeStyles() to write + * out all the named styles as the content of the + * <style> tag. The content is surrounded by + * valid HTML comment markers to ensure that the + * document is viewable in applications/browsers + * that do not support the tag. + * + * @exception IOException on any I/O error + */ + protected void writeHeader() throws IOException { + writeStartTag(""); + writeStartTag(""); + writeEndTag(""); + } + + + + /** + * Writes out all the named styles as the + * content of the <style> tag. + * + * @exception IOException on any I/O error + */ + protected void writeStyles() throws IOException { + /* + * Access to DefaultStyledDocument done to workaround + * a missing API in styled document to access the + * stylenames. + */ + DefaultStyledDocument styledDoc = ((DefaultStyledDocument)getDocument()); + Enumeration styleNames = styledDoc.getStyleNames(); + + while (styleNames.hasMoreElements()) { + Style s = styledDoc.getStyle((String)styleNames.nextElement()); + + /** PENDING: Once the name attribute is removed + from the list we check check for 0. **/ + if (s.getAttributeCount() == 1 && + s.isDefined(StyleConstants.NameAttribute)) { + continue; + } + indent(); + write("p." + addStyleName(s.getName())); + write(" {\n"); + incrIndent(); + writeAttributes(s); + decrIndent(); + indent(); + write("}\n"); + } + } + + + /** + * Iterates over the elements in the document + * and processes elements based on whether they are + * branch elements or leaf elements. This method specially handles + * leaf elements that are text. + * + * @exception IOException on any I/O error + */ + protected void writeBody() throws IOException, BadLocationException { + ElementIterator it = getElementIterator(); + + /* + This will be a section element for a styled document. + We represent this element in HTML as the body tags. + Therefore we ignore it. + */ + it.current(); + + Element next = null; + + writeStartTag(""); + + boolean inContent = false; + + while((next = it.next()) != null) { + if (!inRange(next)) { + continue; + } + if (next instanceof AbstractDocument.BranchElement) { + if (inContent) { + writeEndParagraph(); + inContent = false; + fontMask = 0; + } + writeStartParagraph(next); + } else if (isText(next)) { + writeContent(next, !inContent); + inContent = true; + } else { + writeLeaf(next); + inContent = true; + } + } + if (inContent) { + writeEndParagraph(); + } + writeEndTag(""); + } + + + /** + * Emits an end tag for a <p> + * tag. Before writing out the tag, this method ensures + * that all other tags that have been opened are + * appropriately closed off. + * + * @exception IOException on any I/O error + */ + protected void writeEndParagraph() throws IOException { + writeEndMask(fontMask); + if (inFontTag()) { + endSpanTag(); + } else { + write(NEWLINE); + } + writeEndTag("

    "); + } + + + /** + * Emits the start tag for a paragraph. If + * the paragraph has a named style associated with it, + * then this method also generates a class attribute for the + * <p> tag and sets its value to be the name of the + * style. + * + * @exception IOException on any I/O error + */ + protected void writeStartParagraph(Element elem) throws IOException { + AttributeSet attr = elem.getAttributes(); + Object resolveAttr = attr.getAttribute(StyleConstants.ResolveAttribute); + if (resolveAttr instanceof StyleContext.NamedStyle) { + writeStartTag("

    "); + } else { + writeStartTag("

    "); + } + } + + + /** + * Responsible for writing out other non-text leaf + * elements. + * + * @exception IOException on any I/O error + */ + protected void writeLeaf(Element elem) throws IOException { + indent(); + if (elem.getName() == StyleConstants.IconElementName) { + writeImage(elem); + } else if (elem.getName() == StyleConstants.ComponentElementName) { + writeComponent(elem); + } + } + + + /** + * Responsible for handling Icon Elements; + * deliberately unimplemented. How to implement this method is + * an issue of policy. For example, if you're generating + * an <img> tag, how should you + * represent the src attribute (the location of the image)? + * In certain cases it could be a URL, in others it could + * be read from a stream. + * + * @param elem element of type StyleConstants.IconElementName + */ + protected void writeImage(Element elem) throws IOException { + } + + + /** + * Responsible for handling Component Elements; + * deliberately unimplemented. + * How this method is implemented is a matter of policy. + */ + protected void writeComponent(Element elem) throws IOException { + } + + + /** + * Returns true if the element is a text element. + * + */ + protected boolean isText(Element elem) { + return (elem.getName() == AbstractDocument.ContentElementName); + } + + + /** + * Writes out the attribute set + * in an HTML-compliant manner. + * + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + */ + protected void writeContent(Element elem, boolean needsIndenting) + throws IOException, BadLocationException { + + AttributeSet attr = elem.getAttributes(); + writeNonHTMLAttributes(attr); + if (needsIndenting) { + indent(); + } + writeHTMLTags(attr); + text(elem); + } + + + /** + * Generates + * bold <b>, italic <i>, and <u> tags for the + * text based on its attribute settings. + * + * @exception IOException on any I/O error + */ + + protected void writeHTMLTags(AttributeSet attr) throws IOException { + + int oldMask = fontMask; + setFontMask(attr); + + int endMask = 0; + int startMask = 0; + if ((oldMask & BOLD) != 0) { + if ((fontMask & BOLD) == 0) { + endMask |= BOLD; + } + } else if ((fontMask & BOLD) != 0) { + startMask |= BOLD; + } + + if ((oldMask & ITALIC) != 0) { + if ((fontMask & ITALIC) == 0) { + endMask |= ITALIC; + } + } else if ((fontMask & ITALIC) != 0) { + startMask |= ITALIC; + } + + if ((oldMask & UNDERLINE) != 0) { + if ((fontMask & UNDERLINE) == 0) { + endMask |= UNDERLINE; + } + } else if ((fontMask & UNDERLINE) != 0) { + startMask |= UNDERLINE; + } + writeEndMask(endMask); + writeStartMask(startMask); + } + + + /** + * Tweaks the appropriate bits of fontMask + * to reflect whether the text is to be displayed in + * bold, italic, and/or with an underline. + * + */ + private void setFontMask(AttributeSet attr) { + if (StyleConstants.isBold(attr)) { + fontMask |= BOLD; + } + + if (StyleConstants.isItalic(attr)) { + fontMask |= ITALIC; + } + + if (StyleConstants.isUnderline(attr)) { + fontMask |= UNDERLINE; + } + } + + + + + /** + * Writes out start tags <u>, <i>, and <b> based on + * the mask settings. + * + * @exception IOException on any I/O error + */ + private void writeStartMask(int mask) throws IOException { + if (mask != 0) { + if ((mask & UNDERLINE) != 0) { + write(""); + } + if ((mask & ITALIC) != 0) { + write(""); + } + if ((mask & BOLD) != 0) { + write(""); + } + } + } + + /** + * Writes out end tags for <u>, <i>, and <b> based on + * the mask settings. + * + * @exception IOException on any I/O error + */ + private void writeEndMask(int mask) throws IOException { + if (mask != 0) { + if ((mask & BOLD) != 0) { + write(""); + } + if ((mask & ITALIC) != 0) { + write(""); + } + if ((mask & UNDERLINE) != 0) { + write(""); + } + } + } + + + /** + * Writes out the remaining + * character-level attributes (attributes other than bold, + * italic, and underline) in an HTML-compliant way. Given that + * attributes such as font family and font size have no direct + * mapping to HTML tags, a <span> tag is generated and its + * style attribute is set to contain the list of remaining + * attributes just like inline styles. + * + * @exception IOException on any I/O error + */ + protected void writeNonHTMLAttributes(AttributeSet attr) throws IOException { + + String style = ""; + String separator = "; "; + + if (inFontTag() && fontAttributes.isEqual(attr)) { + return; + } + + boolean first = true; + Color color = (Color)attr.getAttribute(StyleConstants.Foreground); + if (color != null) { + style += "color: " + css.styleConstantsValueToCSSValue + ((StyleConstants)StyleConstants.Foreground, + color); + first = false; + } + Integer size = (Integer)attr.getAttribute(StyleConstants.FontSize); + if (size != null) { + if (!first) { + style += separator; + } + style += "font-size: " + size.intValue() + "pt"; + first = false; + } + + String family = (String)attr.getAttribute(StyleConstants.FontFamily); + if (family != null) { + if (!first) { + style += separator; + } + style += "font-family: " + family; + first = false; + } + + if (style.length() > 0) { + if (fontMask != 0) { + writeEndMask(fontMask); + fontMask = 0; + } + startSpanTag(style); + fontAttributes = attr; + } + else if (fontAttributes != null) { + writeEndMask(fontMask); + fontMask = 0; + endSpanTag(); + } + } + + + /** + * Returns true if we are currently in a <font> tag. + */ + protected boolean inFontTag() { + return (fontAttributes != null); + } + + /** + * This is no longer used, instead <span> will be written out. + *

    + * Writes out an end tag for the <font> tag. + * + * @exception IOException on any I/O error + */ + protected void endFontTag() throws IOException { + write(NEWLINE); + writeEndTag(""); + fontAttributes = null; + } + + + /** + * This is no longer used, instead <span> will be written out. + *

    + * Writes out a start tag for the <font> tag. + * Because font tags cannot be nested, + * this method closes out + * any enclosing font tag before writing out a + * new start tag. + * + * @exception IOException on any I/O error + */ + protected void startFontTag(String style) throws IOException { + boolean callIndent = false; + if (inFontTag()) { + endFontTag(); + callIndent = true; + } + writeStartTag(""); + if (callIndent) { + indent(); + } + } + + /** + * Writes out a start tag for the <font> tag. + * Because font tags cannot be nested, + * this method closes out + * any enclosing font tag before writing out a + * new start tag. + * + * @exception IOException on any I/O error + */ + private void startSpanTag(String style) throws IOException { + boolean callIndent = false; + if (inFontTag()) { + endSpanTag(); + callIndent = true; + } + writeStartTag(""); + if (callIndent) { + indent(); + } + } + + /** + * Writes out an end tag for the <span> tag. + * + * @exception IOException on any I/O error + */ + private void endSpanTag() throws IOException { + write(NEWLINE); + writeEndTag(""); + fontAttributes = null; + } + + /** + * Adds the style named style to the style mapping. This + * returns the name that should be used when outputting. CSS does not + * allow the full Unicode set to be used as a style name. + */ + private String addStyleName(String style) { + if (styleNameMapping == null) { + return style; + } + StringBuffer sb = null; + for (int counter = style.length() - 1; counter >= 0; counter--) { + if (!isValidCharacter(style.charAt(counter))) { + if (sb == null) { + sb = new StringBuffer(style); + } + sb.setCharAt(counter, 'a'); + } + } + String mappedName = (sb != null) ? sb.toString() : style; + while (styleNameMapping.get(mappedName) != null) { + mappedName = mappedName + 'x'; + } + styleNameMapping.put(style, mappedName); + return mappedName; + } + + /** + * Returns the mapped style name corresponding to style. + */ + private String mapStyleName(String style) { + if (styleNameMapping == null) { + return style; + } + String retValue = (String)styleNameMapping.get(style); + return (retValue == null) ? style : retValue; + } + + private boolean isValidCharacter(char character) { + return ((character >= 'a' && character <= 'z') || + (character >= 'A' && character <= 'Z')); + } +} diff --git a/src/share/classes/javax/swing/text/html/MuxingAttributeSet.java b/src/share/classes/javax/swing/text/html/MuxingAttributeSet.java new file mode 100644 index 000000000..4247e15f0 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/MuxingAttributeSet.java @@ -0,0 +1,311 @@ +/* + * Copyright 2001 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import javax.swing.text.*; +import java.io.Serializable; +import java.util.*; + +/** + * An implementation of AttributeSet that can multiplex + * across a set of AttributeSets. + * + */ +class MuxingAttributeSet implements AttributeSet, Serializable { + /** + * Creates a MuxingAttributeSet with the passed in + * attributes. + */ + public MuxingAttributeSet(AttributeSet[] attrs) { + this.attrs = attrs; + } + + /** + * Creates an empty MuxingAttributeSet. This is intended for + * use by subclasses only, and it is also intended that subclasses will + * set the constituent AttributeSets before invoking any + * of the AttributeSet methods. + */ + protected MuxingAttributeSet() { + } + + /** + * Directly sets the AttributeSets that comprise this + * MuxingAttributeSet. + */ + protected synchronized void setAttributes(AttributeSet[] attrs) { + this.attrs = attrs; + } + + /** + * Returns the AttributeSets multiplexing too. When the + * AttributeSets need to be referenced, this should be called. + */ + protected synchronized AttributeSet[] getAttributes() { + return attrs; + } + + /** + * Inserts as at index. This assumes + * the value of index is between 0 and attrs.length, + * inclusive. + */ + protected synchronized void insertAttributeSetAt(AttributeSet as, + int index) { + int numAttrs = attrs.length; + AttributeSet newAttrs[] = new AttributeSet[numAttrs + 1]; + if (index < numAttrs) { + if (index > 0) { + System.arraycopy(attrs, 0, newAttrs, 0, index); + System.arraycopy(attrs, index, newAttrs, index + 1, + numAttrs - index); + } + else { + System.arraycopy(attrs, 0, newAttrs, 1, numAttrs); + } + } + else { + System.arraycopy(attrs, 0, newAttrs, 0, numAttrs); + } + newAttrs[index] = as; + attrs = newAttrs; + } + + /** + * Removes the AttributeSet at index. This assumes + * the value of index is greater than or equal to 0, + * and less than attrs.length. + */ + protected synchronized void removeAttributeSetAt(int index) { + int numAttrs = attrs.length; + AttributeSet[] newAttrs = new AttributeSet[numAttrs - 1]; + if (numAttrs > 0) { + if (index == 0) { + // FIRST + System.arraycopy(attrs, 1, newAttrs, 0, numAttrs - 1); + } + else if (index < (numAttrs - 1)) { + // MIDDLE + System.arraycopy(attrs, 0, newAttrs, 0, index); + System.arraycopy(attrs, index + 1, newAttrs, index, + numAttrs - index - 1); + } + else { + // END + System.arraycopy(attrs, 0, newAttrs, 0, numAttrs - 1); + } + } + attrs = newAttrs; + } + + // --- AttributeSet methods ---------------------------- + + /** + * Gets the number of attributes that are defined. + * + * @return the number of attributes + * @see AttributeSet#getAttributeCount + */ + public int getAttributeCount() { + AttributeSet[] as = getAttributes(); + int n = 0; + for (int i = 0; i < as.length; i++) { + n += as[i].getAttributeCount(); + } + return n; + } + + /** + * Checks whether a given attribute is defined. + * This will convert the key over to CSS if the + * key is a StyleConstants key that has a CSS + * mapping. + * + * @param key the attribute key + * @return true if the attribute is defined + * @see AttributeSet#isDefined + */ + public boolean isDefined(Object key) { + AttributeSet[] as = getAttributes(); + for (int i = 0; i < as.length; i++) { + if (as[i].isDefined(key)) { + return true; + } + } + return false; + } + + /** + * Checks whether two attribute sets are equal. + * + * @param attr the attribute set to check against + * @return true if the same + * @see AttributeSet#isEqual + */ + public boolean isEqual(AttributeSet attr) { + return ((getAttributeCount() == attr.getAttributeCount()) && + containsAttributes(attr)); + } + + /** + * Copies a set of attributes. + * + * @return the copy + * @see AttributeSet#copyAttributes + */ + public AttributeSet copyAttributes() { + AttributeSet[] as = getAttributes(); + MutableAttributeSet a = new SimpleAttributeSet(); + int n = 0; + for (int i = as.length - 1; i >= 0; i--) { + a.addAttributes(as[i]); + } + return a; + } + + /** + * Gets the value of an attribute. If the requested + * attribute is a StyleConstants attribute that has + * a CSS mapping, the request will be converted. + * + * @param key the attribute name + * @return the attribute value + * @see AttributeSet#getAttribute + */ + public Object getAttribute(Object key) { + AttributeSet[] as = getAttributes(); + int n = as.length; + for (int i = 0; i < n; i++) { + Object o = as[i].getAttribute(key); + if (o != null) { + return o; + } + } + return null; + } + + /** + * Gets the names of all attributes. + * + * @return the attribute names + * @see AttributeSet#getAttributeNames + */ + public Enumeration getAttributeNames() { + return new MuxingAttributeNameEnumeration(); + } + + /** + * Checks whether a given attribute name/value is defined. + * + * @param name the attribute name + * @param value the attribute value + * @return true if the name/value is defined + * @see AttributeSet#containsAttribute + */ + public boolean containsAttribute(Object name, Object value) { + return value.equals(getAttribute(name)); + } + + /** + * Checks whether the attribute set contains all of + * the given attributes. + * + * @param attrs the attributes to check + * @return true if the element contains all the attributes + * @see AttributeSet#containsAttributes + */ + public boolean containsAttributes(AttributeSet attrs) { + boolean result = true; + + Enumeration names = attrs.getAttributeNames(); + while (result && names.hasMoreElements()) { + Object name = names.nextElement(); + result = attrs.getAttribute(name).equals(getAttribute(name)); + } + + return result; + } + + /** + * Returns null, subclasses may wish to do something more + * intelligent with this. + */ + public AttributeSet getResolveParent() { + return null; + } + + /** + * The AttributeSets that make up the resulting + * AttributeSet. + */ + private AttributeSet[] attrs; + + + /** + * An Enumeration of the Attribute names in a MuxingAttributeSet. + * This may return the same name more than once. + */ + private class MuxingAttributeNameEnumeration implements Enumeration { + + MuxingAttributeNameEnumeration() { + updateEnum(); + } + + public boolean hasMoreElements() { + if (currentEnum == null) { + return false; + } + return currentEnum.hasMoreElements(); + } + + public Object nextElement() { + if (currentEnum == null) { + throw new NoSuchElementException("No more names"); + } + Object retObject = currentEnum.nextElement(); + if (!currentEnum.hasMoreElements()) { + updateEnum(); + } + return retObject; + } + + void updateEnum() { + AttributeSet[] as = getAttributes(); + currentEnum = null; + while (currentEnum == null && attrIndex < as.length) { + currentEnum = as[attrIndex++].getAttributeNames(); + if (!currentEnum.hasMoreElements()) { + currentEnum = null; + } + } + } + + + /** Index into attrs the current Enumeration came from. */ + private int attrIndex; + /** Enumeration from attrs. */ + private Enumeration currentEnum; + } +} diff --git a/src/share/classes/javax/swing/text/html/NoFramesView.java b/src/share/classes/javax/swing/text/html/NoFramesView.java new file mode 100644 index 000000000..083a1a467 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/NoFramesView.java @@ -0,0 +1,173 @@ +/* + * Copyright 1998-2000 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import javax.swing.text.*; +import java.awt.*; + +/** + * This is the view associated with the html tag NOFRAMES. + * This view has been written to ignore the contents of the + * NOFRAMES tag. The contents of the tag will only be visible + * when the JTextComponent the view is contained in is editable. + * + * @author Sunita Mani + */ +class NoFramesView extends BlockView { + + /** + * Creates a new view that represents an + * html box. This can be used for a number + * of elements. By default this view is not + * visible. + * + * @param elem the element to create a view for + * @param axis either View.X_AXIS or View.Y_AXIS + */ + public NoFramesView(Element elem, int axis) { + super(elem, axis); + visible = false; + } + + + /** + * If this view is not visible, then it returns. + * Otherwise it invokes the superclass. + * + * @param g the rendering surface to use + * @param allocation the allocated region to render into + * @see #isVisible + * @see text.ParagraphView#paint + */ + public void paint(Graphics g, Shape allocation) { + Container host = getContainer(); + if (host != null && + visible != ((JTextComponent)host).isEditable()) { + visible = ((JTextComponent)host).isEditable(); + } + + if (!isVisible()) { + return; + } + super.paint(g, allocation); + } + + + /** + * Determines if the JTextComponent that the view + * is contained in is editable. If so, then this + * view and all its child views are visible. + * Once this has been determined, the superclass + * is invoked to continue processing. + * + * @param p the parent View. + * @see BlockView#setParent + */ + public void setParent(View p) { + if (p != null) { + Container host = p.getContainer(); + if (host != null) { + visible = ((JTextComponent)host).isEditable(); + } + } + super.setParent(p); + } + + /** + * Returns a true/false value that represents + * whether the view is visible or not. + */ + public boolean isVisible() { + return visible; + } + + + /** + * Do nothing if the view is not visible, otherwise + * invoke the superclass to perform layout. + */ + protected void layout(int width, int height) { + if (!isVisible()) { + return; + } + super.layout(width, height); + } + + /** + * Determines the preferred span for this view. Returns + * 0 if the view is not visible, otherwise it calls the + * superclass method to get the preferred span. + * axis. + * + * @param axis may be either View.X_AXIS or View.Y_AXIS + * @return the span the view would like to be rendered into; + * typically the view is told to render into the span + * that is returned, although there is no guarantee; + * the parent may choose to resize or break the view + * @see text.ParagraphView#getPreferredSpan + */ + public float getPreferredSpan(int axis) { + if (!visible) { + return 0; + } + return super.getPreferredSpan(axis); + } + + /** + * Determines the minimum span for this view along an + * axis. Returns 0 if the view is not visible, otherwise + * it calls the superclass method to get the minimum span. + * + * @param axis may be either View.X_AXIS or + * View.Y_AXIS + * @return the minimum span the view can be rendered into + * @see text.ParagraphView#getMinimumSpan + */ + public float getMinimumSpan(int axis) { + if (!visible) { + return 0; + } + return super.getMinimumSpan(axis); + } + + /** + * Determines the maximum span for this view along an + * axis. Returns 0 if the view is not visible, otherwise + * it calls the superclass method ot get the maximum span. + * + * @param axis may be either View.X_AXIS or + * View.Y_AXIS + * @return the maximum span the view can be rendered into + * @see text.ParagraphView#getMaximumSpan + */ + public float getMaximumSpan(int axis) { + if (!visible) { + return 0; + } + return super.getMaximumSpan(axis); + } + + boolean visible; +} diff --git a/src/share/classes/javax/swing/text/html/ObjectView.java b/src/share/classes/javax/swing/text/html/ObjectView.java new file mode 100644 index 000000000..63aa66e7f --- /dev/null +++ b/src/share/classes/javax/swing/text/html/ObjectView.java @@ -0,0 +1,182 @@ +/* + * Copyright 1998-2004 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.util.Enumeration; +import java.awt.*; +import javax.swing.*; +import javax.swing.text.*; +import java.beans.*; +import java.lang.reflect.*; + +/** + * Component decorator that implements the view interface + * for <object> elements. + *

    + * This view will try to load the class specified by the + * classid attribute. If possible, the Classloader + * used to load the associated Document is used. + * This would typically be the same as the ClassLoader + * used to load the EditorKit. If the document's + * ClassLoader is null, Class.forName is used. + *

    + * If the class can successfully be loaded, an attempt will + * be made to create an instance of it by calling + * Class.newInstance. An attempt will be made + * to narrow the instance to type java.awt.Component + * to display the object. + *

    + * This view can also manage a set of parameters with limitations. + * The parameters to the <object> element are expected to + * be present on the associated elements attribute set as simple + * strings. Each bean property will be queried as a key on + * the AttributeSet, with the expectation that a non-null value + * (of type String) will be present if there was a parameter + * specification for the property. Reflection is used to + * set the parameter. Currently, this is limited to a very + * simple single parameter of type String. + *

    + * A simple example HTML invocation is: + *

    + *      <object classid="javax.swing.JLabel">
    + *      <param name="text" value="sample text">
    + *      </object>
    + * 
    + * + * @author Timothy Prinzing + */ +public class ObjectView extends ComponentView { + + /** + * Creates a new ObjectView object. + * + * @param elem the element to decorate + */ + public ObjectView(Element elem) { + super(elem); + } + + /** + * Create the component. The classid is used + * as a specification of the classname, which + * we try to load. + */ + protected Component createComponent() { + AttributeSet attr = getElement().getAttributes(); + String classname = (String) attr.getAttribute(HTML.Attribute.CLASSID); + try { + Class c = Class.forName(classname, true,Thread.currentThread(). + getContextClassLoader()); + Object o = c.newInstance(); + if (o instanceof Component) { + Component comp = (Component) o; + setParameters(comp, attr); + return comp; + } + } catch (Throwable e) { + // couldn't create a component... fall through to the + // couldn't load representation. + } + + return getUnloadableRepresentation(); + } + + /** + * Fetch a component that can be used to represent the + * object if it can't be created. + */ + Component getUnloadableRepresentation() { + // PENDING(prinz) get some artwork and return something + // interesting here. + Component comp = new JLabel("??"); + comp.setForeground(Color.red); + return comp; + } + + /** + * Get a Class object to use for loading the + * classid. If possible, the Classloader + * used to load the associated Document is used. + * This would typically be the same as the ClassLoader + * used to load the EditorKit. If the documents + * ClassLoader is null, + * Class.forName is used. + */ + private Class getClass(String classname) throws ClassNotFoundException { + Class klass; + + Class docClass = getDocument().getClass(); + ClassLoader loader = docClass.getClassLoader(); + if (loader != null) { + klass = loader.loadClass(classname); + } else { + klass = Class.forName(classname); + } + return klass; + } + + /** + * Initialize this component according the KEY/VALUEs passed in + * via the <param> elements in the corresponding + * <object> element. + */ + private void setParameters(Component comp, AttributeSet attr) { + Class k = comp.getClass(); + BeanInfo bi; + try { + bi = Introspector.getBeanInfo(k); + } catch (IntrospectionException ex) { + System.err.println("introspector failed, ex: "+ex); + return; // quit for now + } + PropertyDescriptor props[] = bi.getPropertyDescriptors(); + for (int i=0; i < props.length; i++) { + // System.err.println("checking on props[i]: "+props[i].getName()); + Object v = attr.getAttribute(props[i].getName()); + if (v instanceof String) { + // found a property parameter + String value = (String) v; + Method writer = props[i].getWriteMethod(); + if (writer == null) { + // read-only property. ignore + return; // for now + } + Class[] params = writer.getParameterTypes(); + if (params.length != 1) { + // zero or more than one argument, ignore + return; // for now + } + Object [] args = { value }; + try { + writer.invoke(comp, args); + } catch (Exception ex) { + System.err.println("Invocation failed"); + // invocation code + } + } + } + } + +} diff --git a/src/share/classes/javax/swing/text/html/Option.java b/src/share/classes/javax/swing/text/html/Option.java new file mode 100644 index 000000000..1cafbdff5 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/Option.java @@ -0,0 +1,120 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.io.Serializable; +import javax.swing.text.*; + +/** + * Value for the ListModel used to represent + * <option> elements. This is the object + * installed as items of the DefaultComboBoxModel + * used to represent the <select> element. + *

    + * Warning: + * Serialized objects of this class will not be compatible with + * future Swing releases. The current serialization support is + * appropriate for short term storage or RMI between applications running + * the same version of Swing. As of 1.4, support for long term storage + * of all JavaBeansTM + * has been added to the java.beans package. + * Please see {@link java.beans.XMLEncoder}. + * + * @author Timothy Prinzing + */ +public class Option implements Serializable { + + /** + * Creates a new Option object. + * + * @param attr the attributes associated with the + * option element. The attributes are copied to + * ensure they won't change. + */ + public Option(AttributeSet attr) { + this.attr = attr.copyAttributes(); + selected = (attr.getAttribute(HTML.Attribute.SELECTED) != null); + } + + /** + * Sets the label to be used for the option. + */ + public void setLabel(String label) { + this.label = label; + } + + /** + * Fetch the label associated with the option. + */ + public String getLabel() { + return label; + } + + /** + * Fetch the attributes associated with this option. + */ + public AttributeSet getAttributes() { + return attr; + } + + /** + * String representation is the label. + */ + public String toString() { + return label; + } + + /** + * Sets the selected state. + */ + protected void setSelection(boolean state) { + selected = state; + } + + /** + * Fetches the selection state associated with this option. + */ + public boolean isSelected() { + return selected; + } + + /** + * Convenience method to return the string associated + * with the value attribute. If the + * value has not been specified, the label will be + * returned. + */ + public String getValue() { + String value = (String) attr.getAttribute(HTML.Attribute.VALUE); + if (value == null) { + value = label; + } + return value; + } + + private boolean selected; + private String label; + private AttributeSet attr; +} diff --git a/src/share/classes/javax/swing/text/html/OptionComboBoxModel.java b/src/share/classes/javax/swing/text/html/OptionComboBoxModel.java new file mode 100644 index 000000000..088a9cd04 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/OptionComboBoxModel.java @@ -0,0 +1,63 @@ +/* + * Copyright 1998 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import javax.swing.*; +import javax.swing.event.*; +import java.io.Serializable; + + +/** + * OptionComboBoxModel extends the capabilities of the DefaultComboBoxModel, + * to store the Option that is initially marked as selected. + * This is stored, in order to enable an accurate reset of the + * ComboBox that represents the SELECT form element when the + * user requests a clear/reset. Given that a combobox only allow + * for one item to be selected, the last OPTION that has the + * attribute set wins. + * + @author Sunita Mani + */ + +class OptionComboBoxModel extends DefaultComboBoxModel implements Serializable { + + private Option selectedOption = null; + + /** + * Stores the Option that has been marked its + * selected attribute set. + */ + public void setInitialSelection(Option option) { + selectedOption = option; + } + + /** + * Fetches the Option item that represents that was + * initially set to a selected state. + */ + public Option getInitialSelection() { + return selectedOption; + } +} diff --git a/src/share/classes/javax/swing/text/html/OptionListModel.java b/src/share/classes/javax/swing/text/html/OptionListModel.java new file mode 100644 index 000000000..da8ea51cc --- /dev/null +++ b/src/share/classes/javax/swing/text/html/OptionListModel.java @@ -0,0 +1,571 @@ +/* + * Copyright 1998-2000 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import javax.swing.*; +import javax.swing.event.*; +import java.util.EventListener; +import java.util.BitSet; +import java.io.Serializable; + + +/** + * This class extends DefaultListModel, and also implements + * the ListSelectionModel interface, allowing for it to store state + * relevant to a SELECT form element which is implemented as a List. + * If SELECT has a size attribute whose value is greater than 1, + * or if allows multiple selection then a JList is used to + * represent it and the OptionListModel is used as its model. + * It also stores the initial state of the JList, to ensure an + * accurate reset, if the user requests a reset of the form. + * + @author Sunita Mani + */ + +class OptionListModel extends DefaultListModel implements ListSelectionModel, Serializable { + + + private static final int MIN = -1; + private static final int MAX = Integer.MAX_VALUE; + private int selectionMode = SINGLE_SELECTION; + private int minIndex = MAX; + private int maxIndex = MIN; + private int anchorIndex = -1; + private int leadIndex = -1; + private int firstChangedIndex = MAX; + private int lastChangedIndex = MIN; + private boolean isAdjusting = false; + private BitSet value = new BitSet(32); + private BitSet initialValue = new BitSet(32); + protected EventListenerList listenerList = new EventListenerList(); + + protected boolean leadAnchorNotificationEnabled = true; + + public int getMinSelectionIndex() { return isSelectionEmpty() ? -1 : minIndex; } + + public int getMaxSelectionIndex() { return maxIndex; } + + public boolean getValueIsAdjusting() { return isAdjusting; } + + public int getSelectionMode() { return selectionMode; } + + public void setSelectionMode(int selectionMode) { + switch (selectionMode) { + case SINGLE_SELECTION: + case SINGLE_INTERVAL_SELECTION: + case MULTIPLE_INTERVAL_SELECTION: + this.selectionMode = selectionMode; + break; + default: + throw new IllegalArgumentException("invalid selectionMode"); + } + } + + public boolean isSelectedIndex(int index) { + return ((index < minIndex) || (index > maxIndex)) ? false : value.get(index); + } + + public boolean isSelectionEmpty() { + return (minIndex > maxIndex); + } + + public void addListSelectionListener(ListSelectionListener l) { + listenerList.add(ListSelectionListener.class, l); + } + + public void removeListSelectionListener(ListSelectionListener l) { + listenerList.remove(ListSelectionListener.class, l); + } + + /** + * Returns an array of all the ListSelectionListeners added + * to this OptionListModel with addListSelectionListener(). + * + * @return all of the ListSelectionListeners added or an empty + * array if no listeners have been added + * @since 1.4 + */ + public ListSelectionListener[] getListSelectionListeners() { + return (ListSelectionListener[])listenerList.getListeners( + ListSelectionListener.class); + } + + /** + * Notify listeners that we are beginning or ending a + * series of value changes + */ + protected void fireValueChanged(boolean isAdjusting) { + fireValueChanged(getMinSelectionIndex(), getMaxSelectionIndex(), isAdjusting); + } + + + /** + * Notify ListSelectionListeners that the value of the selection, + * in the closed interval firstIndex,lastIndex, has changed. + */ + protected void fireValueChanged(int firstIndex, int lastIndex) { + fireValueChanged(firstIndex, lastIndex, getValueIsAdjusting()); + } + + /** + * @param firstIndex The first index in the interval. + * @param index1 The last index in the interval. + * @param isAdjusting True if this is the final change in a series of them. + * @see EventListenerList + */ + protected void fireValueChanged(int firstIndex, int lastIndex, boolean isAdjusting) + { + Object[] listeners = listenerList.getListenerList(); + ListSelectionEvent e = null; + + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == ListSelectionListener.class) { + if (e == null) { + e = new ListSelectionEvent(this, firstIndex, lastIndex, isAdjusting); + } + ((ListSelectionListener)listeners[i+1]).valueChanged(e); + } + } + } + + private void fireValueChanged() { + if (lastChangedIndex == MIN) { + return; + } + /* Change the values before sending the event to the + * listeners in case the event causes a listener to make + * another change to the selection. + */ + int oldFirstChangedIndex = firstChangedIndex; + int oldLastChangedIndex = lastChangedIndex; + firstChangedIndex = MAX; + lastChangedIndex = MIN; + fireValueChanged(oldFirstChangedIndex, oldLastChangedIndex); + } + + + // Update first and last change indices + private void markAsDirty(int r) { + firstChangedIndex = Math.min(firstChangedIndex, r); + lastChangedIndex = Math.max(lastChangedIndex, r); + } + + // Set the state at this index and update all relevant state. + private void set(int r) { + if (value.get(r)) { + return; + } + value.set(r); + Option option = (Option)get(r); + option.setSelection(true); + markAsDirty(r); + + // Update minimum and maximum indices + minIndex = Math.min(minIndex, r); + maxIndex = Math.max(maxIndex, r); + } + + // Clear the state at this index and update all relevant state. + private void clear(int r) { + if (!value.get(r)) { + return; + } + value.clear(r); + Option option = (Option)get(r); + option.setSelection(false); + markAsDirty(r); + + // Update minimum and maximum indices + /* + If (r > minIndex) the minimum has not changed. + The case (r < minIndex) is not possible because r'th value was set. + We only need to check for the case when lowest entry has been cleared, + and in this case we need to search for the first value set above it. + */ + if (r == minIndex) { + for(minIndex = minIndex + 1; minIndex <= maxIndex; minIndex++) { + if (value.get(minIndex)) { + break; + } + } + } + /* + If (r < maxIndex) the maximum has not changed. + The case (r > maxIndex) is not possible because r'th value was set. + We only need to check for the case when highest entry has been cleared, + and in this case we need to search for the first value set below it. + */ + if (r == maxIndex) { + for(maxIndex = maxIndex - 1; minIndex <= maxIndex; maxIndex--) { + if (value.get(maxIndex)) { + break; + } + } + } + /* Performance note: This method is called from inside a loop in + changeSelection() but we will only iterate in the loops + above on the basis of one iteration per deselected cell - in total. + Ie. the next time this method is called the work of the previous + deselection will not be repeated. + + We also don't need to worry about the case when the min and max + values are in their unassigned states. This cannot happen because + this method's initial check ensures that the selection was not empty + and therefore that the minIndex and maxIndex had 'real' values. + + If we have cleared the whole selection, set the minIndex and maxIndex + to their cannonical values so that the next set command always works + just by using Math.min and Math.max. + */ + if (isSelectionEmpty()) { + minIndex = MAX; + maxIndex = MIN; + } + } + + /** + * Sets the value of the leadAnchorNotificationEnabled flag. + * @see #isLeadAnchorNotificationEnabled() + */ + public void setLeadAnchorNotificationEnabled(boolean flag) { + leadAnchorNotificationEnabled = flag; + } + + /** + * Returns the value of the leadAnchorNotificationEnabled flag. + * When leadAnchorNotificationEnabled is true the model + * generates notification events with bounds that cover all the changes to + * the selection plus the changes to the lead and anchor indices. + * Setting the flag to false causes a norrowing of the event's bounds to + * include only the elements that have been selected or deselected since + * the last change. Either way, the model continues to maintain the lead + * and anchor variables internally. The default is true. + * @return the value of the leadAnchorNotificationEnabled flag + * @see #setLeadAnchorNotificationEnabled(boolean) + */ + public boolean isLeadAnchorNotificationEnabled() { + return leadAnchorNotificationEnabled; + } + + private void updateLeadAnchorIndices(int anchorIndex, int leadIndex) { + if (leadAnchorNotificationEnabled) { + if (this.anchorIndex != anchorIndex) { + if (this.anchorIndex != -1) { // The unassigned state. + markAsDirty(this.anchorIndex); + } + markAsDirty(anchorIndex); + } + + if (this.leadIndex != leadIndex) { + if (this.leadIndex != -1) { // The unassigned state. + markAsDirty(this.leadIndex); + } + markAsDirty(leadIndex); + } + } + this.anchorIndex = anchorIndex; + this.leadIndex = leadIndex; + } + + private boolean contains(int a, int b, int i) { + return (i >= a) && (i <= b); + } + + private void changeSelection(int clearMin, int clearMax, + int setMin, int setMax, boolean clearFirst) { + for(int i = Math.min(setMin, clearMin); i <= Math.max(setMax, clearMax); i++) { + + boolean shouldClear = contains(clearMin, clearMax, i); + boolean shouldSet = contains(setMin, setMax, i); + + if (shouldSet && shouldClear) { + if (clearFirst) { + shouldClear = false; + } + else { + shouldSet = false; + } + } + + if (shouldSet) { + set(i); + } + if (shouldClear) { + clear(i); + } + } + fireValueChanged(); + } + + /* Change the selection with the effect of first clearing the values + * in the inclusive range [clearMin, clearMax] then setting the values + * in the inclusive range [setMin, setMax]. Do this in one pass so + * that no values are cleared if they would later be set. + */ + private void changeSelection(int clearMin, int clearMax, int setMin, int setMax) { + changeSelection(clearMin, clearMax, setMin, setMax, true); + } + + public void clearSelection() { + removeSelectionInterval(minIndex, maxIndex); + } + + public void setSelectionInterval(int index0, int index1) { + if (index0 == -1 || index1 == -1) { + return; + } + + if (getSelectionMode() == SINGLE_SELECTION) { + index0 = index1; + } + + updateLeadAnchorIndices(index0, index1); + + int clearMin = minIndex; + int clearMax = maxIndex; + int setMin = Math.min(index0, index1); + int setMax = Math.max(index0, index1); + changeSelection(clearMin, clearMax, setMin, setMax); + } + + public void addSelectionInterval(int index0, int index1) + { + if (index0 == -1 || index1 == -1) { + return; + } + + if (getSelectionMode() != MULTIPLE_INTERVAL_SELECTION) { + setSelectionInterval(index0, index1); + return; + } + + updateLeadAnchorIndices(index0, index1); + + int clearMin = MAX; + int clearMax = MIN; + int setMin = Math.min(index0, index1); + int setMax = Math.max(index0, index1); + changeSelection(clearMin, clearMax, setMin, setMax); + } + + + public void removeSelectionInterval(int index0, int index1) + { + if (index0 == -1 || index1 == -1) { + return; + } + + updateLeadAnchorIndices(index0, index1); + + int clearMin = Math.min(index0, index1); + int clearMax = Math.max(index0, index1); + int setMin = MAX; + int setMax = MIN; + changeSelection(clearMin, clearMax, setMin, setMax); + } + + private void setState(int index, boolean state) { + if (state) { + set(index); + } + else { + clear(index); + } + } + + /** + * Insert length indices beginning before/after index. If the value + * at index is itself selected, set all of the newly inserted + * items, otherwise leave them unselected. This method is typically + * called to sync the selection model with a corresponding change + * in the data model. + */ + public void insertIndexInterval(int index, int length, boolean before) + { + /* The first new index will appear at insMinIndex and the last + * one will appear at insMaxIndex + */ + int insMinIndex = (before) ? index : index + 1; + int insMaxIndex = (insMinIndex + length) - 1; + + /* Right shift the entire bitset by length, beginning with + * index-1 if before is true, index+1 if it's false (i.e. with + * insMinIndex). + */ + for(int i = maxIndex; i >= insMinIndex; i--) { + setState(i + length, value.get(i)); + } + + /* Initialize the newly inserted indices. + */ + boolean setInsertedValues = value.get(index); + for(int i = insMinIndex; i <= insMaxIndex; i++) { + setState(i, setInsertedValues); + } + } + + + /** + * Remove the indices in the interval index0,index1 (inclusive) from + * the selection model. This is typically called to sync the selection + * model width a corresponding change in the data model. Note + * that (as always) index0 can be greater than index1. + */ + public void removeIndexInterval(int index0, int index1) + { + int rmMinIndex = Math.min(index0, index1); + int rmMaxIndex = Math.max(index0, index1); + int gapLength = (rmMaxIndex - rmMinIndex) + 1; + + /* Shift the entire bitset to the left to close the index0, index1 + * gap. + */ + for(int i = rmMinIndex; i <= maxIndex; i++) { + setState(i, value.get(i + gapLength)); + } + } + + + public void setValueIsAdjusting(boolean isAdjusting) { + if (isAdjusting != this.isAdjusting) { + this.isAdjusting = isAdjusting; + this.fireValueChanged(isAdjusting); + } + } + + + public String toString() { + String s = ((getValueIsAdjusting()) ? "~" : "=") + value.toString(); + return getClass().getName() + " " + Integer.toString(hashCode()) + " " + s; + } + + /** + * Returns a clone of the receiver with the same selection. + * listenerLists are not duplicated. + * + * @return a clone of the receiver + * @exception CloneNotSupportedException if the receiver does not + * both (a) implement the Cloneable interface + * and (b) define a clone method + */ + public Object clone() throws CloneNotSupportedException { + OptionListModel clone = (OptionListModel)super.clone(); + clone.value = (BitSet)value.clone(); + clone.listenerList = new EventListenerList(); + return clone; + } + + public int getAnchorSelectionIndex() { + return anchorIndex; + } + + public int getLeadSelectionIndex() { + return leadIndex; + } + + /** + * Set the anchor selection index, leaving all selection values unchanged. + * + * @see #getAnchorSelectionIndex + * @see #setLeadSelectionIndex + */ + public void setAnchorSelectionIndex(int anchorIndex) { + this.anchorIndex = anchorIndex; + } + + /** + * Set the lead selection index, ensuring that values between the + * anchor and the new lead are either all selected or all deselected. + * If the value at the anchor index is selected, first clear all the + * values in the range [anchor, oldLeadIndex], then select all the values + * values in the range [anchor, newLeadIndex], where oldLeadIndex is the old + * leadIndex and newLeadIndex is the new one. + *

    + * If the value at the anchor index is not selected, do the same thing in reverse, + * selecting values in the old range and deslecting values in the new one. + *

    + * Generate a single event for this change and notify all listeners. + * For the purposes of generating minimal bounds in this event, do the + * operation in a single pass; that way the first and last index inside the + * ListSelectionEvent that is broadcast will refer to cells that actually + * changed value because of this method. If, instead, this operation were + * done in two steps the effect on the selection state would be the same + * but two events would be generated and the bounds around the changed values + * would be wider, including cells that had been first cleared and only + * to later be set. + *

    + * This method can be used in the mouseDragged() method of a UI class + * to extend a selection. + * + * @see #getLeadSelectionIndex + * @see #setAnchorSelectionIndex + */ + public void setLeadSelectionIndex(int leadIndex) { + int anchorIndex = this.anchorIndex; + if (getSelectionMode() == SINGLE_SELECTION) { + anchorIndex = leadIndex; + } + + int oldMin = Math.min(this.anchorIndex, this.leadIndex);; + int oldMax = Math.max(this.anchorIndex, this.leadIndex);; + int newMin = Math.min(anchorIndex, leadIndex); + int newMax = Math.max(anchorIndex, leadIndex); + if (value.get(this.anchorIndex)) { + changeSelection(oldMin, oldMax, newMin, newMax); + } + else { + changeSelection(newMin, newMax, oldMin, oldMax, false); + } + this.anchorIndex = anchorIndex; + this.leadIndex = leadIndex; + } + + + /** + * This method is responsible for storing the state + * of the initial selection. If the selectionMode + * is the default, i.e allowing only for SINGLE_SELECTION, + * then the very last OPTION that has the selected + * attribute set wins. + */ + public void setInitialSelection(int i) { + if (initialValue.get(i)) { + return; + } + if (selectionMode == SINGLE_SELECTION) { + // reset to empty + initialValue.and(new BitSet()); + } + initialValue.set(i); + } + + /** + * Fetches the BitSet that represents the initial + * set of selected items in the list. + */ + public BitSet getInitialSelection() { + return initialValue; + } +} diff --git a/src/share/classes/javax/swing/text/html/ParagraphView.java b/src/share/classes/javax/swing/text/html/ParagraphView.java new file mode 100644 index 000000000..6d6006b0a --- /dev/null +++ b/src/share/classes/javax/swing/text/html/ParagraphView.java @@ -0,0 +1,294 @@ +/* + * Copyright 1998-2003 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import javax.swing.SizeRequirements; +import javax.swing.event.DocumentEvent; +import javax.swing.text.Document; +import javax.swing.text.Element; +import javax.swing.text.AttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.View; +import javax.swing.text.ViewFactory; +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; + +/** + * Displays the a paragraph, and uses css attributes for its + * configuration. + * + * @author Timothy Prinzing + */ + +public class ParagraphView extends javax.swing.text.ParagraphView { + + /** + * Constructs a ParagraphView for the given element. + * + * @param elem the element that this view is responsible for + */ + public ParagraphView(Element elem) { + super(elem); + } + + /** + * Establishes the parent view for this view. This is + * guaranteed to be called before any other methods if the + * parent view is functioning properly. + *

    + * This is implemented + * to forward to the superclass as well as call the + * setPropertiesFromAttributes + * method to set the paragraph properties from the css + * attributes. The call is made at this time to ensure + * the ability to resolve upward through the parents + * view attributes. + * + * @param parent the new parent, or null if the view is + * being removed from a parent it was previously added + * to + */ + public void setParent(View parent) { + super.setParent(parent); + if (parent != null) { + setPropertiesFromAttributes(); + } + } + + /** + * Fetches the attributes to use when rendering. This is + * implemented to multiplex the attributes specified in the + * model with a StyleSheet. + */ + public AttributeSet getAttributes() { + if (attr == null) { + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + } + return attr; + } + + /** + * Sets up the paragraph from css attributes instead of + * the values found in StyleConstants (i.e. which are used + * by the superclass). Since + */ + protected void setPropertiesFromAttributes() { + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + painter = sheet.getBoxPainter(attr); + if (attr != null) { + super.setPropertiesFromAttributes(); + setInsets((short) painter.getInset(TOP, this), + (short) painter.getInset(LEFT, this), + (short) painter.getInset(BOTTOM, this), + (short) painter.getInset(RIGHT, this)); + Object o = attr.getAttribute(CSS.Attribute.TEXT_ALIGN); + if (o != null) { + // set horizontal alignment + String ta = o.toString(); + if (ta.equals("left")) { + setJustification(StyleConstants.ALIGN_LEFT); + } else if (ta.equals("center")) { + setJustification(StyleConstants.ALIGN_CENTER); + } else if (ta.equals("right")) { + setJustification(StyleConstants.ALIGN_RIGHT); + } else if (ta.equals("justify")) { + setJustification(StyleConstants.ALIGN_JUSTIFIED); + } + } + // Get the width/height + cssWidth = (CSS.LengthValue)attr.getAttribute( + CSS.Attribute.WIDTH); + cssHeight = (CSS.LengthValue)attr.getAttribute( + CSS.Attribute.HEIGHT); + } + } + + protected StyleSheet getStyleSheet() { + HTMLDocument doc = (HTMLDocument) getDocument(); + return doc.getStyleSheet(); + } + + + /** + * Calculate the needs for the paragraph along the minor axis. + * + *

    If size requirements are explicitly specified for the paragraph, + * use that requirements. Otherwise, use the requirements of the + * superclass {@link javax.swing.text.ParagraphView}.

    + * + *

    If the {@code axis} parameter is neither {@code View.X_AXIS} nor + * {@code View.Y_AXIS}, {@link IllegalArgumentException} is thrown. If the + * {@code r} parameter is {@code null,} a new {@code SizeRequirements} + * object is created, otherwise the supplied {@code SizeRequirements} + * object is returned.

    + * + * @param axis the minor axis + * @param r the input {@code SizeRequirements} object + * @return the new or adjusted {@code SizeRequirements} object + * @throw IllegalArgumentException if the {@code axis} parameter is invalid + */ + protected SizeRequirements calculateMinorAxisRequirements( + int axis, SizeRequirements r) { + r = super.calculateMinorAxisRequirements(axis, r); + + if (BlockView.spanSetFromAttributes(axis, r, cssWidth, cssHeight)) { + // Offset by the margins so that pref/min/max return the + // right value. + int margin = (axis == X_AXIS) ? getLeftInset() + getRightInset() : + getTopInset() + getBottomInset(); + r.minimum -= margin; + r.preferred -= margin; + r.maximum -= margin; + } + return r; + } + + + /** + * Indicates whether or not this view should be + * displayed. If none of the children wish to be + * displayed and the only visible child is the + * break that ends the paragraph, the paragraph + * will not be considered visible. Otherwise, + * it will be considered visible and return true. + * + * @return true if the paragraph should be displayed + */ + public boolean isVisible() { + + int n = getLayoutViewCount() - 1; + for (int i = 0; i < n; i++) { + View v = getLayoutView(i); + if (v.isVisible()) { + return true; + } + } + if (n > 0) { + View v = getLayoutView(n); + if ((v.getEndOffset() - v.getStartOffset()) == 1) { + return false; + } + } + // If it's the last paragraph and not editable, it shouldn't + // be visible. + if (getStartOffset() == getDocument().getLength()) { + boolean editable = false; + Component c = getContainer(); + if (c instanceof JTextComponent) { + editable = ((JTextComponent)c).isEditable(); + } + if (!editable) { + return false; + } + } + return true; + } + + /** + * Renders using the given rendering surface and area on that + * surface. This is implemented to delgate to the superclass + * after stashing the base coordinate for tab calculations. + * + * @param g the rendering surface to use + * @param a the allocated region to render into + * @see View#paint + */ + public void paint(Graphics g, Shape a) { + if (a == null) { + return; + } + + Rectangle r; + if (a instanceof Rectangle) { + r = (Rectangle) a; + } else { + r = a.getBounds(); + } + painter.paint(g, r.x, r.y, r.width, r.height, this); + super.paint(g, a); + } + + /** + * Determines the preferred span for this view. Returns + * 0 if the view is not visible, otherwise it calls the + * superclass method to get the preferred span. + * axis. + * + * @param axis may be either View.X_AXIS or View.Y_AXIS + * @return the span the view would like to be rendered into; + * typically the view is told to render into the span + * that is returned, although there is no guarantee; + * the parent may choose to resize or break the view + * @see javax.swing.text.ParagraphView#getPreferredSpan + */ + public float getPreferredSpan(int axis) { + if (!isVisible()) { + return 0; + } + return super.getPreferredSpan(axis); + } + + /** + * Determines the minimum span for this view along an + * axis. Returns 0 if the view is not visible, otherwise + * it calls the superclass method to get the minimum span. + * + * @param axis may be either View.X_AXIS or + * View.Y_AXIS + * @return the minimum span the view can be rendered into + * @see javax.swing.text.ParagraphView#getMinimumSpan + */ + public float getMinimumSpan(int axis) { + if (!isVisible()) { + return 0; + } + return super.getMinimumSpan(axis); + } + + /** + * Determines the maximum span for this view along an + * axis. Returns 0 if the view is not visible, otherwise + * it calls the superclass method ot get the maximum span. + * + * @param axis may be either View.X_AXIS or + * View.Y_AXIS + * @return the maximum span the view can be rendered into + * @see javax.swing.text.ParagraphView#getMaximumSpan + */ + public float getMaximumSpan(int axis) { + if (!isVisible()) { + return 0; + } + return super.getMaximumSpan(axis); + } + + private AttributeSet attr; + private StyleSheet.BoxPainter painter; + private CSS.LengthValue cssWidth; + private CSS.LengthValue cssHeight; +} diff --git a/src/share/classes/javax/swing/text/html/ResourceLoader.java b/src/share/classes/javax/swing/text/html/ResourceLoader.java new file mode 100644 index 000000000..a099cc041 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/ResourceLoader.java @@ -0,0 +1,60 @@ +/* + * Copyright 1999 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html; + +import java.io.InputStream; + +/** + * Simple class to load resources using the 1.2 + * security model. Since the html support is loaded + * lazily, it's resources are potentially fetched with + * applet code in the call stack. By providing this + * functionality in a class that is only built on 1.2, + * reflection can be used from the code that is also + * built on 1.1 to call this functionality (and avoid + * the evils of preprocessing). This functionality + * is called from HTMLEditorKit.getResourceAsStream. + * + * @author Timothy Prinzing + */ +class ResourceLoader implements java.security.PrivilegedAction { + + ResourceLoader(String name) { + this.name = name; + } + + public Object run() { + Object o = HTMLEditorKit.class.getResourceAsStream(name); + return o; + } + + public static InputStream getResourceAsStream(String name) { + java.security.PrivilegedAction a = new ResourceLoader(name); + return (InputStream) java.security.AccessController.doPrivileged(a); + } + + private String name; +} diff --git a/src/share/classes/javax/swing/text/html/StyleSheet.java b/src/share/classes/javax/swing/text/html/StyleSheet.java new file mode 100644 index 000000000..bc5546d71 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/StyleSheet.java @@ -0,0 +1,3340 @@ +/* + * Copyright 1997-2005 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import sun.swing.SwingUtilities2; +import java.util.*; +import java.awt.*; +import java.io.*; +import java.net.*; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.border.*; +import javax.swing.event.ChangeListener; +import javax.swing.text.*; + +/** + * Support for defining the visual characteristics of + * HTML views being rendered. The StyleSheet is used to + * translate the HTML model into visual characteristics. + * This enables views to be customized by a look-and-feel, + * multiple views over the same model can be rendered + * differently, etc. This can be thought of as a CSS + * rule repository. The key for CSS attributes is an + * object of type CSS.Attribute. The type of the value + * is up to the StyleSheet implementation, but the + * toString method is required + * to return a string representation of CSS value. + *

    + * The primary entry point for HTML View implementations + * to get their attributes is the + * getViewAttributes + * method. This should be implemented to establish the + * desired policy used to associate attributes with the view. + * Each HTMLEditorKit (i.e. and therefore each associated + * JEditorPane) can have its own StyleSheet, but by default one + * sheet will be shared by all of the HTMLEditorKit instances. + * HTMLDocument instance can also have a StyleSheet, which + * holds the document-specific CSS specifications. + *

    + * In order for Views to store less state and therefore be + * more lightweight, the StyleSheet can act as a factory for + * painters that handle some of the rendering tasks. This allows + * implementations to determine what they want to cache + * and have the sharing potentially at the level that a + * selector is common to multiple views. Since the StyleSheet + * may be used by views over multiple documents and typically + * the HTML attributes don't effect the selector being used, + * the potential for sharing is significant. + *

    + * The rules are stored as named styles, and other information + * is stored to translate the context of an element to a + * rule quickly. The following code fragment will display + * the named styles, and therefore the CSS rules contained. + *

    + *  
    + *   import java.util.*;
    + *   import javax.swing.text.*;
    + *   import javax.swing.text.html.*;
    + *  
    + *   public class ShowStyles {
    + *  
    + *       public static void main(String[] args) {
    + *         HTMLEditorKit kit = new HTMLEditorKit();
    + *         HTMLDocument doc = (HTMLDocument) kit.createDefaultDocument();
    + *         StyleSheet styles = doc.getStyleSheet();
    + *  
    + *         Enumeration rules = styles.getStyleNames();
    + *         while (rules.hasMoreElements()) {
    + *             String name = (String) rules.nextElement();
    + *             Style rule = styles.getStyle(name);
    + *             System.out.println(rule.toString());
    + *         }
    + *         System.exit(0);
    + *       }
    + *   }
    + *  
    + * 
    + *

    + * The semantics for when a CSS style should overide visual attributes + * defined by an element are not well defined. For example, the html + * <body bgcolor=red> makes the body have a red + * background. But if the html file also contains the CSS rule + * body { background: blue } it becomes less clear as to + * what color the background of the body should be. The current + * implemention gives visual attributes defined in the element the + * highest precedence, that is they are always checked before any styles. + * Therefore, in the previous example the background would have a + * red color as the body element defines the background color to be red. + *

    + * As already mentioned this supports CSS. We don't support the full CSS + * spec. Refer to the javadoc of the CSS class to see what properties + * we support. The two major CSS parsing related + * concepts we do not currently + * support are pseudo selectors, such as A:link { color: red }, + * and the important modifier. + *

    + * Note: This implementation is currently + * incomplete. It can be replaced with alternative implementations + * that are complete. Future versions of this class will provide + * better CSS support. + * + * @author Timothy Prinzing + * @author Sunita Mani + * @author Sara Swanson + * @author Jill Nakata + */ +public class StyleSheet extends StyleContext { + // As the javadoc states, this class maintains a mapping between + // a CSS selector (such as p.bar) and a Style. + // This consists of a number of parts: + // . Each selector is broken down into its constituent simple selectors, + // and stored in an inverted graph, for example: + // p { color: red } ol p { font-size: 10pt } ul p { font-size: 12pt } + // results in the graph: + // root + // | + // p + // / \ + // ol ul + // each node (an instance of SelectorMapping) has an associated + // specificity and potentially a Style. + // . Every rule that is asked for (either by way of getRule(String) or + // getRule(HTML.Tag, Element)) results in a unique instance of + // ResolvedStyle. ResolvedStyles contain the AttributeSets from the + // SelectorMapping. + // . When a new rule is created it is inserted into the graph, and + // the AttributeSets of each ResolvedStyles are updated appropriately. + // . This class creates special AttributeSets, LargeConversionSet and + // SmallConversionSet, that maintain a mapping between StyleConstants + // and CSS so that developers that wish to use the StyleConstants + // methods can do so. + // . When one of the AttributeSets is mutated by way of a + // StyleConstants key, all the associated CSS keys are removed. This is + // done so that the two representations don't get out of sync. For + // example, if the developer adds StyleConsants.BOLD, FALSE to an + // AttributeSet that contains HTML.Tag.B, the HTML.Tag.B entry will + // be removed. + + /** + * Construct a StyleSheet + */ + public StyleSheet() { + super(); + selectorMapping = new SelectorMapping(0); + resolvedStyles = new Hashtable(); + if (css == null) { + css = new CSS(); + } + } + + /** + * Fetches the style to use to render the given type + * of HTML tag. The element given is representing + * the tag and can be used to determine the nesting + * for situations where the attributes will differ + * if nesting inside of elements. + * + * @param t the type to translate to visual attributes + * @param e the element representing the tag; the element + * can be used to determine the nesting for situations where + * the attributes will differ if nested inside of other + * elements + * @return the set of CSS attributes to use to render + * the tag + */ + public Style getRule(HTML.Tag t, Element e) { + SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); + + try { + // Build an array of all the parent elements. + Vector searchContext = sb.getVector(); + + for (Element p = e; p != null; p = p.getParentElement()) { + searchContext.addElement(p); + } + + // Build a fully qualified selector. + int n = searchContext.size(); + StringBuffer cacheLookup = sb.getStringBuffer(); + AttributeSet attr; + String eName; + Object name; + + // >= 1 as the HTML.Tag for the 0th element is passed in. + for (int counter = n - 1; counter >= 1; counter--) { + e = (Element)searchContext.elementAt(counter); + attr = e.getAttributes(); + name = attr.getAttribute(StyleConstants.NameAttribute); + eName = name.toString(); + cacheLookup.append(eName); + if (attr != null) { + if (attr.isDefined(HTML.Attribute.ID)) { + cacheLookup.append('#'); + cacheLookup.append(attr.getAttribute + (HTML.Attribute.ID)); + } + else if (attr.isDefined(HTML.Attribute.CLASS)) { + cacheLookup.append('.'); + cacheLookup.append(attr.getAttribute + (HTML.Attribute.CLASS)); + } + } + cacheLookup.append(' '); + } + cacheLookup.append(t.toString()); + e = (Element)searchContext.elementAt(0); + attr = e.getAttributes(); + if (e.isLeaf()) { + // For leafs, we use the second tier attributes. + Object testAttr = attr.getAttribute(t); + if (testAttr instanceof AttributeSet) { + attr = (AttributeSet)testAttr; + } + else { + attr = null; + } + } + if (attr != null) { + if (attr.isDefined(HTML.Attribute.ID)) { + cacheLookup.append('#'); + cacheLookup.append(attr.getAttribute(HTML.Attribute.ID)); + } + else if (attr.isDefined(HTML.Attribute.CLASS)) { + cacheLookup.append('.'); + cacheLookup.append(attr.getAttribute + (HTML.Attribute.CLASS)); + } + } + + Style style = getResolvedStyle(cacheLookup.toString(), + searchContext, t); + return style; + } + finally { + SearchBuffer.releaseSearchBuffer(sb); + } + } + + /** + * Fetches the rule that best matches the selector given + * in string form. Where selector is a space separated + * String of the element names. For example, selector + * might be 'html body tr td''

    + * The attributes of the returned Style will change + * as rules are added and removed. That is if you to ask for a rule + * with a selector "table p" and a new rule was added with a selector + * of "p" the returned Style would include the new attributes from + * the rule "p". + */ + public Style getRule(String selector) { + selector = cleanSelectorString(selector); + if (selector != null) { + Style style = getResolvedStyle(selector); + return style; + } + return null; + } + + /** + * Adds a set of rules to the sheet. The rules are expected to + * be in valid CSS format. Typically this would be called as + * a result of parsing a <style> tag. + */ + public void addRule(String rule) { + if (rule != null) { + //tweaks to control display properties + //see BasicEditorPaneUI + final String baseUnitsDisable = "BASE_SIZE_DISABLE"; + final String baseUnits = "BASE_SIZE "; + final String w3cLengthUnitsEnable = "W3C_LENGTH_UNITS_ENABLE"; + final String w3cLengthUnitsDisable = "W3C_LENGTH_UNITS_DISABLE"; + if (rule == baseUnitsDisable) { + sizeMap = sizeMapDefault; + } else if (rule.startsWith(baseUnits)) { + rebaseSizeMap(Integer. + parseInt(rule.substring(baseUnits.length()))); + } else if (rule == w3cLengthUnitsEnable) { + w3cLengthUnits = true; + } else if (rule == w3cLengthUnitsDisable) { + w3cLengthUnits = false; + } else { + CssParser parser = new CssParser(); + try { + parser.parse(getBase(), new StringReader(rule), false, false); + } catch (IOException ioe) { } + } + } + } + + /** + * Translates a CSS declaration to an AttributeSet that represents + * the CSS declaration. Typically this would be called as a + * result of encountering an HTML style attribute. + */ + public AttributeSet getDeclaration(String decl) { + if (decl == null) { + return SimpleAttributeSet.EMPTY; + } + CssParser parser = new CssParser(); + return parser.parseDeclaration(decl); + } + + /** + * Loads a set of rules that have been specified in terms of + * CSS1 grammar. If there are collisions with existing rules, + * the newly specified rule will win. + * + * @param in the stream to read the CSS grammar from + * @param ref the reference URL. This value represents the + * location of the stream and may be null. All relative + * URLs specified in the stream will be based upon this + * parameter. + */ + public void loadRules(Reader in, URL ref) throws IOException { + CssParser parser = new CssParser(); + parser.parse(ref, in, false, false); + } + + /** + * Fetches a set of attributes to use in the view for + * displaying. This is basically a set of attributes that + * can be used for View.getAttributes. + */ + public AttributeSet getViewAttributes(View v) { + return new ViewAttributeSet(v); + } + + /** + * Removes a named style previously added to the document. + * + * @param nm the name of the style to remove + */ + public void removeStyle(String nm) { + Style aStyle = getStyle(nm); + + if (aStyle != null) { + String selector = cleanSelectorString(nm); + String[] selectors = getSimpleSelectors(selector); + synchronized(this) { + SelectorMapping mapping = getRootSelectorMapping(); + for (int i = selectors.length - 1; i >= 0; i--) { + mapping = mapping.getChildSelectorMapping(selectors[i], + true); + } + Style rule = mapping.getStyle(); + if (rule != null) { + mapping.setStyle(null); + if (resolvedStyles.size() > 0) { + Enumeration values = resolvedStyles.elements(); + while (values.hasMoreElements()) { + ResolvedStyle style = (ResolvedStyle)values. + nextElement(); + style.removeStyle(rule); + } + } + } + } + } + super.removeStyle(nm); + } + + /** + * Adds the rules from the StyleSheet ss to those of + * the receiver. ss's rules will override the rules of + * any previously added style sheets. An added StyleSheet will never + * override the rules of the receiving style sheet. + * + * @since 1.3 + */ + public void addStyleSheet(StyleSheet ss) { + synchronized(this) { + if (linkedStyleSheets == null) { + linkedStyleSheets = new Vector(); + } + if (!linkedStyleSheets.contains(ss)) { + int index = 0; + if (ss instanceof javax.swing.plaf.UIResource + && linkedStyleSheets.size() > 1) { + index = linkedStyleSheets.size() - 1; + } + linkedStyleSheets.insertElementAt(ss, index); + linkStyleSheetAt(ss, index); + } + } + } + + /** + * Removes the StyleSheet ss from those of the receiver. + * + * @since 1.3 + */ + public void removeStyleSheet(StyleSheet ss) { + synchronized(this) { + if (linkedStyleSheets != null) { + int index = linkedStyleSheets.indexOf(ss); + if (index != -1) { + linkedStyleSheets.removeElementAt(index); + unlinkStyleSheet(ss, index); + if (index == 0 && linkedStyleSheets.size() == 0) { + linkedStyleSheets = null; + } + } + } + } + } + + // + // The following is used to import style sheets. + // + + /** + * Returns an array of the linked StyleSheets. Will return null + * if there are no linked StyleSheets. + * + * @since 1.3 + */ + public StyleSheet[] getStyleSheets() { + StyleSheet[] retValue; + + synchronized(this) { + if (linkedStyleSheets != null) { + retValue = new StyleSheet[linkedStyleSheets.size()]; + linkedStyleSheets.copyInto(retValue); + } + else { + retValue = null; + } + } + return retValue; + } + + /** + * Imports a style sheet from url. The resulting rules + * are directly added to the receiver. If you do not want the rules + * to become part of the receiver, create a new StyleSheet and use + * addStyleSheet to link it in. + * + * @since 1.3 + */ + public void importStyleSheet(URL url) { + try { + InputStream is; + + is = url.openStream(); + Reader r = new BufferedReader(new InputStreamReader(is)); + CssParser parser = new CssParser(); + parser.parse(url, r, false, true); + r.close(); + is.close(); + } catch (Throwable e) { + // on error we simply have no styles... the html + // will look mighty wrong but still function. + } + } + + /** + * Sets the base. All import statements that are relative, will be + * relative to base. + * + * @since 1.3 + */ + public void setBase(URL base) { + this.base = base; + } + + /** + * Returns the base. + * + * @since 1.3 + */ + public URL getBase() { + return base; + } + + /** + * Adds a CSS attribute to the given set. + * + * @since 1.3 + */ + public void addCSSAttribute(MutableAttributeSet attr, CSS.Attribute key, + String value) { + css.addInternalCSSValue(attr, key, value); + } + + /** + * Adds a CSS attribute to the given set. + * + * @since 1.3 + */ + public boolean addCSSAttributeFromHTML(MutableAttributeSet attr, + CSS.Attribute key, String value) { + Object iValue = css.getCssValue(key, value); + if (iValue != null) { + attr.addAttribute(key, iValue); + return true; + } + return false; + } + + // ---- Conversion functionality --------------------------------- + + /** + * Converts a set of HTML attributes to an equivalent + * set of CSS attributes. + * + * @param htmlAttrSet AttributeSet containing the HTML attributes. + */ + public AttributeSet translateHTMLToCSS(AttributeSet htmlAttrSet) { + AttributeSet cssAttrSet = css.translateHTMLToCSS(htmlAttrSet); + + MutableAttributeSet cssStyleSet = addStyle(null, null); + cssStyleSet.addAttributes(cssAttrSet); + + return cssStyleSet; + } + + /** + * Adds an attribute to the given set, and returns + * the new representative set. This is reimplemented to + * convert StyleConstant attributes to CSS prior to forwarding + * to the superclass behavior. The StyleConstants attribute + * has no corresponding CSS entry, the StyleConstants attribute + * is stored (but will likely be unused). + * + * @param old the old attribute set + * @param key the non-null attribute key + * @param value the attribute value + * @return the updated attribute set + * @see MutableAttributeSet#addAttribute + */ + public AttributeSet addAttribute(AttributeSet old, Object key, + Object value) { + if (css == null) { + // supers constructor will call this before returning, + // and we need to make sure CSS is non null. + css = new CSS(); + } + if (key instanceof StyleConstants) { + HTML.Tag tag = HTML.getTagForStyleConstantsKey( + (StyleConstants)key); + + if (tag != null && old.isDefined(tag)) { + old = removeAttribute(old, tag); + } + + Object cssValue = css.styleConstantsValueToCSSValue + ((StyleConstants)key, value); + if (cssValue != null) { + Object cssKey = css.styleConstantsKeyToCSSKey + ((StyleConstants)key); + if (cssKey != null) { + return super.addAttribute(old, cssKey, cssValue); + } + } + } + return super.addAttribute(old, key, value); + } + + /** + * Adds a set of attributes to the element. If any of these attributes + * are StyleConstants attributes, they will be converted to CSS prior + * to forwarding to the superclass behavior. + * + * @param old the old attribute set + * @param attr the attributes to add + * @return the updated attribute set + * @see MutableAttributeSet#addAttribute + */ + public AttributeSet addAttributes(AttributeSet old, AttributeSet attr) { + if (!(attr instanceof HTMLDocument.TaggedAttributeSet)) { + old = removeHTMLTags(old, attr); + } + return super.addAttributes(old, convertAttributeSet(attr)); + } + + /** + * Removes an attribute from the set. If the attribute is a StyleConstants + * attribute, the request will be converted to a CSS attribute prior to + * forwarding to the superclass behavior. + * + * @param old the old set of attributes + * @param key the non-null attribute name + * @return the updated attribute set + * @see MutableAttributeSet#removeAttribute + */ + public AttributeSet removeAttribute(AttributeSet old, Object key) { + if (key instanceof StyleConstants) { + HTML.Tag tag = HTML.getTagForStyleConstantsKey( + (StyleConstants)key); + if (tag != null) { + old = super.removeAttribute(old, tag); + } + + Object cssKey = css.styleConstantsKeyToCSSKey((StyleConstants)key); + if (cssKey != null) { + return super.removeAttribute(old, cssKey); + } + } + return super.removeAttribute(old, key); + } + + /** + * Removes a set of attributes for the element. If any of the attributes + * is a StyleConstants attribute, the request will be converted to a CSS + * attribute prior to forwarding to the superclass behavior. + * + * @param old the old attribute set + * @param names the attribute names + * @return the updated attribute set + * @see MutableAttributeSet#removeAttributes + */ + public AttributeSet removeAttributes(AttributeSet old, Enumeration names) { + // PENDING: Should really be doing something similar to + // removeHTMLTags here, but it is rather expensive to have to + // clone names + return super.removeAttributes(old, names); + } + + /** + * Removes a set of attributes. If any of the attributes + * is a StyleConstants attribute, the request will be converted to a CSS + * attribute prior to forwarding to the superclass behavior. + * + * @param old the old attribute set + * @param attrs the attributes + * @return the updated attribute set + * @see MutableAttributeSet#removeAttributes + */ + public AttributeSet removeAttributes(AttributeSet old, AttributeSet attrs) { + if (old != attrs) { + old = removeHTMLTags(old, attrs); + } + return super.removeAttributes(old, convertAttributeSet(attrs)); + } + + /** + * Creates a compact set of attributes that might be shared. + * This is a hook for subclasses that want to alter the + * behavior of SmallAttributeSet. This can be reimplemented + * to return an AttributeSet that provides some sort of + * attribute conversion. + * + * @param a The set of attributes to be represented in the + * the compact form. + */ + protected SmallAttributeSet createSmallAttributeSet(AttributeSet a) { + return new SmallConversionSet(a); + } + + /** + * Creates a large set of attributes that should trade off + * space for time. This set will not be shared. This is + * a hook for subclasses that want to alter the behavior + * of the larger attribute storage format (which is + * SimpleAttributeSet by default). This can be reimplemented + * to return a MutableAttributeSet that provides some sort of + * attribute conversion. + * + * @param a The set of attributes to be represented in the + * the larger form. + */ + protected MutableAttributeSet createLargeAttributeSet(AttributeSet a) { + return new LargeConversionSet(a); + } + + /** + * For any StyleConstants key in attr that has an associated HTML.Tag, + * it is removed from old. The resulting AttributeSet is then returned. + */ + private AttributeSet removeHTMLTags(AttributeSet old, AttributeSet attr) { + if (!(attr instanceof LargeConversionSet) && + !(attr instanceof SmallConversionSet)) { + Enumeration names = attr.getAttributeNames(); + + while (names.hasMoreElements()) { + Object key = names.nextElement(); + + if (key instanceof StyleConstants) { + HTML.Tag tag = HTML.getTagForStyleConstantsKey( + (StyleConstants)key); + + if (tag != null && old.isDefined(tag)) { + old = super.removeAttribute(old, tag); + } + } + } + } + return old; + } + + /** + * Converts a set of attributes (if necessary) so that + * any attributes that were specified as StyleConstants + * attributes and have a CSS mapping, will be converted + * to CSS attributes. + */ + AttributeSet convertAttributeSet(AttributeSet a) { + if ((a instanceof LargeConversionSet) || + (a instanceof SmallConversionSet)) { + // known to be converted. + return a; + } + // in most cases, there are no StyleConstants attributes + // so we iterate the collection of keys to avoid creating + // a new set. + Enumeration names = a.getAttributeNames(); + while (names.hasMoreElements()) { + Object name = names.nextElement(); + if (name instanceof StyleConstants) { + // we really need to do a conversion, iterate again + // building a new set. + MutableAttributeSet converted = new LargeConversionSet(); + Enumeration keys = a.getAttributeNames(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + Object cssValue = null; + if (key instanceof StyleConstants) { + // convert the StyleConstants attribute if possible + Object cssKey = css.styleConstantsKeyToCSSKey + ((StyleConstants)key); + if (cssKey != null) { + Object value = a.getAttribute(key); + cssValue = css.styleConstantsValueToCSSValue + ((StyleConstants)key, value); + if (cssValue != null) { + converted.addAttribute(cssKey, cssValue); + } + } + } + if (cssValue == null) { + converted.addAttribute(key, a.getAttribute(key)); + } + } + return converted; + } + } + return a; + } + + /** + * Large set of attributes that does conversion of requests + * for attributes of type StyleConstants. + */ + class LargeConversionSet extends SimpleAttributeSet { + + /** + * Creates a new attribute set based on a supplied set of attributes. + * + * @param source the set of attributes + */ + public LargeConversionSet(AttributeSet source) { + super(source); + } + + public LargeConversionSet() { + super(); + } + + /** + * Checks whether a given attribute is defined. + * + * @param key the attribute key + * @return true if the attribute is defined + * @see AttributeSet#isDefined + */ + public boolean isDefined(Object key) { + if (key instanceof StyleConstants) { + Object cssKey = css.styleConstantsKeyToCSSKey + ((StyleConstants)key); + if (cssKey != null) { + return super.isDefined(cssKey); + } + } + return super.isDefined(key); + } + + /** + * Gets the value of an attribute. + * + * @param key the attribute name + * @return the attribute value + * @see AttributeSet#getAttribute + */ + public Object getAttribute(Object key) { + if (key instanceof StyleConstants) { + Object cssKey = css.styleConstantsKeyToCSSKey + ((StyleConstants)key); + if (cssKey != null) { + Object value = super.getAttribute(cssKey); + if (value != null) { + return css.cssValueToStyleConstantsValue + ((StyleConstants)key, value); + } + } + } + return super.getAttribute(key); + } + } + + /** + * Small set of attributes that does conversion of requests + * for attributes of type StyleConstants. + */ + class SmallConversionSet extends SmallAttributeSet { + + /** + * Creates a new attribute set based on a supplied set of attributes. + * + * @param source the set of attributes + */ + public SmallConversionSet(AttributeSet attrs) { + super(attrs); + } + + /** + * Checks whether a given attribute is defined. + * + * @param key the attribute key + * @return true if the attribute is defined + * @see AttributeSet#isDefined + */ + public boolean isDefined(Object key) { + if (key instanceof StyleConstants) { + Object cssKey = css.styleConstantsKeyToCSSKey + ((StyleConstants)key); + if (cssKey != null) { + return super.isDefined(cssKey); + } + } + return super.isDefined(key); + } + + /** + * Gets the value of an attribute. + * + * @param key the attribute name + * @return the attribute value + * @see AttributeSet#getAttribute + */ + public Object getAttribute(Object key) { + if (key instanceof StyleConstants) { + Object cssKey = css.styleConstantsKeyToCSSKey + ((StyleConstants)key); + if (cssKey != null) { + Object value = super.getAttribute(cssKey); + if (value != null) { + return css.cssValueToStyleConstantsValue + ((StyleConstants)key, value); + } + } + } + return super.getAttribute(key); + } + } + + // ---- Resource handling ---------------------------------------- + + /** + * Fetches the font to use for the given set of attributes. + */ + public Font getFont(AttributeSet a) { + return css.getFont(this, a, 12, this); + } + + /** + * Takes a set of attributes and turn it into a foreground color + * specification. This might be used to specify things + * like brighter, more hue, etc. + * + * @param a the set of attributes + * @return the color + */ + public Color getForeground(AttributeSet a) { + Color c = css.getColor(a, CSS.Attribute.COLOR); + if (c == null) { + return Color.black; + } + return c; + } + + /** + * Takes a set of attributes and turn it into a background color + * specification. This might be used to specify things + * like brighter, more hue, etc. + * + * @param a the set of attributes + * @return the color + */ + public Color getBackground(AttributeSet a) { + return css.getColor(a, CSS.Attribute.BACKGROUND_COLOR); + } + + /** + * Fetches the box formatter to use for the given set + * of CSS attributes. + */ + public BoxPainter getBoxPainter(AttributeSet a) { + return new BoxPainter(a, css, this); + } + + /** + * Fetches the list formatter to use for the given set + * of CSS attributes. + */ + public ListPainter getListPainter(AttributeSet a) { + return new ListPainter(a, this); + } + + /** + * Sets the base font size, with valid values between 1 and 7. + */ + public void setBaseFontSize(int sz) { + css.setBaseFontSize(sz); + } + + /** + * Sets the base font size from the passed in String. The string + * can either identify a specific font size, with legal values between + * 1 and 7, or identifiy a relative font size such as +1 or -2. + */ + public void setBaseFontSize(String size) { + css.setBaseFontSize(size); + } + + public static int getIndexOfSize(float pt) { + return CSS.getIndexOfSize(pt, sizeMapDefault); + } + + /** + * Returns the point size, given a size index. + */ + public float getPointSize(int index) { + return css.getPointSize(index, this); + } + + /** + * Given a string such as "+2", "-2", or "2", + * returns a point size value. + */ + public float getPointSize(String size) { + return css.getPointSize(size, this); + } + + /** + * Converts a color string such as "RED" or "#NNNNNN" to a Color. + * Note: This will only convert the HTML3.2 color strings + * or a string of length 7; + * otherwise, it will return null. + */ + public Color stringToColor(String string) { + return CSS.stringToColor(string); + } + + /** + * Returns the ImageIcon to draw in the background for + * attr. + */ + ImageIcon getBackgroundImage(AttributeSet attr) { + Object value = attr.getAttribute(CSS.Attribute.BACKGROUND_IMAGE); + + if (value != null) { + return ((CSS.BackgroundImage)value).getImage(getBase()); + } + return null; + } + + /** + * Adds a rule into the StyleSheet. + * + * @param selector the selector to use for the rule. + * This will be a set of simple selectors, and must + * be a length of 1 or greater. + * @param declaration the set of CSS attributes that + * make up the rule. + */ + void addRule(String[] selector, AttributeSet declaration, + boolean isLinked) { + int n = selector.length; + StringBuffer sb = new StringBuffer(); + sb.append(selector[0]); + for (int counter = 1; counter < n; counter++) { + sb.append(' '); + sb.append(selector[counter]); + } + String selectorName = sb.toString(); + Style rule = getStyle(selectorName); + if (rule == null) { + // Notice how the rule is first created, and it not part of + // the synchronized block. It is done like this as creating + // a new rule will fire a ChangeEvent. We do not want to be + // holding the lock when calling to other objects, it can + // result in deadlock. + Style altRule = addStyle(selectorName, null); + synchronized(this) { + SelectorMapping mapping = getRootSelectorMapping(); + for (int i = n - 1; i >= 0; i--) { + mapping = mapping.getChildSelectorMapping + (selector[i], true); + } + rule = mapping.getStyle(); + if (rule == null) { + rule = altRule; + mapping.setStyle(rule); + refreshResolvedRules(selectorName, selector, rule, + mapping.getSpecificity()); + } + } + } + if (isLinked) { + rule = getLinkedStyle(rule); + } + rule.addAttributes(declaration); + } + + // + // The following gaggle of methods is used in maintaing the rules from + // the sheet. + // + + /** + * Updates the attributes of the rules to reference any related + * rules in ss. + */ + private synchronized void linkStyleSheetAt(StyleSheet ss, int index) { + if (resolvedStyles.size() > 0) { + Enumeration values = resolvedStyles.elements(); + while (values.hasMoreElements()) { + ResolvedStyle rule = (ResolvedStyle)values.nextElement(); + rule.insertExtendedStyleAt(ss.getRule(rule.getName()), + index); + } + } + } + + /** + * Removes references to the rules in ss. + * index gives the index the StyleSheet was at, that is + * how many StyleSheets had been added before it. + */ + private synchronized void unlinkStyleSheet(StyleSheet ss, int index) { + if (resolvedStyles.size() > 0) { + Enumeration values = resolvedStyles.elements(); + while (values.hasMoreElements()) { + ResolvedStyle rule = (ResolvedStyle)values.nextElement(); + rule.removeExtendedStyleAt(index); + } + } + } + + /** + * Returns the simple selectors that comprise selector. + */ + /* protected */ + String[] getSimpleSelectors(String selector) { + selector = cleanSelectorString(selector); + SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); + Vector selectors = sb.getVector(); + int lastIndex = 0; + int length = selector.length(); + while (lastIndex != -1) { + int newIndex = selector.indexOf(' ', lastIndex); + if (newIndex != -1) { + selectors.addElement(selector.substring(lastIndex, newIndex)); + if (++newIndex == length) { + lastIndex = -1; + } + else { + lastIndex = newIndex; + } + } + else { + selectors.addElement(selector.substring(lastIndex)); + lastIndex = -1; + } + } + String[] retValue = new String[selectors.size()]; + selectors.copyInto(retValue); + SearchBuffer.releaseSearchBuffer(sb); + return retValue; + } + + /** + * Returns a string that only has one space between simple selectors, + * which may be the passed in String. + */ + /*protected*/ String cleanSelectorString(String selector) { + boolean lastWasSpace = true; + for (int counter = 0, maxCounter = selector.length(); + counter < maxCounter; counter++) { + switch(selector.charAt(counter)) { + case ' ': + if (lastWasSpace) { + return _cleanSelectorString(selector); + } + lastWasSpace = true; + break; + case '\n': + case '\r': + case '\t': + return _cleanSelectorString(selector); + default: + lastWasSpace = false; + } + } + if (lastWasSpace) { + return _cleanSelectorString(selector); + } + // It was fine. + return selector; + } + + /** + * Returns a new String that contains only one space between non + * white space characters. + */ + private String _cleanSelectorString(String selector) { + SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); + StringBuffer buff = sb.getStringBuffer(); + boolean lastWasSpace = true; + int lastIndex = 0; + char[] chars = selector.toCharArray(); + int numChars = chars.length; + String retValue = null; + try { + for (int counter = 0; counter < numChars; counter++) { + switch(chars[counter]) { + case ' ': + if (!lastWasSpace) { + lastWasSpace = true; + if (lastIndex < counter) { + buff.append(chars, lastIndex, + 1 + counter - lastIndex); + } + } + lastIndex = counter + 1; + break; + case '\n': + case '\r': + case '\t': + if (!lastWasSpace) { + lastWasSpace = true; + if (lastIndex < counter) { + buff.append(chars, lastIndex, + counter - lastIndex); + buff.append(' '); + } + } + lastIndex = counter + 1; + break; + default: + lastWasSpace = false; + break; + } + } + if (lastWasSpace && buff.length() > 0) { + // Remove last space. + buff.setLength(buff.length() - 1); + } + else if (lastIndex < numChars) { + buff.append(chars, lastIndex, numChars - lastIndex); + } + retValue = buff.toString(); + } + finally { + SearchBuffer.releaseSearchBuffer(sb); + } + return retValue; + } + + /** + * Returns the root selector mapping that all selectors are relative + * to. This is an inverted graph of the selectors. + */ + private SelectorMapping getRootSelectorMapping() { + return selectorMapping; + } + + /** + * Returns the specificity of the passed in String. It assumes the + * passed in string doesn't contain junk, that is each selector is + * separated by a space and each selector at most contains one . or one + * #. A simple selector has a weight of 1, an id selector has a weight + * of 100, and a class selector has a weight of 10000. + */ + /*protected*/ static int getSpecificity(String selector) { + int specificity = 0; + boolean lastWasSpace = true; + + for (int counter = 0, maxCounter = selector.length(); + counter < maxCounter; counter++) { + switch(selector.charAt(counter)) { + case '.': + specificity += 100; + break; + case '#': + specificity += 10000; + break; + case ' ': + lastWasSpace = true; + break; + default: + if (lastWasSpace) { + lastWasSpace = false; + specificity += 1; + } + } + } + return specificity; + } + + /** + * Returns the style that linked attributes should be added to. This + * will create the style if necessary. + */ + private Style getLinkedStyle(Style localStyle) { + // NOTE: This is not synchronized, and the caller of this does + // not synchronize. There is the chance for one of the callers to + // overwrite the existing resolved parent, but it is quite rare. + // The reason this is left like this is because setResolveParent + // will fire a ChangeEvent. It is really, REALLY bad for us to + // hold a lock when calling outside of us, it may cause a deadlock. + Style retStyle = (Style)localStyle.getResolveParent(); + if (retStyle == null) { + retStyle = addStyle(null, null); + localStyle.setResolveParent(retStyle); + } + return retStyle; + } + + /** + * Returns the resolved style for selector. This will + * create the resolved style, if necessary. + */ + private synchronized Style getResolvedStyle(String selector, + Vector elements, + HTML.Tag t) { + Style retStyle = (Style)resolvedStyles.get(selector); + if (retStyle == null) { + retStyle = createResolvedStyle(selector, elements, t); + } + return retStyle; + } + + /** + * Returns the resolved style for selector. This will + * create the resolved style, if necessary. + */ + private synchronized Style getResolvedStyle(String selector) { + Style retStyle = (Style)resolvedStyles.get(selector); + if (retStyle == null) { + retStyle = createResolvedStyle(selector); + } + return retStyle; + } + + /** + * Adds mapping to elements. It is added + * such that elements will remain ordered by + * specificity. + */ + private void addSortedStyle(SelectorMapping mapping, Vector elements) { + int size = elements.size(); + + if (size > 0) { + int specificity = mapping.getSpecificity(); + + for (int counter = 0; counter < size; counter++) { + if (specificity >= ((SelectorMapping)elements.elementAt + (counter)).getSpecificity()) { + elements.insertElementAt(mapping, counter); + return; + } + } + } + elements.addElement(mapping); + } + + /** + * Adds parentMapping to styles, and + * recursively calls this method if parentMapping has + * any child mappings for any of the Elements in elements. + */ + private synchronized void getStyles(SelectorMapping parentMapping, + Vector styles, + String[] tags, String[] ids, String[] classes, + int index, int numElements, + Hashtable alreadyChecked) { + // Avoid desending the same mapping twice. + if (alreadyChecked.contains(parentMapping)) { + return; + } + alreadyChecked.put(parentMapping, parentMapping); + Style style = parentMapping.getStyle(); + if (style != null) { + addSortedStyle(parentMapping, styles); + } + for (int counter = index; counter < numElements; counter++) { + String tagString = tags[counter]; + if (tagString != null) { + SelectorMapping childMapping = parentMapping. + getChildSelectorMapping(tagString, false); + if (childMapping != null) { + getStyles(childMapping, styles, tags, ids, classes, + counter + 1, numElements, alreadyChecked); + } + if (classes[counter] != null) { + String className = classes[counter]; + childMapping = parentMapping.getChildSelectorMapping( + tagString + "." + className, false); + if (childMapping != null) { + getStyles(childMapping, styles, tags, ids, classes, + counter + 1, numElements, alreadyChecked); + } + childMapping = parentMapping.getChildSelectorMapping( + "." + className, false); + if (childMapping != null) { + getStyles(childMapping, styles, tags, ids, classes, + counter + 1, numElements, alreadyChecked); + } + } + if (ids[counter] != null) { + String idName = ids[counter]; + childMapping = parentMapping.getChildSelectorMapping( + tagString + "#" + idName, false); + if (childMapping != null) { + getStyles(childMapping, styles, tags, ids, classes, + counter + 1, numElements, alreadyChecked); + } + childMapping = parentMapping.getChildSelectorMapping( + "#" + idName, false); + if (childMapping != null) { + getStyles(childMapping, styles, tags, ids, classes, + counter + 1, numElements, alreadyChecked); + } + } + } + } + } + + /** + * Creates and returns a Style containing all the rules that match + * selector. + */ + private synchronized Style createResolvedStyle(String selector, + String[] tags, + String[] ids, String[] classes) { + SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); + Vector tempVector = sb.getVector(); + Hashtable tempHashtable = sb.getHashtable(); + // Determine all the Styles that are appropriate, placing them + // in tempVector + try { + SelectorMapping mapping = getRootSelectorMapping(); + int numElements = tags.length; + String tagString = tags[0]; + SelectorMapping childMapping = mapping.getChildSelectorMapping( + tagString, false); + if (childMapping != null) { + getStyles(childMapping, tempVector, tags, ids, classes, 1, + numElements, tempHashtable); + } + if (classes[0] != null) { + String className = classes[0]; + childMapping = mapping.getChildSelectorMapping( + tagString + "." + className, false); + if (childMapping != null) { + getStyles(childMapping, tempVector, tags, ids, classes, 1, + numElements, tempHashtable); + } + childMapping = mapping.getChildSelectorMapping( + "." + className, false); + if (childMapping != null) { + getStyles(childMapping, tempVector, tags, ids, classes, + 1, numElements, tempHashtable); + } + } + if (ids[0] != null) { + String idName = ids[0]; + childMapping = mapping.getChildSelectorMapping( + tagString + "#" + idName, false); + if (childMapping != null) { + getStyles(childMapping, tempVector, tags, ids, classes, + 1, numElements, tempHashtable); + } + childMapping = mapping.getChildSelectorMapping( + "#" + idName, false); + if (childMapping != null) { + getStyles(childMapping, tempVector, tags, ids, classes, + 1, numElements, tempHashtable); + } + } + // Create a new Style that will delegate to all the matching + // Styles. + int numLinkedSS = (linkedStyleSheets != null) ? + linkedStyleSheets.size() : 0; + int numStyles = tempVector.size(); + AttributeSet[] attrs = new AttributeSet[numStyles + numLinkedSS]; + for (int counter = 0; counter < numStyles; counter++) { + attrs[counter] = ((SelectorMapping)tempVector. + elementAt(counter)).getStyle(); + } + // Get the AttributeSet from linked style sheets. + for (int counter = 0; counter < numLinkedSS; counter++) { + AttributeSet attr = ((StyleSheet)linkedStyleSheets. + elementAt(counter)).getRule(selector); + if (attr == null) { + attrs[counter + numStyles] = SimpleAttributeSet.EMPTY; + } + else { + attrs[counter + numStyles] = attr; + } + } + ResolvedStyle retStyle = new ResolvedStyle(selector, attrs, + numStyles); + resolvedStyles.put(selector, retStyle); + return retStyle; + } + finally { + SearchBuffer.releaseSearchBuffer(sb); + } + } + + /** + * Creates and returns a Style containing all the rules that + * matches selector. + * + * @param elements a Vector of all the Elements + * the style is being asked for. The + * first Element is the deepest Element, with the last Element + * representing the root. + * @param t the Tag to use for + * the first Element in elements + */ + private Style createResolvedStyle(String selector, Vector elements, + HTML.Tag t) { + int numElements = elements.size(); + // Build three arrays, one for tags, one for class's, and one for + // id's + String tags[] = new String[numElements]; + String ids[] = new String[numElements]; + String classes[] = new String[numElements]; + for (int counter = 0; counter < numElements; counter++) { + Element e = (Element)elements.elementAt(counter); + AttributeSet attr = e.getAttributes(); + if (counter == 0 && e.isLeaf()) { + // For leafs, we use the second tier attributes. + Object testAttr = attr.getAttribute(t); + if (testAttr instanceof AttributeSet) { + attr = (AttributeSet)testAttr; + } + else { + attr = null; + } + } + if (attr != null) { + HTML.Tag tag = (HTML.Tag)attr.getAttribute(StyleConstants. + NameAttribute); + if (tag != null) { + tags[counter] = tag.toString(); + } + else { + tags[counter] = null; + } + if (attr.isDefined(HTML.Attribute.CLASS)) { + classes[counter] = attr.getAttribute + (HTML.Attribute.CLASS).toString(); + } + else { + classes[counter] = null; + } + if (attr.isDefined(HTML.Attribute.ID)) { + ids[counter] = attr.getAttribute(HTML.Attribute.ID). + toString(); + } + else { + ids[counter] = null; + } + } + else { + tags[counter] = ids[counter] = classes[counter] = null; + } + } + tags[0] = t.toString(); + return createResolvedStyle(selector, tags, ids, classes); + } + + /** + * Creates and returns a Style containing all the rules that match + * selector. It is assumed that each simple selector + * in selector is separated by a space. + */ + private Style createResolvedStyle(String selector) { + SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); + // Will contain the tags, ids, and classes, in that order. + Vector elements = sb.getVector(); + try { + boolean done; + int dotIndex = 0; + int spaceIndex = 0; + int poundIndex = 0; + int lastIndex = 0; + int length = selector.length(); + while (lastIndex < length) { + if (dotIndex == lastIndex) { + dotIndex = selector.indexOf('.', lastIndex); + } + if (poundIndex == lastIndex) { + poundIndex = selector.indexOf('#', lastIndex); + } + spaceIndex = selector.indexOf(' ', lastIndex); + if (spaceIndex == -1) { + spaceIndex = length; + } + if (dotIndex != -1 && poundIndex != -1 && + dotIndex < spaceIndex && poundIndex < spaceIndex) { + if (poundIndex < dotIndex) { + // #. + if (lastIndex == poundIndex) { + elements.addElement(""); + } + else { + elements.addElement(selector.substring(lastIndex, + poundIndex)); + } + if ((dotIndex + 1) < spaceIndex) { + elements.addElement(selector.substring + (dotIndex + 1, spaceIndex)); + } + else { + elements.addElement(null); + } + if ((poundIndex + 1) == dotIndex) { + elements.addElement(null); + } + else { + elements.addElement(selector.substring + (poundIndex + 1, dotIndex)); + } + } + else if(poundIndex < spaceIndex) { + // .# + if (lastIndex == dotIndex) { + elements.addElement(""); + } + else { + elements.addElement(selector.substring(lastIndex, + dotIndex)); + } + if ((dotIndex + 1) < poundIndex) { + elements.addElement(selector.substring + (dotIndex + 1, poundIndex)); + } + else { + elements.addElement(null); + } + if ((poundIndex + 1) == spaceIndex) { + elements.addElement(null); + } + else { + elements.addElement(selector.substring + (poundIndex + 1, spaceIndex)); + } + } + dotIndex = poundIndex = spaceIndex + 1; + } + else if (dotIndex != -1 && dotIndex < spaceIndex) { + // . + if (dotIndex == lastIndex) { + elements.addElement(""); + } + else { + elements.addElement(selector.substring(lastIndex, + dotIndex)); + } + if ((dotIndex + 1) == spaceIndex) { + elements.addElement(null); + } + else { + elements.addElement(selector.substring(dotIndex + 1, + spaceIndex)); + } + elements.addElement(null); + dotIndex = spaceIndex + 1; + } + else if (poundIndex != -1 && poundIndex < spaceIndex) { + // # + if (poundIndex == lastIndex) { + elements.addElement(""); + } + else { + elements.addElement(selector.substring(lastIndex, + poundIndex)); + } + elements.addElement(null); + if ((poundIndex + 1) == spaceIndex) { + elements.addElement(null); + } + else { + elements.addElement(selector.substring(poundIndex + 1, + spaceIndex)); + } + poundIndex = spaceIndex + 1; + } + else { + // id + elements.addElement(selector.substring(lastIndex, + spaceIndex)); + elements.addElement(null); + elements.addElement(null); + } + lastIndex = spaceIndex + 1; + } + // Create the tag, id, and class arrays. + int total = elements.size(); + int numTags = total / 3; + String[] tags = new String[numTags]; + String[] ids = new String[numTags]; + String[] classes = new String[numTags]; + for (int index = 0, eIndex = total - 3; index < numTags; + index++, eIndex -= 3) { + tags[index] = (String)elements.elementAt(eIndex); + classes[index] = (String)elements.elementAt(eIndex + 1); + ids[index] = (String)elements.elementAt(eIndex + 2); + } + return createResolvedStyle(selector, tags, ids, classes); + } + finally { + SearchBuffer.releaseSearchBuffer(sb); + } + } + + /** + * Should be invoked when a new rule is added that did not previously + * exist. Goes through and refreshes the necessary resolved + * rules. + */ + private synchronized void refreshResolvedRules(String selectorName, + String[] selector, + Style newStyle, + int specificity) { + if (resolvedStyles.size() > 0) { + Enumeration values = resolvedStyles.elements(); + while (values.hasMoreElements()) { + ResolvedStyle style = (ResolvedStyle)values.nextElement(); + if (style.matches(selectorName)) { + style.insertStyle(newStyle, specificity); + } + } + } + } + + + /** + * A temporary class used to hold a Vector, a StringBuffer and a + * Hashtable. This is used to avoid allocing a lot of garbage when + * searching for rules. Use the static method obtainSearchBuffer and + * releaseSearchBuffer to get a SearchBuffer, and release it when + * done. + */ + private static class SearchBuffer { + /** A stack containing instances of SearchBuffer. Used in getting + * rules. */ + static Stack searchBuffers = new Stack(); + // A set of temporary variables that can be used in whatever way. + Vector vector = null; + StringBuffer stringBuffer = null; + Hashtable hashtable = null; + + /** + * Returns an instance of SearchBuffer. Be sure and issue + * a releaseSearchBuffer when done with it. + */ + static SearchBuffer obtainSearchBuffer() { + SearchBuffer sb; + try { + if(!searchBuffers.empty()) { + sb = (SearchBuffer)searchBuffers.pop(); + } else { + sb = new SearchBuffer(); + } + } catch (EmptyStackException ese) { + sb = new SearchBuffer(); + } + return sb; + } + + /** + * Adds sb to the stack of SearchBuffers that can + * be used. + */ + static void releaseSearchBuffer(SearchBuffer sb) { + sb.empty(); + searchBuffers.push(sb); + } + + StringBuffer getStringBuffer() { + if (stringBuffer == null) { + stringBuffer = new StringBuffer(); + } + return stringBuffer; + } + + Vector getVector() { + if (vector == null) { + vector = new Vector(); + } + return vector; + } + + Hashtable getHashtable() { + if (hashtable == null) { + hashtable = new Hashtable(); + } + return hashtable; + } + + void empty() { + if (stringBuffer != null) { + stringBuffer.setLength(0); + } + if (vector != null) { + vector.removeAllElements(); + } + if (hashtable != null) { + hashtable.clear(); + } + } + } + + + static final Border noBorder = new EmptyBorder(0,0,0,0); + + /** + * Class to carry out some of the duties of + * CSS formatting. Implementations of this + * class enable views to present the CSS formatting + * while not knowing anything about how the CSS values + * are being cached. + *

    + * As a delegate of Views, this object is responsible for + * the insets of a View and making sure the background + * is maintained according to the CSS attributes. + */ + public static class BoxPainter implements Serializable { + + BoxPainter(AttributeSet a, CSS css, StyleSheet ss) { + this.ss = ss; + this.css = css; + border = getBorder(a); + binsets = border.getBorderInsets(null); + topMargin = getLength(CSS.Attribute.MARGIN_TOP, a); + bottomMargin = getLength(CSS.Attribute.MARGIN_BOTTOM, a); + leftMargin = getLength(CSS.Attribute.MARGIN_LEFT, a); + rightMargin = getLength(CSS.Attribute.MARGIN_RIGHT, a); + bg = ss.getBackground(a); + if (ss.getBackgroundImage(a) != null) { + bgPainter = new BackgroundImagePainter(a, css, ss); + } + } + + /** + * Fetches a border to render for the given attributes. + * PENDING(prinz) This is pretty badly hacked at the + * moment. + */ + Border getBorder(AttributeSet a) { + return new CSSBorder(a); + } + + /** + * Fetches the color to use for borders. This will either be + * the value specified by the border-color attribute (which + * is not inherited), or it will default to the color attribute + * (which is inherited). + */ + Color getBorderColor(AttributeSet a) { + Color color = css.getColor(a, CSS.Attribute.BORDER_COLOR); + if (color == null) { + color = css.getColor(a, CSS.Attribute.COLOR); + if (color == null) { + return Color.black; + } + } + return color; + } + + /** + * Fetches the inset needed on a given side to + * account for the margin, border, and padding. + * + * @param side The size of the box to fetch the + * inset for. This can be View.TOP, + * View.LEFT, View.BOTTOM, or View.RIGHT. + * @param v the view making the request. This is + * used to get the AttributeSet, and may be used to + * resolve percentage arguments. + * @exception IllegalArgumentException for an invalid direction + */ + public float getInset(int side, View v) { + AttributeSet a = v.getAttributes(); + float inset = 0; + switch(side) { + case View.LEFT: + inset += getOrientationMargin(HorizontalMargin.LEFT, + leftMargin, a, isLeftToRight(v)); + inset += binsets.left; + inset += getLength(CSS.Attribute.PADDING_LEFT, a); + break; + case View.RIGHT: + inset += getOrientationMargin(HorizontalMargin.RIGHT, + rightMargin, a, isLeftToRight(v)); + inset += binsets.right; + inset += getLength(CSS.Attribute.PADDING_RIGHT, a); + break; + case View.TOP: + inset += topMargin; + inset += binsets.top; + inset += getLength(CSS.Attribute.PADDING_TOP, a); + break; + case View.BOTTOM: + inset += bottomMargin; + inset += binsets.bottom; + inset += getLength(CSS.Attribute.PADDING_BOTTOM, a); + break; + default: + throw new IllegalArgumentException("Invalid side: " + side); + } + return inset; + } + + /** + * Paints the CSS box according to the attributes + * given. This should paint the border, padding, + * and background. + * + * @param g the rendering surface. + * @param x the x coordinate of the allocated area to + * render into. + * @param y the y coordinate of the allocated area to + * render into. + * @param w the width of the allocated area to render into. + * @param h the height of the allocated area to render into. + * @param v the view making the request. This is + * used to get the AttributeSet, and may be used to + * resolve percentage arguments. + */ + public void paint(Graphics g, float x, float y, float w, float h, View v) { + // PENDING(prinz) implement real rendering... which would + // do full set of border and background capabilities. + // remove margin + + float dx = 0; + float dy = 0; + float dw = 0; + float dh = 0; + AttributeSet a = v.getAttributes(); + boolean isLeftToRight = isLeftToRight(v); + float localLeftMargin = getOrientationMargin(HorizontalMargin.LEFT, + leftMargin, + a, isLeftToRight); + float localRightMargin = getOrientationMargin(HorizontalMargin.RIGHT, + rightMargin, + a, isLeftToRight); + if (!(v instanceof HTMLEditorKit.HTMLFactory.BodyBlockView)) { + dx = localLeftMargin; + dy = topMargin; + dw = -(localLeftMargin + localRightMargin); + dh = -(topMargin + bottomMargin); + } + if (bg != null) { + g.setColor(bg); + g.fillRect((int) (x + dx), + (int) (y + dy), + (int) (w + dw), + (int) (h + dh)); + } + if (bgPainter != null) { + bgPainter.paint(g, x + dx, y + dy, w + dw, h + dh, v); + } + x += localLeftMargin; + y += topMargin; + w -= localLeftMargin + localRightMargin; + h -= topMargin + bottomMargin; + if (border instanceof BevelBorder) { + //BevelBorder does not support border width + int bw = (int) getLength(CSS.Attribute.BORDER_TOP_WIDTH, a); + for (int i = bw - 1; i >= 0; i--) { + border.paintBorder(null, g, (int) x + i, (int) y + i, + (int) w - 2 * i, (int) h - 2 * i); + } + } else { + border.paintBorder(null, g, (int) x, (int) y, (int) w, (int) h); + } + } + + float getLength(CSS.Attribute key, AttributeSet a) { + return css.getLength(a, key, ss); + } + + static boolean isLeftToRight(View v) { + boolean ret = true; + if (isOrientationAware(v)) { + Container container = null; + if (v != null && (container = v.getContainer()) != null) { + ret = container.getComponentOrientation().isLeftToRight(); + } + } + return ret; + } + + /* + * only certain tags are concerned about orientation + *

    , ,
      ,
        + * for all others we return true. It is implemented this way + * for performance purposes + */ + static boolean isOrientationAware(View v) { + boolean ret = false; + AttributeSet attr = null; + Object obj = null; + if (v != null + && (attr = v.getElement().getAttributes()) != null + && (obj = attr.getAttribute(StyleConstants.NameAttribute)) instanceof HTML.Tag + && (obj == HTML.Tag.DIR + || obj == HTML.Tag.MENU + || obj == HTML.Tag.UL + || obj == HTML.Tag.OL)) { + ret = true; + } + + return ret; + } + + static enum HorizontalMargin { LEFT, RIGHT }; + + /** + * for , ,
          etc. + * margins are Left-To-Right/Right-To-Left depended. + * see 5088268 for more details + * margin-(left|right)-(ltr|rtl) were introduced to describe it + * if margin-(left|right) is present we are to use it. + * + * @param side The horizontal side to fetch margin for + * This can be HorizontalMargin.LEFT or HorizontalMargin.RIGHT + * @param cssMargin margin from css + * @param a AttributeSet for the View we getting margin for + * @param isLeftToRight + * @return orientation depended margin + */ + float getOrientationMargin(HorizontalMargin side, float cssMargin, + AttributeSet a, boolean isLeftToRight) { + float margin = cssMargin; + float orientationMargin = cssMargin; + Object cssMarginValue = null; + switch (side) { + case RIGHT: + { + orientationMargin = (isLeftToRight) ? + getLength(CSS.Attribute.MARGIN_RIGHT_LTR, a) : + getLength(CSS.Attribute.MARGIN_RIGHT_RTL, a); + cssMarginValue = a.getAttribute(CSS.Attribute.MARGIN_RIGHT); + } + break; + case LEFT : + { + orientationMargin = (isLeftToRight) ? + getLength(CSS.Attribute.MARGIN_LEFT_LTR, a) : + getLength(CSS.Attribute.MARGIN_LEFT_RTL, a); + cssMarginValue = a.getAttribute(CSS.Attribute.MARGIN_LEFT); + } + break; + } + + if (cssMarginValue == null + && orientationMargin != Integer.MIN_VALUE) { + margin = orientationMargin; + } + return margin; + } + + float topMargin; + float bottomMargin; + float leftMargin; + float rightMargin; + // Bitmask, used to indicate what margins are relative: + // bit 0 for top, 1 for bottom, 2 for left and 3 for right. + short marginFlags; + Border border; + Insets binsets; + CSS css; + StyleSheet ss; + Color bg; + BackgroundImagePainter bgPainter; + } + + /** + * Class to carry out some of the duties of CSS list + * formatting. Implementations of this + * class enable views to present the CSS formatting + * while not knowing anything about how the CSS values + * are being cached. + */ + public static class ListPainter implements Serializable { + + ListPainter(AttributeSet attr, StyleSheet ss) { + this.ss = ss; + /* Get the image to use as a list bullet */ + String imgstr = (String)attr.getAttribute(CSS.Attribute. + LIST_STYLE_IMAGE); + type = null; + if (imgstr != null && !imgstr.equals("none")) { + String tmpstr = null; + try { + StringTokenizer st = new StringTokenizer(imgstr, "()"); + if (st.hasMoreTokens()) + tmpstr = st.nextToken(); + if (st.hasMoreTokens()) + tmpstr = st.nextToken(); + URL u = new URL(tmpstr); + img = new ImageIcon(u); + } catch (MalformedURLException e) { + if (tmpstr != null && ss != null && ss.getBase() != null) { + try { + URL u = new URL(ss.getBase(), tmpstr); + img = new ImageIcon(u); + } catch (MalformedURLException murle) { + img = null; + } + } + else { + img = null; + } + } + } + + /* Get the type of bullet to use in the list */ + if (img == null) { + type = (CSS.Value)attr.getAttribute(CSS.Attribute. + LIST_STYLE_TYPE); + } + start = 1; + + paintRect = new Rectangle(); + } + + /** + * Returns a string that represents the value + * of the HTML.Attribute.TYPE attribute. + * If this attributes is not defined, then + * then the type defaults to "disc" unless + * the tag is on Ordered list. In the case + * of the latter, the default type is "decimal". + */ + private CSS.Value getChildType(View childView) { + CSS.Value childtype = (CSS.Value)childView.getAttributes(). + getAttribute(CSS.Attribute.LIST_STYLE_TYPE); + + if (childtype == null) { + if (type == null) { + // Parent view. + View v = childView.getParent(); + HTMLDocument doc = (HTMLDocument)v.getDocument(); + if (doc.matchNameAttribute(v.getElement().getAttributes(), + HTML.Tag.OL)) { + childtype = CSS.Value.DECIMAL; + } else { + childtype = CSS.Value.DISC; + } + } else { + childtype = type; + } + } + return childtype; + } + + /** + * Obtains the starting index from parent. + */ + private void getStart(View parent) { + checkedForStart = true; + Element element = parent.getElement(); + if (element != null) { + AttributeSet attr = element.getAttributes(); + Object startValue; + if (attr != null && attr.isDefined(HTML.Attribute.START) && + (startValue = attr.getAttribute + (HTML.Attribute.START)) != null && + (startValue instanceof String)) { + + try { + start = Integer.parseInt((String)startValue); + } + catch (NumberFormatException nfe) {} + } + } + } + + /** + * Returns an integer that should be used to render the child at + * childIndex with. The retValue will usually be + * childIndex + 1, unless parentView + * has some Views that do not represent LI's, or one of the views + * has a HTML.Attribute.START specified. + */ + private int getRenderIndex(View parentView, int childIndex) { + if (!checkedForStart) { + getStart(parentView); + } + int retIndex = childIndex; + for (int counter = childIndex; counter >= 0; counter--) { + AttributeSet as = parentView.getElement().getElement(counter). + getAttributes(); + if (as.getAttribute(StyleConstants.NameAttribute) != + HTML.Tag.LI) { + retIndex--; + } else if (as.isDefined(HTML.Attribute.VALUE)) { + Object value = as.getAttribute(HTML.Attribute.VALUE); + if (value != null && + (value instanceof String)) { + try { + int iValue = Integer.parseInt((String)value); + return retIndex - counter + iValue; + } + catch (NumberFormatException nfe) {} + } + } + } + return retIndex + start; + } + + /** + * Paints the CSS list decoration according to the + * attributes given. + * + * @param g the rendering surface. + * @param x the x coordinate of the list item allocation + * @param y the y coordinate of the list item allocation + * @param w the width of the list item allocation + * @param h the height of the list item allocation + * @param v the allocated area to paint into. + * @param item which list item is being painted. This + * is a number greater than or equal to 0. + */ + public void paint(Graphics g, float x, float y, float w, float h, View v, int item) { + View cv = v.getView(item); + Object name = cv.getElement().getAttributes().getAttribute + (StyleConstants.NameAttribute); + // Only draw something if the View is a list item. This won't + // be the case for comments. + if (!(name instanceof HTML.Tag) || + name != HTML.Tag.LI) { + return; + } + // deside on what side draw bullets, etc. + isLeftToRight = + cv.getContainer().getComponentOrientation().isLeftToRight(); + + // How the list indicator is aligned is not specified, it is + // left up to the UA. IE and NS differ on this behavior. + // This is closer to NS where we align to the first line of text. + // If the child is not text we draw the indicator at the + // origin (0). + float align = 0; + if (cv.getViewCount() > 0) { + View pView = cv.getView(0); + Object cName = pView.getElement().getAttributes(). + getAttribute(StyleConstants.NameAttribute); + if ((cName == HTML.Tag.P || cName == HTML.Tag.IMPLIED) && + pView.getViewCount() > 0) { + paintRect.setBounds((int)x, (int)y, (int)w, (int)h); + Shape shape = cv.getChildAllocation(0, paintRect); + if (shape != null && (shape = pView.getView(0). + getChildAllocation(0, shape)) != null) { + Rectangle rect = (shape instanceof Rectangle) ? + (Rectangle)shape : shape.getBounds(); + + align = pView.getView(0).getAlignment(View.Y_AXIS); + y = rect.y; + h = rect.height; + } + } + } + + // set the color of a decoration + if (ss != null) { + g.setColor(ss.getForeground(cv.getAttributes())); + } else { + g.setColor(Color.black); + } + + if (img != null) { + drawIcon(g, (int) x, (int) y, (int) w, (int) h, align, + v.getContainer()); + return; + } + CSS.Value childtype = getChildType(cv); + Font font = ((StyledDocument)cv.getDocument()). + getFont(cv.getAttributes()); + if (font != null) { + g.setFont(font); + } + if (childtype == CSS.Value.SQUARE || childtype == CSS.Value.CIRCLE + || childtype == CSS.Value.DISC) { + drawShape(g, childtype, (int) x, (int) y, + (int) w, (int) h, align); + } else if (childtype == CSS.Value.DECIMAL) { + drawLetter(g, '1', (int) x, (int) y, (int) w, (int) h, align, + getRenderIndex(v, item)); + } else if (childtype == CSS.Value.LOWER_ALPHA) { + drawLetter(g, 'a', (int) x, (int) y, (int) w, (int) h, align, + getRenderIndex(v, item)); + } else if (childtype == CSS.Value.UPPER_ALPHA) { + drawLetter(g, 'A', (int) x, (int) y, (int) w, (int) h, align, + getRenderIndex(v, item)); + } else if (childtype == CSS.Value.LOWER_ROMAN) { + drawLetter(g, 'i', (int) x, (int) y, (int) w, (int) h, align, + getRenderIndex(v, item)); + } else if (childtype == CSS.Value.UPPER_ROMAN) { + drawLetter(g, 'I', (int) x, (int) y, (int) w, (int) h, align, + getRenderIndex(v, item)); + } + } + + /** + * Draws the bullet icon specified by the list-style-image argument. + * + * @param g the graphics context + * @param ax x coordinate to place the bullet + * @param ay y coordinate to place the bullet + * @param aw width of the container the bullet is placed in + * @param ah height of the container the bullet is placed in + * @param align preferred alignment factor for the child view + */ + void drawIcon(Graphics g, int ax, int ay, int aw, int ah, + float align, Component c) { + // Align to bottom of icon. + int gap = isLeftToRight ? - (img.getIconWidth() + bulletgap) : + (aw + bulletgap); + int x = ax + gap; + int y = Math.max(ay, ay + (int)(align * ah) -img.getIconHeight()); + + img.paintIcon(c, g, x, y); + } + + /** + * Draws the graphical bullet item specified by the type argument. + * + * @param g the graphics context + * @param type type of bullet to draw (circle, square, disc) + * @param ax x coordinate to place the bullet + * @param ay y coordinate to place the bullet + * @param aw width of the container the bullet is placed in + * @param ah height of the container the bullet is placed in + * @param align preferred alignment factor for the child view + */ + void drawShape(Graphics g, CSS.Value type, int ax, int ay, int aw, + int ah, float align) { + // Align to bottom of shape. + int gap = isLeftToRight ? - (bulletgap + 8) : (aw + bulletgap); + int x = ax + gap; + int y = Math.max(ay, ay + (int)(align * ah) - 8); + + if (type == CSS.Value.SQUARE) { + g.drawRect(x, y, 8, 8); + } else if (type == CSS.Value.CIRCLE) { + g.drawOval(x, y, 8, 8); + } else { + g.fillOval(x, y, 8, 8); + } + } + + /** + * Draws the letter or number for an ordered list. + * + * @param g the graphics context + * @param letter type of ordered list to draw + * @param ax x coordinate to place the bullet + * @param ay y coordinate to place the bullet + * @param aw width of the container the bullet is placed in + * @param ah height of the container the bullet is placed in + * @param index position of the list item in the list + */ + void drawLetter(Graphics g, char letter, int ax, int ay, int aw, + int ah, float align, int index) { + String str = formatItemNum(index, letter); + str = isLeftToRight ? str + "." : "." + str; + FontMetrics fm = SwingUtilities2.getFontMetrics(null, g); + int stringwidth = SwingUtilities2.stringWidth(null, fm, str); + int gap = isLeftToRight ? - (stringwidth + bulletgap) : + (aw + bulletgap); + int x = ax + gap; + int y = Math.max(ay + fm.getAscent(), ay + (int)(ah * align)); + SwingUtilities2.drawString(null, g, str, x, y); + } + + /** + * Converts the item number into the ordered list number + * (i.e. 1 2 3, i ii iii, a b c, etc. + * + * @param itemNum number to format + * @param type type of ordered list + */ + String formatItemNum(int itemNum, char type) { + String numStyle = "1"; + + boolean uppercase = false; + + String formattedNum; + + switch (type) { + case '1': + default: + formattedNum = String.valueOf(itemNum); + break; + + case 'A': + uppercase = true; + // fall through + case 'a': + formattedNum = formatAlphaNumerals(itemNum); + break; + + case 'I': + uppercase = true; + // fall through + case 'i': + formattedNum = formatRomanNumerals(itemNum); + } + + if (uppercase) { + formattedNum = formattedNum.toUpperCase(); + } + + return formattedNum; + } + + /** + * Converts the item number into an alphabetic character + * + * @param itemNum number to format + */ + String formatAlphaNumerals(int itemNum) { + String result = ""; + + if (itemNum > 26) { + result = formatAlphaNumerals(itemNum / 26) + + formatAlphaNumerals(itemNum % 26); + } else { + // -1 because item is 1 based. + result = String.valueOf((char)('a' + itemNum - 1)); + } + + return result; + } + + /* list of roman numerals */ + static final char romanChars[][] = { + {'i', 'v'}, + {'x', 'l' }, + {'c', 'd' }, + {'m', '?' }, + }; + + /** + * Converts the item number into a roman numeral + * + * @param num number to format + */ + String formatRomanNumerals(int num) { + return formatRomanNumerals(0, num); + } + + /** + * Converts the item number into a roman numeral + * + * @param num number to format + */ + String formatRomanNumerals(int level, int num) { + if (num < 10) { + return formatRomanDigit(level, num); + } else { + return formatRomanNumerals(level + 1, num / 10) + + formatRomanDigit(level, num % 10); + } + } + + + /** + * Converts the item number into a roman numeral + * + * @param level position + * @param num digit to format + */ + String formatRomanDigit(int level, int digit) { + String result = ""; + if (digit == 9) { + result = result + romanChars[level][0]; + result = result + romanChars[level + 1][0]; + return result; + } else if (digit == 4) { + result = result + romanChars[level][0]; + result = result + romanChars[level][1]; + return result; + } else if (digit >= 5) { + result = result + romanChars[level][1]; + digit -= 5; + } + + for (int i = 0; i < digit; i++) { + result = result + romanChars[level][0]; + } + + return result; + } + + private Rectangle paintRect; + private boolean checkedForStart; + private int start; + private CSS.Value type; + URL imageurl; + private StyleSheet ss = null; + Icon img = null; + private int bulletgap = 5; + private boolean isLeftToRight; + } + + + /** + * Paints the background image. + */ + static class BackgroundImagePainter implements Serializable { + ImageIcon backgroundImage; + float hPosition; + float vPosition; + // bit mask: 0 for repeat x, 1 for repeat y, 2 for horiz relative, + // 3 for vert relative + short flags; + // These are used when painting, updatePaintCoordinates updates them. + private int paintX; + private int paintY; + private int paintMaxX; + private int paintMaxY; + + BackgroundImagePainter(AttributeSet a, CSS css, StyleSheet ss) { + backgroundImage = ss.getBackgroundImage(a); + // Determine the position. + CSS.BackgroundPosition pos = (CSS.BackgroundPosition)a.getAttribute + (CSS.Attribute.BACKGROUND_POSITION); + if (pos != null) { + hPosition = pos.getHorizontalPosition(); + vPosition = pos.getVerticalPosition(); + if (pos.isHorizontalPositionRelativeToSize()) { + flags |= 4; + } + else if (pos.isHorizontalPositionRelativeToSize()) { + hPosition *= css.getFontSize(a, 12, ss); + } + if (pos.isVerticalPositionRelativeToSize()) { + flags |= 8; + } + else if (pos.isVerticalPositionRelativeToFontSize()) { + vPosition *= css.getFontSize(a, 12, ss); + } + } + // Determine any repeating values. + CSS.Value repeats = (CSS.Value)a.getAttribute(CSS.Attribute. + BACKGROUND_REPEAT); + if (repeats == null || repeats == CSS.Value.BACKGROUND_REPEAT) { + flags |= 3; + } + else if (repeats == CSS.Value.BACKGROUND_REPEAT_X) { + flags |= 1; + } + else if (repeats == CSS.Value.BACKGROUND_REPEAT_Y) { + flags |= 2; + } + } + + void paint(Graphics g, float x, float y, float w, float h, View v) { + Rectangle clip = g.getClipRect(); + if (clip != null) { + // Constrain the clip so that images don't draw outside the + // legal bounds. + g.clipRect((int)x, (int)y, (int)w, (int)h); + } + if ((flags & 3) == 0) { + // no repeating + int width = backgroundImage.getIconWidth(); + int height = backgroundImage.getIconWidth(); + if ((flags & 4) == 4) { + paintX = (int)(x + w * hPosition - + (float)width * hPosition); + } + else { + paintX = (int)x + (int)hPosition; + } + if ((flags & 8) == 8) { + paintY = (int)(y + h * vPosition - + (float)height * vPosition); + } + else { + paintY = (int)y + (int)vPosition; + } + if (clip == null || + !((paintX + width <= clip.x) || + (paintY + height <= clip.y) || + (paintX >= clip.x + clip.width) || + (paintY >= clip.y + clip.height))) { + backgroundImage.paintIcon(null, g, paintX, paintY); + } + } + else { + int width = backgroundImage.getIconWidth(); + int height = backgroundImage.getIconHeight(); + if (width > 0 && height > 0) { + paintX = (int)x; + paintY = (int)y; + paintMaxX = (int)(x + w); + paintMaxY = (int)(y + h); + if (updatePaintCoordinates(clip, width, height)) { + while (paintX < paintMaxX) { + int ySpot = paintY; + while (ySpot < paintMaxY) { + backgroundImage.paintIcon(null, g, paintX, + ySpot); + ySpot += height; + } + paintX += width; + } + } + } + } + if (clip != null) { + // Reset clip. + g.setClip(clip.x, clip.y, clip.width, clip.height); + } + } + + private boolean updatePaintCoordinates + (Rectangle clip, int width, int height){ + if ((flags & 3) == 1) { + paintMaxY = paintY + 1; + } + else if ((flags & 3) == 2) { + paintMaxX = paintX + 1; + } + if (clip != null) { + if ((flags & 3) == 1 && ((paintY + height <= clip.y) || + (paintY > clip.y + clip.height))) { + // not visible. + return false; + } + if ((flags & 3) == 2 && ((paintX + width <= clip.x) || + (paintX > clip.x + clip.width))) { + // not visible. + return false; + } + if ((flags & 1) == 1) { + if ((clip.x + clip.width) < paintMaxX) { + if ((clip.x + clip.width - paintX) % width == 0) { + paintMaxX = clip.x + clip.width; + } + else { + paintMaxX = ((clip.x + clip.width - paintX) / + width + 1) * width + paintX; + } + } + if (clip.x > paintX) { + paintX = (clip.x - paintX) / width * width + paintX; + } + } + if ((flags & 2) == 2) { + if ((clip.y + clip.height) < paintMaxY) { + if ((clip.y + clip.height - paintY) % height == 0) { + paintMaxY = clip.y + clip.height; + } + else { + paintMaxY = ((clip.y + clip.height - paintY) / + height + 1) * height + paintY; + } + } + if (clip.y > paintY) { + paintY = (clip.y - paintY) / height * height + paintY; + } + } + } + // Valid + return true; + } + } + + + /** + * A subclass of MuxingAttributeSet that translates between + * CSS and HTML and StyleConstants. The AttributeSets used are + * the CSS rules that match the Views Elements. + */ + class ViewAttributeSet extends MuxingAttributeSet { + ViewAttributeSet(View v) { + host = v; + + // PENDING(prinz) fix this up to be a more realistic + // implementation. + Document doc = v.getDocument(); + SearchBuffer sb = SearchBuffer.obtainSearchBuffer(); + Vector muxList = sb.getVector(); + try { + if (doc instanceof HTMLDocument) { + StyleSheet styles = StyleSheet.this; + Element elem = v.getElement(); + AttributeSet a = elem.getAttributes(); + AttributeSet htmlAttr = styles.translateHTMLToCSS(a); + + if (htmlAttr.getAttributeCount() != 0) { + muxList.addElement(htmlAttr); + } + if (elem.isLeaf()) { + Enumeration keys = a.getAttributeNames(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key instanceof HTML.Tag) { + if ((HTML.Tag)key == HTML.Tag.A) { + Object o = a.getAttribute((HTML.Tag)key); + /** + In the case of an A tag, the css rules + apply only for tags that have their + href attribute defined and not for + anchors that only have their name attributes + defined, i.e anchors that function as + destinations. Hence we do not add the + attributes for that latter kind of + anchors. When CSS2 support is added, + it will be possible to specificity this + kind of conditional behaviour in the + stylesheet. + **/ + if (o != null && o instanceof AttributeSet) { + AttributeSet attr = (AttributeSet)o; + if (attr.getAttribute(HTML.Attribute.HREF) == null) { + continue; + } + } + } + AttributeSet cssRule = styles.getRule((HTML.Tag) key, elem); + if (cssRule != null) { + muxList.addElement(cssRule); + } + } + } + } else { + HTML.Tag t = (HTML.Tag) a.getAttribute + (StyleConstants.NameAttribute); + AttributeSet cssRule = styles.getRule(t, elem); + if (cssRule != null) { + muxList.addElement(cssRule); + } + } + } + AttributeSet[] attrs = new AttributeSet[muxList.size()]; + muxList.copyInto(attrs); + setAttributes(attrs); + } + finally { + SearchBuffer.releaseSearchBuffer(sb); + } + } + + // --- AttributeSet methods ---------------------------- + + /** + * Checks whether a given attribute is defined. + * This will convert the key over to CSS if the + * key is a StyleConstants key that has a CSS + * mapping. + * + * @param key the attribute key + * @return true if the attribute is defined + * @see AttributeSet#isDefined + */ + public boolean isDefined(Object key) { + if (key instanceof StyleConstants) { + Object cssKey = css.styleConstantsKeyToCSSKey + ((StyleConstants)key); + if (cssKey != null) { + key = cssKey; + } + } + return super.isDefined(key); + } + + /** + * Gets the value of an attribute. If the requested + * attribute is a StyleConstants attribute that has + * a CSS mapping, the request will be converted. + * + * @param key the attribute name + * @return the attribute value + * @see AttributeSet#getAttribute + */ + public Object getAttribute(Object key) { + if (key instanceof StyleConstants) { + Object cssKey = css.styleConstantsKeyToCSSKey + ((StyleConstants)key); + if (cssKey != null) { + Object value = doGetAttribute(cssKey); + if (value instanceof CSS.CssValue) { + return ((CSS.CssValue)value).toStyleConstants + ((StyleConstants)key, host); + } + } + } + return doGetAttribute(key); + } + + Object doGetAttribute(Object key) { + Object retValue = super.getAttribute(key); + if (retValue != null) { + return retValue; + } + // didn't find it... try parent if it's a css attribute + // that is inherited. + if (key instanceof CSS.Attribute) { + CSS.Attribute css = (CSS.Attribute) key; + if (css.isInherited()) { + AttributeSet parent = getResolveParent(); + if (parent != null) + return parent.getAttribute(key); + } + } + return null; + } + + /** + * If not overriden, the resolving parent defaults to + * the parent element. + * + * @return the attributes from the parent + * @see AttributeSet#getResolveParent + */ + public AttributeSet getResolveParent() { + if (host == null) { + return null; + } + View parent = host.getParent(); + return (parent != null) ? parent.getAttributes() : null; + } + + /** View created for. */ + View host; + } + + + /** + * A subclass of MuxingAttributeSet that implements Style. Currently + * the MutableAttributeSet methods are unimplemented, that is they + * do nothing. + */ + // PENDING(sky): Decide what to do with this. Either make it + // contain a SimpleAttributeSet that modify methods are delegated to, + // or change getRule to return an AttributeSet and then don't make this + // implement Style. + static class ResolvedStyle extends MuxingAttributeSet implements + Serializable, Style { + ResolvedStyle(String name, AttributeSet[] attrs, int extendedIndex) { + super(attrs); + this.name = name; + this.extendedIndex = extendedIndex; + } + + /** + * Inserts a Style into the receiver so that the styles the + * receiver represents are still ordered by specificity. + * style will be added before any extended styles, that + * is before extendedIndex. + */ + synchronized void insertStyle(Style style, int specificity) { + AttributeSet[] attrs = getAttributes(); + int maxCounter = attrs.length; + int counter = 0; + for (;counter < extendedIndex; counter++) { + if (specificity > getSpecificity(((Style)attrs[counter]). + getName())) { + break; + } + } + insertAttributeSetAt(style, counter); + extendedIndex++; + } + + /** + * Removes a previously added style. This will do nothing if + * style is not referenced by the receiver. + */ + synchronized void removeStyle(Style style) { + AttributeSet[] attrs = getAttributes(); + + for (int counter = attrs.length - 1; counter >= 0; counter--) { + if (attrs[counter] == style) { + removeAttributeSetAt(counter); + if (counter < extendedIndex) { + extendedIndex--; + } + break; + } + } + } + + /** + * Adds s as one of the Attributesets to look up + * attributes in. + */ + synchronized void insertExtendedStyleAt(Style attr, int index) { + insertAttributeSetAt(attr, extendedIndex + index); + } + + /** + * Adds s as one of the AttributeSets to look up + * attributes in. It will be the AttributeSet last checked. + */ + synchronized void addExtendedStyle(Style attr) { + insertAttributeSetAt(attr, getAttributes().length); + } + + /** + * Removes the style at index + + * extendedIndex. + */ + synchronized void removeExtendedStyleAt(int index) { + removeAttributeSetAt(extendedIndex + index); + } + + /** + * Returns true if the receiver matches selector, where + * a match is defined by the CSS rule matching. + * Each simple selector must be separated by a single space. + */ + protected boolean matches(String selector) { + int sLast = selector.length(); + + if (sLast == 0) { + return false; + } + int thisLast = name.length(); + int sCurrent = selector.lastIndexOf(' '); + int thisCurrent = name.lastIndexOf(' '); + if (sCurrent >= 0) { + sCurrent++; + } + if (thisCurrent >= 0) { + thisCurrent++; + } + if (!matches(selector, sCurrent, sLast, thisCurrent, thisLast)) { + return false; + } + while (sCurrent != -1) { + sLast = sCurrent - 1; + sCurrent = selector.lastIndexOf(' ', sLast - 1); + if (sCurrent >= 0) { + sCurrent++; + } + boolean match = false; + while (!match && thisCurrent != -1) { + thisLast = thisCurrent - 1; + thisCurrent = name.lastIndexOf(' ', thisLast - 1); + if (thisCurrent >= 0) { + thisCurrent++; + } + match = matches(selector, sCurrent, sLast, thisCurrent, + thisLast); + } + if (!match) { + return false; + } + } + return true; + } + + /** + * Returns true if the substring of the receiver, in the range + * thisCurrent, thisLast matches the substring of selector in + * the ranme sCurrent to sLast based on CSS selector matching. + */ + boolean matches(String selector, int sCurrent, int sLast, + int thisCurrent, int thisLast) { + sCurrent = Math.max(sCurrent, 0); + thisCurrent = Math.max(thisCurrent, 0); + int thisDotIndex = boundedIndexOf(name, '.', thisCurrent, + thisLast); + int thisPoundIndex = boundedIndexOf(name, '#', thisCurrent, + thisLast); + int sDotIndex = boundedIndexOf(selector, '.', sCurrent, sLast); + int sPoundIndex = boundedIndexOf(selector, '#', sCurrent, sLast); + if (sDotIndex != -1) { + // Selector has a '.', which indicates name must match it, + // or if the '.' starts the selector than name must have + // the same class (doesn't matter what element name). + if (thisDotIndex == -1) { + return false; + } + if (sCurrent == sDotIndex) { + if ((thisLast - thisDotIndex) != (sLast - sDotIndex) || + !selector.regionMatches(sCurrent, name, thisDotIndex, + (thisLast - thisDotIndex))) { + return false; + } + } + else { + // Has to fully match. + if ((sLast - sCurrent) != (thisLast - thisCurrent) || + !selector.regionMatches(sCurrent, name, thisCurrent, + (thisLast - thisCurrent))) { + return false; + } + } + return true; + } + if (sPoundIndex != -1) { + // Selector has a '#', which indicates name must match it, + // or if the '#' starts the selector than name must have + // the same id (doesn't matter what element name). + if (thisPoundIndex == -1) { + return false; + } + if (sCurrent == sPoundIndex) { + if ((thisLast - thisPoundIndex) !=(sLast - sPoundIndex) || + !selector.regionMatches(sCurrent, name, thisPoundIndex, + (thisLast - thisPoundIndex))) { + return false; + } + } + else { + // Has to fully match. + if ((sLast - sCurrent) != (thisLast - thisCurrent) || + !selector.regionMatches(sCurrent, name, thisCurrent, + (thisLast - thisCurrent))) { + return false; + } + } + return true; + } + if (thisDotIndex != -1) { + // Reciever references a class, just check element name. + return (((thisDotIndex - thisCurrent) == (sLast - sCurrent)) && + selector.regionMatches(sCurrent, name, thisCurrent, + thisDotIndex - thisCurrent)); + } + if (thisPoundIndex != -1) { + // Reciever references an id, just check element name. + return (((thisPoundIndex - thisCurrent) ==(sLast - sCurrent))&& + selector.regionMatches(sCurrent, name, thisCurrent, + thisPoundIndex - thisCurrent)); + } + // Fail through, no classes or ides, just check string. + return (((thisLast - thisCurrent) == (sLast - sCurrent)) && + selector.regionMatches(sCurrent, name, thisCurrent, + thisLast - thisCurrent)); + } + + /** + * Similiar to String.indexOf, but allows an upper bound + * (this is slower in that it will still check string starting at + * start. + */ + int boundedIndexOf(String string, char search, int start, + int end) { + int retValue = string.indexOf(search, start); + if (retValue >= end) { + return -1; + } + return retValue; + } + + public void addAttribute(Object name, Object value) {} + public void addAttributes(AttributeSet attributes) {} + public void removeAttribute(Object name) {} + public void removeAttributes(Enumeration names) {} + public void removeAttributes(AttributeSet attributes) {} + public void setResolveParent(AttributeSet parent) {} + public String getName() {return name;} + public void addChangeListener(ChangeListener l) {} + public void removeChangeListener(ChangeListener l) {} + public ChangeListener[] getChangeListeners() { + return new ChangeListener[0]; + } + + /** The name of the Style, which is the selector. + * This will NEVER change! + */ + String name; + /** Start index of styles coming from other StyleSheets. */ + private int extendedIndex; + } + + + /** + * SelectorMapping contains a specifitiy, as an integer, and an associated + * Style. It can also reference children SelectorMappings, + * so that it behaves like a tree. + *

          + * This is not thread safe, it is assumed the caller will take the + * necessary precations if this is to be used in a threaded environment. + */ + static class SelectorMapping implements Serializable { + public SelectorMapping(int specificity) { + this.specificity = specificity; + } + + /** + * Returns the specificity this mapping represents. + */ + public int getSpecificity() { + return specificity; + } + + /** + * Sets the Style associated with this mapping. + */ + public void setStyle(Style style) { + this.style = style; + } + + /** + * Returns the Style associated with this mapping. + */ + public Style getStyle() { + return style; + } + + /** + * Returns the child mapping identified by the simple selector + * selector. If a child mapping does not exist for + *selector, and create is true, a new + * one will be created. + */ + public SelectorMapping getChildSelectorMapping(String selector, + boolean create) { + SelectorMapping retValue = null; + + if (children != null) { + retValue = (SelectorMapping)children.get(selector); + } + else if (create) { + children = new HashMap(7); + } + if (retValue == null && create) { + int specificity = getChildSpecificity(selector); + + retValue = createChildSelectorMapping(specificity); + children.put(selector, retValue); + } + return retValue; + } + + /** + * Creates a child SelectorMapping with the specified + * specificity. + */ + protected SelectorMapping createChildSelectorMapping(int specificity) { + return new SelectorMapping(specificity); + } + + /** + * Returns the specificity for the child selector + * selector. + */ + protected int getChildSpecificity(String selector) { + // class (.) 100 + // id (#) 10000 + char firstChar = selector.charAt(0); + int specificity = getSpecificity(); + + if (firstChar == '.') { + specificity += 100; + } + else if (firstChar == '#') { + specificity += 10000; + } + else { + specificity += 1; + if (selector.indexOf('.') != -1) { + specificity += 100; + } + if (selector.indexOf('#') != -1) { + specificity += 10000; + } + } + return specificity; + } + + /** + * The specificity for this selector. + */ + private int specificity; + /** + * Style for this selector. + */ + private Style style; + /** + * Any sub selectors. Key will be String, and value will be + * another SelectorMapping. + */ + private HashMap children; + } + + + // ---- Variables --------------------------------------------- + + final static int DEFAULT_FONT_SIZE = 3; + + private CSS css; + + /** + * An inverted graph of the selectors. + */ + private SelectorMapping selectorMapping; + + /** Maps from selector (as a string) to Style that includes all + * relevant styles. */ + private Hashtable resolvedStyles; + + /** Vector of StyleSheets that the rules are to reference. + */ + private Vector linkedStyleSheets; + + /** Where the style sheet was found. Used for relative imports. */ + private URL base; + + + /** + * Default parser for CSS specifications that get loaded into + * the StyleSheet.

          + * This class is NOT thread safe, do not ask it to parse while it is + * in the middle of parsing. + */ + class CssParser implements CSSParser.CSSParserCallback { + + /** + * Parses the passed in CSS declaration into an AttributeSet. + */ + public AttributeSet parseDeclaration(String string) { + try { + return parseDeclaration(new StringReader(string)); + } catch (IOException ioe) {} + return null; + } + + /** + * Parses the passed in CSS declaration into an AttributeSet. + */ + public AttributeSet parseDeclaration(Reader r) throws IOException { + parse(base, r, true, false); + return declaration.copyAttributes(); + } + + /** + * Parse the given CSS stream + */ + public void parse(URL base, Reader r, boolean parseDeclaration, + boolean isLink) throws IOException { + this.base = base; + this.isLink = isLink; + this.parsingDeclaration = parseDeclaration; + declaration.removeAttributes(declaration); + selectorTokens.removeAllElements(); + selectors.removeAllElements(); + propertyName = null; + parser.parse(r, this, parseDeclaration); + } + + // + // CSSParserCallback methods, public to implement the interface. + // + + /** + * Invoked when a valid @import is encountered, will call + * importStyleSheet if a + * MalformedURLException is not thrown in creating + * the URL. + */ + public void handleImport(String importString) { + URL url = CSS.getURL(base, importString); + if (url != null) { + importStyleSheet(url); + } + } + + /** + * A selector has been encountered. + */ + public void handleSelector(String selector) { + //class and index selectors are case sensitive + if (!(selector.startsWith(".") + || selector.startsWith("#"))) { + selector = selector.toLowerCase(); + } + int length = selector.length(); + + if (selector.endsWith(",")) { + if (length > 1) { + selector = selector.substring(0, length - 1); + selectorTokens.addElement(selector); + } + addSelector(); + } + else if (length > 0) { + selectorTokens.addElement(selector); + } + } + + /** + * Invoked when the start of a rule is encountered. + */ + public void startRule() { + if (selectorTokens.size() > 0) { + addSelector(); + } + propertyName = null; + } + + /** + * Invoked when a property name is encountered. + */ + public void handleProperty(String property) { + propertyName = property; + } + + /** + * Invoked when a property value is encountered. + */ + public void handleValue(String value) { + if (propertyName != null && value != null && value.length() > 0) { + CSS.Attribute cssKey = CSS.getAttribute(propertyName); + if (cssKey != null) { + // There is currently no mechanism to determine real + // base that style sheet was loaded from. For the time + // being, this maps for LIST_STYLE_IMAGE, which appear + // to be the only one that currently matters. A more + // general mechanism is definately needed. + if (cssKey == CSS.Attribute.LIST_STYLE_IMAGE) { + if (value != null && !value.equals("none")) { + URL url = CSS.getURL(base, value); + + if (url != null) { + value = url.toString(); + } + } + } + addCSSAttribute(declaration, cssKey, value); + } + propertyName = null; + } + } + + /** + * Invoked when the end of a rule is encountered. + */ + public void endRule() { + int n = selectors.size(); + for (int i = 0; i < n; i++) { + String[] selector = (String[]) selectors.elementAt(i); + if (selector.length > 0) { + StyleSheet.this.addRule(selector, declaration, isLink); + } + } + declaration.removeAttributes(declaration); + selectors.removeAllElements(); + } + + private void addSelector() { + String[] selector = new String[selectorTokens.size()]; + selectorTokens.copyInto(selector); + selectors.addElement(selector); + selectorTokens.removeAllElements(); + } + + + Vector selectors = new Vector(); + Vector selectorTokens = new Vector(); + /** Name of the current property. */ + String propertyName; + MutableAttributeSet declaration = new SimpleAttributeSet(); + /** True if parsing a declaration, that is the Reader will not + * contain a selector. */ + boolean parsingDeclaration; + /** True if the attributes are coming from a linked/imported style. */ + boolean isLink; + /** Where the CSS stylesheet lives. */ + URL base; + CSSParser parser = new CSSParser(); + } + + void rebaseSizeMap(int base) { + final int minimalFontSize = 4; + sizeMap = new int[sizeMapDefault.length]; + for (int i = 0; i < sizeMapDefault.length; i++) { + sizeMap[i] = Math.max(base * sizeMapDefault[i] / + sizeMapDefault[CSS.baseFontSizeIndex], + minimalFontSize); + } + + } + + int[] getSizeMap() { + return sizeMap; + } + boolean isW3CLengthUnits() { + return w3cLengthUnits; + } + + /** + * The HTML/CSS size model has seven slots + * that one can assign sizes to. + */ + static final int sizeMapDefault[] = { 8, 10, 12, 14, 18, 24, 36 }; + + private int sizeMap[] = sizeMapDefault; + private boolean w3cLengthUnits = false; +} diff --git a/src/share/classes/javax/swing/text/html/TableView.java b/src/share/classes/javax/swing/text/html/TableView.java new file mode 100644 index 000000000..db0dc790f --- /dev/null +++ b/src/share/classes/javax/swing/text/html/TableView.java @@ -0,0 +1,1801 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import java.util.BitSet; +import java.util.Vector; +import java.util.Arrays; +import javax.swing.SizeRequirements; +import javax.swing.event.DocumentEvent; + +import javax.swing.text.*; + +/** + * HTML table view. + * + * @author Timothy Prinzing + * @see View + */ +/*public*/ class TableView extends BoxView implements ViewFactory { + + /** + * Constructs a TableView for the given element. + * + * @param elem the element that this view is responsible for + */ + public TableView(Element elem) { + super(elem, View.Y_AXIS); + rows = new Vector(); + gridValid = false; + captionIndex = -1; + totalColumnRequirements = new SizeRequirements(); + } + + /** + * Creates a new table row. + * + * @param elem an element + * @return the row + */ + protected RowView createTableRow(Element elem) { + // PENDING(prinz) need to add support for some of the other + // elements, but for now just ignore anything that is not + // a TR. + Object o = elem.getAttributes().getAttribute(StyleConstants.NameAttribute); + if (o == HTML.Tag.TR) { + return new RowView(elem); + } + return null; + } + + /** + * The number of columns in the table. + */ + public int getColumnCount() { + return columnSpans.length; + } + + /** + * Fetches the span (width) of the given column. + * This is used by the nested cells to query the + * sizes of grid locations outside of themselves. + */ + public int getColumnSpan(int col) { + if (col < columnSpans.length) { + return columnSpans[col]; + } + return 0; + } + + /** + * The number of rows in the table. + */ + public int getRowCount() { + return rows.size(); + } + + /** + * Fetch the span of multiple rows. This includes + * the border area. + */ + public int getMultiRowSpan(int row0, int row1) { + RowView rv0 = getRow(row0); + RowView rv1 = getRow(row1); + if ((rv0 != null) && (rv1 != null)) { + int index0 = rv0.viewIndex; + int index1 = rv1.viewIndex; + int span = getOffset(Y_AXIS, index1) - getOffset(Y_AXIS, index0) + + getSpan(Y_AXIS, index1); + return span; + } + return 0; + } + + /** + * Fetches the span (height) of the given row. + */ + public int getRowSpan(int row) { + RowView rv = getRow(row); + if (rv != null) { + return getSpan(Y_AXIS, rv.viewIndex); + } + return 0; + } + + RowView getRow(int row) { + if (row < rows.size()) { + return (RowView) rows.elementAt(row); + } + return null; + } + + protected View getViewAtPoint(int x, int y, Rectangle alloc) { + int n = getViewCount(); + View v = null; + Rectangle allocation = new Rectangle(); + for (int i = 0; i < n; i++) { + allocation.setBounds(alloc); + childAllocation(i, allocation); + v = getView(i); + if (v instanceof RowView) { + v = ((RowView)v).findViewAtPoint(x, y, allocation); + if (v != null) { + alloc.setBounds(allocation); + return v; + } + } + } + return super.getViewAtPoint(x, y, alloc); + } + + /** + * Determines the number of columns occupied by + * the table cell represented by given element. + */ + protected int getColumnsOccupied(View v) { + AttributeSet a = v.getElement().getAttributes(); + + if (a.isDefined(HTML.Attribute.COLSPAN)) { + String s = (String) a.getAttribute(HTML.Attribute.COLSPAN); + if (s != null) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException nfe) { + // fall through to one column + } + } + } + + return 1; + } + + /** + * Determines the number of rows occupied by + * the table cell represented by given element. + */ + protected int getRowsOccupied(View v) { + AttributeSet a = v.getElement().getAttributes(); + + if (a.isDefined(HTML.Attribute.ROWSPAN)) { + String s = (String) a.getAttribute(HTML.Attribute.ROWSPAN); + if (s != null) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException nfe) { + // fall through to one row + } + } + } + + return 1; + } + + protected void invalidateGrid() { + gridValid = false; + } + + protected StyleSheet getStyleSheet() { + HTMLDocument doc = (HTMLDocument) getDocument(); + return doc.getStyleSheet(); + } + + /** + * Update the insets, which contain the caption if there + * is a caption. + */ + void updateInsets() { + short top = (short) painter.getInset(TOP, this); + short bottom = (short) painter.getInset(BOTTOM, this); + if (captionIndex != -1) { + View caption = getView(captionIndex); + short h = (short) caption.getPreferredSpan(Y_AXIS); + AttributeSet a = caption.getAttributes(); + Object align = a.getAttribute(CSS.Attribute.CAPTION_SIDE); + if ((align != null) && (align.equals("bottom"))) { + bottom += h; + } else { + top += h; + } + } + setInsets(top, (short) painter.getInset(LEFT, this), + bottom, (short) painter.getInset(RIGHT, this)); + } + + /** + * Update any cached values that come from attributes. + */ + protected void setPropertiesFromAttributes() { + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + painter = sheet.getBoxPainter(attr); + if (attr != null) { + setInsets((short) painter.getInset(TOP, this), + (short) painter.getInset(LEFT, this), + (short) painter.getInset(BOTTOM, this), + (short) painter.getInset(RIGHT, this)); + + CSS.LengthValue lv = (CSS.LengthValue) + attr.getAttribute(CSS.Attribute.BORDER_SPACING); + if (lv != null) { + cellSpacing = (int) lv.getValue(); + } else { + cellSpacing = 0; + } + lv = (CSS.LengthValue) + attr.getAttribute(CSS.Attribute.BORDER_TOP_WIDTH); + if (lv != null) { + borderWidth = (int) lv.getValue(); + } else { + borderWidth = 0; + } + + } + } + + /** + * Fill in the grid locations that are placeholders + * for multi-column, multi-row, and missing grid + * locations. + */ + void updateGrid() { + if (! gridValid) { + relativeCells = false; + multiRowCells = false; + + // determine which views are table rows and clear out + // grid points marked filled. + captionIndex = -1; + rows.removeAllElements(); + int n = getViewCount(); + for (int i = 0; i < n; i++) { + View v = getView(i); + if (v instanceof RowView) { + rows.addElement(v); + RowView rv = (RowView) v; + rv.clearFilledColumns(); + rv.rowIndex = rows.size() - 1; + rv.viewIndex = i; + } else { + Object o = v.getElement().getAttributes().getAttribute(StyleConstants.NameAttribute); + if (o instanceof HTML.Tag) { + HTML.Tag kind = (HTML.Tag) o; + if (kind == HTML.Tag.CAPTION) { + captionIndex = i; + } + } + } + } + + int maxColumns = 0; + int nrows = rows.size(); + for (int row = 0; row < nrows; row++) { + RowView rv = getRow(row); + int col = 0; + for (int cell = 0; cell < rv.getViewCount(); cell++, col++) { + View cv = rv.getView(cell); + if (! relativeCells) { + AttributeSet a = cv.getAttributes(); + CSS.LengthValue lv = (CSS.LengthValue) + a.getAttribute(CSS.Attribute.WIDTH); + if ((lv != null) && (lv.isPercentage())) { + relativeCells = true; + } + } + // advance to a free column + for (; rv.isFilled(col); col++); + int rowSpan = getRowsOccupied(cv); + if (rowSpan > 1) { + multiRowCells = true; + } + int colSpan = getColumnsOccupied(cv); + if ((colSpan > 1) || (rowSpan > 1)) { + // fill in the overflow entries for this cell + int rowLimit = row + rowSpan; + int colLimit = col + colSpan; + for (int i = row; i < rowLimit; i++) { + for (int j = col; j < colLimit; j++) { + if (i != row || j != col) { + addFill(i, j); + } + } + } + if (colSpan > 1) { + col += colSpan - 1; + } + } + } + maxColumns = Math.max(maxColumns, col); + } + + // setup the column layout/requirements + columnSpans = new int[maxColumns]; + columnOffsets = new int[maxColumns]; + columnRequirements = new SizeRequirements[maxColumns]; + for (int i = 0; i < maxColumns; i++) { + columnRequirements[i] = new SizeRequirements(); + columnRequirements[i].maximum = Integer.MAX_VALUE; + } + gridValid = true; + } + } + + /** + * Mark a grid location as filled in for a cells overflow. + */ + void addFill(int row, int col) { + RowView rv = getRow(row); + if (rv != null) { + rv.fillColumn(col); + } + } + + /** + * Layout the columns to fit within the given target span. + * + * @param targetSpan the given span for total of all the table + * columns + * @param reqs the requirements desired for each column. This + * is the column maximum of the cells minimum, preferred, and + * maximum requested span + * @param spans the return value of how much to allocated to + * each column + * @param offsets the return value of the offset from the + * origin for each column + * @return the offset from the origin and the span for each column + * in the offsets and spans parameters + */ + protected void layoutColumns(int targetSpan, int[] offsets, int[] spans, + SizeRequirements[] reqs) { + //clean offsets and spans + Arrays.fill(offsets, 0); + Arrays.fill(spans, 0); + colIterator.setLayoutArrays(offsets, spans, targetSpan); + CSS.calculateTiledLayout(colIterator, targetSpan); + } + + /** + * Calculate the requirements for each column. The calculation + * is done as two passes over the table. The table cells that + * occupy a single column are scanned first to determine the + * maximum of minimum, preferred, and maximum spans along the + * give axis. Table cells that span multiple columns are excluded + * from the first pass. A second pass is made to determine if + * the cells that span multiple columns are satisfied. If the + * column requirements are not satisified, the needs of the + * multi-column cell is mixed into the existing column requirements. + * The calculation of the multi-column distribution is based upon + * the proportions of the existing column requirements and taking + * into consideration any constraining maximums. + */ + void calculateColumnRequirements(int axis) { + // clean columnRequirements + for (SizeRequirements req : columnRequirements) { + req.minimum = 0; + req.preferred = 0; + req.maximum = Integer.MAX_VALUE; + } + Container host = getContainer(); + if (host != null) { + if (host instanceof JTextComponent) { + skipComments = !((JTextComponent)host).isEditable(); + } else { + skipComments = true; + } + } + // pass 1 - single column cells + boolean hasMultiColumn = false; + int nrows = getRowCount(); + for (int i = 0; i < nrows; i++) { + RowView row = getRow(i); + int col = 0; + int ncells = row.getViewCount(); + for (int cell = 0; cell < ncells; cell++) { + View cv = row.getView(cell); + if (skipComments && !(cv instanceof CellView)) { + continue; + } + for (; row.isFilled(col); col++); // advance to a free column + int rowSpan = getRowsOccupied(cv); + int colSpan = getColumnsOccupied(cv); + if (colSpan == 1) { + checkSingleColumnCell(axis, col, cv); + } else { + hasMultiColumn = true; + col += colSpan - 1; + } + col++; + } + } + + // pass 2 - multi-column cells + if (hasMultiColumn) { + for (int i = 0; i < nrows; i++) { + RowView row = getRow(i); + int col = 0; + int ncells = row.getViewCount(); + for (int cell = 0; cell < ncells; cell++) { + View cv = row.getView(cell); + if (skipComments && !(cv instanceof CellView)) { + continue; + } + for (; row.isFilled(col); col++); // advance to a free column + int colSpan = getColumnsOccupied(cv); + if (colSpan > 1) { + checkMultiColumnCell(axis, col, colSpan, cv); + col += colSpan - 1; + } + col++; + } + } + } + } + + /** + * check the requirements of a table cell that spans a single column. + */ + void checkSingleColumnCell(int axis, int col, View v) { + SizeRequirements req = columnRequirements[col]; + req.minimum = Math.max((int) v.getMinimumSpan(axis), req.minimum); + req.preferred = Math.max((int) v.getPreferredSpan(axis), req.preferred); + } + + /** + * check the requirements of a table cell that spans multiple + * columns. + */ + void checkMultiColumnCell(int axis, int col, int ncols, View v) { + // calculate the totals + long min = 0; + long pref = 0; + long max = 0; + for (int i = 0; i < ncols; i++) { + SizeRequirements req = columnRequirements[col + i]; + min += req.minimum; + pref += req.preferred; + max += req.maximum; + } + + // check if the minimum size needs adjustment. + int cmin = (int) v.getMinimumSpan(axis); + if (cmin > min) { + /* + * the columns that this cell spans need adjustment to fit + * this table cell.... calculate the adjustments. + */ + SizeRequirements[] reqs = new SizeRequirements[ncols]; + for (int i = 0; i < ncols; i++) { + reqs[i] = columnRequirements[col + i]; + } + int[] spans = new int[ncols]; + int[] offsets = new int[ncols]; + SizeRequirements.calculateTiledPositions(cmin, null, reqs, + offsets, spans); + // apply the adjustments + for (int i = 0; i < ncols; i++) { + SizeRequirements req = reqs[i]; + req.minimum = Math.max(spans[i], req.minimum); + req.preferred = Math.max(req.minimum, req.preferred); + req.maximum = Math.max(req.preferred, req.maximum); + } + } + + // check if the preferred size needs adjustment. + int cpref = (int) v.getPreferredSpan(axis); + if (cpref > pref) { + /* + * the columns that this cell spans need adjustment to fit + * this table cell.... calculate the adjustments. + */ + SizeRequirements[] reqs = new SizeRequirements[ncols]; + for (int i = 0; i < ncols; i++) { + reqs[i] = columnRequirements[col + i]; + } + int[] spans = new int[ncols]; + int[] offsets = new int[ncols]; + SizeRequirements.calculateTiledPositions(cpref, null, reqs, + offsets, spans); + // apply the adjustments + for (int i = 0; i < ncols; i++) { + SizeRequirements req = reqs[i]; + req.preferred = Math.max(spans[i], req.preferred); + req.maximum = Math.max(req.preferred, req.maximum); + } + } + + } + + // --- BoxView methods ----------------------------------------- + + /** + * Calculate the requirements for the minor axis. This is called by + * the superclass whenever the requirements need to be updated (i.e. + * a preferenceChanged was messaged through this view). + *

          + * This is implemented to calculate the requirements as the sum of the + * requirements of the columns and then adjust it if the + * CSS width or height attribute is specified and applicable to + * the axis. + */ + protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { + updateGrid(); + + // calculate column requirements for each column + calculateColumnRequirements(axis); + + + // the requirements are the sum of the columns. + if (r == null) { + r = new SizeRequirements(); + } + long min = 0; + long pref = 0; + int n = columnRequirements.length; + for (int i = 0; i < n; i++) { + SizeRequirements req = columnRequirements[i]; + min += req.minimum; + pref += req.preferred; + } + int adjust = (n + 1) * cellSpacing + 2 * borderWidth; + min += adjust; + pref += adjust; + r.minimum = (int) min; + r.preferred = (int) pref; + r.maximum = (int) pref; + + + AttributeSet attr = getAttributes(); + CSS.LengthValue cssWidth = (CSS.LengthValue)attr.getAttribute( + CSS.Attribute.WIDTH); + + if (BlockView.spanSetFromAttributes(axis, r, cssWidth, null)) { + if (r.minimum < (int)min) { + // The user has requested a smaller size than is needed to + // show the table, override it. + r.maximum = r.minimum = r.preferred = (int) min; + } + } + totalColumnRequirements.minimum = r.minimum; + totalColumnRequirements.preferred = r.preferred; + totalColumnRequirements.maximum = r.maximum; + + // set the alignment + Object o = attr.getAttribute(CSS.Attribute.TEXT_ALIGN); + if (o != null) { + // set horizontal alignment + String ta = o.toString(); + if (ta.equals("left")) { + r.alignment = 0; + } else if (ta.equals("center")) { + r.alignment = 0.5f; + } else if (ta.equals("right")) { + r.alignment = 1; + } else { + r.alignment = 0; + } + } else { + r.alignment = 0; + } + + return r; + } + + /** + * Calculate the requirements for the major axis. This is called by + * the superclass whenever the requirements need to be updated (i.e. + * a preferenceChanged was messaged through this view). + *

          + * This is implemented to provide the superclass behavior adjusted for + * multi-row table cells. + */ + protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) { + updateInsets(); + rowIterator.updateAdjustments(); + r = CSS.calculateTiledRequirements(rowIterator, r); + r.maximum = r.preferred; + return r; + } + + /** + * Perform layout for the minor axis of the box (i.e. the + * axis orthoginal to the axis that it represents). The results + * of the layout should be placed in the given arrays which represent + * the allocations to the children along the minor axis. This + * is called by the superclass whenever the layout needs to be + * updated along the minor axis. + *

          + * This is implemented to call the + * layoutColumns method, and then + * forward to the superclass to actually carry out the layout + * of the tables rows. + * + * @param targetSpan the total span given to the view, which + * whould be used to layout the children + * @param axis the axis being layed out + * @param offsets the offsets from the origin of the view for + * each of the child views. This is a return value and is + * filled in by the implementation of this method + * @param spans the span of each child view; this is a return + * value and is filled in by the implementation of this method + * @return the offset and span for each child view in the + * offsets and spans parameters + */ + protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { + // make grid is properly represented + updateGrid(); + + // all of the row layouts are invalid, so mark them that way + int n = getRowCount(); + for (int i = 0; i < n; i++) { + RowView row = getRow(i); + row.layoutChanged(axis); + } + + // calculate column spans + layoutColumns(targetSpan, columnOffsets, columnSpans, columnRequirements); + + // continue normal layout + super.layoutMinorAxis(targetSpan, axis, offsets, spans); + } + + + /** + * Perform layout for the major axis of the box (i.e. the + * axis that it represents). The results + * of the layout should be placed in the given arrays which represent + * the allocations to the children along the minor axis. This + * is called by the superclass whenever the layout needs to be + * updated along the minor axis. + *

          + * This method is where the layout of the table rows within the + * table takes place. This method is implemented to call the use + * the RowIterator and the CSS collapsing tile to layout + * with border spacing and border collapsing capabilities. + * + * @param targetSpan the total span given to the view, which + * whould be used to layout the children + * @param axis the axis being layed out + * @param offsets the offsets from the origin of the view for + * each of the child views; this is a return value and is + * filled in by the implementation of this method + * @param spans the span of each child view; this is a return + * value and is filled in by the implementation of this method + * @return the offset and span for each child view in the + * offsets and spans parameters + */ + protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { + rowIterator.setLayoutArrays(offsets, spans); + CSS.calculateTiledLayout(rowIterator, targetSpan); + + if (captionIndex != -1) { + // place the caption + View caption = getView(captionIndex); + int h = (int) caption.getPreferredSpan(Y_AXIS); + spans[captionIndex] = h; + short boxBottom = (short) painter.getInset(BOTTOM, this); + if (boxBottom != getBottomInset()) { + offsets[captionIndex] = targetSpan + boxBottom; + } else { + offsets[captionIndex] = - getTopInset(); + } + } + } + + /** + * Fetches the child view that represents the given position in + * the model. This is implemented to walk through the children + * looking for a range that contains the given position. In this + * view the children do not necessarily have a one to one mapping + * with the child elements. + * + * @param pos the search position >= 0 + * @param a the allocation to the table on entry, and the + * allocation of the view containing the position on exit + * @return the view representing the given position, or + * null if there isn't one + */ + protected View getViewAtPosition(int pos, Rectangle a) { + int n = getViewCount(); + for (int i = 0; i < n; i++) { + View v = getView(i); + int p0 = v.getStartOffset(); + int p1 = v.getEndOffset(); + if ((pos >= p0) && (pos < p1)) { + // it's in this view. + if (a != null) { + childAllocation(i, a); + } + return v; + } + } + if (pos == getEndOffset()) { + View v = getView(n - 1); + if (a != null) { + this.childAllocation(n - 1, a); + } + return v; + } + return null; + } + + // --- View methods --------------------------------------------- + + /** + * Fetches the attributes to use when rendering. This is + * implemented to multiplex the attributes specified in the + * model with a StyleSheet. + */ + public AttributeSet getAttributes() { + if (attr == null) { + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + } + return attr; + } + + /** + * Renders using the given rendering surface and area on that + * surface. This is implemented to delegate to the css box + * painter to paint the border and background prior to the + * interior. The superclass culls rendering the children + * that don't directly intersect the clip and the row may + * have cells hanging from a row above in it. The table + * does not use the superclass rendering behavior and instead + * paints all of the rows and lets the rows cull those + * cells not intersecting the clip region. + * + * @param g the rendering surface to use + * @param allocation the allocated region to render into + * @see View#paint + */ + public void paint(Graphics g, Shape allocation) { + // paint the border + Rectangle a = allocation.getBounds(); + setSize(a.width, a.height); + if (captionIndex != -1) { + // adjust the border for the caption + short top = (short) painter.getInset(TOP, this); + short bottom = (short) painter.getInset(BOTTOM, this); + if (top != getTopInset()) { + int h = getTopInset() - top; + a.y += h; + a.height -= h; + } else { + a.height -= getBottomInset() - bottom; + } + } + painter.paint(g, a.x, a.y, a.width, a.height, this); + // paint interior + int n = getViewCount(); + for (int i = 0; i < n; i++) { + View v = getView(i); + v.paint(g, getChildAllocation(i, allocation)); + } + //super.paint(g, a); + } + + /** + * Establishes the parent view for this view. This is + * guaranteed to be called before any other methods if the + * parent view is functioning properly. + *

          + * This is implemented + * to forward to the superclass as well as call the + * setPropertiesFromAttributes + * method to set the paragraph properties from the css + * attributes. The call is made at this time to ensure + * the ability to resolve upward through the parents + * view attributes. + * + * @param parent the new parent, or null if the view is + * being removed from a parent it was previously added + * to + */ + public void setParent(View parent) { + super.setParent(parent); + if (parent != null) { + setPropertiesFromAttributes(); + } + } + + /** + * Fetches the ViewFactory implementation that is feeding + * the view hierarchy. + * This replaces the ViewFactory with an implementation that + * calls through to the createTableRow and createTableCell + * methods. If the element given to the factory isn't a + * table row or cell, the request is delegated to the factory + * produced by the superclass behavior. + * + * @return the factory, null if none + */ + public ViewFactory getViewFactory() { + return this; + } + + /** + * Gives notification that something was inserted into + * the document in a location that this view is responsible for. + * This replaces the ViewFactory with an implementation that + * calls through to the createTableRow and createTableCell + * methods. If the element given to the factory isn't a + * table row or cell, the request is delegated to the factory + * passed as an argument. + * + * @param e the change information from the associated document + * @param a the current allocation of the view + * @param f the factory to use to rebuild if the view has children + * @see View#insertUpdate + */ + public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { + super.insertUpdate(e, a, this); + } + + /** + * Gives notification that something was removed from the document + * in a location that this view is responsible for. + * This replaces the ViewFactory with an implementation that + * calls through to the createTableRow and createTableCell + * methods. If the element given to the factory isn't a + * table row or cell, the request is delegated to the factory + * passed as an argument. + * + * @param e the change information from the associated document + * @param a the current allocation of the view + * @param f the factory to use to rebuild if the view has children + * @see View#removeUpdate + */ + public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { + super.removeUpdate(e, a, this); + } + + /** + * Gives notification from the document that attributes were changed + * in a location that this view is responsible for. + * This replaces the ViewFactory with an implementation that + * calls through to the createTableRow and createTableCell + * methods. If the element given to the factory isn't a + * table row or cell, the request is delegated to the factory + * passed as an argument. + * + * @param e the change information from the associated document + * @param a the current allocation of the view + * @param f the factory to use to rebuild if the view has children + * @see View#changedUpdate + */ + public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { + super.changedUpdate(e, a, this); + } + + protected void forwardUpdate(DocumentEvent.ElementChange ec, + DocumentEvent e, Shape a, ViewFactory f) { + super.forwardUpdate(ec, e, a, f); + // A change in any of the table cells usually effects the whole table, + // so redraw it all! + if (a != null) { + Component c = getContainer(); + if (c != null) { + Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : + a.getBounds(); + c.repaint(alloc.x, alloc.y, alloc.width, alloc.height); + } + } + } + + /** + * Change the child views. This is implemented to + * provide the superclass behavior and invalidate the + * grid so that rows and columns will be recalculated. + */ + public void replace(int offset, int length, View[] views) { + super.replace(offset, length, views); + invalidateGrid(); + } + + // --- ViewFactory methods ------------------------------------------ + + /** + * The table itself acts as a factory for the various + * views that actually represent pieces of the table. + * All other factory activity is delegated to the factory + * returned by the parent of the table. + */ + 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.TR) { + return createTableRow(elem); + } else if ((kind == HTML.Tag.TD) || (kind == HTML.Tag.TH)) { + return new CellView(elem); + } else if (kind == HTML.Tag.CAPTION) { + return new javax.swing.text.html.ParagraphView(elem); + } + } + // default is to delegate to the normal factory + View p = getParent(); + if (p != null) { + ViewFactory f = p.getViewFactory(); + if (f != null) { + return f.create(elem); + } + } + return null; + } + + // ---- variables ---------------------------------------------------- + + private AttributeSet attr; + private StyleSheet.BoxPainter painter; + + private int cellSpacing; + private int borderWidth; + + /** + * The index of the caption view if there is a caption. + * This has a value of -1 if there is no caption. The + * caption lives in the inset area of the table, and is + * updated with each time the grid is recalculated. + */ + private int captionIndex; + + /** + * Do any of the table cells contain a relative size + * specification? This is updated with each call to + * updateGrid(). If this is true, the ColumnIterator + * will do extra work to calculate relative cell + * specifications. + */ + private boolean relativeCells; + + /** + * Do any of the table cells span multiple rows? If + * true, the RowRequirementIterator will do additional + * work to adjust the requirements of rows spanned by + * a single table cell. This is updated with each call to + * updateGrid(). + */ + private boolean multiRowCells; + + int[] columnSpans; + int[] columnOffsets; + /** + * SizeRequirements for all the columns. + */ + SizeRequirements totalColumnRequirements; + SizeRequirements[] columnRequirements; + + RowIterator rowIterator = new RowIterator(); + ColumnIterator colIterator = new ColumnIterator(); + + Vector rows; + + // whether to display comments inside table or not. + boolean skipComments = false; + + boolean gridValid; + static final private BitSet EMPTY = new BitSet(); + + class ColumnIterator implements CSS.LayoutIterator { + + /** + * Disable percentage adjustments which should only apply + * when calculating layout, not requirements. + */ + void disablePercentages() { + percentages = null; + } + + /** + * Update percentage adjustments if they are needed. + */ + private void updatePercentagesAndAdjustmentWeights(int span) { + adjustmentWeights = new int[columnRequirements.length]; + for (int i = 0; i < columnRequirements.length; i++) { + adjustmentWeights[i] = 0; + } + if (relativeCells) { + percentages = new int[columnRequirements.length]; + } else { + percentages = null; + } + int nrows = getRowCount(); + for (int rowIndex = 0; rowIndex < nrows; rowIndex++) { + RowView row = getRow(rowIndex); + int col = 0; + int ncells = row.getViewCount(); + for (int cell = 0; cell < ncells; cell++, col++) { + View cv = row.getView(cell); + for (; row.isFilled(col); col++); // advance to a free column + int rowSpan = getRowsOccupied(cv); + int colSpan = getColumnsOccupied(cv); + AttributeSet a = cv.getAttributes(); + CSS.LengthValue lv = (CSS.LengthValue) + a.getAttribute(CSS.Attribute.WIDTH); + if ( lv != null ) { + int len = (int) (lv.getValue(span) / colSpan + 0.5f); + for (int i = 0; i < colSpan; i++) { + if (lv.isPercentage()) { + // add a percentage requirement + percentages[col+i] = Math.max(percentages[col+i], len); + adjustmentWeights[col + i] = Math.max(adjustmentWeights[col + i], WorstAdjustmentWeight); + } else { + adjustmentWeights[col + i] = Math.max(adjustmentWeights[col + i], WorstAdjustmentWeight - 1); + } + } + } + col += colSpan - 1; + } + } + } + + /** + * Set the layout arrays to use for holding layout results + */ + public void setLayoutArrays(int offsets[], int spans[], int targetSpan) { + this.offsets = offsets; + this.spans = spans; + updatePercentagesAndAdjustmentWeights(targetSpan); + } + + // --- RequirementIterator methods ------------------- + + public int getCount() { + return columnRequirements.length; + } + + public void setIndex(int i) { + col = i; + } + + public void setOffset(int offs) { + offsets[col] = offs; + } + + public int getOffset() { + return offsets[col]; + } + + public void setSpan(int span) { + spans[col] = span; + } + + public int getSpan() { + return spans[col]; + } + + public float getMinimumSpan(float parentSpan) { + // do not care for percentages, since min span can't + // be less than columnRequirements[col].minimum, + // but can be less than percentage value. + return columnRequirements[col].minimum; + } + + public float getPreferredSpan(float parentSpan) { + if ((percentages != null) && (percentages[col] != 0)) { + return Math.max(percentages[col], columnRequirements[col].minimum); + } + return columnRequirements[col].preferred; + } + + public float getMaximumSpan(float parentSpan) { + return columnRequirements[col].maximum; + } + + public float getBorderWidth() { + return borderWidth; + } + + + public float getLeadingCollapseSpan() { + return cellSpacing; + } + + public float getTrailingCollapseSpan() { + return cellSpacing; + } + + public int getAdjustmentWeight() { + return adjustmentWeights[col]; + } + + /** + * Current column index + */ + private int col; + + /** + * percentage values (may be null since there + * might not be any). + */ + private int[] percentages; + + private int[] adjustmentWeights; + + private int[] offsets; + private int[] spans; + } + + class RowIterator implements CSS.LayoutIterator { + + RowIterator() { + } + + void updateAdjustments() { + int axis = Y_AXIS; + if (multiRowCells) { + // adjust requirements of multi-row cells + int n = getRowCount(); + adjustments = new int[n]; + for (int i = 0; i < n; i++) { + RowView rv = getRow(i); + if (rv.multiRowCells == true) { + int ncells = rv.getViewCount(); + for (int j = 0; j < ncells; j++) { + View v = rv.getView(j); + int nrows = getRowsOccupied(v); + if (nrows > 1) { + int spanNeeded = (int) v.getPreferredSpan(axis); + adjustMultiRowSpan(spanNeeded, nrows, i); + } + } + } + } + } else { + adjustments = null; + } + } + + /** + * Fixup preferences to accomodate a multi-row table cell + * if not already covered by existing preferences. This is + * a no-op if not all of the rows needed (to do this check/fixup) + * have arrived yet. + */ + void adjustMultiRowSpan(int spanNeeded, int nrows, int rowIndex) { + if ((rowIndex + nrows) > getCount()) { + // rows are missing (could be a bad rowspan specification) + // or not all the rows have arrived. Do the best we can with + // the current set of rows. + nrows = getCount() - rowIndex; + if (nrows < 1) { + return; + } + } + int span = 0; + for (int i = 0; i < nrows; i++) { + RowView rv = getRow(rowIndex + i); + span += rv.getPreferredSpan(Y_AXIS); + } + if (spanNeeded > span) { + int adjust = (spanNeeded - span); + int rowAdjust = adjust / nrows; + int firstAdjust = rowAdjust + (adjust - (rowAdjust * nrows)); + RowView rv = getRow(rowIndex); + adjustments[rowIndex] = Math.max(adjustments[rowIndex], + firstAdjust); + for (int i = 1; i < nrows; i++) { + adjustments[rowIndex + i] = Math.max( + adjustments[rowIndex + i], rowAdjust); + } + } + } + + void setLayoutArrays(int[] offsets, int[] spans) { + this.offsets = offsets; + this.spans = spans; + } + + // --- RequirementIterator methods ------------------- + + public void setOffset(int offs) { + RowView rv = getRow(row); + if (rv != null) { + offsets[rv.viewIndex] = offs; + } + } + + public int getOffset() { + RowView rv = getRow(row); + if (rv != null) { + return offsets[rv.viewIndex]; + } + return 0; + } + + public void setSpan(int span) { + RowView rv = getRow(row); + if (rv != null) { + spans[rv.viewIndex] = span; + } + } + + public int getSpan() { + RowView rv = getRow(row); + if (rv != null) { + return spans[rv.viewIndex]; + } + return 0; + } + + public int getCount() { + return rows.size(); + } + + public void setIndex(int i) { + row = i; + } + + public float getMinimumSpan(float parentSpan) { + return getPreferredSpan(parentSpan); + } + + public float getPreferredSpan(float parentSpan) { + RowView rv = getRow(row); + if (rv != null) { + int adjust = (adjustments != null) ? adjustments[row] : 0; + return rv.getPreferredSpan(TableView.this.getAxis()) + adjust; + } + return 0; + } + + public float getMaximumSpan(float parentSpan) { + return getPreferredSpan(parentSpan); + } + + public float getBorderWidth() { + return borderWidth; + } + + public float getLeadingCollapseSpan() { + return cellSpacing; + } + + public float getTrailingCollapseSpan() { + return cellSpacing; + } + + public int getAdjustmentWeight() { + return 0; + } + + /** + * Current row index + */ + private int row; + + /** + * Adjustments to the row requirements to handle multi-row + * table cells. + */ + private int[] adjustments; + + private int[] offsets; + private int[] spans; + } + + /** + * View of a row in a row-centric table. + */ + public class RowView extends BoxView { + + /** + * Constructs a TableView for the given element. + * + * @param elem the element that this view is responsible for + */ + public RowView(Element elem) { + super(elem, View.X_AXIS); + fillColumns = new BitSet(); + RowView.this.setPropertiesFromAttributes(); + } + + void clearFilledColumns() { + fillColumns.and(EMPTY); + } + + void fillColumn(int col) { + fillColumns.set(col); + } + + boolean isFilled(int col) { + return fillColumns.get(col); + } + + /** + * The number of columns present in this row. + */ + int getColumnCount() { + int nfill = 0; + int n = fillColumns.size(); + for (int i = 0; i < n; i++) { + if (fillColumns.get(i)) { + nfill ++; + } + } + return getViewCount() + nfill; + } + + /** + * Fetches the attributes to use when rendering. This is + * implemented to multiplex the attributes specified in the + * model with a StyleSheet. + */ + public AttributeSet getAttributes() { + return attr; + } + + View findViewAtPoint(int x, int y, Rectangle alloc) { + int n = getViewCount(); + for (int i = 0; i < n; i++) { + if (getChildAllocation(i, alloc).contains(x, y)) { + childAllocation(i, alloc); + return getView(i); + } + } + return null; + } + + protected StyleSheet getStyleSheet() { + HTMLDocument doc = (HTMLDocument) getDocument(); + return doc.getStyleSheet(); + } + + /** + * This is called by a child to indicate its + * preferred span has changed. This is implemented to + * execute the superclass behavior and well as try to + * determine if a row with a multi-row cell hangs across + * this row. If a multi-row cell covers this row it also + * needs to propagate a preferenceChanged so that it will + * recalculate the multi-row cell. + * + * @param child the child view + * @param width true if the width preference should change + * @param height true if the height preference should change + */ + public void preferenceChanged(View child, boolean width, boolean height) { + super.preferenceChanged(child, width, height); + if (TableView.this.multiRowCells && height) { + for (int i = rowIndex - 1; i >= 0; i--) { + RowView rv = TableView.this.getRow(i); + if (rv.multiRowCells) { + rv.preferenceChanged(null, false, true); + break; + } + } + } + } + + // The major axis requirements for a row are dictated by the column + // requirements. These methods use the value calculated by + // TableView. + protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) { + SizeRequirements req = new SizeRequirements(); + req.minimum = totalColumnRequirements.minimum; + req.maximum = totalColumnRequirements.maximum; + req.preferred = totalColumnRequirements.preferred; + req.alignment = 0f; + return req; + } + + public float getMinimumSpan(int axis) { + float value; + + if (axis == View.X_AXIS) { + value = totalColumnRequirements.minimum + getLeftInset() + + getRightInset(); + } + else { + value = super.getMinimumSpan(axis); + } + return value; + } + + public float getMaximumSpan(int axis) { + float value; + + if (axis == View.X_AXIS) { + // We're flexible. + value = (float)Integer.MAX_VALUE; + } + else { + value = super.getMaximumSpan(axis); + } + return value; + } + + public float getPreferredSpan(int axis) { + float value; + + if (axis == View.X_AXIS) { + value = totalColumnRequirements.preferred + getLeftInset() + + getRightInset(); + } + else { + value = super.getPreferredSpan(axis); + } + return value; + } + + public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { + super.changedUpdate(e, a, f); + int pos = e.getOffset(); + if (pos <= getStartOffset() && (pos + e.getLength()) >= + getEndOffset()) { + RowView.this.setPropertiesFromAttributes(); + } + } + + /** + * Renders using the given rendering surface and area on that + * surface. This is implemented to delegate to the css box + * painter to paint the border and background prior to the + * interior. + * + * @param g the rendering surface to use + * @param allocation the allocated region to render into + * @see View#paint + */ + public void paint(Graphics g, Shape allocation) { + Rectangle a = (Rectangle) allocation; + painter.paint(g, a.x, a.y, a.width, a.height, this); + super.paint(g, a); + } + + /** + * Change the child views. This is implemented to + * provide the superclass behavior and invalidate the + * grid so that rows and columns will be recalculated. + */ + public void replace(int offset, int length, View[] views) { + super.replace(offset, length, views); + invalidateGrid(); + } + + /** + * Calculate the height requirements of the table row. The + * requirements of multi-row cells are not considered for this + * calculation. The table itself will check and adjust the row + * requirements for all the rows that have multi-row cells spanning + * them. This method updates the multi-row flag that indicates that + * this row and rows below need additional consideration. + */ + protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { +// return super.calculateMinorAxisRequirements(axis, r); + long min = 0; + long pref = 0; + long max = 0; + multiRowCells = false; + int n = getViewCount(); + for (int i = 0; i < n; i++) { + View v = getView(i); + if (getRowsOccupied(v) > 1) { + multiRowCells = true; + max = Math.max((int) v.getMaximumSpan(axis), max); + } else { + min = Math.max((int) v.getMinimumSpan(axis), min); + pref = Math.max((int) v.getPreferredSpan(axis), pref); + max = Math.max((int) v.getMaximumSpan(axis), max); + } + } + + if (r == null) { + r = new SizeRequirements(); + r.alignment = 0.5f; + } + r.preferred = (int) pref; + r.minimum = (int) min; + r.maximum = (int) max; + return r; + } + + /** + * Perform layout for the major axis of the box (i.e. the + * axis that it represents). The results of the layout should + * be placed in the given arrays which represent the allocations + * to the children along the major axis. + *

          + * This is re-implemented to give each child the span of the column + * width for the table, and to give cells that span multiple columns + * the multi-column span. + * + * @param targetSpan the total span given to the view, which + * whould be used to layout the children + * @param axis the axis being layed out + * @param offsets the offsets from the origin of the view for + * each of the child views; this is a return value and is + * filled in by the implementation of this method + * @param spans the span of each child view; this is a return + * value and is filled in by the implementation of this method + * @return the offset and span for each child view in the + * offsets and spans parameters + */ + protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { + int col = 0; + int ncells = getViewCount(); + for (int cell = 0; cell < ncells; cell++) { + View cv = getView(cell); + if (skipComments && !(cv instanceof CellView)) { + continue; + } + for (; isFilled(col); col++); // advance to a free column + int colSpan = getColumnsOccupied(cv); + spans[cell] = columnSpans[col]; + offsets[cell] = columnOffsets[col]; + if (colSpan > 1) { + int n = columnSpans.length; + for (int j = 1; j < colSpan; j++) { + // Because the table may be only partially formed, some + // of the columns may not yet exist. Therefore we check + // the bounds. + if ((col+j) < n) { + spans[cell] += columnSpans[col+j]; + spans[cell] += cellSpacing; + } + } + col += colSpan - 1; + } + col++; + } + } + + /** + * Perform layout for the minor axis of the box (i.e. the + * axis orthoginal to the axis that it represents). The results + * of the layout should be placed in the given arrays which represent + * the allocations to the children along the minor axis. This + * is called by the superclass whenever the layout needs to be + * updated along the minor axis. + *

          + * This is implemented to delegate to the superclass, then adjust + * the span for any cell that spans multiple rows. + * + * @param targetSpan the total span given to the view, which + * whould be used to layout the children + * @param axis the axis being layed out + * @param offsets the offsets from the origin of the view for + * each of the child views; this is a return value and is + * filled in by the implementation of this method + * @param spans the span of each child view; this is a return + * value and is filled in by the implementation of this method + * @return the offset and span for each child view in the + * offsets and spans parameters + */ + protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { + super.layoutMinorAxis(targetSpan, axis, offsets, spans); + int col = 0; + int ncells = getViewCount(); + for (int cell = 0; cell < ncells; cell++, col++) { + View cv = getView(cell); + for (; isFilled(col); col++); // advance to a free column + int colSpan = getColumnsOccupied(cv); + int rowSpan = getRowsOccupied(cv); + if (rowSpan > 1) { + + int row0 = rowIndex; + int row1 = Math.min(rowIndex + rowSpan - 1, getRowCount()-1); + spans[cell] = getMultiRowSpan(row0, row1); + } + if (colSpan > 1) { + col += colSpan - 1; + } + } + } + + /** + * Determines the resizability of the view along the + * given axis. A value of 0 or less is not resizable. + * + * @param axis may be either View.X_AXIS or View.Y_AXIS + * @return the resize weight + * @exception IllegalArgumentException for an invalid axis + */ + public int getResizeWeight(int axis) { + return 1; + } + + /** + * Fetches the child view that represents the given position in + * the model. This is implemented to walk through the children + * looking for a range that contains the given position. In this + * view the children do not necessarily have a one to one mapping + * with the child elements. + * + * @param pos the search position >= 0 + * @param a the allocation to the table on entry, and the + * allocation of the view containing the position on exit + * @return the view representing the given position, or + * null if there isn't one + */ + protected View getViewAtPosition(int pos, Rectangle a) { + int n = getViewCount(); + for (int i = 0; i < n; i++) { + View v = getView(i); + int p0 = v.getStartOffset(); + int p1 = v.getEndOffset(); + if ((pos >= p0) && (pos < p1)) { + // it's in this view. + if (a != null) { + childAllocation(i, a); + } + return v; + } + } + if (pos == getEndOffset()) { + View v = getView(n - 1); + if (a != null) { + this.childAllocation(n - 1, a); + } + return v; + } + return null; + } + + /** + * Update any cached values that come from attributes. + */ + void setPropertiesFromAttributes() { + StyleSheet sheet = getStyleSheet(); + attr = sheet.getViewAttributes(this); + painter = sheet.getBoxPainter(attr); + } + + private StyleSheet.BoxPainter painter; + private AttributeSet attr; + + /** columns filled by multi-column or multi-row cells */ + BitSet fillColumns; + + /** + * The row index within the overall grid + */ + int rowIndex; + + /** + * The view index (for row index to view index conversion). + * This is set by the updateGrid method. + */ + int viewIndex; + + /** + * Does this table row have cells that span multiple rows? + */ + boolean multiRowCells; + + } + + /** + * Default view of an html table cell. This needs to be moved + * somewhere else. + */ + class CellView extends BlockView { + + /** + * Constructs a TableCell for the given element. + * + * @param elem the element that this view is responsible for + */ + public CellView(Element elem) { + super(elem, Y_AXIS); + } + + /** + * Perform layout for the major axis of the box (i.e. the + * axis that it represents). The results of the layout should + * be placed in the given arrays which represent the allocations + * to the children along the major axis. This is called by the + * superclass to recalculate the positions of the child views + * when the layout might have changed. + *

          + * This is implemented to delegate to the superclass to + * tile the children. If the target span is greater than + * was needed, the offsets are adjusted to align the children + * (i.e. position according to the html valign attribute). + * + * @param targetSpan the total span given to the view, which + * whould be used to layout the children + * @param axis the axis being layed out + * @param offsets the offsets from the origin of the view for + * each of the child views; this is a return value and is + * filled in by the implementation of this method + * @param spans the span of each child view; this is a return + * value and is filled in by the implementation of this method + * @return the offset and span for each child view in the + * offsets and spans parameters + */ + protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { + super.layoutMajorAxis(targetSpan, axis, offsets, spans); + // calculate usage + int used = 0; + int n = spans.length; + for (int i = 0; i < n; i++) { + used += spans[i]; + } + + // calculate adjustments + int adjust = 0; + if (used < targetSpan) { + // PENDING(prinz) change to use the css alignment. + String valign = (String) getElement().getAttributes().getAttribute( + HTML.Attribute.VALIGN); + if (valign == null) { + AttributeSet rowAttr = getElement().getParentElement().getAttributes(); + valign = (String) rowAttr.getAttribute(HTML.Attribute.VALIGN); + } + if ((valign == null) || valign.equals("middle")) { + adjust = (targetSpan - used) / 2; + } else if (valign.equals("bottom")) { + adjust = targetSpan - used; + } + } + + // make adjustments. + if (adjust != 0) { + for (int i = 0; i < n; i++) { + offsets[i] += adjust; + } + } + } + + /** + * Calculate the requirements needed along the major axis. + * This is called by the superclass whenever the requirements + * need to be updated (i.e. a preferenceChanged was messaged + * through this view). + *

          + * This is implemented to delegate to the superclass, but + * indicate the maximum size is very large (i.e. the cell + * is willing to expend to occupy the full height of the row). + * + * @param axis the axis being layed out. + * @param r the requirements to fill in. If null, a new one + * should be allocated. + */ + protected SizeRequirements calculateMajorAxisRequirements(int axis, + SizeRequirements r) { + SizeRequirements req = super.calculateMajorAxisRequirements(axis, r); + req.maximum = Integer.MAX_VALUE; + return req; + } + + @Override + protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { + SizeRequirements rv = super.calculateMinorAxisRequirements(axis, r); + //for the cell the minimum should be derived from the child views + //the parent behaviour is to use CSS for that + int n = getViewCount(); + int min = 0; + for (int i = 0; i < n; i++) { + View v = getView(i); + min = Math.max((int) v.getMinimumSpan(axis), min); + } + rv.minimum = Math.min(rv.minimum, min); + return rv; + } + } + + +} diff --git a/src/share/classes/javax/swing/text/html/TextAreaDocument.java b/src/share/classes/javax/swing/text/html/TextAreaDocument.java new file mode 100644 index 000000000..7f9648da6 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/TextAreaDocument.java @@ -0,0 +1,68 @@ +/* + * Copyright 1998 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import javax.swing.text.*; + + +/** + * TextAreaDocument extends the capabilities of the PlainDocument + * to store the data that is initially set in the Document. + * This is stored in order to enable an accurate reset of the + * state when a reset is requested. + * + * @author Sunita Mani + */ + +class TextAreaDocument extends PlainDocument { + + String initialText; + + + /** + * Resets the model by removing all the data, + * and restoring it to its initial state. + */ + void reset() { + try { + remove(0, getLength()); + if (initialText != null) { + insertString(0, initialText, null); + } + } catch (BadLocationException e) { + } + } + + /** + * Stores the data that the model is initially + * loaded with. + */ + void storeInitialText() { + try { + initialText = getText(0, getLength()); + } catch (BadLocationException e) { + } + } +} diff --git a/src/share/classes/javax/swing/text/html/default.css b/src/share/classes/javax/swing/text/html/default.css new file mode 100644 index 000000000..9edc66354 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/default.css @@ -0,0 +1,267 @@ +/* + * Copyright 1997-2005 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +/* + */ + +body {font-size: 14pt; + font-family: Serif; + font-weight: normal; + margin-left: 0; + margin-right: 0; + color: black} + +p {margin-top: 15} + +h1 {font-size: x-large; + font-weight: bold; + margin-top: 10; + margin-bottom: 10} + +h2 {font-size: large; + font-weight: bold; + margin-top: 10; + margin-bottom: 10} + +h3 {font-size: medium; + font-weight: bold; + margin-top: 10; + margin-bottom: 10} + +h4 {font-size: small; + font-weight: bold; + margin-top: 10; + margin-bottom: 10} + +h5 {font-size: x-small; + font-weight: bold; + margin-top: 10; + margin-bottom: 10} + +h6 {font-size: xx-small; + font-weight: bold; + margin-top: 10; + margin-bottom: 10} + +li p {margin-top: 0; + margin-bottom: 0} + +td p {margin-top: 0} + +menu li p {margin-top: 0; + margin-bottom: 0} + +menu li {margin-left: 0; + margin-right: 0; + margin-top: 0; + margin-bottom: 0} + +menu {margin-left-ltr: 40; + margin-right-rtl: 40; + margin-top: 10; + margin-bottom: 10} + +dir li p {margin-top: 0; + margin-bottom: 0} + +dir li {margin-left: 0; + margin-right: 0; + margin-top: 0; + margin-bottom: 0} + +dir {margin-left-ltr: 40; + margin-right-rtl: 40; + margin-top: 10; + margin-bottom: 10} + +dd {margin-left-ltr: 40; + margin-right-rtl: 40; + margin-top: 0; + margin-bottom: 0} + +dd p {margin-left: 0; + margin-rigth: 0; + margin-top: 0; + margin-bottom: 0} + +dt {margin-top: 0; + margin-bottom: 0} + +dl {margin-left: 0; + margin-top: 10; + margin-bottom: 10} + +ol li {margin-left: 0; + margin-right: 0; + margin-top: 0; + margin-bottom: 0} + +ol { + margin-top: 10; + margin-bottom: 10; + margin-left-ltr: 50; + margin-right-rtl: 50; + list-style-type: decimal +} + +ol li p {margin-top: 0; + margin-bottom:0} + +ul li {margin-left: 0; + margin-right: 0; + margin-top: 0; + margin-bottom: 0} + +ul {margin-top: 10; + margin-bottom: 10; + margin-left-ltr: 50; + margin-right-rtl: 50; + list-style-type: disc; + -bullet-gap: 10} + +ul li ul li {margin-left: 0; + margin-right: 0; + margin-top: 0; + margin-bottom: 0} + +ul li ul {list-style-type: circle; + margin-left-ltr: 25; + margin-right-rtl: 25;} + +ul li ul li ul li {margin-left: 0; + margin-right: 0; + margin-top: 0; + margin-bottom: 0} + +ul li ul li ul {list-style-type: square; + margin-left-ltr: 25; + margin-right-rtl: 25} + +ul li menu {list-style-type: circle; + margin-left-ltr: 25; + margin-right-rtl: 25;} + +ul li p {margin-top: 0; + margin-bottom:0} + +a {color: blue; + text-decoration: underline} + +big {font-size: x-large} + +small {font-size: x-small} + +samp {font-size: small; + font-family: Monospaced} + +cite {font-style: italic} + +code {font-size: small; + font-family: Monospaced} + +dfn {font-style: italic} + +em {font-style: italic} + +i {font-style: italic} + +b {font-weight: bold} + +kbd {font-size: small; + font-family: Monospaced} + +s {text-decoration: line-through} + +strike {text-decoration: line-through} + +strong {font-weight: bold} + +sub {vertical-align: sub} + +sup {vertical-align: sup} + +tt {font-family: Monospaced} + +u {text-decoration: underline} + +var {font-weight: bold; + font-style: italic} + +table { + border-style: outset; + border-width: 0; +} + +tr { + text-align: left +} + +td { + border-width: 0; + border-style: inset; + padding-left: 3; + padding-right: 3; + padding-top: 3; + padding-bottom: 3 +} + +th { + text-align: center; + font-weight: bold; + border-width: 0; + border-style: inset; + padding-left: 3; + padding-right: 3; + padding-top: 3; + padding-bottom: 3 +} + +address { + color: blue; + font-style: italic +} + +blockquote { + margin-top: 5; + margin-bottom: 5; + margin-left: 35; + margin-right: 35 +} + +center {text-align: center} + +pre {margin-top: 5; + margin-bottom: 5; + font-family: Monospaced} + +pre p {margin-top: 0} + +caption { + caption-side: top; + text-align: center +} + +nobr { white-space: nowrap } + diff --git a/src/share/classes/javax/swing/text/html/package.html b/src/share/classes/javax/swing/text/html/package.html new file mode 100644 index 000000000..cbad9ecb8 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/package.html @@ -0,0 +1,59 @@ + + + + + + + + +Provides the class HTMLEditorKit and supporting classes +for creating HTML text editors. + +

          +Note: +Most of the Swing API is not thread safe. +For details, see +Threads and Swing, +a section in +The Java Tutorial. + +

          Package Specification

          + + + + +@since 1.2 +@serial exclude + + + diff --git a/src/share/classes/javax/swing/text/html/parser/AttributeList.java b/src/share/classes/javax/swing/text/html/parser/AttributeList.java new file mode 100644 index 000000000..a2d48603d --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/AttributeList.java @@ -0,0 +1,172 @@ +/* + * Copyright 1998-2004 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +import java.util.Vector; +import java.util.Hashtable; +import java.util.Enumeration; +import java.io.*; + +/** + * This class defines the attributes of an SGML element + * as described in a DTD using the ATTLIST construct. + * An AttributeList can be obtained from the Element + * class using the getAttributes() method. + *

          + * It is actually an element in a linked list. Use the + * getNext() method repeatedly to enumerate all the attributes + * of an element. + * + * @see Element + * @author Arthur Van Hoff + * + */ +public final +class AttributeList implements DTDConstants, Serializable { + public String name; + public int type; + public Vector values; + public int modifier; + public String value; + public AttributeList next; + + AttributeList() { + } + + /** + * Create an attribute list element. + */ + public AttributeList(String name) { + this.name = name; + } + + /** + * Create an attribute list element. + */ + public AttributeList(String name, int type, int modifier, String value, Vector values, AttributeList next) { + this.name = name; + this.type = type; + this.modifier = modifier; + this.value = value; + this.values = values; + this.next = next; + } + + /** + * @return attribute name + */ + public String getName() { + return name; + } + + /** + * @return attribute type + * @see DTDConstants + */ + public int getType() { + return type; + } + + /** + * @return attribute modifier + * @see DTDConstants + */ + public int getModifier() { + return modifier; + } + + /** + * @return possible attribute values + */ + public Enumeration getValues() { + return (values != null) ? values.elements() : null; + } + + /** + * @return default attribute value + */ + public String getValue() { + return value; + } + + /** + * @return the next attribute in the list + */ + public AttributeList getNext() { + return next; + } + + /** + * @return string representation + */ + public String toString() { + return name; + } + + /** + * Create a hashtable of attribute types. + */ + static Hashtable attributeTypes = new Hashtable(); + + static void defineAttributeType(String nm, int val) { + Integer num = new Integer(val); + attributeTypes.put(nm, num); + attributeTypes.put(num, nm); + } + + static { + defineAttributeType("CDATA", CDATA); + defineAttributeType("ENTITY", ENTITY); + defineAttributeType("ENTITIES", ENTITIES); + defineAttributeType("ID", ID); + defineAttributeType("IDREF", IDREF); + defineAttributeType("IDREFS", IDREFS); + defineAttributeType("NAME", NAME); + defineAttributeType("NAMES", NAMES); + defineAttributeType("NMTOKEN", NMTOKEN); + defineAttributeType("NMTOKENS", NMTOKENS); + defineAttributeType("NOTATION", NOTATION); + defineAttributeType("NUMBER", NUMBER); + defineAttributeType("NUMBERS", NUMBERS); + defineAttributeType("NUTOKEN", NUTOKEN); + defineAttributeType("NUTOKENS", NUTOKENS); + + attributeTypes.put("fixed", new Integer(FIXED)); + attributeTypes.put("required", new Integer(REQUIRED)); + attributeTypes.put("current", new Integer(CURRENT)); + attributeTypes.put("conref", new Integer(CONREF)); + attributeTypes.put("implied", new Integer(IMPLIED)); + } + + public static int name2type(String nm) { + Integer i = (Integer)attributeTypes.get(nm); + return (i == null) ? CDATA : i.intValue(); + } + + public static String type2name(int tp) { + return (String)attributeTypes.get(new Integer(tp)); + } +} diff --git a/src/share/classes/javax/swing/text/html/parser/ContentModel.java b/src/share/classes/javax/swing/text/html/parser/ContentModel.java new file mode 100644 index 000000000..852391f4b --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/ContentModel.java @@ -0,0 +1,255 @@ +/* + * Copyright 1998-2004 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +import java.util.Vector; +import java.util.Enumeration; +import java.io.*; + + +/** + * A representation of a content model. A content model is + * basically a restricted BNF expression. It is restricted in + * the sense that it must be deterministic. This means that you + * don't have to represent it as a finite state automata.

          + * See Annex H on page 556 of the SGML handbook for more information. + * + * @author Arthur van Hoff + * + */ +public final class ContentModel implements Serializable { + /** + * Type. Either '*', '?', '+', ',', '|', '&'. + */ + public int type; + + /** + * The content. Either an Element or a ContentModel. + */ + public Object content; + + /** + * The next content model (in a ',', '|' or '&' expression). + */ + public ContentModel next; + + public ContentModel() { + } + + /** + * Create a content model for an element. + */ + public ContentModel(Element content) { + this(0, content, null); + } + + /** + * Create a content model of a particular type. + */ + public ContentModel(int type, ContentModel content) { + this(type, content, null); + } + + /** + * Create a content model of a particular type. + */ + public ContentModel(int type, Object content, ContentModel next) { + this.type = type; + this.content = content; + this.next = next; + } + + /** + * Return true if the content model could + * match an empty input stream. + */ + public boolean empty() { + switch (type) { + case '*': + case '?': + return true; + + case '+': + case '|': + for (ContentModel m = (ContentModel)content ; m != null ; m = m.next) { + if (m.empty()) { + return true; + } + } + return false; + + case ',': + case '&': + for (ContentModel m = (ContentModel)content ; m != null ; m = m.next) { + if (!m.empty()) { + return false; + } + } + return true; + + default: + return false; + } + } + + /** + * Update elemVec with the list of elements that are + * part of the this contentModel. + */ + public void getElements(Vector elemVec) { + switch (type) { + case '*': + case '?': + case '+': + ((ContentModel)content).getElements(elemVec); + break; + case ',': + case '|': + case '&': + for (ContentModel m=(ContentModel)content; m != null; m=m.next){ + m.getElements(elemVec); + } + break; + default: + elemVec.addElement((Element)content); + } + } + + private boolean valSet[]; + private boolean val[]; + // A cache used by first(). This cache was found to speed parsing + // by about 10% (based on measurements of the 4-12 code base after + // buffering was fixed). + + /** + * Return true if the token could potentially be the + * first token in the input stream. + */ + public boolean first(Object token) { + switch (type) { + case '*': + case '?': + case '+': + return ((ContentModel)content).first(token); + + case ',': + for (ContentModel m = (ContentModel)content ; m != null ; m = m.next) { + if (m.first(token)) { + return true; + } + if (!m.empty()) { + return false; + } + } + return false; + + case '|': + case '&': { + Element e = (Element) token; + if (valSet == null) { + valSet = new boolean[Element.maxIndex + 1]; + val = new boolean[Element.maxIndex + 1]; + // All Element instances are created before this ever executes + } + if (valSet[e.index]) { + return val[e.index]; + } + for (ContentModel m = (ContentModel)content ; m != null ; m = m.next) { + if (m.first(token)) { + val[e.index] = true; + break; + } + } + valSet[e.index] = true; + return val[e.index]; + } + + default: + return (content == token); + // PENDING: refer to comment in ContentModelState +/* + if (content == token) { + return true; + } + Element e = (Element)content; + if (e.omitStart() && e.content != null) { + return e.content.first(token); + } + return false; +*/ + } + } + + /** + * Return the element that must be next. + */ + public Element first() { + switch (type) { + case '&': + case '|': + case '*': + case '?': + return null; + + case '+': + case ',': + return ((ContentModel)content).first(); + + default: + return (Element)content; + } + } + + /** + * Convert to a string. + */ + public String toString() { + switch (type) { + case '*': + return content + "*"; + case '?': + return content + "?"; + case '+': + return content + "+"; + + case ',': + case '|': + case '&': + char data[] = {' ', (char)type, ' '}; + String str = ""; + for (ContentModel m = (ContentModel)content ; m != null ; m = m.next) { + str = str + m; + if (m.next != null) { + str += new String(data); + } + } + return "(" + str + ")"; + + default: + return content.toString(); + } + } +} diff --git a/src/share/classes/javax/swing/text/html/parser/ContentModelState.java b/src/share/classes/javax/swing/text/html/parser/ContentModelState.java new file mode 100644 index 000000000..42444452a --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/ContentModelState.java @@ -0,0 +1,295 @@ +/* + * Copyright 1998-2000 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +/** + * A content model state. This is basically a list of pointers to + * the BNF expression representing the model (the ContentModel). + * Each element in a DTD has a content model which describes the + * elements that may occur inside, and the order in which they can + * occur. + *

          + * Each time a token is reduced a new state is created. + *

          + * See Annex H on page 556 of the SGML handbook for more information. + * + * @see Parser + * @see DTD + * @see Element + * @see ContentModel + * @author Arthur van Hoff + */ +class ContentModelState { + ContentModel model; + long value; + ContentModelState next; + + /** + * Create a content model state for a content model. + */ + public ContentModelState(ContentModel model) { + this(model, null, 0); + } + + /** + * Create a content model state for a content model given the + * remaining state that needs to be reduce. + */ + ContentModelState(Object content, ContentModelState next) { + this(content, next, 0); + } + + /** + * Create a content model state for a content model given the + * remaining state that needs to be reduce. + */ + ContentModelState(Object content, ContentModelState next, long value) { + this.model = (ContentModel)content; + this.next = next; + this.value = value; + } + + /** + * Return the content model that is relevant to the current state. + */ + public ContentModel getModel() { + ContentModel m = model; + for (int i = 0; i < value; i++) { + if (m.next != null) { + m = m.next; + } else { + return null; + } + } + return m; + } + + /** + * Check if the state can be terminated. That is there are no more + * tokens required in the input stream. + * @return true if the model can terminate without further input + */ + public boolean terminate() { + switch (model.type) { + case '+': + if ((value == 0) && !(model).empty()) { + return false; + } + case '*': + case '?': + return (next == null) || next.terminate(); + + case '|': + for (ContentModel m = (ContentModel)model.content ; m != null ; m = m.next) { + if (m.empty()) { + return (next == null) || next.terminate(); + } + } + return false; + + case '&': { + ContentModel m = (ContentModel)model.content; + + for (int i = 0 ; m != null ; i++, m = m.next) { + if ((value & (1L << i)) == 0) { + if (!m.empty()) { + return false; + } + } + } + return (next == null) || next.terminate(); + } + + case ',': { + ContentModel m = (ContentModel)model.content; + for (int i = 0 ; i < value ; i++, m = m.next); + + for (; (m != null) && m.empty() ; m = m.next); + if (m != null) { + return false; + } + return (next == null) || next.terminate(); + } + + default: + return false; + } + } + + /** + * Check if the state can be terminated. That is there are no more + * tokens required in the input stream. + * @return the only possible element that can occur next + */ + public Element first() { + switch (model.type) { + case '*': + case '?': + case '|': + case '&': + return null; + + case '+': + return model.first(); + + case ',': { + ContentModel m = (ContentModel)model.content; + for (int i = 0 ; i < value ; i++, m = m.next); + return m.first(); + } + + default: + return model.first(); + } + } + + /** + * Advance this state to a new state. An exception is thrown if the + * token is illegal at this point in the content model. + * @return next state after reducing a token + */ + public ContentModelState advance(Object token) { + switch (model.type) { + case '+': + if (model.first(token)) { + return new ContentModelState(model.content, + new ContentModelState(model, next, value + 1)).advance(token); + } + if (value != 0) { + if (next != null) { + return next.advance(token); + } else { + return null; + } + } + break; + + case '*': + if (model.first(token)) { + return new ContentModelState(model.content, this).advance(token); + } + if (next != null) { + return next.advance(token); + } else { + return null; + } + + case '?': + if (model.first(token)) { + return new ContentModelState(model.content, next).advance(token); + } + if (next != null) { + return next.advance(token); + } else { + return null; + } + + case '|': + for (ContentModel m = (ContentModel)model.content ; m != null ; m = m.next) { + if (m.first(token)) { + return new ContentModelState(m, next).advance(token); + } + } + break; + + case ',': { + ContentModel m = (ContentModel)model.content; + for (int i = 0 ; i < value ; i++, m = m.next); + + if (m.first(token) || m.empty()) { + if (m.next == null) { + return new ContentModelState(m, next).advance(token); + } else { + return new ContentModelState(m, + new ContentModelState(model, next, value + 1)).advance(token); + } + } + break; + } + + case '&': { + ContentModel m = (ContentModel)model.content; + boolean complete = true; + + for (int i = 0 ; m != null ; i++, m = m.next) { + if ((value & (1L << i)) == 0) { + if (m.first(token)) { + return new ContentModelState(m, + new ContentModelState(model, next, value | (1L << i))).advance(token); + } + if (!m.empty()) { + complete = false; + } + } + } + if (complete) { + if (next != null) { + return next.advance(token); + } else { + return null; + } + } + break; + } + + default: + if (model.content == token) { + if (next == null && (token instanceof Element) && + ((Element)token).content != null) { + return new ContentModelState(((Element)token).content); + } + return next; + } + // PENDING: Currently we don't correctly deal with optional start + // tags. This can most notably be seen with the 4.01 spec where + // TBODY's start and end tags are optional. + // Uncommenting this and the PENDING in ContentModel will + // correctly skip the omit tags, but the delegate is not notified. + // Some additional API needs to be added to track skipped tags, + // and this can then be added back. +/* + if ((model.content instanceof Element)) { + Element e = (Element)model.content; + + if (e.omitStart() && e.content != null) { + return new ContentModelState(e.content, next).advance( + token); + } + } +*/ + } + + // We used to throw this exception at this point. However, it + // was determined that throwing this exception was more expensive + // than returning null, and we could not justify to ourselves why + // it was necessary to throw an exception, rather than simply + // returning null. I'm leaving it in a commented out state so + // that it can be easily restored if the situation ever arises. + // + // throw new IllegalArgumentException("invalid token: " + token); + return null; + } +} diff --git a/src/share/classes/javax/swing/text/html/parser/DTD.java b/src/share/classes/javax/swing/text/html/parser/DTD.java new file mode 100644 index 000000000..3fd48f16b --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/DTD.java @@ -0,0 +1,451 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +import java.io.PrintStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.FileNotFoundException; +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.util.Hashtable; +import java.util.Vector; +import java.util.BitSet; +import java.util.StringTokenizer; +import java.util.Enumeration; +import java.util.Properties; +import java.net.URL; + +/** + * The representation of an SGML DTD. DTD describes a document + * syntax and is used in parsing of HTML documents. It contains + * a list of elements and their attributes as well as a list of + * entities defined in the DTD. + * + * @see Element + * @see AttributeList + * @see ContentModel + * @see Parser + * @author Arthur van Hoff + */ +public +class DTD implements DTDConstants { + public String name; + public Vector elements = new Vector(); + public Hashtable elementHash + = new Hashtable(); + public Hashtable entityHash + = new Hashtable(); + public final Element pcdata = getElement("#pcdata"); + public final Element html = getElement("html"); + public final Element meta = getElement("meta"); + public final Element base = getElement("base"); + public final Element isindex = getElement("isindex"); + public final Element head = getElement("head"); + public final Element body = getElement("body"); + public final Element applet = getElement("applet"); + public final Element param = getElement("param"); + public final Element p = getElement("p"); + public final Element title = getElement("title"); + final Element style = getElement("style"); + final Element link = getElement("link"); + final Element script = getElement("script"); + + public static final int FILE_VERSION = 1; + + /** + * Creates a new DTD with the specified name. + * @param name the name, as a String of the new DTD + */ + protected DTD(String name) { + this.name = name; + defEntity("#RE", GENERAL, '\r'); + defEntity("#RS", GENERAL, '\n'); + defEntity("#SPACE", GENERAL, ' '); + defineElement("unknown", EMPTY, false, true, null, null, null, null); + } + + /** + * Gets the name of the DTD. + * @return the name of the DTD + */ + public String getName() { + return name; + } + + /** + * Gets an entity by name. + * @return the Entity corresponding to the + * name String + */ + public Entity getEntity(String name) { + return (Entity)entityHash.get(name); + } + + /** + * Gets a character entity. + * @return the Entity corresponding to the + * ch character + */ + public Entity getEntity(int ch) { + return (Entity)entityHash.get(new Integer(ch)); + } + + /** + * Returns true if the element is part of the DTD, + * otherwise returns false. + * + * @param name the requested String + * @return true if name exists as + * part of the DTD, otherwise returns false + */ + boolean elementExists(String name) { + return !"unknown".equals(name) && (elementHash.get(name) != null); + } + + /** + * Gets an element by name. A new element is + * created if the element doesn't exist. + * + * @param name the requested String + * @return the Element corresponding to + * name, which may be newly created + */ + public Element getElement(String name) { + Element e = (Element)elementHash.get(name); + if (e == null) { + e = new Element(name, elements.size()); + elements.addElement(e); + elementHash.put(name, e); + } + return e; + } + + /** + * Gets an element by index. + * + * @param index the requested index + * @return the Element corresponding to + * index + */ + public Element getElement(int index) { + return (Element)elements.elementAt(index); + } + + /** + * Defines an entity. If the Entity specified + * by name, type, and data + * exists, it is returned; otherwise a new Entity + * is created and is returned. + * + * @param name the name of the Entity as a String + * @param type the type of the Entity + * @param data the Entity's data + * @return the Entity requested or a new Entity + * if not found + */ + public Entity defineEntity(String name, int type, char data[]) { + Entity ent = (Entity)entityHash.get(name); + if (ent == null) { + ent = new Entity(name, type, data); + entityHash.put(name, ent); + if (((type & GENERAL) != 0) && (data.length == 1)) { + switch (type & ~GENERAL) { + case CDATA: + case SDATA: + entityHash.put(new Integer(data[0]), ent); + break; + } + } + } + return ent; + } + + /** + * Returns the Element which matches the + * specified parameters. If one doesn't exist, a new + * one is created and returned. + * + * @param name the name of the Element + * @param type the type of the Element + * @param omitStart true if start should be omitted + * @param omitEnd true if end should be omitted + * @param content the ContentModel + * @param atts the AttributeList specifying the + * Element + * @return the Element specified + */ + public Element defineElement(String name, int type, + boolean omitStart, boolean omitEnd, ContentModel content, + BitSet exclusions, BitSet inclusions, AttributeList atts) { + Element e = getElement(name); + e.type = type; + e.oStart = omitStart; + e.oEnd = omitEnd; + e.content = content; + e.exclusions = exclusions; + e.inclusions = inclusions; + e.atts = atts; + return e; + } + + /** + * Defines attributes for an {@code Element}. + * + * @param name the name of the Element + * @param atts the AttributeList specifying the + * Element + */ + public void defineAttributes(String name, AttributeList atts) { + Element e = getElement(name); + e.atts = atts; + } + + /** + * Creates and returns a character Entity. + * @param name the entity's name + * @return the new character Entity + */ + public Entity defEntity(String name, int type, int ch) { + char data[] = {(char)ch}; + return defineEntity(name, type, data); + } + + /** + * Creates and returns an Entity. + * @param name the entity's name + * @return the new Entity + */ + protected Entity defEntity(String name, int type, String str) { + int len = str.length(); + char data[] = new char[len]; + str.getChars(0, len, data, 0); + return defineEntity(name, type, data); + } + + /** + * Creates and returns an Element. + * @param name the element's name + * @return the new Element + */ + protected Element defElement(String name, int type, + boolean omitStart, boolean omitEnd, ContentModel content, + String[] exclusions, String[] inclusions, AttributeList atts) { + BitSet excl = null; + if (exclusions != null && exclusions.length > 0) { + excl = new BitSet(); + for (int i = 0; i < exclusions.length; i++) { + String str = exclusions[i]; + if (str.length() > 0) { + excl.set(getElement(str).getIndex()); + } + } + } + BitSet incl = null; + if (inclusions != null && inclusions.length > 0) { + incl = new BitSet(); + for (int i = 0; i < inclusions.length; i++) { + String str = inclusions[i]; + if (str.length() > 0) { + incl.set(getElement(str).getIndex()); + } + } + } + return defineElement(name, type, omitStart, omitEnd, content, excl, incl, atts); + } + + /** + * Creates and returns an AttributeList. + * @param name the attribute list's name + * @return the new AttributeList + */ + protected AttributeList defAttributeList(String name, int type, int modifier, String value, String values, AttributeList atts) { + Vector vals = null; + if (values != null) { + vals = new Vector(); + for (StringTokenizer s = new StringTokenizer(values, "|") ; s.hasMoreTokens() ;) { + String str = s.nextToken(); + if (str.length() > 0) { + vals.addElement(str); + } + } + } + return new AttributeList(name, type, modifier, value, vals, atts); + } + + /** + * Creates and returns a new content model. + * @param type the type of the new content model + * @return the new ContentModel + */ + protected ContentModel defContentModel(int type, Object obj, ContentModel next) { + return new ContentModel(type, obj, next); + } + + /** + * Returns a string representation of this DTD. + * @return the string representation of this DTD + */ + public String toString() { + return name; + } + + /** + * The hashtable of DTDs. + */ + static Hashtable dtdHash = new Hashtable(); + + public static void putDTDHash(String name, DTD dtd) { + dtdHash.put(name, dtd); + } + /** + * Returns a DTD with the specified name. If + * a DTD with that name doesn't exist, one is created + * and returned. Any uppercase characters in the name + * are converted to lowercase. + * + * @param name the name of the DTD + * @return the DTD which corresponds to name + */ + public static DTD getDTD(String name) throws IOException { + name = name.toLowerCase(); + DTD dtd = (DTD)dtdHash.get(name); + if (dtd == null) + dtd = new DTD(name); + + return dtd; + } + + /** + * Recreates a DTD from an archived format. + * @param in the DataInputStream to read from + */ + public void read(DataInputStream in) throws IOException { + if (in.readInt() != FILE_VERSION) { + } + + // + // Read the list of names + // + String[] names = new String[in.readShort()]; + for (int i = 0; i < names.length; i++) { + names[i] = in.readUTF(); + } + + + // + // Read the entities + // + int num = in.readShort(); + for (int i = 0; i < num; i++) { + short nameId = in.readShort(); + int type = in.readByte(); + String name = in.readUTF(); + defEntity(names[nameId], type | GENERAL, name); + } + + // Read the elements + // + num = in.readShort(); + for (int i = 0; i < num; i++) { + short nameId = in.readShort(); + int type = in.readByte(); + byte flags = in.readByte(); + ContentModel m = readContentModel(in, names); + String[] exclusions = readNameArray(in, names); + String[] inclusions = readNameArray(in, names); + AttributeList atts = readAttributeList(in, names); + defElement(names[nameId], type, + ((flags & 0x01) != 0), ((flags & 0x02) != 0), + m, exclusions, inclusions, atts); + } + } + + private ContentModel readContentModel(DataInputStream in, String[] names) + throws IOException { + byte flag = in.readByte(); + switch(flag) { + case 0: // null + return null; + case 1: { // content_c + int type = in.readByte(); + ContentModel m = readContentModel(in, names); + ContentModel next = readContentModel(in, names); + return defContentModel(type, m, next); + } + case 2: { // content_e + int type = in.readByte(); + Element el = getElement(names[in.readShort()]); + ContentModel next = readContentModel(in, names); + return defContentModel(type, el, next); + } + default: + throw new IOException("bad bdtd"); + } + } + + private String[] readNameArray(DataInputStream in, String[] names) + throws IOException { + int num = in.readShort(); + if (num == 0) { + return null; + } + String[] result = new String[num]; + for (int i = 0; i < num; i++) { + result[i] = names[in.readShort()]; + } + return result; + } + + + private AttributeList readAttributeList(DataInputStream in, String[] names) + throws IOException { + AttributeList result = null; + for (int num = in.readByte(); num > 0; --num) { + short nameId = in.readShort(); + int type = in.readByte(); + int modifier = in.readByte(); + short valueId = in.readShort(); + String value = (valueId == -1) ? null : names[valueId]; + Vector values = null; + short numValues = in.readShort(); + if (numValues > 0) { + values = new Vector(numValues); + for (int i = 0; i < numValues; i++) { + values.addElement(names[in.readShort()]); + } + } +result = new AttributeList(names[nameId], type, modifier, value, + values, result); + // We reverse the order of the linked list by doing this, but + // that order isn't important. + } + return result; + } + +} diff --git a/src/share/classes/javax/swing/text/html/parser/DTDConstants.java b/src/share/classes/javax/swing/text/html/parser/DTDConstants.java new file mode 100644 index 000000000..f3fba925e --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/DTDConstants.java @@ -0,0 +1,82 @@ +/* + * Copyright 1998-1999 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +/** + * SGML constants used in a DTD. The names of the + * constants correspond the the equivalent SGML constructs + * as described in "The SGML Handbook" by Charles F. Goldfarb. + * + * @see DTD + * @see Element + * @author Arthur van Hoff + */ +public +interface DTDConstants { + // Attribute value types + int CDATA = 1; + int ENTITY = 2; + int ENTITIES = 3; + int ID = 4; + int IDREF = 5; + int IDREFS = 6; + int NAME = 7; + int NAMES = 8; + int NMTOKEN = 9; + int NMTOKENS = 10; + int NOTATION = 11; + int NUMBER = 12; + int NUMBERS = 13; + int NUTOKEN = 14; + int NUTOKENS = 15; + + // Content model types + int RCDATA = 16; + int EMPTY = 17; + int MODEL = 18; + int ANY = 19; + + // Attribute value modifiers + int FIXED = 1; + int REQUIRED = 2; + int CURRENT = 3; + int CONREF = 4; + int IMPLIED = 5; + + // Entity types + int PUBLIC = 10; + int SDATA = 11; + int PI = 12; + int STARTTAG = 13; + int ENDTAG = 14; + int MS = 15; + int MD = 16; + int SYSTEM = 17; + + int GENERAL = 1<<16; + int DEFAULT = 1<<17; + int PARAMETER = 1<<18; +} diff --git a/src/share/classes/javax/swing/text/html/parser/DocumentParser.java b/src/share/classes/javax/swing/text/html/parser/DocumentParser.java new file mode 100644 index 000000000..80417b5f7 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/DocumentParser.java @@ -0,0 +1,281 @@ +/* + * Copyright 1998-2003 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.html.HTMLEditorKit; +import javax.swing.text.html.HTML; +import javax.swing.text.ChangedCharSetException; + +import java.util.*; +import java.io.*; +import java.net.*; + +/** + * A Parser for HTML Documents (actually, you can specify a DTD, but + * you should really only use this class with the html dtd in swing). + * Reads an InputStream of HTML and + * invokes the appropriate methods in the ParserCallback class. This + * is the default parser used by HTMLEditorKit to parse HTML url's. + *

          This will message the callback for all valid tags, as well as + * tags that are implied but not explicitly specified. For example, the + * html string (<p>blah) only has a p tag defined. The callback + * will see the following methods: + *

          1. handleStartTag(html, ...)
          2. + *
          3. handleStartTag(head, ...)
          4. + *
          5. handleEndTag(head)
          6. + *
          7. handleStartTag(body, ...)
          8. + *
          9. handleStartTag(p, ...)
          10. + *
          11. handleText(...)
          12. + *
          13. handleEndTag(p)
          14. + *
          15. handleEndTag(body)
          16. + *
          17. handleEndTag(html)
          18. + *
          + * The items in italic are implied, that is, although they were not + * explicitly specified, to be correct html they should have been present + * (head isn't necessary, but it is still generated). For tags that + * are implied, the AttributeSet argument will have a value of + * Boolean.TRUE for the key + * HTMLEditorKit.ParserCallback.IMPLIED. + *

          HTML.Attributes defines a type safe enumeration of html attributes. + * If an attribute key of a tag is defined in HTML.Attribute, the + * HTML.Attribute will be used as the key, otherwise a String will be used. + * For example <p foo=bar class=neat> has two attributes. foo is + * not defined in HTML.Attribute, where as class is, therefore the + * AttributeSet will have two values in it, HTML.Attribute.CLASS with + * a String value of 'neat' and the String key 'foo' with a String value of + * 'bar'. + *

          The position argument will indicate the start of the tag, comment + * or text. Similiar to arrays, the first character in the stream has a + * position of 0. For tags that are + * implied the position will indicate + * the location of the next encountered tag. In the first example, + * the implied start body and html tags will have the same position as the + * p tag, and the implied end p, html and body tags will all have the same + * position. + *

          As html skips whitespace the position for text will be the position + * of the first valid character, eg in the string '\n\n\nblah' + * the text 'blah' will have a position of 3, the newlines are skipped. + *

          + * For attributes that do not have a value, eg in the html + * string <foo blah> the attribute blah + * does not have a value, there are two possible values that will be + * placed in the AttributeSet's value: + *

            + *
          • If the DTD does not contain an definition for the element, or the + * definition does not have an explicit value then the value in the + * AttributeSet will be HTML.NULL_ATTRIBUTE_VALUE. + *
          • If the DTD contains an explicit value, as in: + * <!ATTLIST OPTION selected (selected) #IMPLIED> + * this value from the dtd (in this case selected) will be used. + *
          + *

          + * Once the stream has been parsed, the callback is notified of the most + * likely end of line string. The end of line string will be one of + * \n, \r or \r\n, which ever is encountered the most in parsing the + * stream. + * + * @author Sunita Mani + */ +public class DocumentParser extends javax.swing.text.html.parser.Parser { + + private int inbody; + private int intitle; + private int inhead; + private int instyle; + private int inscript; + private boolean seentitle; + private HTMLEditorKit.ParserCallback callback = null; + private boolean ignoreCharSet = false; + private static final boolean debugFlag = false; + + public DocumentParser(DTD dtd) { + super(dtd); + } + + public void parse(Reader in, HTMLEditorKit.ParserCallback callback, boolean ignoreCharSet) throws IOException { + this.ignoreCharSet = ignoreCharSet; + this.callback = callback; + parse(in); + // end of line + callback.handleEndOfLineString(getEndOfLineString()); + } + + /** + * Handle Start Tag. + */ + protected void handleStartTag(TagElement tag) { + + Element elem = tag.getElement(); + if (elem == dtd.body) { + inbody++; + } else if (elem == dtd.html) { + } else if (elem == dtd.head) { + inhead++; + } else if (elem == dtd.title) { + intitle++; + } else if (elem == dtd.style) { + instyle++; + } else if (elem == dtd.script) { + inscript++; + } + if (debugFlag) { + if (tag.fictional()) { + debug("Start Tag: " + tag.getHTMLTag() + " pos: " + getCurrentPos()); + } else { + debug("Start Tag: " + tag.getHTMLTag() + " attributes: " + + getAttributes() + " pos: " + getCurrentPos()); + } + } + if (tag.fictional()) { + SimpleAttributeSet attrs = new SimpleAttributeSet(); + attrs.addAttribute(HTMLEditorKit.ParserCallback.IMPLIED, + Boolean.TRUE); + callback.handleStartTag(tag.getHTMLTag(), attrs, + getBlockStartPosition()); + } else { + callback.handleStartTag(tag.getHTMLTag(), getAttributes(), + getBlockStartPosition()); + flushAttributes(); + } + } + + + protected void handleComment(char text[]) { + if (debugFlag) { + debug("comment: ->" + new String(text) + "<-" + + " pos: " + getCurrentPos()); + } + callback.handleComment(text, getBlockStartPosition()); + } + + /** + * Handle Empty Tag. + */ + protected void handleEmptyTag(TagElement tag) throws ChangedCharSetException { + + Element elem = tag.getElement(); + if (elem == dtd.meta && !ignoreCharSet) { + SimpleAttributeSet atts = getAttributes(); + if (atts != null) { + String content = (String)atts.getAttribute(HTML.Attribute.CONTENT); + if (content != null) { + if ("content-type".equalsIgnoreCase((String)atts.getAttribute(HTML.Attribute.HTTPEQUIV))) { + if (!content.equalsIgnoreCase("text/html") && + !content.equalsIgnoreCase("text/plain")) { + throw new ChangedCharSetException(content, false); + } + } else if ("charset" .equalsIgnoreCase((String)atts.getAttribute(HTML.Attribute.HTTPEQUIV))) { + throw new ChangedCharSetException(content, true); + } + } + } + } + if (inbody != 0 || elem == dtd.meta || elem == dtd.base || elem == dtd.isindex || elem == dtd.style || elem == dtd.link) { + if (debugFlag) { + if (tag.fictional()) { + debug("Empty Tag: " + tag.getHTMLTag() + " pos: " + getCurrentPos()); + } else { + debug("Empty Tag: " + tag.getHTMLTag() + " attributes: " + + getAttributes() + " pos: " + getCurrentPos()); + } + } + if (tag.fictional()) { + SimpleAttributeSet attrs = new SimpleAttributeSet(); + attrs.addAttribute(HTMLEditorKit.ParserCallback.IMPLIED, + Boolean.TRUE); + callback.handleSimpleTag(tag.getHTMLTag(), attrs, + getBlockStartPosition()); + } else { + callback.handleSimpleTag(tag.getHTMLTag(), getAttributes(), + getBlockStartPosition()); + flushAttributes(); + } + } + } + + /** + * Handle End Tag. + */ + protected void handleEndTag(TagElement tag) { + Element elem = tag.getElement(); + if (elem == dtd.body) { + inbody--; + } else if (elem == dtd.title) { + intitle--; + seentitle = true; + } else if (elem == dtd.head) { + inhead--; + } else if (elem == dtd.style) { + instyle--; + } else if (elem == dtd.script) { + inscript--; + } + if (debugFlag) { + debug("End Tag: " + tag.getHTMLTag() + " pos: " + getCurrentPos()); + } + callback.handleEndTag(tag.getHTMLTag(), getBlockStartPosition()); + + } + + /** + * Handle Text. + */ + protected void handleText(char data[]) { + if (data != null) { + if (inscript != 0) { + callback.handleComment(data, getBlockStartPosition()); + return; + } + if (inbody != 0 || ((instyle != 0) || + ((intitle != 0) && !seentitle))) { + if (debugFlag) { + debug("text: ->" + new String(data) + "<-" + " pos: " + getCurrentPos()); + } + callback.handleText(data, getBlockStartPosition()); + } + } + } + + /* + * Error handling. + */ + protected void handleError(int ln, String errorMsg) { + if (debugFlag) { + debug("Error: ->" + errorMsg + "<-" + " pos: " + getCurrentPos()); + } + /* PENDING: need to improve the error string. */ + callback.handleError(errorMsg, getCurrentPos()); + } + + + /* + * debug messages + */ + private void debug(String msg) { + System.out.println(msg); + } +} diff --git a/src/share/classes/javax/swing/text/html/parser/Element.java b/src/share/classes/javax/swing/text/html/parser/Element.java new file mode 100644 index 000000000..54b68618d --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/Element.java @@ -0,0 +1,175 @@ +/* + * Copyright 1998 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +import java.util.Hashtable; +import java.util.BitSet; +import java.io.*; + +/** + * An element as described in a DTD using the ELEMENT construct. + * This is essentiall the description of a tag. It describes the + * type, content model, attributes, attribute types etc. It is used + * to correctly parse a document by the Parser. + * + * @see DTD + * @see AttributeList + * @author Arthur van Hoff + */ +public final +class Element implements DTDConstants, Serializable { + public int index; + public String name; + public boolean oStart; + public boolean oEnd; + public BitSet inclusions; + public BitSet exclusions; + public int type = ANY; + public ContentModel content; + public AttributeList atts; + + static int maxIndex = 0; + + /** + * A field to store user data. Mostly used to store + * style sheets. + */ + public Object data; + + Element() { + } + + /** + * Create a new element. + */ + Element(String name, int index) { + this.name = name; + this.index = index; + maxIndex = Math.max(maxIndex, index); + } + + /** + * Get the name of the element. + */ + public String getName() { + return name; + } + + /** + * Return true if the start tag can be omitted. + */ + public boolean omitStart() { + return oStart; + } + + /** + * Return true if the end tag can be omitted. + */ + public boolean omitEnd() { + return oEnd; + } + + /** + * Get type. + */ + public int getType() { + return type; + } + + /** + * Get content model + */ + public ContentModel getContent() { + return content; + } + + /** + * Get the attributes. + */ + public AttributeList getAttributes() { + return atts; + } + + /** + * Get index. + */ + public int getIndex() { + return index; + } + + /** + * Check if empty + */ + public boolean isEmpty() { + return type == EMPTY; + } + + /** + * Convert to a string. + */ + public String toString() { + return name; + } + + /** + * Get an attribute by name. + */ + public AttributeList getAttribute(String name) { + for (AttributeList a = atts ; a != null ; a = a.next) { + if (a.name.equals(name)) { + return a; + } + } + return null; + } + + /** + * Get an attribute by value. + */ + public AttributeList getAttributeByValue(String name) { + for (AttributeList a = atts ; a != null ; a = a.next) { + if ((a.values != null) && a.values.contains(name)) { + return a; + } + } + return null; + } + + + static Hashtable contentTypes = new Hashtable(); + + static { + contentTypes.put("CDATA", new Integer(CDATA)); + contentTypes.put("RCDATA", new Integer(RCDATA)); + contentTypes.put("EMPTY", new Integer(EMPTY)); + contentTypes.put("ANY", new Integer(ANY)); + } + + public static int name2type(String nm) { + Integer val = (Integer)contentTypes.get(nm); + return (val != null) ? val.intValue() : 0; + } +} diff --git a/src/share/classes/javax/swing/text/html/parser/Entity.java b/src/share/classes/javax/swing/text/html/parser/Entity.java new file mode 100644 index 000000000..f4a5cb9a7 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/Entity.java @@ -0,0 +1,139 @@ +/* + * Copyright 1998-2001 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +import java.util.Hashtable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.CharArrayReader; +import java.net.URL; + +/** + * An entity is described in a DTD using the ENTITY construct. + * It defines the type and value of the the entity. + * + * @see DTD + * @author Arthur van Hoff + */ +public final +class Entity implements DTDConstants { + public String name; + public int type; + public char data[]; + + /** + * Creates an entity. + * @param name the name of the entity + * @param type the type of the entity + * @param data the char array of data + */ + public Entity(String name, int type, char data[]) { + this.name = name; + this.type = type; + this.data = data; + } + + /** + * Gets the name of the entity. + * @return the name of the entity, as a String + */ + public String getName() { + return name; + } + + /** + * Gets the type of the entity. + * @return the type of the entity + */ + public int getType() { + return type & 0xFFFF; + } + + /** + * Returns true if it is a parameter entity. + * @return true if it is a parameter entity + */ + public boolean isParameter() { + return (type & PARAMETER) != 0; + } + + /** + * Returns true if it is a general entity. + * @return true if it is a general entity + */ + public boolean isGeneral() { + return (type & GENERAL) != 0; + } + + /** + * Returns the data. + * @return the data + */ + public char getData()[] { + return data; + } + + /** + * Returns the data as a String. + * @return the data as a String + */ + public String getString() { + return new String(data, 0, data.length); + } + + + static Hashtable entityTypes = new Hashtable(); + + static { + entityTypes.put("PUBLIC", new Integer(PUBLIC)); + entityTypes.put("CDATA", new Integer(CDATA)); + entityTypes.put("SDATA", new Integer(SDATA)); + entityTypes.put("PI", new Integer(PI)); + entityTypes.put("STARTTAG", new Integer(STARTTAG)); + entityTypes.put("ENDTAG", new Integer(ENDTAG)); + entityTypes.put("MS", new Integer(MS)); + entityTypes.put("MD", new Integer(MD)); + entityTypes.put("SYSTEM", new Integer(SYSTEM)); + } + + /** + * Converts nm string to the corresponding + * entity type. If the string does not have a corresponding + * entity type, returns the type corresponding to "CDATA". + * Valid entity types are: "PUBLIC", "CDATA", "SDATA", "PI", + * "STARTTAG", "ENDTAG", "MS", "MD", "SYSTEM". + * + * @param nm the string to be converted + * @return the corresponding entity type, or the type corresponding + * to "CDATA", if none exists + */ + public static int name2type(String nm) { + Integer i = (Integer)entityTypes.get(nm); + return (i == null) ? CDATA : i.intValue(); + } +} diff --git a/src/share/classes/javax/swing/text/html/parser/Parser.java b/src/share/classes/javax/swing/text/html/parser/Parser.java new file mode 100644 index 000000000..24ba58fff --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/Parser.java @@ -0,0 +1,2312 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. 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. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ + +package javax.swing.text.html.parser; + +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.html.HTML; +import javax.swing.text.ChangedCharSetException; +import java.io.*; +import java.util.Hashtable; +import java.util.Properties; +import java.util.Vector; +import java.util.Enumeration; +import java.net.URL; + +import sun.misc.MessageUtils; + +/** + * A simple DTD-driven HTML parser. The parser reads an + * HTML file from an InputStream and calls various methods + * (which should be overridden in a subclass) when tags and + * data are encountered. + *

          + * Unfortunately there are many badly implemented HTML parsers + * out there, and as a result there are many badly formatted + * HTML files. This parser attempts to parse most HTML files. + * This means that the implementation sometimes deviates from + * the SGML specification in favor of HTML. + *

          + * The parser treats \r and \r\n as \n. Newlines after starttags + * and before end tags are ignored just as specified in the SGML/HTML + * specification. + *

          + * The html spec does not specify how spaces are to be coalesced very well. + * Specifically, the following scenarios are not discussed (note that a + * space should be used here, but I am using &nbsp to force the space to + * be displayed): + *

          + * '<b>blah <i> <strike> foo' which can be treated as: + * '<b>blah <i><strike>foo' + *

          as well as: + * '<p><a href="xx"> <em>Using</em></a></p>' + * which appears to be treated as: + * '<p><a href="xx"><em>Using</em></a></p>' + *

          + * If strict is false, when a tag that breaks flow, + * (TagElement.breaksFlows) or trailing whitespace is + * encountered, all whitespace will be ignored until a non whitespace + * character is encountered. This appears to give behavior closer to + * the popular browsers. + * + * @see DTD + * @see TagElement + * @see SimpleAttributeSet + * @author Arthur van Hoff + * @author Sunita Mani + */ +public +class Parser implements DTDConstants { + + private char text[] = new char[1024]; + private int textpos = 0; + private TagElement last; + private boolean space; + + private char str[] = new char[128]; + private int strpos = 0; + + protected DTD dtd = null; + + private int ch; + private int ln; + private Reader in; + + private Element recent; + private TagStack stack; + private boolean skipTag = false; + private TagElement lastFormSent = null; + private SimpleAttributeSet attributes = new SimpleAttributeSet(); + + // State for , and . Since people like to slap + // together HTML documents without thinking, occasionally they + // have multiple instances of these tags. These booleans track + // the first sightings of these tags so they can be safely ignored + // by the parser if repeated. + private boolean seenHtml = false; + private boolean seenHead = false; + private boolean seenBody = false; + + /** + * The html spec does not specify how spaces are coalesced very well. + * If strict == false, ignoreSpace is used to try and mimic the behavior + * of the popular browsers. + *

          + * The problematic scenarios are: + * '<b>blah <i> <strike> foo' which can be treated as: + * '<b>blah <i><strike>foo' + * as well as: + * '<p><a href="xx"> <em>Using</em></a></p>' + * which appears to be treated as: + * '<p><a href="xx"><em>Using</em></a></p>' + *

          + * When a tag that breaks flow, or trailing whitespace is encountered + * ignoreSpace is set to true. From then on, all whitespace will be + * ignored. + * ignoreSpace will be set back to false the first time a + * non whitespace character is encountered. This appears to give + * behavior closer to the popular browsers. + */ + private boolean ignoreSpace; + + /** + * This flag determines whether or not the Parser will be strict + * in enforcing SGML compatibility. If false, it will be lenient + * with certain common classes of erroneous HTML constructs. + * Strict or not, in either case an error will be recorded. + * + */ + protected boolean strict = false; + + + /** Number of \r\n's encountered. */ + private int crlfCount; + /** Number of \r's encountered. A \r\n will not increment this. */ + private int crCount; + /** Number of \n's encountered. A \r\n will not increment this. */ + private int lfCount; + + // + // To correctly identify the start of a tag/comment/text we need two + // ivars. Two are needed as handleText isn't invoked until the tag + // after the text has been parsed, that is the parser parses the text, + // then a tag, then invokes handleText followed by handleStart. + // + /** The start position of the current block. Block is overloaded here, + * it really means the current start position for the current comment, + * tag, text. Use getBlockStartPosition to access this. */ + private int currentBlockStartPos; + /** Start position of the last block. */ + private int lastBlockStartPos; + + /** + * array for mapping numeric references in range + * 130-159 to displayable Unicode characters. + */ + private static final char[] cp1252Map = { + 8218, // ‚ + 402, // ƒ + 8222, // „ + 8230, // … + 8224, // † + 8225, // ‡ + 710, // ˆ + 8240, // ‰ + 352, // Š + 8249, // ‹ + 338, // Œ + 141, //  + 142, // Ž + 143, //  + 144, //  + 8216, // ‘ + 8217, // ’ + 8220, // “ + 8221, // ” + 8226, // • + 8211, // – + 8212, // — + 732, // ˜ + 8482, // ™ + 353, // š + 8250, // › + 339, // œ + 157, //  + 158, // ž + 376 // Ÿ + }; + + public Parser(DTD dtd) { + this.dtd = dtd; + } + + + /** + * @return the line number of the line currently being parsed + */ + protected int getCurrentLine() { + return ln; + } + + /** + * Returns the start position of the current block. Block is + * overloaded here, it really means the current start position for + * the current comment tag, text, block.... This is provided for + * subclassers that wish to know the start of the current block when + * called with one of the handleXXX methods. + */ + int getBlockStartPosition() { + return Math.max(0, lastBlockStartPos - 1); + } + + /** + * Makes a TagElement. + */ + protected TagElement makeTag(Element elem, boolean fictional) { + return new TagElement(elem, fictional); + } + + protected TagElement makeTag(Element elem) { + return makeTag(elem, false); + } + + protected SimpleAttributeSet getAttributes() { + return attributes; + } + + protected void flushAttributes() { + attributes.removeAttributes(attributes); + } + + /** + * Called when PCDATA is encountered. + */ + protected void handleText(char text[]) { + } + + /** + * Called when an HTML title tag is encountered. + */ + protected void handleTitle(char text[]) { + // default behavior is to call handleText. Subclasses + // can override if necessary. + handleText(text); + } + + /** + * Called when an HTML comment is encountered. + */ + protected void handleComment(char text[]) { + } + + protected void handleEOFInComment() { + // We've reached EOF. Our recovery strategy is to + // see if we have more than one line in the comment; + // if so, we pretend that the comment was an unterminated + // single line comment, and reparse the lines after the + // first line as normal HTML content. + + int commentEndPos = strIndexOf('\n'); + if (commentEndPos >= 0) { + handleComment(getChars(0, commentEndPos)); + try { + in.close(); + in = new CharArrayReader(getChars(commentEndPos + 1)); + ch = '>'; + } catch (IOException e) { + error("ioexception"); + } + + resetStrBuffer(); + } else { + // no newline, so signal an error + error("eof.comment"); + } + } + + /** + * Called when an empty tag is encountered. + */ + protected void handleEmptyTag(TagElement tag) throws ChangedCharSetException { + } + + /** + * Called when a start tag is encountered. + */ + protected void handleStartTag(TagElement tag) { + } + + /** + * Called when an end tag is encountered. + */ + protected void handleEndTag(TagElement tag) { + } + + /** + * An error has occurred. + */ + protected void handleError(int ln, String msg) { + /* + Thread.dumpStack(); + System.out.println("**** " + stack); + System.out.println("line " + ln + ": error: " + msg); + System.out.println(); + */ + } + + /** + * Output text. + */ + void handleText(TagElement tag) { + if (tag.breaksFlow()) { + space = false; + if (!strict) { + ignoreSpace = true; + } + } + if (textpos == 0) { + if ((!space) || (stack == null) || last.breaksFlow() || + !stack.advance(dtd.pcdata)) { + last = tag; + space = false; + lastBlockStartPos = currentBlockStartPos; + return; + } + } + if (space) { + if (!ignoreSpace) { + // enlarge buffer if needed + if (textpos + 1 > text.length) { + char newtext[] = new char[text.length + 200]; + System.arraycopy(text, 0, newtext, 0, text.length); + text = newtext; + } + + // output pending space + text[textpos++] = ' '; + if (!strict && !tag.getElement().isEmpty()) { + ignoreSpace = true; + } + } + space = false; + } + char newtext[] = new char[textpos]; + System.arraycopy(text, 0, newtext, 0, textpos); + // Handles cases of bad html where the title tag + // was getting lost when we did error recovery. + if (tag.getElement().getName().equals("title")) { + handleTitle(newtext); + } else { + handleText(newtext); + } + lastBlockStartPos = currentBlockStartPos; + textpos = 0; + last = tag; + space = false; + } + + /** + * Invoke the error handler. + */ + protected void error(String err, String arg1, String arg2, + String arg3) { + handleError(ln, err + " " + arg1 + " " + arg2 + " " + arg3); + } + + protected void error(String err, String arg1, String arg2) { + error(err, arg1, arg2, "?"); + } + protected void error(String err, String arg1) { + error(err, arg1, "?", "?"); + } + protected void error(String err) { + error(err, "?", "?", "?"); + } + + + /** + * Handle a start tag. The new tag is pushed + * onto the tag stack. The attribute list is + * checked for required attributes. + */ + protected void startTag(TagElement tag) throws ChangedCharSetException { + Element elem = tag.getElement(); + + // If the tag is an empty tag and texpos != 0 + // this implies that there is text before the + // start tag that needs to be processed before + // handling the tag. + // + if (!elem.isEmpty() || + ((last != null) && !last.breaksFlow()) || + (textpos != 0)) { + handleText(tag); + } else { + // this variable gets updated in handleText(). + // Since in this case we do not call handleText() + // we need to update it here. + // + last = tag; + // Note that we should really check last.breakFlows before + // assuming this should be false. + space = false; + } + lastBlockStartPos = currentBlockStartPos; + + // check required attributes + for (AttributeList a = elem.atts ; a != null ; a = a.next) { + if ((a.modifier == REQUIRED) && + ((attributes.isEmpty()) || + ((!attributes.isDefined(a.name)) && + (!attributes.isDefined(HTML.getAttributeKey(a.name)))))) { + error("req.att ", a.getName(), elem.getName()); + } + } + + if (elem.isEmpty()) { + handleEmptyTag(tag); + /* + } else if (elem.getName().equals("form")) { + handleStartTag(tag); + */ + } else { + recent = elem; + stack = new TagStack(tag, stack); + handleStartTag(tag); + } + } + + /** + * Handle an end tag. The end tag is popped + * from the tag stack. + */ + protected void endTag(boolean omitted) { + handleText(stack.tag); + + if (omitted && !stack.elem.omitEnd()) { + error("end.missing", stack.elem.getName()); + } else if (!stack.terminate()) { + error("end.unexpected", stack.elem.getName()); + } + + // handle the tag + handleEndTag(stack.tag); + stack = stack.next; + recent = (stack != null) ? stack.elem : null; + } + + + boolean ignoreElement(Element elem) { + + String stackElement = stack.elem.getName(); + String elemName = elem.getName(); + /* We ignore all elements that are not valid in the context of + a table except , (these we handle in + legalElementContext()) and #pcdata. We also ignore the + tag in the context of

            and
              We additonally + ignore the and the