aboutsummaryrefslogtreecommitdiff
path: root/src/share/classes/javax/swing/text
diff options
context:
space:
mode:
authorduke <none@none>2007-12-01 00:00:00 +0000
committerduke <none@none>2007-12-01 00:00:00 +0000
commit59308f67f9b7038cfa2ceb9ee9ba27645b927cb5 (patch)
tree182810ab2fece13f57a928d026f93e9ede0827f9 /src/share/classes/javax/swing/text
Initial loadjdk7-b24
Diffstat (limited to 'src/share/classes/javax/swing/text')
-rw-r--r--src/share/classes/javax/swing/text/AbstractDocument.java3121
-rw-r--r--src/share/classes/javax/swing/text/AbstractWriter.java713
-rw-r--r--src/share/classes/javax/swing/text/AsyncBoxView.java1420
-rw-r--r--src/share/classes/javax/swing/text/AttributeSet.java193
-rw-r--r--src/share/classes/javax/swing/text/BadLocationException.java65
-rw-r--r--src/share/classes/javax/swing/text/BoxView.java1188
-rw-r--r--src/share/classes/javax/swing/text/Caret.java204
-rw-r--r--src/share/classes/javax/swing/text/ChangedCharSetException.java53
-rw-r--r--src/share/classes/javax/swing/text/ComponentView.java503
-rw-r--r--src/share/classes/javax/swing/text/CompositeView.java794
-rw-r--r--src/share/classes/javax/swing/text/DateFormatter.java160
-rw-r--r--src/share/classes/javax/swing/text/DefaultCaret.java1917
-rw-r--r--src/share/classes/javax/swing/text/DefaultEditorKit.java2306
-rw-r--r--src/share/classes/javax/swing/text/DefaultFormatter.java758
-rw-r--r--src/share/classes/javax/swing/text/DefaultFormatterFactory.java313
-rw-r--r--src/share/classes/javax/swing/text/DefaultHighlighter.java638
-rw-r--r--src/share/classes/javax/swing/text/DefaultStyledDocument.java2742
-rw-r--r--src/share/classes/javax/swing/text/DefaultTextUI.java42
-rw-r--r--src/share/classes/javax/swing/text/Document.java476
-rw-r--r--src/share/classes/javax/swing/text/DocumentFilter.java186
-rw-r--r--src/share/classes/javax/swing/text/EditorKit.java208
-rw-r--r--src/share/classes/javax/swing/text/Element.java139
-rw-r--r--src/share/classes/javax/swing/text/ElementIterator.java381
-rw-r--r--src/share/classes/javax/swing/text/FieldView.java311
-rw-r--r--src/share/classes/javax/swing/text/FlowView.java863
-rw-r--r--src/share/classes/javax/swing/text/GapContent.java954
-rw-r--r--src/share/classes/javax/swing/text/GapVector.java299
-rw-r--r--src/share/classes/javax/swing/text/GlyphPainter1.java250
-rw-r--r--src/share/classes/javax/swing/text/GlyphPainter2.java381
-rw-r--r--src/share/classes/javax/swing/text/GlyphView.java1334
-rw-r--r--src/share/classes/javax/swing/text/Highlighter.java152
-rw-r--r--src/share/classes/javax/swing/text/IconView.java168
-rw-r--r--src/share/classes/javax/swing/text/InternationalFormatter.java1103
-rw-r--r--src/share/classes/javax/swing/text/JTextComponent.java5042
-rw-r--r--src/share/classes/javax/swing/text/Keymap.java145
-rw-r--r--src/share/classes/javax/swing/text/LabelView.java316
-rw-r--r--src/share/classes/javax/swing/text/LayeredHighlighter.java63
-rw-r--r--src/share/classes/javax/swing/text/LayoutQueue.java121
-rw-r--r--src/share/classes/javax/swing/text/MaskFormatter.java1015
-rw-r--r--src/share/classes/javax/swing/text/MutableAttributeSet.java87
-rw-r--r--src/share/classes/javax/swing/text/NavigationFilter.java147
-rw-r--r--src/share/classes/javax/swing/text/NumberFormatter.java505
-rw-r--r--src/share/classes/javax/swing/text/ParagraphView.java1219
-rw-r--r--src/share/classes/javax/swing/text/PasswordView.java236
-rw-r--r--src/share/classes/javax/swing/text/PlainDocument.java324
-rw-r--r--src/share/classes/javax/swing/text/PlainView.java715
-rw-r--r--src/share/classes/javax/swing/text/Position.java93
-rw-r--r--src/share/classes/javax/swing/text/Segment.java316
-rw-r--r--src/share/classes/javax/swing/text/SegmentCache.java127
-rw-r--r--src/share/classes/javax/swing/text/SimpleAttributeSet.java392
-rw-r--r--src/share/classes/javax/swing/text/StateInvariantError.java45
-rw-r--r--src/share/classes/javax/swing/text/StringContent.java498
-rw-r--r--src/share/classes/javax/swing/text/Style.java75
-rw-r--r--src/share/classes/javax/swing/text/StyleConstants.java852
-rw-r--r--src/share/classes/javax/swing/text/StyleContext.java1625
-rw-r--r--src/share/classes/javax/swing/text/StyledDocument.java177
-rw-r--r--src/share/classes/javax/swing/text/StyledEditorKit.java895
-rw-r--r--src/share/classes/javax/swing/text/TabExpander.java47
-rw-r--r--src/share/classes/javax/swing/text/TabSet.java212
-rw-r--r--src/share/classes/javax/swing/text/TabStop.java177
-rw-r--r--src/share/classes/javax/swing/text/TabableView.java70
-rw-r--r--src/share/classes/javax/swing/text/TableView.java908
-rw-r--r--src/share/classes/javax/swing/text/TextAction.java137
-rw-r--r--src/share/classes/javax/swing/text/TextLayoutStrategy.java539
-rw-r--r--src/share/classes/javax/swing/text/Utilities.java1050
-rw-r--r--src/share/classes/javax/swing/text/View.java1343
-rw-r--r--src/share/classes/javax/swing/text/ViewFactory.java48
-rw-r--r--src/share/classes/javax/swing/text/WhitespaceBasedBreakIterator.java119
-rw-r--r--src/share/classes/javax/swing/text/WrappedPlainView.java877
-rw-r--r--src/share/classes/javax/swing/text/ZoneView.java651
-rw-r--r--src/share/classes/javax/swing/text/doc-files/Document-coord.gifbin0 -> 2480 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/Document-insert.gifbin0 -> 3747 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/Document-notification.gifbin0 -> 12309 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/Document-remove.gifbin0 -> 4059 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/Document-structure.gifbin0 -> 5664 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/OpenBookIcon.gifbin0 -> 2241 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/View-flexibility.jpgbin0 -> 26097 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/View-layout.jpgbin0 -> 40479 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/editor.gifbin0 -> 4768 bytes
-rw-r--r--src/share/classes/javax/swing/text/doc-files/paragraph.gifbin0 -> 4695 bytes
-rw-r--r--src/share/classes/javax/swing/text/html/AccessibleHTML.java3058
-rw-r--r--src/share/classes/javax/swing/text/html/BRView.java57
-rw-r--r--src/share/classes/javax/swing/text/html/BlockView.java443
-rw-r--r--src/share/classes/javax/swing/text/html/CSS.java3383
-rw-r--r--src/share/classes/javax/swing/text/html/CSSBorder.java435
-rw-r--r--src/share/classes/javax/swing/text/html/CSSParser.java854
-rw-r--r--src/share/classes/javax/swing/text/html/CommentView.java141
-rw-r--r--src/share/classes/javax/swing/text/html/EditableView.java128
-rw-r--r--src/share/classes/javax/swing/text/html/FormSubmitEvent.java92
-rw-r--r--src/share/classes/javax/swing/text/html/FormView.java932
-rw-r--r--src/share/classes/javax/swing/text/html/FrameSetView.java322
-rw-r--r--src/share/classes/javax/swing/text/html/FrameView.java479
-rw-r--r--src/share/classes/javax/swing/text/html/HRuleView.java316
-rw-r--r--src/share/classes/javax/swing/text/html/HTML.java697
-rw-r--r--src/share/classes/javax/swing/text/html/HTMLDocument.java4185
-rw-r--r--src/share/classes/javax/swing/text/html/HTMLEditorKit.java2279
-rw-r--r--src/share/classes/javax/swing/text/html/HTMLFrameHyperlinkEvent.java134
-rw-r--r--src/share/classes/javax/swing/text/html/HTMLWriter.java1276
-rw-r--r--src/share/classes/javax/swing/text/html/HiddenTagView.java361
-rw-r--r--src/share/classes/javax/swing/text/html/ImageView.java997
-rw-r--r--src/share/classes/javax/swing/text/html/InlineView.java225
-rw-r--r--src/share/classes/javax/swing/text/html/IsindexView.java114
-rw-r--r--src/share/classes/javax/swing/text/html/LineView.java186
-rw-r--r--src/share/classes/javax/swing/text/html/ListView.java122
-rw-r--r--src/share/classes/javax/swing/text/html/Map.java505
-rw-r--r--src/share/classes/javax/swing/text/html/MinimalHTMLWriter.java726
-rw-r--r--src/share/classes/javax/swing/text/html/MuxingAttributeSet.java311
-rw-r--r--src/share/classes/javax/swing/text/html/NoFramesView.java173
-rw-r--r--src/share/classes/javax/swing/text/html/ObjectView.java182
-rw-r--r--src/share/classes/javax/swing/text/html/Option.java120
-rw-r--r--src/share/classes/javax/swing/text/html/OptionComboBoxModel.java63
-rw-r--r--src/share/classes/javax/swing/text/html/OptionListModel.java571
-rw-r--r--src/share/classes/javax/swing/text/html/ParagraphView.java294
-rw-r--r--src/share/classes/javax/swing/text/html/ResourceLoader.java60
-rw-r--r--src/share/classes/javax/swing/text/html/StyleSheet.java3340
-rw-r--r--src/share/classes/javax/swing/text/html/TableView.java1801
-rw-r--r--src/share/classes/javax/swing/text/html/TextAreaDocument.java68
-rw-r--r--src/share/classes/javax/swing/text/html/default.css267
-rw-r--r--src/share/classes/javax/swing/text/html/package.html59
-rw-r--r--src/share/classes/javax/swing/text/html/parser/AttributeList.java172
-rw-r--r--src/share/classes/javax/swing/text/html/parser/ContentModel.java255
-rw-r--r--src/share/classes/javax/swing/text/html/parser/ContentModelState.java295
-rw-r--r--src/share/classes/javax/swing/text/html/parser/DTD.java451
-rw-r--r--src/share/classes/javax/swing/text/html/parser/DTDConstants.java82
-rw-r--r--src/share/classes/javax/swing/text/html/parser/DocumentParser.java281
-rw-r--r--src/share/classes/javax/swing/text/html/parser/Element.java175
-rw-r--r--src/share/classes/javax/swing/text/html/parser/Entity.java139
-rw-r--r--src/share/classes/javax/swing/text/html/parser/Parser.java2312
-rw-r--r--src/share/classes/javax/swing/text/html/parser/ParserDelegator.java120
-rw-r--r--src/share/classes/javax/swing/text/html/parser/ResourceLoader.java60
-rw-r--r--src/share/classes/javax/swing/text/html/parser/TagElement.java74
-rw-r--r--src/share/classes/javax/swing/text/html/parser/TagStack.java220
-rw-r--r--src/share/classes/javax/swing/text/html/parser/html32.bdtdbin0 -> 16173 bytes
-rw-r--r--src/share/classes/javax/swing/text/html/parser/package.html54
-rw-r--r--src/share/classes/javax/swing/text/package.html62
-rw-r--r--src/share/classes/javax/swing/text/rtf/AbstractFilter.java229
-rw-r--r--src/share/classes/javax/swing/text/rtf/Constants.java78
-rw-r--r--src/share/classes/javax/swing/text/rtf/MockAttributeSet.java124
-rw-r--r--src/share/classes/javax/swing/text/rtf/RTFAttribute.java67
-rw-r--r--src/share/classes/javax/swing/text/rtf/RTFAttributes.java422
-rw-r--r--src/share/classes/javax/swing/text/rtf/RTFEditorKit.java155
-rw-r--r--src/share/classes/javax/swing/text/rtf/RTFGenerator.java1009
-rw-r--r--src/share/classes/javax/swing/text/rtf/RTFParser.java333
-rw-r--r--src/share/classes/javax/swing/text/rtf/RTFReader.java1647
-rw-r--r--src/share/classes/javax/swing/text/rtf/charsets/NeXT.txt33
-rw-r--r--src/share/classes/javax/swing/text/rtf/charsets/ansi.txt38
-rw-r--r--src/share/classes/javax/swing/text/rtf/charsets/cpg437.txt45
-rw-r--r--src/share/classes/javax/swing/text/rtf/charsets/cpg850.txt44
-rw-r--r--src/share/classes/javax/swing/text/rtf/charsets/mac.txt43
-rw-r--r--src/share/classes/javax/swing/text/rtf/package.html51
150 files changed, 84369 insertions, 0 deletions
diff --git a/src/share/classes/javax/swing/text/AbstractDocument.java b/src/share/classes/javax/swing/text/AbstractDocument.java
new file mode 100644
index 000000000..d9ca3f387
--- /dev/null
+++ b/src/share/classes/javax/swing/text/AbstractDocument.java
@@ -0,0 +1,3121 @@
+/*
+ * 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;
+
+import java.util.*;
+import java.io.*;
+import java.awt.font.TextAttribute;
+import java.text.Bidi;
+
+import javax.swing.UIManager;
+import javax.swing.undo.*;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.*;
+import javax.swing.tree.TreeNode;
+
+import sun.font.BidiUtils;
+import sun.swing.SwingUtilities2;
+
+/**
+ * An implementation of the document interface to serve as a
+ * basis for implementing various kinds of documents. At this
+ * level there is very little policy, so there is a corresponding
+ * increase in difficulty of use.
+ * <p>
+ * This class implements a locking mechanism for the document. It
+ * allows multiple readers or one writer, and writers must wait until
+ * all observers of the document have been notified of a previous
+ * change before beginning another mutation to the document. The
+ * read lock is acquired and released using the <code>render</code>
+ * method. A write lock is aquired by the methods that mutate the
+ * document, and are held for the duration of the method call.
+ * Notification is done on the thread that produced the mutation,
+ * and the thread has full read access to the document for the
+ * duration of the notification, but other readers are kept out
+ * until the notification has finished. The notification is a
+ * beans event notification which does not allow any further
+ * mutations until all listeners have been notified.
+ * <p>
+ * Any models subclassed from this class and used in conjunction
+ * with a text component that has a look and feel implementation
+ * that is derived from BasicTextUI may be safely updated
+ * asynchronously, because all access to the View hierarchy
+ * is serialized by BasicTextUI if the document is of type
+ * <code>AbstractDocument</code>. The locking assumes that an
+ * independent thread will access the View hierarchy only from
+ * the DocumentListener methods, and that there will be only
+ * one event thread active at a time.
+ * <p>
+ * If concurrency support is desired, there are the following
+ * additional implications. The code path for any DocumentListener
+ * implementation and any UndoListener implementation must be threadsafe,
+ * and not access the component lock if trying to be safe from deadlocks.
+ * The <code>repaint</code> and <code>revalidate</code> methods
+ * on JComponent are safe.
+ * <p>
+ * AbstractDocument models an implied break at the end of the document.
+ * Among other things this allows you to position the caret after the last
+ * character. As a result of this, <code>getLength</code> returns one less
+ * than the length of the Content. If you create your own Content, be
+ * sure and initialize it to have an additional character. Refer to
+ * StringContent and GapContent for examples of this. Another implication
+ * of this is that Elements that model the implied end character will have
+ * an endOffset == (getLength() + 1). For example, in DefaultStyledDocument
+ * <code>getParagraphElement(getLength()).getEndOffset() == getLength() + 1
+ * </code>.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Timothy Prinzing
+ */
+public abstract class AbstractDocument implements Document, Serializable {
+
+ /**
+ * Constructs a new <code>AbstractDocument</code>, wrapped around some
+ * specified content storage mechanism.
+ *
+ * @param data the content
+ */
+ protected AbstractDocument(Content data) {
+ this(data, StyleContext.getDefaultStyleContext());
+ }
+
+ /**
+ * Constructs a new <code>AbstractDocument</code>, wrapped around some
+ * specified content storage mechanism.
+ *
+ * @param data the content
+ * @param context the attribute context
+ */
+ protected AbstractDocument(Content data, AttributeContext context) {
+ this.data = data;
+ this.context = context;
+ bidiRoot = new BidiRootElement();
+
+ if (defaultI18NProperty == null) {
+ // determine default setting for i18n support
+ Object o = java.security.AccessController.doPrivileged(
+ new java.security.PrivilegedAction() {
+ public Object run() {
+ return System.getProperty(I18NProperty);
+ }
+ }
+ );
+ if (o != null) {
+ defaultI18NProperty = Boolean.valueOf((String)o);
+ } else {
+ defaultI18NProperty = Boolean.FALSE;
+ }
+ }
+ putProperty( I18NProperty, defaultI18NProperty);
+
+ //REMIND(bcb) This creates an initial bidi element to account for
+ //the \n that exists by default in the content. Doing it this way
+ //seems to expose a little too much knowledge of the content given
+ //to us by the sub-class. Consider having the sub-class' constructor
+ //make an initial call to insertUpdate.
+ writeLock();
+ try {
+ Element[] p = new Element[1];
+ p[0] = new BidiElement( bidiRoot, 0, 1, 0 );
+ bidiRoot.replace(0,0,p);
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ /**
+ * Supports managing a set of properties. Callers
+ * can use the <code>documentProperties</code> dictionary
+ * to annotate the document with document-wide properties.
+ *
+ * @return a non-<code>null</code> <code>Dictionary</code>
+ * @see #setDocumentProperties
+ */
+ public Dictionary<Object,Object> getDocumentProperties() {
+ if (documentProperties == null) {
+ documentProperties = new Hashtable(2);
+ }
+ return documentProperties;
+ }
+
+ /**
+ * Replaces the document properties dictionary for this document.
+ *
+ * @param x the new dictionary
+ * @see #getDocumentProperties
+ */
+ public void setDocumentProperties(Dictionary<Object,Object> x) {
+ documentProperties = x;
+ }
+
+ /**
+ * 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 fireInsertUpdate(DocumentEvent e) {
+ notifyingListeners = true;
+ try {
+ // Guaranteed to return a non-null array
+ Object[] listeners = listenerList.getListenerList();
+ // Process the listeners last to first, notifying
+ // those that are interested in this event
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==DocumentListener.class) {
+ // Lazily create the event:
+ // if (e == null)
+ // e = new ListSelectionEvent(this, firstIndex, lastIndex);
+ ((DocumentListener)listeners[i+1]).insertUpdate(e);
+ }
+ }
+ } finally {
+ notifyingListeners = false;
+ }
+ }
+
+ /**
+ * 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) {
+ notifyingListeners = true;
+ try {
+ // Guaranteed to return a non-null array
+ Object[] listeners = listenerList.getListenerList();
+ // Process the listeners last to first, notifying
+ // those that are interested in this event
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==DocumentListener.class) {
+ // Lazily create the event:
+ // if (e == null)
+ // e = new ListSelectionEvent(this, firstIndex, lastIndex);
+ ((DocumentListener)listeners[i+1]).changedUpdate(e);
+ }
+ }
+ } finally {
+ notifyingListeners = false;
+ }
+ }
+
+ /**
+ * 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 fireRemoveUpdate(DocumentEvent e) {
+ notifyingListeners = true;
+ try {
+ // Guaranteed to return a non-null array
+ Object[] listeners = listenerList.getListenerList();
+ // Process the listeners last to first, notifying
+ // those that are interested in this event
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==DocumentListener.class) {
+ // Lazily create the event:
+ // if (e == null)
+ // e = new ListSelectionEvent(this, firstIndex, lastIndex);
+ ((DocumentListener)listeners[i+1]).removeUpdate(e);
+ }
+ }
+ } finally {
+ notifyingListeners = false;
+ }
+ }
+
+ /**
+ * 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) {
+ // Guaranteed to return a non-null array
+ Object[] listeners = listenerList.getListenerList();
+ // Process the listeners last to first, notifying
+ // those that are interested in this event
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==UndoableEditListener.class) {
+ // Lazily create the event:
+ // if (e == null)
+ // e = new ListSelectionEvent(this, firstIndex, lastIndex);
+ ((UndoableEditListener)listeners[i+1]).undoableEditHappened(e);
+ }
+ }
+ }
+
+ /**
+ * Returns an array of all the objects currently registered
+ * as <code><em>Foo</em>Listener</code>s
+ * upon this document.
+ * <code><em>Foo</em>Listener</code>s are registered using the
+ * <code>add<em>Foo</em>Listener</code> method.
+ *
+ * <p>
+ * You can specify the <code>listenerType</code> argument
+ * with a class literal, such as
+ * <code><em>Foo</em>Listener.class</code>.
+ * For example, you can query a
+ * document <code>d</code>
+ * for its document listeners with the following code:
+ *
+ * <pre>DocumentListener[] mls = (DocumentListener[])(d.getListeners(DocumentListener.class));</pre>
+ *
+ * If no such listeners exist, this method returns an empty array.
+ *
+ * @param listenerType the type of listeners requested; this parameter
+ * should specify an interface that descends from
+ * <code>java.util.EventListener</code>
+ * @return an array of all objects registered as
+ * <code><em>Foo</em>Listener</code>s on this component,
+ * or an empty array if no such
+ * listeners have been added
+ * @exception ClassCastException if <code>listenerType</code>
+ * doesn't specify a class or interface that implements
+ * <code>java.util.EventListener</code>
+ *
+ * @see #getDocumentListeners
+ * @see #getUndoableEditListeners
+ *
+ * @since 1.3
+ */
+ public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
+ return listenerList.getListeners(listenerType);
+ }
+
+ /**
+ * Gets the asynchronous loading priority. If less than zero,
+ * the document should not be loaded asynchronously.
+ *
+ * @return the asynchronous loading priority, or <code>-1</code>
+ * if the document should not be loaded asynchronously
+ */
+ public int getAsynchronousLoadPriority() {
+ Integer loadPriority = (Integer)
+ getProperty(AbstractDocument.AsyncLoadPriority);
+ if (loadPriority != null) {
+ return loadPriority.intValue();
+ }
+ return -1;
+ }
+
+ /**
+ * Sets the asynchronous loading priority.
+ * @param p the new asynchronous loading priority; a value
+ * less than zero indicates that the document should not be
+ * loaded asynchronously
+ */
+ public void setAsynchronousLoadPriority(int p) {
+ Integer loadPriority = (p >= 0) ? new Integer(p) : null;
+ putProperty(AbstractDocument.AsyncLoadPriority, loadPriority);
+ }
+
+ /**
+ * Sets the <code>DocumentFilter</code>. The <code>DocumentFilter</code>
+ * is passed <code>insert</code> and <code>remove</code> to conditionally
+ * allow inserting/deleting of the text. A <code>null</code> value
+ * indicates that no filtering will occur.
+ *
+ * @param filter the <code>DocumentFilter</code> used to constrain text
+ * @see #getDocumentFilter
+ * @since 1.4
+ */
+ public void setDocumentFilter(DocumentFilter filter) {
+ documentFilter = filter;
+ }
+
+ /**
+ * Returns the <code>DocumentFilter</code> that is responsible for
+ * filtering of insertion/removal. A <code>null</code> return value
+ * implies no filtering is to occur.
+ *
+ * @since 1.4
+ * @see #setDocumentFilter
+ * @return the DocumentFilter
+ */
+ public DocumentFilter getDocumentFilter() {
+ return documentFilter;
+ }
+
+ // --- Document methods -----------------------------------------
+
+ /**
+ * This allows the model to be safely rendered in the presence
+ * of currency, if the model supports being updated asynchronously.
+ * The given runnable will be executed in a way that allows it
+ * to safely read the model with no changes while the runnable
+ * is being executed. The runnable itself may <em>not</em>
+ * make any mutations.
+ * <p>
+ * This is implemented to aquire a read lock for the duration
+ * of the runnables execution. There may be multiple runnables
+ * executing at the same time, and all writers will be blocked
+ * while there are active rendering runnables. If the runnable
+ * throws an exception, its lock will be safely released.
+ * There is no protection against a runnable that never exits,
+ * which will effectively leave the document locked for it's
+ * lifetime.
+ * <p>
+ * If the given runnable attempts to make any mutations in
+ * this implementation, a deadlock will occur. There is
+ * no tracking of individual rendering threads to enable
+ * detecting this situation, but a subclass could incur
+ * the overhead of tracking them and throwing an error.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param r the renderer to execute
+ */
+ public void render(Runnable r) {
+ readLock();
+ try {
+ r.run();
+ } finally {
+ readUnlock();
+ }
+ }
+
+ /**
+ * Returns the length of the data. This is the number of
+ * characters of content that represents the users data.
+ *
+ * @return the length >= 0
+ * @see Document#getLength
+ */
+ public int getLength() {
+ return data.length() - 1;
+ }
+
+ /**
+ * Adds a document listener for notification of any changes.
+ *
+ * @param listener the <code>DocumentListener</code> to add
+ * @see Document#addDocumentListener
+ */
+ public void addDocumentListener(DocumentListener listener) {
+ listenerList.add(DocumentListener.class, listener);
+ }
+
+ /**
+ * Removes a document listener.
+ *
+ * @param listener the <code>DocumentListener</code> to remove
+ * @see Document#removeDocumentListener
+ */
+ public void removeDocumentListener(DocumentListener listener) {
+ listenerList.remove(DocumentListener.class, listener);
+ }
+
+ /**
+ * Returns an array of all the document listeners
+ * registered on this document.
+ *
+ * @return all of this document's <code>DocumentListener</code>s
+ * or an empty array if no document listeners are
+ * currently registered
+ *
+ * @see #addDocumentListener
+ * @see #removeDocumentListener
+ * @since 1.4
+ */
+ public DocumentListener[] getDocumentListeners() {
+ return (DocumentListener[])listenerList.getListeners(
+ DocumentListener.class);
+ }
+
+ /**
+ * Adds an undo listener for notification of any changes.
+ * Undo/Redo operations performed on the <code>UndoableEdit</code>
+ * will cause the appropriate DocumentEvent to be fired to keep
+ * the view(s) in sync with the model.
+ *
+ * @param listener the <code>UndoableEditListener</code> to add
+ * @see Document#addUndoableEditListener
+ */
+ public void addUndoableEditListener(UndoableEditListener listener) {
+ listenerList.add(UndoableEditListener.class, listener);
+ }
+
+ /**
+ * Removes an undo listener.
+ *
+ * @param listener the <code>UndoableEditListener</code> to remove
+ * @see Document#removeDocumentListener
+ */
+ public void removeUndoableEditListener(UndoableEditListener listener) {
+ listenerList.remove(UndoableEditListener.class, listener);
+ }
+
+ /**
+ * Returns an array of all the undoable edit listeners
+ * registered on this document.
+ *
+ * @return all of this document's <code>UndoableEditListener</code>s
+ * or an empty array if no undoable edit listeners are
+ * currently registered
+ *
+ * @see #addUndoableEditListener
+ * @see #removeUndoableEditListener
+ *
+ * @since 1.4
+ */
+ public UndoableEditListener[] getUndoableEditListeners() {
+ return (UndoableEditListener[])listenerList.getListeners(
+ UndoableEditListener.class);
+ }
+
+ /**
+ * A convenience method for looking up a property value. It is
+ * equivalent to:
+ * <pre>
+ * getDocumentProperties().get(key);
+ * </pre>
+ *
+ * @param key the non-<code>null</code> property key
+ * @return the value of this property or <code>null</code>
+ * @see #getDocumentProperties
+ */
+ public final Object getProperty(Object key) {
+ return getDocumentProperties().get(key);
+ }
+
+
+ /**
+ * A convenience method for storing up a property value. It is
+ * equivalent to:
+ * <pre>
+ * getDocumentProperties().put(key, value);
+ * </pre>
+ * If <code>value</code> is <code>null</code> this method will
+ * remove the property.
+ *
+ * @param key the non-<code>null</code> key
+ * @param value the property value
+ * @see #getDocumentProperties
+ */
+ public final void putProperty(Object key, Object value) {
+ if (value != null) {
+ getDocumentProperties().put(key, value);
+ } else {
+ getDocumentProperties().remove(key);
+ }
+ if( key == TextAttribute.RUN_DIRECTION
+ && Boolean.TRUE.equals(getProperty(I18NProperty)) )
+ {
+ //REMIND - this needs to flip on the i18n property if run dir
+ //is rtl and the i18n property is not already on.
+ writeLock();
+ try {
+ DefaultDocumentEvent e
+ = new DefaultDocumentEvent(0, getLength(),
+ DocumentEvent.EventType.INSERT);
+ updateBidi( e );
+ } finally {
+ writeUnlock();
+ }
+ }
+ }
+
+ /**
+ * Removes some content from the document.
+ * Removing content causes a write lock to be held while the
+ * actual changes are taking place. Observers are notified
+ * of the change on the thread that called this method.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param offs the starting offset >= 0
+ * @param len the number of characters to remove >= 0
+ * @exception BadLocationException the given remove position is not a valid
+ * position within the document
+ * @see Document#remove
+ */
+ public void remove(int offs, int len) throws BadLocationException {
+ DocumentFilter filter = getDocumentFilter();
+
+ writeLock();
+ try {
+ if (filter != null) {
+ filter.remove(getFilterBypass(), offs, len);
+ }
+ else {
+ handleRemove(offs, len);
+ }
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ /**
+ * Performs the actual work of the remove. It is assumed the caller
+ * will have obtained a <code>writeLock</code> before invoking this.
+ */
+ void handleRemove(int offs, int len) throws BadLocationException {
+ if (len > 0) {
+ if (offs < 0 || (offs + len) > getLength()) {
+ throw new BadLocationException("Invalid remove",
+ getLength() + 1);
+ }
+ DefaultDocumentEvent chng =
+ new DefaultDocumentEvent(offs, len, DocumentEvent.EventType.REMOVE);
+
+ boolean isComposedTextElement = false;
+ // Check whether the position of interest is the composed text
+ isComposedTextElement = Utilities.isComposedTextElement(this, offs);
+
+ removeUpdate(chng);
+ UndoableEdit u = data.remove(offs, len);
+ if (u != null) {
+ chng.addEdit(u);
+ }
+ postRemoveUpdate(chng);
+ // Mark the edit as done.
+ chng.end();
+ fireRemoveUpdate(chng);
+ // only fire undo if Content implementation supports it
+ // undo for the composed text is not supported for now
+ if ((u != null) && !isComposedTextElement) {
+ fireUndoableEditUpdate(new UndoableEditEvent(this, chng));
+ }
+ }
+ }
+
+ /**
+ * Deletes the region of text from <code>offset</code> to
+ * <code>offset + length</code>, and replaces it with <code>text</code>.
+ * It is up to the implementation as to how this is implemented, some
+ * implementations may treat this as two distinct operations: a remove
+ * followed by an insert, others may treat the replace as one atomic
+ * operation.
+ *
+ * @param offset index of child element
+ * @param length length of text to delete, may be 0 indicating don't
+ * delete anything
+ * @param text text to insert, <code>null</code> indicates no text to insert
+ * @param attrs AttributeSet indicating attributes of inserted text,
+ * <code>null</code>
+ * is legal, and typically treated as an empty attributeset,
+ * but exact interpretation is left to the subclass
+ * @exception BadLocationException the given position is not a valid
+ * position within the document
+ * @since 1.4
+ */
+ public void replace(int offset, int length, String text,
+ AttributeSet attrs) throws BadLocationException {
+ if (length == 0 && (text == null || text.length() == 0)) {
+ return;
+ }
+ DocumentFilter filter = getDocumentFilter();
+
+ writeLock();
+ try {
+ if (filter != null) {
+ filter.replace(getFilterBypass(), offset, length, text,
+ attrs);
+ }
+ else {
+ if (length > 0) {
+ remove(offset, length);
+ }
+ if (text != null && text.length() > 0) {
+ insertString(offset, text, attrs);
+ }
+ }
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ /**
+ * Inserts some content into the document.
+ * Inserting content causes a write lock to be held while the
+ * actual changes are taking place, followed by notification
+ * to the observers on the thread that grabbed the write lock.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param offs the starting offset >= 0
+ * @param str the string to insert; does nothing with null/empty strings
+ * @param a the attributes for the inserted content
+ * @exception BadLocationException the given insert position is not a valid
+ * position within the document
+ * @see Document#insertString
+ */
+ public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
+ if ((str == null) || (str.length() == 0)) {
+ return;
+ }
+ DocumentFilter filter = getDocumentFilter();
+
+ writeLock();
+ try {
+ if (filter != null) {
+ filter.insertString(getFilterBypass(), offs, str, a);
+ }
+ else {
+ handleInsertString(offs, str, a);
+ }
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ /**
+ * Performs the actual work of inserting the text; it is assumed the
+ * caller has obtained a write lock before invoking this.
+ */
+ void handleInsertString(int offs, String str, AttributeSet a)
+ throws BadLocationException {
+ if ((str == null) || (str.length() == 0)) {
+ return;
+ }
+ UndoableEdit u = data.insertString(offs, str);
+ DefaultDocumentEvent e =
+ new DefaultDocumentEvent(offs, str.length(), DocumentEvent.EventType.INSERT);
+ if (u != null) {
+ e.addEdit(u);
+ }
+
+ // see if complex glyph layout support is needed
+ if( 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))) {
+ putProperty( I18NProperty, Boolean.TRUE);
+ } else {
+ char[] chars = str.toCharArray();
+ if (SwingUtilities2.isComplexLayout(chars, 0, chars.length)) {
+ putProperty( I18NProperty, Boolean.TRUE);
+ }
+ }
+ }
+
+ insertUpdate(e, a);
+ // Mark the edit as done.
+ e.end();
+ fireInsertUpdate(e);
+ // only fire undo if Content implementation supports it
+ // undo for the composed text is not supported for now
+ if (u != null &&
+ (a == null || !a.isDefined(StyleConstants.ComposedTextAttribute))) {
+ fireUndoableEditUpdate(new UndoableEditEvent(this, e));
+ }
+ }
+
+ /**
+ * Gets a sequence of text from the document.
+ *
+ * @param offset the starting offset >= 0
+ * @param length the number of characters to retrieve >= 0
+ * @return the text
+ * @exception BadLocationException the range given includes a position
+ * that is not a valid position within the document
+ * @see Document#getText
+ */
+ public String getText(int offset, int length) throws BadLocationException {
+ if (length < 0) {
+ throw new BadLocationException("Length must be positive", length);
+ }
+ String str = data.getString(offset, length);
+ return str;
+ }
+
+ /**
+ * Fetches the text contained within the given portion
+ * of the document.
+ * <p>
+ * If the partialReturn property on the txt parameter is false, the
+ * data returned in the Segment will be the entire length requested and
+ * may or may not be a copy depending upon how the data was stored.
+ * If the partialReturn property is true, only the amount of text that
+ * can be returned without creating a copy is returned. Using partial
+ * returns will give better performance for situations where large
+ * parts of the document are being scanned. The following is an example
+ * of using the partial return to access the entire document:
+ * <p>
+ * <pre>
+ * &nbsp; int nleft = doc.getDocumentLength();
+ * &nbsp; Segment text = new Segment();
+ * &nbsp; int offs = 0;
+ * &nbsp; text.setPartialReturn(true);
+ * &nbsp; while (nleft > 0) {
+ * &nbsp; doc.getText(offs, nleft, text);
+ * &nbsp; // do something with text
+ * &nbsp; nleft -= text.count;
+ * &nbsp; offs += text.count;
+ * &nbsp; }
+ * </pre>
+ *
+ * @param offset the starting offset >= 0
+ * @param length the number of characters to retrieve >= 0
+ * @param txt the Segment object to retrieve the text into
+ * @exception BadLocationException the range given includes a position
+ * that is not a valid position within the document
+ */
+ public void getText(int offset, int length, Segment txt) throws BadLocationException {
+ if (length < 0) {
+ throw new BadLocationException("Length must be positive", length);
+ }
+ data.getChars(offset, length, txt);
+ }
+
+ /**
+ * Returns a position that will track change as the document
+ * is altered.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param offs the position in the model >= 0
+ * @return the position
+ * @exception BadLocationException if the given position does not
+ * represent a valid location in the associated document
+ * @see Document#createPosition
+ */
+ public synchronized Position createPosition(int offs) throws BadLocationException {
+ return data.createPosition(offs);
+ }
+
+ /**
+ * Returns a position that represents the start of the document. The
+ * position returned can be counted on to track change and stay
+ * located at the beginning of the document.
+ *
+ * @return the position
+ */
+ public final Position getStartPosition() {
+ Position p;
+ try {
+ p = createPosition(0);
+ } catch (BadLocationException bl) {
+ p = null;
+ }
+ return p;
+ }
+
+ /**
+ * Returns a position that represents the end of the document. The
+ * position returned can be counted on to track change and stay
+ * located at the end of the document.
+ *
+ * @return the position
+ */
+ public final Position getEndPosition() {
+ Position p;
+ try {
+ p = createPosition(data.length());
+ } catch (BadLocationException bl) {
+ p = null;
+ }
+ return p;
+ }
+
+ /**
+ * Gets all root elements defined. Typically, there
+ * will only be one so the default implementation
+ * is to return the default root element.
+ *
+ * @return the root element
+ */
+ public Element[] getRootElements() {
+ Element[] elems = new Element[2];
+ elems[0] = getDefaultRootElement();
+ elems[1] = getBidiRootElement();
+ return elems;
+ }
+
+ /**
+ * Returns the root element that views should be based upon
+ * unless some other mechanism for assigning views to element
+ * structures is provided.
+ *
+ * @return the root element
+ * @see Document#getDefaultRootElement
+ */
+ public abstract Element getDefaultRootElement();
+
+ // ---- local methods -----------------------------------------
+
+ /**
+ * Returns the <code>FilterBypass</code>. This will create one if one
+ * does not yet exist.
+ */
+ private DocumentFilter.FilterBypass getFilterBypass() {
+ if (filterBypass == null) {
+ filterBypass = new DefaultFilterBypass();
+ }
+ return filterBypass;
+ }
+
+ /**
+ * Returns the root element of the bidirectional structure for this
+ * document. Its children represent character runs with a given
+ * Unicode bidi level.
+ */
+ public Element getBidiRootElement() {
+ return bidiRoot;
+ }
+
+ /**
+ * Returns true if the text in the range <code>p0</code> to
+ * <code>p1</code> is left to right.
+ */
+ boolean isLeftToRight(int p0, int p1) {
+ if(!getProperty(I18NProperty).equals(Boolean.TRUE)) {
+ return true;
+ }
+ Element bidiRoot = getBidiRootElement();
+ int index = bidiRoot.getElementIndex(p0);
+ Element bidiElem = bidiRoot.getElement(index);
+ if(bidiElem.getEndOffset() >= p1) {
+ AttributeSet bidiAttrs = bidiElem.getAttributes();
+ return ((StyleConstants.getBidiLevel(bidiAttrs) % 2) == 0);
+ }
+ return true;
+ }
+
+ /**
+ * Get the paragraph element containing the given position. Sub-classes
+ * must define for themselves what exactly constitutes a paragraph. They
+ * should keep in mind however that a paragraph should at least be the
+ * unit of text over which to run the Unicode bidirectional algorithm.
+ *
+ * @param pos the starting offset >= 0
+ * @return the element */
+ public abstract Element getParagraphElement(int pos);
+
+
+ /**
+ * Fetches the context for managing attributes. This
+ * method effectively establishes the strategy used
+ * for compressing AttributeSet information.
+ *
+ * @return the context
+ */
+ protected final AttributeContext getAttributeContext() {
+ return context;
+ }
+
+ /**
+ * Updates document structure as a result of text insertion. This
+ * will happen within a write lock. If a subclass of
+ * this class reimplements this method, it should delegate to the
+ * superclass as well.
+ *
+ * @param chng a description of the change
+ * @param attr the attributes for the change
+ */
+ protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
+ if( getProperty(I18NProperty).equals( Boolean.TRUE ) )
+ updateBidi( chng );
+
+ // Check if a multi byte is encountered in the inserted text.
+ if (chng.type == DocumentEvent.EventType.INSERT &&
+ chng.getLength() > 0 &&
+ !Boolean.TRUE.equals(getProperty(MultiByteProperty))) {
+ Segment segment = SegmentCache.getSharedSegment();
+ try {
+ getText(chng.getOffset(), chng.getLength(), segment);
+ segment.first();
+ do {
+ if ((int)segment.current() > 255) {
+ putProperty(MultiByteProperty, Boolean.TRUE);
+ break;
+ }
+ } while (segment.next() != Segment.DONE);
+ } catch (BadLocationException ble) {
+ // Should never happen
+ }
+ SegmentCache.releaseSharedSegment(segment);
+ }
+ }
+
+ /**
+ * Updates any document structure as a result of text removal. This
+ * method is called before the text is actually removed from the Content.
+ * This will happen within a write lock. If a subclass
+ * of this class reimplements this method, it should delegate to the
+ * superclass as well.
+ *
+ * @param chng a description of the change
+ */
+ protected void removeUpdate(DefaultDocumentEvent chng) {
+ }
+
+ /**
+ * Updates any document structure as a result of text removal. This
+ * method is called after the text has been removed from the Content.
+ * This will happen within a write lock. If a subclass
+ * of this class reimplements this method, it should delegate to the
+ * superclass as well.
+ *
+ * @param chng a description of the change
+ */
+ protected void postRemoveUpdate(DefaultDocumentEvent chng) {
+ if( getProperty(I18NProperty).equals( Boolean.TRUE ) )
+ updateBidi( chng );
+ }
+
+
+ /**
+ * Update the bidi element structure as a result of the given change
+ * to the document. The given change will be updated to reflect the
+ * changes made to the bidi structure.
+ *
+ * This method assumes that every offset in the model is contained in
+ * exactly one paragraph. This method also assumes that it is called
+ * after the change is made to the default element structure.
+ */
+ void updateBidi( DefaultDocumentEvent chng ) {
+
+ // Calculate the range of paragraphs affected by the change.
+ int firstPStart;
+ int lastPEnd;
+ if( chng.type == DocumentEvent.EventType.INSERT
+ || chng.type == DocumentEvent.EventType.CHANGE )
+ {
+ int chngStart = chng.getOffset();
+ int chngEnd = chngStart + chng.getLength();
+ firstPStart = getParagraphElement(chngStart).getStartOffset();
+ lastPEnd = getParagraphElement(chngEnd).getEndOffset();
+ } else if( chng.type == DocumentEvent.EventType.REMOVE ) {
+ Element paragraph = getParagraphElement( chng.getOffset() );
+ firstPStart = paragraph.getStartOffset();
+ lastPEnd = paragraph.getEndOffset();
+ } else {
+ throw new Error("Internal error: unknown event type.");
+ }
+ //System.out.println("updateBidi: firstPStart = " + firstPStart + " lastPEnd = " + lastPEnd );
+
+
+ // Calculate the bidi levels for the affected range of paragraphs. The
+ // levels array will contain a bidi level for each character in the
+ // affected text.
+ byte levels[] = calculateBidiLevels( firstPStart, lastPEnd );
+
+
+ Vector newElements = new Vector();
+
+ // Calculate the first span of characters in the affected range with
+ // the same bidi level. If this level is the same as the level of the
+ // previous bidi element (the existing bidi element containing
+ // firstPStart-1), then merge in the previous element. If not, but
+ // the previous element overlaps the affected range, truncate the
+ // previous element at firstPStart.
+ int firstSpanStart = firstPStart;
+ int removeFromIndex = 0;
+ if( firstSpanStart > 0 ) {
+ int prevElemIndex = bidiRoot.getElementIndex(firstPStart-1);
+ removeFromIndex = prevElemIndex;
+ Element prevElem = bidiRoot.getElement(prevElemIndex);
+ int prevLevel=StyleConstants.getBidiLevel(prevElem.getAttributes());
+ //System.out.println("createbidiElements: prevElem= " + prevElem + " prevLevel= " + prevLevel + "level[0] = " + levels[0]);
+ if( prevLevel==levels[0] ) {
+ firstSpanStart = prevElem.getStartOffset();
+ } else if( prevElem.getEndOffset() > firstPStart ) {
+ newElements.addElement(new BidiElement(bidiRoot,
+ prevElem.getStartOffset(),
+ firstPStart, prevLevel));
+ } else {
+ removeFromIndex++;
+ }
+ }
+
+ int firstSpanEnd = 0;
+ while((firstSpanEnd<levels.length) && (levels[firstSpanEnd]==levels[0]))
+ firstSpanEnd++;
+
+
+ // Calculate the last span of characters in the affected range with
+ // the same bidi level. If this level is the same as the level of the
+ // next bidi element (the existing bidi element containing lastPEnd),
+ // then merge in the next element. If not, but the next element
+ // overlaps the affected range, adjust the next element to start at
+ // lastPEnd.
+ int lastSpanEnd = lastPEnd;
+ Element newNextElem = null;
+ int removeToIndex = bidiRoot.getElementCount() - 1;
+ if( lastSpanEnd <= getLength() ) {
+ int nextElemIndex = bidiRoot.getElementIndex( lastPEnd );
+ removeToIndex = nextElemIndex;
+ Element nextElem = bidiRoot.getElement( nextElemIndex );
+ int nextLevel = StyleConstants.getBidiLevel(nextElem.getAttributes());
+ if( nextLevel == levels[levels.length-1] ) {
+ lastSpanEnd = nextElem.getEndOffset();
+ } else if( nextElem.getStartOffset() < lastPEnd ) {
+ newNextElem = new BidiElement(bidiRoot, lastPEnd,
+ nextElem.getEndOffset(),
+ nextLevel);
+ } else {
+ removeToIndex--;
+ }
+ }
+
+ int lastSpanStart = levels.length;
+ while( (lastSpanStart>firstSpanEnd)
+ && (levels[lastSpanStart-1]==levels[levels.length-1]) )
+ lastSpanStart--;
+
+
+ // If the first and last spans are contiguous and have the same level,
+ // merge them and create a single new element for the entire span.
+ // Otherwise, create elements for the first and last spans as well as
+ // any spans in between.
+ if((firstSpanEnd==lastSpanStart)&&(levels[0]==levels[levels.length-1])){
+ newElements.addElement(new BidiElement(bidiRoot, firstSpanStart,
+ lastSpanEnd, levels[0]));
+ } else {
+ // Create an element for the first span.
+ newElements.addElement(new BidiElement(bidiRoot, firstSpanStart,
+ firstSpanEnd+firstPStart,
+ levels[0]));
+ // Create elements for the spans in between the first and last
+ for( int i=firstSpanEnd; i<lastSpanStart; ) {
+ //System.out.println("executed line 872");
+ int j;
+ for( j=i; (j<levels.length) && (levels[j] == levels[i]); j++ );
+ newElements.addElement(new BidiElement(bidiRoot, firstPStart+i,
+ firstPStart+j,
+ (int)levels[i]));
+ i=j;
+ }
+ // Create an element for the last span.
+ newElements.addElement(new BidiElement(bidiRoot,
+ lastSpanStart+firstPStart,
+ lastSpanEnd,
+ levels[levels.length-1]));
+ }
+
+ if( newNextElem != null )
+ newElements.addElement( newNextElem );
+
+
+ // Calculate the set of existing bidi elements which must be
+ // removed.
+ int removedElemCount = 0;
+ if( bidiRoot.getElementCount() > 0 ) {
+ removedElemCount = removeToIndex - removeFromIndex + 1;
+ }
+ Element[] removedElems = new Element[removedElemCount];
+ for( int i=0; i<removedElemCount; i++ ) {
+ removedElems[i] = bidiRoot.getElement(removeFromIndex+i);
+ }
+
+ Element[] addedElems = new Element[ newElements.size() ];
+ newElements.copyInto( addedElems );
+
+ // Update the change record.
+ ElementEdit ee = new ElementEdit( bidiRoot, removeFromIndex,
+ removedElems, addedElems );
+ chng.addEdit( ee );
+
+ // Update the bidi element structure.
+ bidiRoot.replace( removeFromIndex, removedElems.length, addedElems );
+ }
+
+
+ /**
+ * Calculate the levels array for a range of paragraphs.
+ */
+ private byte[] calculateBidiLevels( int firstPStart, int lastPEnd ) {
+
+ byte levels[] = new byte[ lastPEnd - firstPStart ];
+ int levelsEnd = 0;
+ Boolean defaultDirection = null;
+ Object d = getProperty(TextAttribute.RUN_DIRECTION);
+ if (d instanceof Boolean) {
+ defaultDirection = (Boolean) d;
+ }
+
+ // For each paragraph in the given range of paragraphs, get its
+ // levels array and add it to the levels array for the entire span.
+ for(int o=firstPStart; o<lastPEnd; ) {
+ Element p = getParagraphElement( o );
+ int pStart = p.getStartOffset();
+ int pEnd = p.getEndOffset();
+
+ // default run direction for the paragraph. This will be
+ // null if there is no direction override specified (i.e.
+ // the direction will be determined from the content).
+ Boolean direction = defaultDirection;
+ d = p.getAttributes().getAttribute(TextAttribute.RUN_DIRECTION);
+ if (d instanceof Boolean) {
+ direction = (Boolean) d;
+ }
+
+ //System.out.println("updateBidi: paragraph start = " + pStart + " paragraph end = " + pEnd);
+
+ // Create a Bidi over this paragraph then get the level
+ // array.
+ Segment seg = SegmentCache.getSharedSegment();
+ try {
+ getText(pStart, pEnd-pStart, seg);
+ } catch (BadLocationException e ) {
+ throw new Error("Internal error: " + e.toString());
+ }
+ // REMIND(bcb) we should really be using a Segment here.
+ Bidi bidiAnalyzer;
+ int bidiflag = Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
+ if (direction != null) {
+ if (TextAttribute.RUN_DIRECTION_LTR.equals(direction)) {
+ bidiflag = Bidi.DIRECTION_LEFT_TO_RIGHT;
+ } else {
+ bidiflag = Bidi.DIRECTION_RIGHT_TO_LEFT;
+ }
+ }
+ bidiAnalyzer = new Bidi(seg.array, seg.offset, null, 0, seg.count,
+ bidiflag);
+ BidiUtils.getLevels(bidiAnalyzer, levels, levelsEnd);
+ levelsEnd += bidiAnalyzer.getLength();
+
+ o = p.getEndOffset();
+ SegmentCache.releaseSharedSegment(seg);
+ }
+
+ // REMIND(bcb) remove this code when debugging is done.
+ if( levelsEnd != levels.length )
+ throw new Error("levelsEnd assertion failed.");
+
+ return levels;
+ }
+
+ /**
+ * Gives a diagnostic dump.
+ *
+ * @param out the output stream
+ */
+ public void dump(PrintStream out) {
+ Element root = getDefaultRootElement();
+ if (root instanceof AbstractElement) {
+ ((AbstractElement)root).dump(out, 0);
+ }
+ bidiRoot.dump(out,0);
+ }
+
+ /**
+ * Gets the content for the document.
+ *
+ * @return the content
+ */
+ protected final Content getContent() {
+ return data;
+ }
+
+ /**
+ * Creates a document leaf element.
+ * Hook through which elements are created to represent the
+ * document structure. Because this implementation keeps
+ * structure and content separate, elements grow automatically
+ * when content is extended so splits of existing elements
+ * follow. The document itself gets to decide how to generate
+ * elements to give flexibility in the type of elements used.
+ *
+ * @param parent the parent element
+ * @param a the attributes for the element
+ * @param p0 the beginning of the range >= 0
+ * @param p1 the end of the range >= p0
+ * @return the new element
+ */
+ protected Element createLeafElement(Element parent, AttributeSet a, int p0, int p1) {
+ return new LeafElement(parent, a, p0, p1);
+ }
+
+ /**
+ * Creates a document branch element, that can contain other elements.
+ *
+ * @param parent the parent element
+ * @param a the attributes
+ * @return the element
+ */
+ protected Element createBranchElement(Element parent, AttributeSet a) {
+ return new BranchElement(parent, a);
+ }
+
+ // --- Document locking ----------------------------------
+
+ /**
+ * Fetches the current writing thread if there is one.
+ * This can be used to distinguish whether a method is
+ * being called as part of an existing modification or
+ * if a lock needs to be acquired and a new transaction
+ * started.
+ *
+ * @return the thread actively modifying the document
+ * or <code>null</code> if there are no modifications in progress
+ */
+ protected synchronized final Thread getCurrentWriter() {
+ return currWriter;
+ }
+
+ /**
+ * Acquires a lock to begin mutating the document this lock
+ * protects. There can be no writing, notification of changes, or
+ * reading going on in order to gain the lock. Additionally a thread is
+ * allowed to gain more than one <code>writeLock</code>,
+ * as long as it doesn't attempt to gain additional <code>writeLock</code>s
+ * from within document notification. Attempting to gain a
+ * <code>writeLock</code> from within a DocumentListener notification will
+ * result in an <code>IllegalStateException</code>. The ability
+ * to obtain more than one <code>writeLock</code> per thread allows
+ * subclasses to gain a writeLock, perform a number of operations, then
+ * release the lock.
+ * <p>
+ * Calls to <code>writeLock</code>
+ * must be balanced with calls to <code>writeUnlock</code>, else the
+ * <code>Document</code> will be left in a locked state so that no
+ * reading or writing can be done.
+ *
+ * @exception IllegalStateException thrown on illegal lock
+ * attempt. If the document is implemented properly, this can
+ * only happen if a document listener attempts to mutate the
+ * document. This situation violates the bean event model
+ * where order of delivery is not guaranteed and all listeners
+ * should be notified before further mutations are allowed.
+ */
+ protected synchronized final void writeLock() {
+ try {
+ while ((numReaders > 0) || (currWriter != null)) {
+ if (Thread.currentThread() == currWriter) {
+ if (notifyingListeners) {
+ // Assuming one doesn't do something wrong in a
+ // subclass this should only happen if a
+ // DocumentListener tries to mutate the document.
+ throw new IllegalStateException(
+ "Attempt to mutate in notification");
+ }
+ numWriters++;
+ return;
+ }
+ wait();
+ }
+ currWriter = Thread.currentThread();
+ numWriters = 1;
+ } catch (InterruptedException e) {
+ throw new Error("Interrupted attempt to aquire write lock");
+ }
+ }
+
+ /**
+ * Releases a write lock previously obtained via <code>writeLock</code>.
+ * After decrementing the lock count if there are no oustanding locks
+ * this will allow a new writer, or readers.
+ *
+ * @see #writeLock
+ */
+ protected synchronized final void writeUnlock() {
+ if (--numWriters <= 0) {
+ numWriters = 0;
+ currWriter = null;
+ notifyAll();
+ }
+ }
+
+ /**
+ * Acquires a lock to begin reading some state from the
+ * document. There can be multiple readers at the same time.
+ * Writing blocks the readers until notification of the change
+ * to the listeners has been completed. This method should
+ * be used very carefully to avoid unintended compromise
+ * of the document. It should always be balanced with a
+ * <code>readUnlock</code>.
+ *
+ * @see #readUnlock
+ */
+ public synchronized final void readLock() {
+ try {
+ while (currWriter != null) {
+ if (currWriter == Thread.currentThread()) {
+ // writer has full read access.... may try to acquire
+ // lock in notification
+ return;
+ }
+ wait();
+ }
+ numReaders += 1;
+ } catch (InterruptedException e) {
+ throw new Error("Interrupted attempt to aquire read lock");
+ }
+ }
+
+ /**
+ * Does a read unlock. This signals that one
+ * of the readers is done. If there are no more readers
+ * then writing can begin again. This should be balanced
+ * with a readLock, and should occur in a finally statement
+ * so that the balance is guaranteed. The following is an
+ * example.
+ * <pre><code>
+ * &nbsp; readLock();
+ * &nbsp; try {
+ * &nbsp; // do something
+ * &nbsp; } finally {
+ * &nbsp; readUnlock();
+ * &nbsp; }
+ * </code></pre>
+ *
+ * @see #readLock
+ */
+ public synchronized final void readUnlock() {
+ if (currWriter == Thread.currentThread()) {
+ // writer has full read access.... may try to acquire
+ // lock in notification
+ return;
+ }
+ if (numReaders <= 0) {
+ throw new StateInvariantError(BAD_LOCK_STATE);
+ }
+ numReaders -= 1;
+ notify();
+ }
+
+ // --- serialization ---------------------------------------------
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException
+ {
+ s.defaultReadObject();
+ listenerList = new EventListenerList();
+
+ // Restore bidi structure
+ //REMIND(bcb) This creates an initial bidi element to account for
+ //the \n that exists by default in the content.
+ bidiRoot = new BidiRootElement();
+ try {
+ writeLock();
+ Element[] p = new Element[1];
+ p[0] = new BidiElement( bidiRoot, 0, 1, 0 );
+ bidiRoot.replace(0,0,p);
+ } finally {
+ writeUnlock();
+ }
+ // At this point bidi root is only partially correct. To fully
+ // restore it we need access to getDefaultRootElement. But, this
+ // is created by the subclass and at this point will be null. We
+ // thus use registerValidation.
+ s.registerValidation(new ObjectInputValidation() {
+ public void validateObject() {
+ try {
+ writeLock();
+ DefaultDocumentEvent e = new DefaultDocumentEvent
+ (0, getLength(),
+ DocumentEvent.EventType.INSERT);
+ updateBidi( e );
+ }
+ finally {
+ writeUnlock();
+ }
+ }
+ }, 0);
+ }
+
+ // ----- member variables ------------------------------------------
+
+ private transient int numReaders;
+ private transient Thread currWriter;
+ /**
+ * The number of writers, all obtained from <code>currWriter</code>.
+ */
+ private transient int numWriters;
+ /**
+ * True will notifying listeners.
+ */
+ private transient boolean notifyingListeners;
+
+ private static Boolean defaultI18NProperty;
+
+ /**
+ * Storage for document-wide properties.
+ */
+ private Dictionary<Object,Object> documentProperties = null;
+
+ /**
+ * The event listener list for the document.
+ */
+ protected EventListenerList listenerList = new EventListenerList();
+
+ /**
+ * Where the text is actually stored, and a set of marks
+ * that track change as the document is edited are managed.
+ */
+ private Content data;
+
+ /**
+ * Factory for the attributes. This is the strategy for
+ * attribute compression and control of the lifetime of
+ * a set of attributes as a collection. This may be shared
+ * with other documents.
+ */
+ private AttributeContext context;
+
+ /**
+ * The root of the bidirectional structure for this document. Its children
+ * represent character runs with the same Unicode bidi level.
+ */
+ private transient BranchElement bidiRoot;
+
+ /**
+ * Filter for inserting/removing of text.
+ */
+ private DocumentFilter documentFilter;
+
+ /**
+ * Used by DocumentFilter to do actual insert/remove.
+ */
+ private transient DocumentFilter.FilterBypass filterBypass;
+
+ private static final String BAD_LOCK_STATE = "document lock failure";
+
+ /**
+ * Error message to indicate a bad location.
+ */
+ protected static final String BAD_LOCATION = "document location failure";
+
+ /**
+ * Name of elements used to represent paragraphs
+ */
+ public static final String ParagraphElementName = "paragraph";
+
+ /**
+ * Name of elements used to represent content
+ */
+ public static final String ContentElementName = "content";
+
+ /**
+ * Name of elements used to hold sections (lines/paragraphs).
+ */
+ public static final String SectionElementName = "section";
+
+ /**
+ * Name of elements used to hold a unidirectional run
+ */
+ public static final String BidiElementName = "bidi level";
+
+ /**
+ * Name of the attribute used to specify element
+ * names.
+ */
+ public static final String ElementNameAttribute = "$ename";
+
+ /**
+ * Document property that indicates whether internationalization
+ * functions such as text reordering or reshaping should be
+ * performed. This property should not be publicly exposed,
+ * since it is used for implementation convenience only. As a
+ * side effect, copies of this property may be in its subclasses
+ * that live in different packages (e.g. HTMLDocument as of now),
+ * so those copies should also be taken care of when this property
+ * needs to be modified.
+ */
+ static final String I18NProperty = "i18n";
+
+ /**
+ * Document property that indicates if a character has been inserted
+ * into the document that is more than one byte long. GlyphView uses
+ * this to determine if it should use BreakIterator.
+ */
+ static final Object MultiByteProperty = "multiByte";
+
+ /**
+ * Document property that indicates asynchronous loading is
+ * desired, with the thread priority given as the value.
+ */
+ static final String AsyncLoadPriority = "load priority";
+
+ /**
+ * Interface to describe a sequence of character content that
+ * can be edited. Implementations may or may not support a
+ * history mechanism which will be reflected by whether or not
+ * mutations return an UndoableEdit implementation.
+ * @see AbstractDocument
+ */
+ public interface Content {
+
+ /**
+ * Creates a position within the content that will
+ * track change as the content is mutated.
+ *
+ * @param offset the offset in the content >= 0
+ * @return a Position
+ * @exception BadLocationException for an invalid offset
+ */
+ public Position createPosition(int offset) throws BadLocationException;
+
+ /**
+ * Current length of the sequence of character content.
+ *
+ * @return the length >= 0
+ */
+ public int length();
+
+ /**
+ * Inserts a string of characters into the sequence.
+ *
+ * @param where offset into the sequence to make the insertion >= 0
+ * @param str string to insert
+ * @return if the implementation supports a history mechanism,
+ * a reference to an <code>Edit</code> implementation will be returned,
+ * otherwise returns <code>null</code>
+ * @exception BadLocationException thrown if the area covered by
+ * the arguments is not contained in the character sequence
+ */
+ public UndoableEdit insertString(int where, String str) throws BadLocationException;
+
+ /**
+ * Removes some portion of the sequence.
+ *
+ * @param where The offset into the sequence to make the
+ * insertion >= 0.
+ * @param nitems The number of items in the sequence to remove >= 0.
+ * @return If the implementation supports a history mechansim,
+ * a reference to an Edit implementation will be returned,
+ * otherwise null.
+ * @exception BadLocationException Thrown if the area covered by
+ * the arguments is not contained in the character sequence.
+ */
+ public UndoableEdit remove(int where, int nitems) throws BadLocationException;
+
+ /**
+ * Fetches a string of characters contained in the sequence.
+ *
+ * @param where Offset into the sequence to fetch >= 0.
+ * @param len number of characters to copy >= 0.
+ * @return the string
+ * @exception BadLocationException Thrown if the area covered by
+ * the arguments is not contained in the character sequence.
+ */
+ public String getString(int where, int len) throws BadLocationException;
+
+ /**
+ * Gets a sequence of characters and copies them into a Segment.
+ *
+ * @param where the starting offset >= 0
+ * @param len the number of characters >= 0
+ * @param txt the target location to copy into
+ * @exception BadLocationException Thrown if the area covered by
+ * the arguments is not contained in the character sequence.
+ */
+ public void getChars(int where, int len, Segment txt) throws BadLocationException;
+ }
+
+ /**
+ * An interface that can be used to allow MutableAttributeSet
+ * implementations to use pluggable attribute compression
+ * techniques. Each mutation of the attribute set can be
+ * used to exchange a previous AttributeSet instance with
+ * another, preserving the possibility of the AttributeSet
+ * remaining immutable. An implementation is provided by
+ * the StyleContext class.
+ *
+ * The Element implementations provided by this class use
+ * this interface to provide their MutableAttributeSet
+ * implementations, so that different AttributeSet compression
+ * techniques can be employed. The method
+ * <code>getAttributeContext</code> should be implemented to
+ * return the object responsible for implementing the desired
+ * compression technique.
+ *
+ * @see StyleContext
+ */
+ public interface AttributeContext {
+
+ /**
+ * Adds an attribute to the given set, and returns
+ * the new representative set.
+ *
+ * @param old the old attribute set
+ * @param name the non-null attribute name
+ * @param value the attribute value
+ * @return the updated attribute set
+ * @see MutableAttributeSet#addAttribute
+ */
+ public AttributeSet addAttribute(AttributeSet old, Object name, Object value);
+
+ /**
+ * Adds a set of attributes to the element.
+ *
+ * @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);
+
+ /**
+ * Removes an attribute from the set.
+ *
+ * @param old the old attribute set
+ * @param name the non-null attribute name
+ * @return the updated attribute set
+ * @see MutableAttributeSet#removeAttribute
+ */
+ public AttributeSet removeAttribute(AttributeSet old, Object name);
+
+ /**
+ * Removes a set of attributes for the element.
+ *
+ * @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);
+
+ /**
+ * Removes a set of attributes for the element.
+ *
+ * @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);
+
+ /**
+ * Fetches an empty AttributeSet.
+ *
+ * @return the attribute set
+ */
+ public AttributeSet getEmptySet();
+
+ /**
+ * Reclaims an attribute set.
+ * This is a way for a MutableAttributeSet to mark that it no
+ * longer need a particular immutable set. This is only necessary
+ * in 1.1 where there are no weak references. A 1.1 implementation
+ * would call this in its finalize method.
+ *
+ * @param a the attribute set to reclaim
+ */
+ public void reclaim(AttributeSet a);
+ }
+
+ /**
+ * Implements the abstract part of an element. By default elements
+ * support attributes by having a field that represents the immutable
+ * part of the current attribute set for the element. The element itself
+ * implements MutableAttributeSet which can be used to modify the set
+ * by fetching a new immutable set. The immutable sets are provided
+ * by the AttributeContext associated with the document.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public abstract class AbstractElement implements Element, MutableAttributeSet, Serializable, TreeNode {
+
+ /**
+ * Creates a new AbstractElement.
+ *
+ * @param parent the parent element
+ * @param a the attributes for the element
+ * @since 1.4
+ */
+ public AbstractElement(Element parent, AttributeSet a) {
+ this.parent = parent;
+ attributes = getAttributeContext().getEmptySet();
+ if (a != null) {
+ addAttributes(a);
+ }
+ }
+
+ private final void indent(PrintWriter out, int n) {
+ for (int i = 0; i < n; i++) {
+ out.print(" ");
+ }
+ }
+
+ /**
+ * Dumps a debugging representation of the element hierarchy.
+ *
+ * @param psOut the output stream
+ * @param indentAmount the indentation level >= 0
+ */
+ public void dump(PrintStream psOut, int indentAmount) {
+ PrintWriter out;
+ try {
+ out = new PrintWriter(new OutputStreamWriter(psOut,"JavaEsc"),
+ true);
+ } catch (UnsupportedEncodingException e){
+ out = new PrintWriter(psOut,true);
+ }
+ indent(out, indentAmount);
+ if (getName() == null) {
+ out.print("<??");
+ } else {
+ out.print("<" + getName());
+ }
+ if (getAttributeCount() > 0) {
+ out.println("");
+ // dump the attributes
+ Enumeration names = attributes.getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object name = names.nextElement();
+ indent(out, indentAmount + 1);
+ out.println(name + "=" + getAttribute(name));
+ }
+ indent(out, indentAmount);
+ }
+ out.println(">");
+
+ if (isLeaf()) {
+ indent(out, indentAmount+1);
+ out.print("[" + getStartOffset() + "," + getEndOffset() + "]");
+ Content c = getContent();
+ try {
+ String contentStr = c.getString(getStartOffset(),
+ getEndOffset() - getStartOffset())/*.trim()*/;
+ if (contentStr.length() > 40) {
+ contentStr = contentStr.substring(0, 40) + "...";
+ }
+ out.println("["+contentStr+"]");
+ } catch (BadLocationException e) {
+ ;
+ }
+
+ } else {
+ int n = getElementCount();
+ for (int i = 0; i < n; i++) {
+ AbstractElement e = (AbstractElement) getElement(i);
+ e.dump(psOut, indentAmount+1);
+ }
+ }
+ }
+
+ // --- AttributeSet ----------------------------
+ // delegated to the immutable field "attributes"
+
+ /**
+ * Gets the number of attributes that are defined.
+ *
+ * @return the number of attributes >= 0
+ * @see AttributeSet#getAttributeCount
+ */
+ public int getAttributeCount() {
+ return attributes.getAttributeCount();
+ }
+
+ /**
+ * Checks whether a given attribute is defined.
+ *
+ * @param attrName the non-null attribute name
+ * @return true if the attribute is defined
+ * @see AttributeSet#isDefined
+ */
+ public boolean isDefined(Object attrName) {
+ return attributes.isDefined(attrName);
+ }
+
+ /**
+ * 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 attributes.isEqual(attr);
+ }
+
+ /**
+ * Copies a set of attributes.
+ *
+ * @return the copy
+ * @see AttributeSet#copyAttributes
+ */
+ public AttributeSet copyAttributes() {
+ return attributes.copyAttributes();
+ }
+
+ /**
+ * Gets the value of an attribute.
+ *
+ * @param attrName the non-null attribute name
+ * @return the attribute value
+ * @see AttributeSet#getAttribute
+ */
+ public Object getAttribute(Object attrName) {
+ Object value = attributes.getAttribute(attrName);
+ if (value == null) {
+ // The delegate nor it's resolvers had a match,
+ // so we'll try to resolve through the parent
+ // element.
+ AttributeSet a = (parent != null) ? parent.getAttributes() : null;
+ if (a != null) {
+ value = a.getAttribute(attrName);
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Gets the names of all attributes.
+ *
+ * @return the attribute names as an enumeration
+ * @see AttributeSet#getAttributeNames
+ */
+ public Enumeration<?> getAttributeNames() {
+ return attributes.getAttributeNames();
+ }
+
+ /**
+ * Checks whether a given attribute name/value is defined.
+ *
+ * @param name the non-null 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 attributes.containsAttribute(name, value);
+ }
+
+
+ /**
+ * Checks whether the element contains all the attributes.
+ *
+ * @param attrs the attributes to check
+ * @return true if the element contains all the attributes
+ * @see AttributeSet#containsAttributes
+ */
+ public boolean containsAttributes(AttributeSet attrs) {
+ return attributes.containsAttributes(attrs);
+ }
+
+ /**
+ * Gets the resolving parent.
+ * If not overridden, the resolving parent defaults to
+ * the parent element.
+ *
+ * @return the attributes from the parent, <code>null</code> if none
+ * @see AttributeSet#getResolveParent
+ */
+ public AttributeSet getResolveParent() {
+ AttributeSet a = attributes.getResolveParent();
+ if ((a == null) && (parent != null)) {
+ a = parent.getAttributes();
+ }
+ return a;
+ }
+
+ // --- MutableAttributeSet ----------------------------------
+ // should fetch a new immutable record for the field
+ // "attributes".
+
+ /**
+ * Adds an attribute to the element.
+ *
+ * @param name the non-null attribute name
+ * @param value the attribute value
+ * @see MutableAttributeSet#addAttribute
+ */
+ public void addAttribute(Object name, Object value) {
+ checkForIllegalCast();
+ AttributeContext context = getAttributeContext();
+ attributes = context.addAttribute(attributes, name, value);
+ }
+
+ /**
+ * Adds a set of attributes to the element.
+ *
+ * @param attr the attributes to add
+ * @see MutableAttributeSet#addAttribute
+ */
+ public void addAttributes(AttributeSet attr) {
+ checkForIllegalCast();
+ AttributeContext context = getAttributeContext();
+ attributes = context.addAttributes(attributes, attr);
+ }
+
+ /**
+ * Removes an attribute from the set.
+ *
+ * @param name the non-null attribute name
+ * @see MutableAttributeSet#removeAttribute
+ */
+ public void removeAttribute(Object name) {
+ checkForIllegalCast();
+ AttributeContext context = getAttributeContext();
+ attributes = context.removeAttribute(attributes, name);
+ }
+
+ /**
+ * Removes a set of attributes for the element.
+ *
+ * @param names the attribute names
+ * @see MutableAttributeSet#removeAttributes
+ */
+ public void removeAttributes(Enumeration<?> names) {
+ checkForIllegalCast();
+ AttributeContext context = getAttributeContext();
+ attributes = context.removeAttributes(attributes, names);
+ }
+
+ /**
+ * Removes a set of attributes for the element.
+ *
+ * @param attrs the attributes
+ * @see MutableAttributeSet#removeAttributes
+ */
+ public void removeAttributes(AttributeSet attrs) {
+ checkForIllegalCast();
+ AttributeContext context = getAttributeContext();
+ if (attrs == this) {
+ attributes = context.getEmptySet();
+ } else {
+ attributes = context.removeAttributes(attributes, attrs);
+ }
+ }
+
+ /**
+ * Sets the resolving parent.
+ *
+ * @param parent the parent, null if none
+ * @see MutableAttributeSet#setResolveParent
+ */
+ public void setResolveParent(AttributeSet parent) {
+ checkForIllegalCast();
+ AttributeContext context = getAttributeContext();
+ if (parent != null) {
+ attributes =
+ context.addAttribute(attributes, StyleConstants.ResolveAttribute,
+ parent);
+ } else {
+ attributes =
+ context.removeAttribute(attributes, StyleConstants.ResolveAttribute);
+ }
+ }
+
+ private final void checkForIllegalCast() {
+ Thread t = getCurrentWriter();
+ if ((t == null) || (t != Thread.currentThread())) {
+ throw new StateInvariantError("Illegal cast to MutableAttributeSet");
+ }
+ }
+
+ // --- Element methods -------------------------------------
+
+ /**
+ * Retrieves the underlying model.
+ *
+ * @return the model
+ */
+ public Document getDocument() {
+ return AbstractDocument.this;
+ }
+
+ /**
+ * Gets the parent of the element.
+ *
+ * @return the parent
+ */
+ public Element getParentElement() {
+ return parent;
+ }
+
+ /**
+ * Gets the attributes for the element.
+ *
+ * @return the attribute set
+ */
+ public AttributeSet getAttributes() {
+ return this;
+ }
+
+ /**
+ * Gets the name of the element.
+ *
+ * @return the name, null if none
+ */
+ public String getName() {
+ if (attributes.isDefined(ElementNameAttribute)) {
+ return (String) attributes.getAttribute(ElementNameAttribute);
+ }
+ return null;
+ }
+
+ /**
+ * Gets the starting offset in the model for the element.
+ *
+ * @return the offset >= 0
+ */
+ public abstract int getStartOffset();
+
+ /**
+ * Gets the ending offset in the model for the element.
+ *
+ * @return the offset >= 0
+ */
+ public abstract int getEndOffset();
+
+ /**
+ * Gets a child element.
+ *
+ * @param index the child index, >= 0 && < getElementCount()
+ * @return the child element
+ */
+ public abstract Element getElement(int index);
+
+ /**
+ * Gets the number of children for the element.
+ *
+ * @return the number of children >= 0
+ */
+ public abstract int getElementCount();
+
+ /**
+ * Gets the child element index closest to the given model offset.
+ *
+ * @param offset the offset >= 0
+ * @return the element index >= 0
+ */
+ public abstract int getElementIndex(int offset);
+
+ /**
+ * Checks whether the element is a leaf.
+ *
+ * @return true if a leaf
+ */
+ public abstract boolean isLeaf();
+
+ // --- TreeNode methods -------------------------------------
+
+ /**
+ * Returns the child <code>TreeNode</code> at index
+ * <code>childIndex</code>.
+ */
+ public TreeNode getChildAt(int childIndex) {
+ return (TreeNode)getElement(childIndex);
+ }
+
+ /**
+ * Returns the number of children <code>TreeNode</code>'s
+ * receiver contains.
+ * @return the number of children <code>TreeNodews</code>'s
+ * receiver contains
+ */
+ public int getChildCount() {
+ return getElementCount();
+ }
+
+ /**
+ * Returns the parent <code>TreeNode</code> of the receiver.
+ * @return the parent <code>TreeNode</code> of the receiver
+ */
+ public TreeNode getParent() {
+ return (TreeNode)getParentElement();
+ }
+
+ /**
+ * Returns the index of <code>node</code> in the receivers children.
+ * If the receiver does not contain <code>node</code>, -1 will be
+ * returned.
+ * @param node the location of interest
+ * @return the index of <code>node</code> in the receiver's
+ * children, or -1 if absent
+ */
+ public int getIndex(TreeNode node) {
+ for(int counter = getChildCount() - 1; counter >= 0; counter--)
+ if(getChildAt(counter) == node)
+ return counter;
+ return -1;
+ }
+
+ /**
+ * Returns true if the receiver allows children.
+ * @return true if the receiver allows children, otherwise false
+ */
+ public abstract boolean getAllowsChildren();
+
+
+ /**
+ * Returns the children of the receiver as an
+ * <code>Enumeration</code>.
+ * @return the children of the receiver as an <code>Enumeration</code>
+ */
+ public abstract Enumeration children();
+
+
+ // --- serialization ---------------------------------------------
+
+ private void writeObject(ObjectOutputStream s) throws IOException {
+ s.defaultWriteObject();
+ StyleContext.writeAttributeSet(s, attributes);
+ }
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException
+ {
+ s.defaultReadObject();
+ MutableAttributeSet attr = new SimpleAttributeSet();
+ StyleContext.readAttributeSet(s, attr);
+ AttributeContext context = getAttributeContext();
+ attributes = context.addAttributes(SimpleAttributeSet.EMPTY, attr);
+ }
+
+ // ---- variables -----------------------------------------------------
+
+ private Element parent;
+ private transient AttributeSet attributes;
+
+ }
+
+ /**
+ * Implements a composite element that contains other elements.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public class BranchElement extends AbstractElement {
+
+ /**
+ * 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 BranchElement(Element parent, AttributeSet a) {
+ super(parent, a);
+ children = new AbstractElement[1];
+ nchildren = 0;
+ lastIndex = -1;
+ }
+
+ /**
+ * Gets the child element that contains
+ * the given model position.
+ *
+ * @param pos the position >= 0
+ * @return the element, null if none
+ */
+ public Element positionToElement(int pos) {
+ int index = getElementIndex(pos);
+ Element child = children[index];
+ int p0 = child.getStartOffset();
+ int p1 = child.getEndOffset();
+ if ((pos >= p0) && (pos < p1)) {
+ return child;
+ }
+ return null;
+ }
+
+ /**
+ * Replaces content with a new set of elements.
+ *
+ * @param offset the starting offset >= 0
+ * @param length the length to replace >= 0
+ * @param elems the new elements
+ */
+ public void replace(int offset, int length, Element[] elems) {
+ int delta = elems.length - length;
+ int src = offset + length;
+ int nmove = nchildren - src;
+ int dest = src + delta;
+ if ((nchildren + delta) >= children.length) {
+ // need to grow the array
+ int newLength = Math.max(2*children.length, nchildren + delta);
+ AbstractElement[] newChildren = new AbstractElement[newLength];
+ System.arraycopy(children, 0, newChildren, 0, offset);
+ System.arraycopy(elems, 0, newChildren, offset, elems.length);
+ System.arraycopy(children, src, newChildren, dest, nmove);
+ children = newChildren;
+ } else {
+ // patch the existing array
+ System.arraycopy(children, src, children, dest, nmove);
+ System.arraycopy(elems, 0, children, offset, elems.length);
+ }
+ nchildren = nchildren + delta;
+ }
+
+ /**
+ * Converts the element to a string.
+ *
+ * @return the string
+ */
+ public String toString() {
+ return "BranchElement(" + getName() + ") " + getStartOffset() + "," +
+ getEndOffset() + "\n";
+ }
+
+ // --- Element methods -----------------------------------
+
+ /**
+ * Gets the element name.
+ *
+ * @return the element name
+ */
+ public String getName() {
+ String nm = super.getName();
+ if (nm == null) {
+ nm = ParagraphElementName;
+ }
+ return nm;
+ }
+
+ /**
+ * Gets the starting offset in the model for the element.
+ *
+ * @return the offset >= 0
+ */
+ public int getStartOffset() {
+ return children[0].getStartOffset();
+ }
+
+ /**
+ * Gets the ending offset in the model for the element.
+ * @throws NullPointerException if this element has no children
+ *
+ * @return the offset >= 0
+ */
+ public int getEndOffset() {
+ Element child =
+ (nchildren > 0) ? children[nchildren - 1] : children[0];
+ return child.getEndOffset();
+ }
+
+ /**
+ * Gets a child element.
+ *
+ * @param index the child index, >= 0 && < getElementCount()
+ * @return the child element, null if none
+ */
+ public Element getElement(int index) {
+ if (index < nchildren) {
+ return children[index];
+ }
+ return null;
+ }
+
+ /**
+ * Gets the number of children for the element.
+ *
+ * @return the number of children >= 0
+ */
+ public int getElementCount() {
+ return nchildren;
+ }
+
+ /**
+ * Gets the child element index closest to the given model offset.
+ *
+ * @param offset the offset >= 0
+ * @return the element index >= 0
+ */
+ public int getElementIndex(int offset) {
+ int index;
+ int lower = 0;
+ int upper = nchildren - 1;
+ int mid = 0;
+ int p0 = getStartOffset();
+ int p1;
+
+ if (nchildren == 0) {
+ return 0;
+ }
+ if (offset >= getEndOffset()) {
+ return nchildren - 1;
+ }
+
+ // see if the last index can be used.
+ if ((lastIndex >= lower) && (lastIndex <= upper)) {
+ Element lastHit = children[lastIndex];
+ p0 = lastHit.getStartOffset();
+ p1 = lastHit.getEndOffset();
+ if ((offset >= p0) && (offset < p1)) {
+ return lastIndex;
+ }
+
+ // last index wasn't a hit, but it does give useful info about
+ // where a hit (if any) would be.
+ if (offset < p0) {
+ upper = lastIndex;
+ } else {
+ lower = lastIndex;
+ }
+ }
+
+ while (lower <= upper) {
+ mid = lower + ((upper - lower) / 2);
+ Element elem = children[mid];
+ p0 = elem.getStartOffset();
+ p1 = elem.getEndOffset();
+ if ((offset >= p0) && (offset < p1)) {
+ // found the location
+ index = mid;
+ lastIndex = index;
+ return index;
+ } else if (offset < p0) {
+ upper = mid - 1;
+ } else {
+ lower = mid + 1;
+ }
+ }
+
+ // didn't find it, but we indicate the index of where it would belong
+ if (offset < p0) {
+ index = mid;
+ } else {
+ index = mid + 1;
+ }
+ lastIndex = index;
+ return index;
+ }
+
+ /**
+ * Checks whether the element is a leaf.
+ *
+ * @return true if a leaf
+ */
+ public boolean isLeaf() {
+ return false;
+ }
+
+
+ // ------ TreeNode ----------------------------------------------
+
+ /**
+ * Returns true if the receiver allows children.
+ * @return true if the receiver allows children, otherwise false
+ */
+ public boolean getAllowsChildren() {
+ return true;
+ }
+
+
+ /**
+ * Returns the children of the receiver as an
+ * <code>Enumeration</code>.
+ * @return the children of the receiver
+ */
+ public Enumeration children() {
+ if(nchildren == 0)
+ return null;
+
+ Vector tempVector = new Vector(nchildren);
+
+ for(int counter = 0; counter < nchildren; counter++)
+ tempVector.addElement(children[counter]);
+ return tempVector.elements();
+ }
+
+ // ------ members ----------------------------------------------
+
+ private AbstractElement[] children;
+ private int nchildren;
+ private int lastIndex;
+ }
+
+ /**
+ * Implements an element that directly represents content of
+ * some kind.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see Element
+ */
+ public class LeafElement extends AbstractElement {
+
+ /**
+ * 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 >= 0
+ * @param offs1 The end offset >= offs0
+ * @since 1.4
+ */
+ public LeafElement(Element parent, AttributeSet a, int offs0, int offs1) {
+ super(parent, a);
+ try {
+ p0 = createPosition(offs0);
+ p1 = createPosition(offs1);
+ } catch (BadLocationException e) {
+ p0 = null;
+ p1 = null;
+ throw new StateInvariantError("Can't create Position references");
+ }
+ }
+
+ /**
+ * Converts the element to a string.
+ *
+ * @return the string
+ */
+ public String toString() {
+ return "LeafElement(" + getName() + ") " + p0 + "," + p1 + "\n";
+ }
+
+ // --- Element methods ---------------------------------------------
+
+ /**
+ * Gets the starting offset in the model for the element.
+ *
+ * @return the offset >= 0
+ */
+ public int getStartOffset() {
+ return p0.getOffset();
+ }
+
+ /**
+ * Gets the ending offset in the model for the element.
+ *
+ * @return the offset >= 0
+ */
+ public int getEndOffset() {
+ return p1.getOffset();
+ }
+
+ /**
+ * Gets the element name.
+ *
+ * @return the name
+ */
+ public String getName() {
+ String nm = super.getName();
+ if (nm == null) {
+ nm = ContentElementName;
+ }
+ return nm;
+ }
+
+ /**
+ * Gets the child element index closest to the given model offset.
+ *
+ * @param pos the offset >= 0
+ * @return the element index >= 0
+ */
+ public int getElementIndex(int pos) {
+ return -1;
+ }
+
+ /**
+ * Gets a child element.
+ *
+ * @param index the child index, >= 0 && < getElementCount()
+ * @return the child element
+ */
+ public Element getElement(int index) {
+ return null;
+ }
+
+ /**
+ * Returns the number of child elements.
+ *
+ * @return the number of children >= 0
+ */
+ public int getElementCount() {
+ return 0;
+ }
+
+ /**
+ * Checks whether the element is a leaf.
+ *
+ * @return true if a leaf
+ */
+ public boolean isLeaf() {
+ return true;
+ }
+
+ // ------ TreeNode ----------------------------------------------
+
+ /**
+ * Returns true if the receiver allows children.
+ * @return true if the receiver allows children, otherwise false
+ */
+ public boolean getAllowsChildren() {
+ return false;
+ }
+
+
+ /**
+ * Returns the children of the receiver as an
+ * <code>Enumeration</code>.
+ * @return the children of the receiver
+ */
+ public Enumeration children() {
+ return null;
+ }
+
+ // --- serialization ---------------------------------------------
+
+ private void writeObject(ObjectOutputStream s) throws IOException {
+ s.defaultWriteObject();
+ s.writeInt(p0.getOffset());
+ s.writeInt(p1.getOffset());
+ }
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException
+ {
+ s.defaultReadObject();
+
+ // set the range with positions that track change
+ int off0 = s.readInt();
+ int off1 = s.readInt();
+ try {
+ p0 = createPosition(off0);
+ p1 = createPosition(off1);
+ } catch (BadLocationException e) {
+ p0 = null;
+ p1 = null;
+ throw new IOException("Can't restore Position references");
+ }
+ }
+
+ // ---- members -----------------------------------------------------
+
+ private transient Position p0;
+ private transient Position p1;
+ }
+
+ /**
+ * Represents the root element of the bidirectional element structure.
+ * The root element is the only element in the bidi element structure
+ * which contains children.
+ */
+ class BidiRootElement extends BranchElement {
+
+ BidiRootElement() {
+ super( null, null );
+ }
+
+ /**
+ * Gets the name of the element.
+ * @return the name
+ */
+ public String getName() {
+ return "bidi root";
+ }
+ }
+
+ /**
+ * Represents an element of the bidirectional element structure.
+ */
+ class BidiElement extends LeafElement {
+
+ /**
+ * Creates a new BidiElement.
+ */
+ BidiElement(Element parent, int start, int end, int level) {
+ super(parent, new SimpleAttributeSet(), start, end);
+ addAttribute(StyleConstants.BidiLevel, new Integer(level));
+ //System.out.println("BidiElement: start = " + start
+ // + " end = " + end + " level = " + level );
+ }
+
+ /**
+ * Gets the name of the element.
+ * @return the name
+ */
+ public String getName() {
+ return BidiElementName;
+ }
+
+ int getLevel() {
+ Integer o = (Integer) getAttribute(StyleConstants.BidiLevel);
+ if (o != null) {
+ return o.intValue();
+ }
+ return 0; // Level 0 is base level (non-embedded) left-to-right
+ }
+
+ boolean isLeftToRight() {
+ return ((getLevel() % 2) == 0);
+ }
+ }
+
+ /**
+ * Stores document changes as the document is being
+ * modified. Can subsequently be used for change notification
+ * when done with the document modification transaction.
+ * This is used by the AbstractDocument class and its extensions
+ * for broadcasting change information to the document listeners.
+ */
+ public class DefaultDocumentEvent extends CompoundEdit implements DocumentEvent {
+
+ /**
+ * Constructs a change record.
+ *
+ * @param offs the offset into the document of the change >= 0
+ * @param len the length of the change >= 0
+ * @param type the type of event (DocumentEvent.EventType)
+ * @since 1.4
+ */
+ public DefaultDocumentEvent(int offs, int len, DocumentEvent.EventType type) {
+ super();
+ offset = offs;
+ length = len;
+ this.type = type;
+ }
+
+ /**
+ * Returns a string description of the change event.
+ *
+ * @return a string
+ */
+ public String toString() {
+ return edits.toString();
+ }
+
+ // --- CompoundEdit methods --------------------------
+
+ /**
+ * Adds a document edit. If the number of edits crosses
+ * a threshold, this switches on a hashtable lookup for
+ * ElementChange implementations since access of these
+ * needs to be relatively quick.
+ *
+ * @param anEdit a document edit record
+ * @return true if the edit was added
+ */
+ public boolean addEdit(UndoableEdit anEdit) {
+ // if the number of changes gets too great, start using
+ // a hashtable for to locate the change for a given element.
+ if ((changeLookup == null) && (edits.size() > 10)) {
+ changeLookup = new Hashtable();
+ int n = edits.size();
+ for (int i = 0; i < n; i++) {
+ Object o = edits.elementAt(i);
+ if (o instanceof DocumentEvent.ElementChange) {
+ DocumentEvent.ElementChange ec = (DocumentEvent.ElementChange) o;
+ changeLookup.put(ec.getElement(), ec);
+ }
+ }
+ }
+
+ // if we have a hashtable... add the entry if it's
+ // an ElementChange.
+ if ((changeLookup != null) && (anEdit instanceof DocumentEvent.ElementChange)) {
+ DocumentEvent.ElementChange ec = (DocumentEvent.ElementChange) anEdit;
+ changeLookup.put(ec.getElement(), ec);
+ }
+ return super.addEdit(anEdit);
+ }
+
+ /**
+ * Redoes a change.
+ *
+ * @exception CannotRedoException if the change cannot be redone
+ */
+ public void redo() throws CannotRedoException {
+ writeLock();
+ try {
+ // change the state
+ super.redo();
+ // fire a DocumentEvent to notify the view(s)
+ UndoRedoDocumentEvent ev = new UndoRedoDocumentEvent(this, false);
+ if (type == DocumentEvent.EventType.INSERT) {
+ fireInsertUpdate(ev);
+ } else if (type == DocumentEvent.EventType.REMOVE) {
+ fireRemoveUpdate(ev);
+ } else {
+ fireChangedUpdate(ev);
+ }
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ /**
+ * Undoes a change.
+ *
+ * @exception CannotUndoException if the change cannot be undone
+ */
+ public void undo() throws CannotUndoException {
+ writeLock();
+ try {
+ // change the state
+ super.undo();
+ // fire a DocumentEvent to notify the view(s)
+ UndoRedoDocumentEvent ev = new UndoRedoDocumentEvent(this, true);
+ if (type == DocumentEvent.EventType.REMOVE) {
+ fireInsertUpdate(ev);
+ } else if (type == DocumentEvent.EventType.INSERT) {
+ fireRemoveUpdate(ev);
+ } else {
+ fireChangedUpdate(ev);
+ }
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ /**
+ * DefaultDocument events are significant. If you wish to aggregate
+ * DefaultDocumentEvents to present them as a single edit to the user
+ * place them into a CompoundEdit.
+ *
+ * @return whether the event is significant for edit undo purposes
+ */
+ public boolean isSignificant() {
+ return true;
+ }
+
+
+ /**
+ * Provides a localized, human readable description of this edit
+ * suitable for use in, say, a change log.
+ *
+ * @return the description
+ */
+ public String getPresentationName() {
+ DocumentEvent.EventType type = getType();
+ if(type == DocumentEvent.EventType.INSERT)
+ return UIManager.getString("AbstractDocument.additionText");
+ if(type == DocumentEvent.EventType.REMOVE)
+ return UIManager.getString("AbstractDocument.deletionText");
+ return UIManager.getString("AbstractDocument.styleChangeText");
+ }
+
+ /**
+ * Provides a localized, human readable description of the undoable
+ * form of this edit, e.g. for use as an Undo menu item. Typically
+ * derived from getDescription();
+ *
+ * @return the description
+ */
+ public String getUndoPresentationName() {
+ return UIManager.getString("AbstractDocument.undoText") + " " +
+ getPresentationName();
+ }
+
+ /**
+ * Provides a localized, human readable description of the redoable
+ * form of this edit, e.g. for use as a Redo menu item. Typically
+ * derived from getPresentationName();
+ *
+ * @return the description
+ */
+ public String getRedoPresentationName() {
+ return UIManager.getString("AbstractDocument.redoText") + " " +
+ getPresentationName();
+ }
+
+ // --- DocumentEvent methods --------------------------
+
+ /**
+ * Returns the type of event.
+ *
+ * @return the event type as a DocumentEvent.EventType
+ * @see DocumentEvent#getType
+ */
+ public DocumentEvent.EventType getType() {
+ return type;
+ }
+
+ /**
+ * Returns the offset within the document of the start of the change.
+ *
+ * @return the offset >= 0
+ * @see DocumentEvent#getOffset
+ */
+ public int getOffset() {
+ return offset;
+ }
+
+ /**
+ * Returns the length of the change.
+ *
+ * @return the length >= 0
+ * @see DocumentEvent#getLength
+ */
+ public int getLength() {
+ return length;
+ }
+
+ /**
+ * Gets the document that sourced the change event.
+ *
+ * @return the document
+ * @see DocumentEvent#getDocument
+ */
+ public Document getDocument() {
+ return AbstractDocument.this;
+ }
+
+ /**
+ * Gets the changes for an element.
+ *
+ * @param elem the element
+ * @return the changes
+ */
+ public DocumentEvent.ElementChange getChange(Element elem) {
+ if (changeLookup != null) {
+ return (DocumentEvent.ElementChange) changeLookup.get(elem);
+ }
+ int n = edits.size();
+ for (int i = 0; i < n; i++) {
+ Object o = edits.elementAt(i);
+ if (o instanceof DocumentEvent.ElementChange) {
+ DocumentEvent.ElementChange c = (DocumentEvent.ElementChange) o;
+ if (elem.equals(c.getElement())) {
+ return c;
+ }
+ }
+ }
+ return null;
+ }
+
+ // --- member variables ------------------------------------
+
+ private int offset;
+ private int length;
+ private Hashtable changeLookup;
+ private DocumentEvent.EventType type;
+
+ }
+
+ /**
+ * This event used when firing document changes while Undo/Redo
+ * operations. It just wraps DefaultDocumentEvent and delegates
+ * all calls to it except getType() which depends on operation
+ * (Undo or Redo).
+ */
+ class UndoRedoDocumentEvent implements DocumentEvent {
+ private DefaultDocumentEvent src = null;
+ private boolean isUndo;
+ private EventType type = null;
+
+ public UndoRedoDocumentEvent(DefaultDocumentEvent src, boolean isUndo) {
+ this.src = src;
+ this.isUndo = isUndo;
+ if(isUndo) {
+ if(src.getType().equals(EventType.INSERT)) {
+ type = EventType.REMOVE;
+ } else if(src.getType().equals(EventType.REMOVE)) {
+ type = EventType.INSERT;
+ } else {
+ type = src.getType();
+ }
+ } else {
+ type = src.getType();
+ }
+ }
+
+ public DefaultDocumentEvent getSource() {
+ return src;
+ }
+
+ // DocumentEvent methods delegated to DefaultDocumentEvent source
+ // except getType() which depends on operation (Undo or Redo).
+ public int getOffset() {
+ return src.getOffset();
+ }
+
+ public int getLength() {
+ return src.getLength();
+ }
+
+ public Document getDocument() {
+ return src.getDocument();
+ }
+
+ public DocumentEvent.EventType getType() {
+ return type;
+ }
+
+ public DocumentEvent.ElementChange getChange(Element elem) {
+ return src.getChange(elem);
+ }
+ }
+
+ /**
+ * An implementation of ElementChange that can be added to the document
+ * event.
+ */
+ public static class ElementEdit extends AbstractUndoableEdit implements DocumentEvent.ElementChange {
+
+ /**
+ * Constructs an edit record. This does not modify the element
+ * so it can safely be used to <em>catch up</em> a view to the
+ * current model state for views that just attached to a model.
+ *
+ * @param e the element
+ * @param index the index into the model >= 0
+ * @param removed a set of elements that were removed
+ * @param added a set of elements that were added
+ */
+ public ElementEdit(Element e, int index, Element[] removed, Element[] added) {
+ super();
+ this.e = e;
+ this.index = index;
+ this.removed = removed;
+ this.added = added;
+ }
+
+ /**
+ * Returns the underlying element.
+ *
+ * @return the element
+ */
+ public Element getElement() {
+ return e;
+ }
+
+ /**
+ * Returns the index into the list of elements.
+ *
+ * @return the index >= 0
+ */
+ public int getIndex() {
+ return index;
+ }
+
+ /**
+ * Gets a list of children that were removed.
+ *
+ * @return the list
+ */
+ public Element[] getChildrenRemoved() {
+ return removed;
+ }
+
+ /**
+ * Gets a list of children that were added.
+ *
+ * @return the list
+ */
+ public Element[] getChildrenAdded() {
+ return added;
+ }
+
+ /**
+ * Redoes a change.
+ *
+ * @exception CannotRedoException if the change cannot be redone
+ */
+ public void redo() throws CannotRedoException {
+ super.redo();
+
+ // Since this event will be reused, switch around added/removed.
+ Element[] tmp = removed;
+ removed = added;
+ added = tmp;
+
+ // PENDING(prinz) need MutableElement interface, canRedo() should check
+ ((AbstractDocument.BranchElement)e).replace(index, removed.length, added);
+ }
+
+ /**
+ * Undoes a change.
+ *
+ * @exception CannotUndoException if the change cannot be undone
+ */
+ public void undo() throws CannotUndoException {
+ super.undo();
+ // PENDING(prinz) need MutableElement interface, canUndo() should check
+ ((AbstractDocument.BranchElement)e).replace(index, added.length, removed);
+
+ // Since this event will be reused, switch around added/removed.
+ Element[] tmp = removed;
+ removed = added;
+ added = tmp;
+ }
+
+ private Element e;
+ private int index;
+ private Element[] removed;
+ private Element[] added;
+ }
+
+
+ private class DefaultFilterBypass extends DocumentFilter.FilterBypass {
+ public Document getDocument() {
+ return AbstractDocument.this;
+ }
+
+ public void remove(int offset, int length) throws
+ BadLocationException {
+ handleRemove(offset, length);
+ }
+
+ public void insertString(int offset, String string,
+ AttributeSet attr) throws
+ BadLocationException {
+ handleInsertString(offset, string, attr);
+ }
+
+ public void replace(int offset, int length, String text,
+ AttributeSet attrs) throws BadLocationException {
+ handleRemove(offset, length);
+ handleInsertString(offset, text, attrs);
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/AbstractWriter.java b/src/share/classes/javax/swing/text/AbstractWriter.java
new file mode 100644
index 000000000..c2b0b8aa4
--- /dev/null
+++ b/src/share/classes/javax/swing/text/AbstractWriter.java
@@ -0,0 +1,713 @@
+/*
+ * 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;
+
+import java.io.Writer;
+import java.io.IOException;
+import java.util.Enumeration;
+
+/**
+ * AbstractWriter is an abstract class that actually
+ * does the work of writing out the element tree
+ * including the attributes. In terms of how much is
+ * written out per line, the writer defaults to 100.
+ * But this value can be set by subclasses.
+ *
+ * @author Sunita Mani
+ */
+
+public abstract class AbstractWriter {
+
+ private ElementIterator it;
+ private Writer out;
+ private int indentLevel = 0;
+ private int indentSpace = 2;
+ private Document doc = null;
+ private int maxLineLength = 100;
+ private int currLength = 0;
+ private int startOffset = 0;
+ private int endOffset = 0;
+ // If (indentLevel * indentSpace) becomes >= maxLineLength, this will
+ // get incremened instead of indentLevel to avoid indenting going greater
+ // than line length.
+ private int offsetIndent = 0;
+
+ /**
+ * String used for end of line. If the Document has the property
+ * EndOfLineStringProperty, it will be used for newlines. Otherwise
+ * the System property line.separator will be used. The line separator
+ * can also be set.
+ */
+ private String lineSeparator;
+
+ /**
+ * True indicates that when writing, the line can be split, false
+ * indicates that even if the line is > than max line length it should
+ * not be split.
+ */
+ private boolean canWrapLines;
+
+ /**
+ * True while the current line is empty. This will remain true after
+ * indenting.
+ */
+ private boolean isLineEmpty;
+
+ /**
+ * Used when indenting. Will contain the spaces.
+ */
+ private char[] indentChars;
+
+ /**
+ * Used when writing out a string.
+ */
+ private char[] tempChars;
+
+ /**
+ * This is used in <code>writeLineSeparator</code> instead of
+ * tempChars. If tempChars were used it would mean write couldn't invoke
+ * <code>writeLineSeparator</code> as it might have been passed
+ * tempChars.
+ */
+ private char[] newlineChars;
+
+ /**
+ * Used for writing text.
+ */
+ private Segment segment;
+
+ /**
+ * How the text packages models newlines.
+ * @see #getLineSeparator
+ */
+ protected static final char NEWLINE = '\n';
+
+
+ /**
+ * Creates a new AbstractWriter.
+ * Initializes the ElementIterator with the default
+ * root of the document.
+ *
+ * @param w a Writer.
+ * @param doc a Document
+ */
+ protected AbstractWriter(Writer w, Document doc) {
+ this(w, doc, 0, doc.getLength());
+ }
+
+ /**
+ * Creates a new AbstractWriter.
+ * Initializes the ElementIterator with the
+ * element passed in.
+ *
+ * @param w a Writer
+ * @param doc an Element
+ * @param pos The location in the document to fetch the
+ * content.
+ * @param len The amount to write out.
+ */
+ protected AbstractWriter(Writer w, Document doc, int pos, int len) {
+ this.doc = doc;
+ it = new ElementIterator(doc.getDefaultRootElement());
+ out = w;
+ startOffset = pos;
+ endOffset = pos + len;
+ Object docNewline = doc.getProperty(DefaultEditorKit.
+ EndOfLineStringProperty);
+ if (docNewline instanceof String) {
+ setLineSeparator((String)docNewline);
+ }
+ else {
+ String newline = null;
+ try {
+ newline = System.getProperty("line.separator");
+ } catch (SecurityException se) {}
+ if (newline == null) {
+ // Should not get here, but if we do it means we could not
+ // find a newline string, use \n in this case.
+ newline = "\n";
+ }
+ setLineSeparator(newline);
+ }
+ canWrapLines = true;
+ }
+
+ /**
+ * Creates a new AbstractWriter.
+ * Initializes the ElementIterator with the
+ * element passed in.
+ *
+ * @param w a Writer
+ * @param root an Element
+ */
+ protected AbstractWriter(Writer w, Element root) {
+ this(w, root, 0, root.getEndOffset());
+ }
+
+ /**
+ * Creates a new AbstractWriter.
+ * Initializes the ElementIterator with the
+ * element passed in.
+ *
+ * @param w a Writer
+ * @param root an Element
+ * @param pos The location in the document to fetch the
+ * content.
+ * @param len The amount to write out.
+ */
+ protected AbstractWriter(Writer w, Element root, int pos, int len) {
+ this.doc = root.getDocument();
+ it = new ElementIterator(root);
+ out = w;
+ startOffset = pos;
+ endOffset = pos + len;
+ canWrapLines = true;
+ }
+
+ /**
+ * Returns the first offset to be output.
+ *
+ * @since 1.3
+ */
+ public int getStartOffset() {
+ return startOffset;
+ }
+
+ /**
+ * Returns the last offset to be output.
+ *
+ * @since 1.3
+ */
+ public int getEndOffset() {
+ return endOffset;
+ }
+
+ /**
+ * Fetches the ElementIterator.
+ *
+ * @return the ElementIterator.
+ */
+ protected ElementIterator getElementIterator() {
+ return it;
+ }
+
+ /**
+ * Returns the Writer that is used to output the content.
+ *
+ * @since 1.3
+ */
+ protected Writer getWriter() {
+ return out;
+ }
+
+ /**
+ * Fetches the document.
+ *
+ * @return the Document.
+ */
+ protected Document getDocument() {
+ return doc;
+ }
+
+ /**
+ * This method determines whether the current element
+ * is in the range specified. When no range is specified,
+ * the range is initialized to be the entire document.
+ * inRange() returns true if the range specified intersects
+ * with the element's range.
+ *
+ * @param next an Element.
+ * @return boolean that indicates whether the element
+ * is in the range.
+ */
+ protected boolean inRange(Element next) {
+ int startOffset = getStartOffset();
+ int endOffset = getEndOffset();
+ if ((next.getStartOffset() >= startOffset &&
+ next.getStartOffset() < endOffset) ||
+ (startOffset >= next.getStartOffset() &&
+ startOffset < next.getEndOffset())) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This abstract method needs to be implemented
+ * by subclasses. Its responsibility is to
+ * iterate over the elements and use the write()
+ * methods to generate output in the desired format.
+ */
+ abstract protected void write() throws IOException, BadLocationException;
+
+ /**
+ * Returns the text associated with the element.
+ * The assumption here is that the element is a
+ * leaf element. Throws a BadLocationException
+ * when encountered.
+ *
+ * @param elem an <code>Element</code>
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document
+ * @return the text as a <code>String</code>
+ */
+ protected String getText(Element elem) throws BadLocationException {
+ return doc.getText(elem.getStartOffset(),
+ elem.getEndOffset() - elem.getStartOffset());
+ }
+
+
+ /**
+ * 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);
+ if (segment.count > 0) {
+ write(segment.array, segment.offset, segment.count);
+ }
+ }
+ }
+
+ /**
+ * Enables subclasses to set the number of characters they
+ * want written per line. The default is 100.
+ *
+ * @param l the maximum line length.
+ */
+ protected void setLineLength(int l) {
+ maxLineLength = l;
+ }
+
+ /**
+ * Returns the maximum line length.
+ *
+ * @since 1.3
+ */
+ protected int getLineLength() {
+ return maxLineLength;
+ }
+
+ /**
+ * Sets the current line length.
+ *
+ * @since 1.3
+ */
+ protected void setCurrentLineLength(int length) {
+ currLength = length;
+ isLineEmpty = (currLength == 0);
+ }
+
+ /**
+ * Returns the current line length.
+ *
+ * @since 1.3
+ */
+ protected int getCurrentLineLength() {
+ return currLength;
+ }
+
+ /**
+ * Returns true if the current line should be considered empty. This
+ * is true when <code>getCurrentLineLength</code> == 0 ||
+ * <code>indent</code> has been invoked on an empty line.
+ *
+ * @since 1.3
+ */
+ protected boolean isLineEmpty() {
+ return isLineEmpty;
+ }
+
+ /**
+ * Sets whether or not lines can be wrapped. This can be toggled
+ * during the writing of lines. For example, outputting HTML might
+ * set this to false when outputting a quoted string.
+ *
+ * @since 1.3
+ */
+ protected void setCanWrapLines(boolean newValue) {
+ canWrapLines = newValue;
+ }
+
+ /**
+ * Returns whether or not the lines can be wrapped. If this is false
+ * no lineSeparator's will be output.
+ *
+ * @since 1.3
+ */
+ protected boolean getCanWrapLines() {
+ return canWrapLines;
+ }
+
+ /**
+ * Enables subclasses to specify how many spaces an indent
+ * maps to. When indentation takes place, the indent level
+ * is multiplied by this mapping. The default is 2.
+ *
+ * @param space an int representing the space to indent mapping.
+ */
+ protected void setIndentSpace(int space) {
+ indentSpace = space;
+ }
+
+ /**
+ * Returns the amount of space to indent.
+ *
+ * @since 1.3
+ */
+ protected int getIndentSpace() {
+ return indentSpace;
+ }
+
+ /**
+ * Sets the String used to reprsent newlines. This is initialized
+ * in the constructor from either the Document, or the System property
+ * line.separator.
+ *
+ * @since 1.3
+ */
+ public void setLineSeparator(String value) {
+ lineSeparator = value;
+ }
+
+ /**
+ * Returns the string used to represent newlines.
+ *
+ * @since 1.3
+ */
+ public String getLineSeparator() {
+ return lineSeparator;
+ }
+
+ /**
+ * Increments the indent level. If indenting would cause
+ * <code>getIndentSpace()</code> *<code>getIndentLevel()</code> to be >
+ * than <code>getLineLength()</code> this will not cause an indent.
+ */
+ protected void incrIndent() {
+ // Only increment to a certain point.
+ if (offsetIndent > 0) {
+ offsetIndent++;
+ }
+ else {
+ if (++indentLevel * getIndentSpace() >= getLineLength()) {
+ offsetIndent++;
+ --indentLevel;
+ }
+ }
+ }
+
+ /**
+ * Decrements the indent level.
+ */
+ protected void decrIndent() {
+ if (offsetIndent > 0) {
+ --offsetIndent;
+ }
+ else {
+ indentLevel--;
+ }
+ }
+
+ /**
+ * Returns the current indentation level. That is, the number of times
+ * <code>incrIndent</code> has been invoked minus the number of times
+ * <code>decrIndent</code> has been invoked.
+ *
+ * @since 1.3
+ */
+ protected int getIndentLevel() {
+ return indentLevel;
+ }
+
+ /**
+ * Does indentation. The number of spaces written
+ * out is indent level times the space to map mapping. If the current
+ * line is empty, this will not make it so that the current line is
+ * still considered empty.
+ *
+ * @exception IOException on any I/O error
+ */
+ protected void indent() throws IOException {
+ int max = getIndentLevel() * getIndentSpace();
+ if (indentChars == null || max > indentChars.length) {
+ indentChars = new char[max];
+ for (int counter = 0; counter < max; counter++) {
+ indentChars[counter] = ' ';
+ }
+ }
+ int length = getCurrentLineLength();
+ boolean wasEmpty = isLineEmpty();
+ output(indentChars, 0, max);
+ if (wasEmpty && length == 0) {
+ isLineEmpty = true;
+ }
+ }
+
+ /**
+ * Writes out a character. This is implemented to invoke
+ * the <code>write</code> method that takes a char[].
+ *
+ * @param ch a char.
+ * @exception IOException on any I/O error
+ */
+ protected void write(char ch) throws IOException {
+ if (tempChars == null) {
+ tempChars = new char[128];
+ }
+ tempChars[0] = ch;
+ write(tempChars, 0, 1);
+ }
+
+ /**
+ * Writes out a string. This is implemented to invoke the
+ * <code>write</code> method that takes a char[].
+ *
+ * @param content a String.
+ * @exception IOException on any I/O error
+ */
+ protected void write(String content) throws IOException {
+ if (content == null) {
+ return;
+ }
+ int size = content.length();
+ if (tempChars == null || tempChars.length < size) {
+ tempChars = new char[size];
+ }
+ content.getChars(0, size, tempChars, 0);
+ write(tempChars, 0, size);
+ }
+
+ /**
+ * Writes the line separator. This invokes <code>output</code> directly
+ * as well as setting the <code>lineLength</code> to 0.
+ *
+ * @since 1.3
+ */
+ protected void writeLineSeparator() throws IOException {
+ String newline = getLineSeparator();
+ int length = newline.length();
+ if (newlineChars == null || newlineChars.length < length) {
+ newlineChars = new char[length];
+ }
+ newline.getChars(0, length, newlineChars, 0);
+ output(newlineChars, 0, length);
+ setCurrentLineLength(0);
+ }
+
+ /**
+ * All write methods call into this one. If <code>getCanWrapLines()</code>
+ * returns false, this will call <code>output</code> with each sequence
+ * of <code>chars</code> that doesn't contain a NEWLINE, followed
+ * by a call to <code>writeLineSeparator</code>. On the other hand,
+ * if <code>getCanWrapLines()</code> returns true, this will split the
+ * string, as necessary, so <code>getLineLength</code> is honored.
+ * The only exception is if the current string contains no whitespace,
+ * and won't fit in which case the line length will exceed
+ * <code>getLineLength</code>.
+ *
+ * @since 1.3
+ */
+ protected void write(char[] chars, int startIndex, int length)
+ throws IOException {
+ if (!getCanWrapLines()) {
+ // We can not break string, just track if a newline
+ // is in it.
+ int lastIndex = startIndex;
+ int endIndex = startIndex + length;
+ int newlineIndex = indexOf(chars, NEWLINE, startIndex, endIndex);
+ while (newlineIndex != -1) {
+ if (newlineIndex > lastIndex) {
+ output(chars, lastIndex, newlineIndex - lastIndex);
+ }
+ writeLineSeparator();
+ lastIndex = newlineIndex + 1;
+ newlineIndex = indexOf(chars, '\n', lastIndex, endIndex);
+ }
+ if (lastIndex < endIndex) {
+ output(chars, lastIndex, endIndex - lastIndex);
+ }
+ }
+ else {
+ // We can break chars if the length exceeds maxLength.
+ int lastIndex = startIndex;
+ int endIndex = startIndex + length;
+ int lineLength = getCurrentLineLength();
+ int maxLength = getLineLength();
+
+ while (lastIndex < endIndex) {
+ int newlineIndex = indexOf(chars, NEWLINE, lastIndex,
+ endIndex);
+ boolean needsNewline = false;
+ boolean forceNewLine = false;
+
+ lineLength = getCurrentLineLength();
+ if (newlineIndex != -1 && (lineLength +
+ (newlineIndex - lastIndex)) < maxLength) {
+ if (newlineIndex > lastIndex) {
+ output(chars, lastIndex, newlineIndex - lastIndex);
+ }
+ lastIndex = newlineIndex + 1;
+ forceNewLine = true;
+ }
+ else if (newlineIndex == -1 && (lineLength +
+ (endIndex - lastIndex)) < maxLength) {
+ if (endIndex > lastIndex) {
+ output(chars, lastIndex, endIndex - lastIndex);
+ }
+ lastIndex = endIndex;
+ }
+ else {
+ // Need to break chars, find a place to split chars at,
+ // from lastIndex to endIndex,
+ // or maxLength - lineLength whichever is smaller
+ int breakPoint = -1;
+ int maxBreak = Math.min(endIndex - lastIndex,
+ maxLength - lineLength - 1);
+ int counter = 0;
+ while (counter < maxBreak) {
+ if (Character.isWhitespace(chars[counter +
+ lastIndex])) {
+ breakPoint = counter;
+ }
+ counter++;
+ }
+ if (breakPoint != -1) {
+ // Found a place to break at.
+ breakPoint += lastIndex + 1;
+ output(chars, lastIndex, breakPoint - lastIndex);
+ lastIndex = breakPoint;
+ needsNewline = true;
+ }
+ else {
+ // No where good to break.
+
+ // find the next whitespace, or write out the
+ // whole string.
+ // maxBreak will be negative if current line too
+ // long.
+ counter = Math.max(0, maxBreak);
+ maxBreak = endIndex - lastIndex;
+ while (counter < maxBreak) {
+ if (Character.isWhitespace(chars[counter +
+ lastIndex])) {
+ breakPoint = counter;
+ break;
+ }
+ counter++;
+ }
+ if (breakPoint == -1) {
+ output(chars, lastIndex, endIndex - lastIndex);
+ breakPoint = endIndex;
+ }
+ else {
+ breakPoint += lastIndex;
+ if (chars[breakPoint] == NEWLINE) {
+ output(chars, lastIndex, breakPoint++ -
+ lastIndex);
+ forceNewLine = true;
+ }
+ else {
+ output(chars, lastIndex, ++breakPoint -
+ lastIndex);
+ needsNewline = true;
+ }
+ }
+ lastIndex = breakPoint;
+ }
+ }
+ if (forceNewLine || needsNewline || lastIndex < endIndex) {
+ writeLineSeparator();
+ if (lastIndex < endIndex || !forceNewLine) {
+ indent();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Writes out the set of attributes as " <name>=<value>"
+ * pairs. It throws an IOException when encountered.
+ *
+ * @param attr an AttributeSet.
+ * @exception IOException on any I/O error
+ */
+ protected void writeAttributes(AttributeSet attr) throws IOException {
+
+ Enumeration names = attr.getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object name = names.nextElement();
+ write(" " + name + "=" + attr.getAttribute(name));
+ }
+ }
+
+ /**
+ * The last stop in writing out content. All the write methods eventually
+ * make it to this method, which invokes <code>write</code> on the
+ * Writer.
+ * <p>This method also updates the line length based on
+ * <code>length</code>. If this is invoked to output a newline, the
+ * current line length will need to be reset as will no longer be
+ * valid. If it is up to the caller to do this. Use
+ * <code>writeLineSeparator</code> to write out a newline, which will
+ * property update the current line length.
+ *
+ * @since 1.3
+ */
+ protected void output(char[] content, int start, int length)
+ throws IOException {
+ getWriter().write(content, start, length);
+ setCurrentLineLength(getCurrentLineLength() + length);
+ }
+
+ /**
+ * Support method to locate an occurence of a particular character.
+ */
+ private int indexOf(char[] chars, char sChar, int startIndex,
+ int endIndex) {
+ while(startIndex < endIndex) {
+ if (chars[startIndex] == sChar) {
+ return startIndex;
+ }
+ startIndex++;
+ }
+ return -1;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/AsyncBoxView.java b/src/share/classes/javax/swing/text/AsyncBoxView.java
new file mode 100644
index 000000000..54a0e9735
--- /dev/null
+++ b/src/share/classes/javax/swing/text/AsyncBoxView.java
@@ -0,0 +1,1420 @@
+/*
+ * Copyright 1999-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;
+
+import java.util.*;
+import java.awt.*;
+import javax.swing.SwingUtilities;
+import javax.swing.event.DocumentEvent;
+
+/**
+ * A box that does layout asynchronously. This
+ * is useful to keep the GUI event thread moving by
+ * not doing any layout on it. The layout is done
+ * on a granularity of operations on the child views.
+ * After each child view is accessed for some part
+ * of layout (a potentially time consuming operation)
+ * the remaining tasks can be abandoned or a new higher
+ * priority task (i.e. to service a synchronous request
+ * or a visible area) can be taken on.
+ * <p>
+ * While the child view is being accessed
+ * a read lock is aquired on the associated document
+ * so that the model is stable while being accessed.
+ *
+ * @author Timothy Prinzing
+ * @since 1.3
+ */
+public class AsyncBoxView extends View {
+
+ /**
+ * Construct a box view that does asynchronous layout.
+ *
+ * @param elem the element of the model to represent
+ * @param axis the axis to tile along. This can be
+ * either X_AXIS or Y_AXIS.
+ */
+ public AsyncBoxView(Element elem, int axis) {
+ super(elem);
+ stats = new ArrayList();
+ this.axis = axis;
+ locator = new ChildLocator();
+ flushTask = new FlushTask();
+ minorSpan = Short.MAX_VALUE;
+ estimatedMajorSpan = false;
+ }
+
+ /**
+ * Fetch the major axis (the axis the children
+ * are tiled along). This will have a value of
+ * either X_AXIS or Y_AXIS.
+ */
+ public int getMajorAxis() {
+ return axis;
+ }
+
+ /**
+ * Fetch the minor axis (the axis orthoginal
+ * to the tiled axis). This will have a value of
+ * either X_AXIS or Y_AXIS.
+ */
+ public int getMinorAxis() {
+ return (axis == X_AXIS) ? Y_AXIS : X_AXIS;
+ }
+
+ /**
+ * Get the top part of the margin around the view.
+ */
+ public float getTopInset() {
+ return topInset;
+ }
+
+ /**
+ * Set the top part of the margin around the view.
+ *
+ * @param i the value of the inset
+ */
+ public void setTopInset(float i) {
+ topInset = i;
+ }
+
+ /**
+ * Get the bottom part of the margin around the view.
+ */
+ public float getBottomInset() {
+ return bottomInset;
+ }
+
+ /**
+ * Set the bottom part of the margin around the view.
+ *
+ * @param i the value of the inset
+ */
+ public void setBottomInset(float i) {
+ bottomInset = i;
+ }
+
+ /**
+ * Get the left part of the margin around the view.
+ */
+ public float getLeftInset() {
+ return leftInset;
+ }
+
+ /**
+ * Set the left part of the margin around the view.
+ *
+ * @param i the value of the inset
+ */
+ public void setLeftInset(float i) {
+ leftInset = i;
+ }
+
+ /**
+ * Get the right part of the margin around the view.
+ */
+ public float getRightInset() {
+ return rightInset;
+ }
+
+ /**
+ * Set the right part of the margin around the view.
+ *
+ * @param i the value of the inset
+ */
+ public void setRightInset(float i) {
+ rightInset = i;
+ }
+
+ /**
+ * Fetch the span along an axis that is taken up by the insets.
+ *
+ * @param axis the axis to determine the total insets along,
+ * either X_AXIS or Y_AXIS.
+ * @since 1.4
+ */
+ protected float getInsetSpan(int axis) {
+ float margin = (axis == X_AXIS) ?
+ getLeftInset() + getRightInset() : getTopInset() + getBottomInset();
+ return margin;
+ }
+
+ /**
+ * Set the estimatedMajorSpan property that determines if the
+ * major span should be treated as being estimated. If this
+ * property is true, the value of setSize along the major axis
+ * will change the requirements along the major axis and incremental
+ * changes will be ignored until all of the children have been updated
+ * (which will cause the property to automatically be set to false).
+ * If the property is false the value of the majorSpan will be
+ * considered to be accurate and incremental changes will be
+ * added into the total as they are calculated.
+ *
+ * @since 1.4
+ */
+ protected void setEstimatedMajorSpan(boolean isEstimated) {
+ estimatedMajorSpan = isEstimated;
+ }
+
+ /**
+ * Is the major span currently estimated?
+ *
+ * @since 1.4
+ */
+ protected boolean getEstimatedMajorSpan() {
+ return estimatedMajorSpan;
+ }
+
+ /**
+ * Fetch the object representing the layout state of
+ * of the child at the given index.
+ *
+ * @param index the child index. This should be a
+ * value >= 0 and < getViewCount().
+ */
+ protected ChildState getChildState(int index) {
+ synchronized(stats) {
+ if ((index >= 0) && (index < stats.size())) {
+ return (ChildState) stats.get(index);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Fetch the queue to use for layout.
+ */
+ protected LayoutQueue getLayoutQueue() {
+ return LayoutQueue.getDefaultQueue();
+ }
+
+ /**
+ * New ChildState records are created through
+ * this method to allow subclasses the extend
+ * the ChildState records to do/hold more
+ */
+ protected ChildState createChildState(View v) {
+ return new ChildState(v);
+ }
+
+ /**
+ * Requirements changed along the major axis.
+ * This is called by the thread doing layout for
+ * the given ChildState object when it has completed
+ * fetching the child views new preferences.
+ * Typically this would be the layout thread, but
+ * might be the event thread if it is trying to update
+ * something immediately (such as to perform a
+ * model/view translation).
+ * <p>
+ * This is implemented to mark the major axis as having
+ * changed so that a future check to see if the requirements
+ * need to be published to the parent view will consider
+ * the major axis. If the span along the major axis is
+ * not estimated, it is updated by the given delta to reflect
+ * the incremental change. The delta is ignored if the
+ * major span is estimated.
+ */
+ protected synchronized void majorRequirementChange(ChildState cs, float delta) {
+ if (estimatedMajorSpan == false) {
+ majorSpan += delta;
+ }
+ majorChanged = true;
+ }
+
+ /**
+ * Requirements changed along the minor axis.
+ * This is called by the thread doing layout for
+ * the given ChildState object when it has completed
+ * fetching the child views new preferences.
+ * Typically this would be the layout thread, but
+ * might be the GUI thread if it is trying to update
+ * something immediately (such as to perform a
+ * model/view translation).
+ */
+ protected synchronized void minorRequirementChange(ChildState cs) {
+ minorChanged = true;
+ }
+
+ /**
+ * Publish the changes in preferences upward to the parent
+ * view. This is normally called by the layout thread.
+ */
+ protected void flushRequirementChanges() {
+ AbstractDocument doc = (AbstractDocument) getDocument();
+ try {
+ doc.readLock();
+
+ View parent = null;
+ boolean horizontal = false;
+ boolean vertical = false;
+
+ synchronized(this) {
+ // perform tasks that iterate over the children while
+ // preventing the collection from changing.
+ synchronized(stats) {
+ int n = getViewCount();
+ if ((n > 0) && (minorChanged || estimatedMajorSpan)) {
+ LayoutQueue q = getLayoutQueue();
+ ChildState min = getChildState(0);
+ ChildState pref = getChildState(0);
+ float span = 0f;
+ for (int i = 1; i < n; i++) {
+ ChildState cs = getChildState(i);
+ if (minorChanged) {
+ if (cs.min > min.min) {
+ min = cs;
+ }
+ if (cs.pref > pref.pref) {
+ pref = cs;
+ }
+ }
+ if (estimatedMajorSpan) {
+ span += cs.getMajorSpan();
+ }
+ }
+
+ if (minorChanged) {
+ minRequest = min;
+ prefRequest = pref;
+ }
+ if (estimatedMajorSpan) {
+ majorSpan = span;
+ estimatedMajorSpan = false;
+ majorChanged = true;
+ }
+ }
+ }
+
+ // message preferenceChanged
+ if (majorChanged || minorChanged) {
+ parent = getParent();
+ if (parent != null) {
+ if (axis == X_AXIS) {
+ horizontal = majorChanged;
+ vertical = minorChanged;
+ } else {
+ vertical = majorChanged;
+ horizontal = minorChanged;
+ }
+ }
+ majorChanged = false;
+ minorChanged = false;
+ }
+ }
+
+ // propagate a preferenceChanged, using the
+ // layout thread.
+ if (parent != null) {
+ parent.preferenceChanged(this, horizontal, vertical);
+
+ // probably want to change this to be more exact.
+ Component c = getContainer();
+ if (c != null) {
+ c.repaint();
+ }
+ }
+ } finally {
+ doc.readUnlock();
+ }
+ }
+
+ /**
+ * Calls the superclass to update the child views, and
+ * updates the status records for the children. This
+ * is expected to be called while a write lock is held
+ * on the model so that interaction with the layout
+ * thread will not happen (i.e. the layout thread
+ * acquires a read lock before doing anything).
+ *
+ * @param offset the starting offset into the child views >= 0
+ * @param length the number of existing views to replace >= 0
+ * @param views the child views to insert
+ */
+ public void replace(int offset, int length, View[] views) {
+ synchronized(stats) {
+ // remove the replaced state records
+ for (int i = 0; i < length; i++) {
+ ChildState cs = (ChildState)stats.remove(offset);
+ float csSpan = cs.getMajorSpan();
+
+ cs.getChildView().setParent(null);
+ if (csSpan != 0) {
+ majorRequirementChange(cs, -csSpan);
+ }
+ }
+
+ // insert the state records for the new children
+ LayoutQueue q = getLayoutQueue();
+ if (views != null) {
+ for (int i = 0; i < views.length; i++) {
+ ChildState s = createChildState(views[i]);
+ stats.add(offset + i, s);
+ q.addTask(s);
+ }
+ }
+
+ // notify that the size changed
+ q.addTask(flushTask);
+ }
+ }
+
+ /**
+ * Loads all of the children to initialize the view.
+ * This is called by the <a href="#setParent">setParent</a>
+ * method. Subclasses can reimplement this to initialize
+ * their child views in a different manner. The default
+ * implementation creates a child view for each
+ * child element.
+ * <p>
+ * Normally a write-lock is held on the Document while
+ * the children are being changed, which keeps the rendering
+ * and layout threads safe. The exception to this is when
+ * the view is initialized to represent an existing element
+ * (via this method), so it is synchronized to exclude
+ * preferenceChanged while we are initializing.
+ *
+ * @param f the view factory
+ * @see #setParent
+ */
+ protected void loadChildren(ViewFactory f) {
+ Element e = getElement();
+ int n = e.getElementCount();
+ if (n > 0) {
+ View[] added = new View[n];
+ for (int i = 0; i < n; i++) {
+ added[i] = f.create(e.getElement(i));
+ }
+ replace(0, 0, added);
+ }
+ }
+
+ /**
+ * Fetches the child view index representing the given position in
+ * the model. This is implemented to fetch the view in the case
+ * where there is a child view for each child element.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ */
+ protected synchronized int getViewIndexAtPosition(int pos, Position.Bias b) {
+ boolean isBackward = (b == Position.Bias.Backward);
+ pos = (isBackward) ? Math.max(0, pos - 1) : pos;
+ Element elem = getElement();
+ return elem.getElementIndex(pos);
+ }
+
+ /**
+ * Update the layout in response to receiving notification of
+ * change from the model. This is implemented to note the
+ * change on the ChildLocator so that offsets of the children
+ * will be correctly computed.
+ *
+ * @param ec changes to the element this view is responsible
+ * for (may be null if there were no changes).
+ * @param e the change information from the associated document
+ * @param a the current allocation of the view
+ * @see #insertUpdate
+ * @see #removeUpdate
+ * @see #changedUpdate
+ */
+ protected void updateLayout(DocumentEvent.ElementChange ec,
+ DocumentEvent e, Shape a) {
+ if (ec != null) {
+ // the newly inserted children don't have a valid
+ // offset so the child locator needs to be messaged
+ // that the child prior to the new children has
+ // changed size.
+ int index = Math.max(ec.getIndex() - 1, 0);
+ ChildState cs = getChildState(index);
+ locator.childChanged(cs);
+ }
+ }
+
+ // --- View methods ------------------------------------
+
+ /**
+ * Sets the parent of the view.
+ * This is reimplemented to provide the superclass
+ * behavior as well as calling the <code>loadChildren</code>
+ * method if this view does not already have children.
+ * The children should not be loaded in the
+ * constructor because the act of setting the parent
+ * may cause them to try to search up the hierarchy
+ * (to get the hosting Container for example).
+ * If this view has children (the view is being moved
+ * from one place in the view hierarchy to another),
+ * the <code>loadChildren</code> method will not be called.
+ *
+ * @param parent the parent of the view, null if none
+ */
+ public void setParent(View parent) {
+ super.setParent(parent);
+ if ((parent != null) && (getViewCount() == 0)) {
+ ViewFactory f = getViewFactory();
+ loadChildren(f);
+ }
+ }
+
+ /**
+ * Child views can call this on the parent to indicate that
+ * the preference has changed and should be reconsidered
+ * for layout. This is reimplemented to queue new work
+ * on the layout thread. This method gets messaged from
+ * multiple threads via the children.
+ *
+ * @param child the child view
+ * @param width true if the width preference has changed
+ * @param height true if the height preference has changed
+ * @see javax.swing.JComponent#revalidate
+ */
+ public synchronized void preferenceChanged(View child, boolean width, boolean height) {
+ if (child == null) {
+ getParent().preferenceChanged(this, width, height);
+ } else {
+ if (changing != null) {
+ View cv = changing.getChildView();
+ if (cv == child) {
+ // size was being changed on the child, no need to
+ // queue work for it.
+ changing.preferenceChanged(width, height);
+ return;
+ }
+ }
+ int index = getViewIndex(child.getStartOffset(),
+ Position.Bias.Forward);
+ ChildState cs = getChildState(index);
+ cs.preferenceChanged(width, height);
+ LayoutQueue q = getLayoutQueue();
+ q.addTask(cs);
+ q.addTask(flushTask);
+ }
+ }
+
+ /**
+ * Sets the size of the view. This should cause
+ * layout of the view if the view caches any layout
+ * information.
+ * <p>
+ * Since the major axis is updated asynchronously and should be
+ * the sum of the tiled children the call is ignored for the major
+ * axis. Since the minor axis is flexible, work is queued to resize
+ * the children if the minor span changes.
+ *
+ * @param width the width >= 0
+ * @param height the height >= 0
+ */
+ public void setSize(float width, float height) {
+ setSpanOnAxis(X_AXIS, width);
+ setSpanOnAxis(Y_AXIS, height);
+ }
+
+ /**
+ * Retrieves the size of the view along an axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @return the current span of the view along the given axis, >= 0
+ */
+ float getSpanOnAxis(int axis) {
+ if (axis == getMajorAxis()) {
+ return majorSpan;
+ }
+ return minorSpan;
+ }
+
+ /**
+ * Sets the size of the view along an axis. Since the major
+ * axis is updated asynchronously and should be the sum of the
+ * tiled children the call is ignored for the major axis. Since
+ * the minor axis is flexible, work is queued to resize the
+ * children if the minor span changes.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @param span the span to layout to >= 0
+ */
+ void setSpanOnAxis(int axis, float span) {
+ float margin = getInsetSpan(axis);
+ if (axis == getMinorAxis()) {
+ float targetSpan = span - margin;
+ if (targetSpan != minorSpan) {
+ minorSpan = targetSpan;
+
+ // mark all of the ChildState instances as needing to
+ // resize the child, and queue up work to fix them.
+ int n = getViewCount();
+ if (n != 0) {
+ LayoutQueue q = getLayoutQueue();
+ for (int i = 0; i < n; i++) {
+ ChildState cs = getChildState(i);
+ cs.childSizeValid = false;
+ q.addTask(cs);
+ }
+ q.addTask(flushTask);
+ }
+ }
+ } else {
+ // along the major axis the value is ignored
+ // unless the estimatedMajorSpan property is
+ // true.
+ if (estimatedMajorSpan) {
+ majorSpan = span - margin;
+ }
+ }
+ }
+
+ /**
+ * Render the view using the given allocation and
+ * rendering surface.
+ * <p>
+ * This is implemented to determine whether or not the
+ * desired region to be rendered (i.e. the unclipped
+ * area) is up to date or not. If up-to-date the children
+ * are rendered. If not up-to-date, a task to build
+ * the desired area is placed on the layout queue as
+ * a high priority task. This keeps by event thread
+ * moving by rendering if ready, and postponing until
+ * a later time if not ready (since paint requests
+ * can be rescheduled).
+ *
+ * @param g the rendering surface to use
+ * @param alloc the allocated region to render into
+ * @see View#paint
+ */
+ public void paint(Graphics g, Shape alloc) {
+ synchronized (locator) {
+ locator.setAllocation(alloc);
+ locator.paintChildren(g);
+ }
+ }
+
+ /**
+ * 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) {
+ float margin = getInsetSpan(axis);
+ if (axis == this.axis) {
+ return majorSpan + margin;
+ }
+ if (prefRequest != null) {
+ View child = prefRequest.getChildView();
+ return child.getPreferredSpan(axis) + margin;
+ }
+
+ // nothing is known about the children yet
+ return margin + 30;
+ }
+
+ /**
+ * 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) {
+ if (axis == this.axis) {
+ return getPreferredSpan(axis);
+ }
+ if (minRequest != null) {
+ View child = minRequest.getChildView();
+ return child.getMinimumSpan(axis);
+ }
+
+ // nothing is known about the children yet
+ if (axis == X_AXIS) {
+ return getLeftInset() + getRightInset() + 5;
+ } else {
+ return getTopInset() + getBottomInset() + 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 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) {
+ if (axis == this.axis) {
+ return getPreferredSpan(axis);
+ }
+ return Integer.MAX_VALUE;
+ }
+
+
+ /**
+ * Returns the number of views in this view. Since
+ * the default is to not be a composite view this
+ * returns 0.
+ *
+ * @return the number of views >= 0
+ * @see View#getViewCount
+ */
+ public int getViewCount() {
+ synchronized(stats) {
+ return stats.size();
+ }
+ }
+
+ /**
+ * Gets the nth child view. Since there are no
+ * children by default, this returns null.
+ *
+ * @param n the number of the view to get, >= 0 && < getViewCount()
+ * @return the view
+ */
+ public View getView(int n) {
+ ChildState cs = getChildState(n);
+ if (cs != null) {
+ return cs.getChildView();
+ }
+ return null;
+ }
+
+ /**
+ * Fetches the allocation for the given child view.
+ * This enables finding out where various views
+ * are located, without assuming the views store
+ * their location. This returns null since the
+ * default is to not have any child views.
+ *
+ * @param index the index of the child, >= 0 && < getViewCount()
+ * @param a the allocation to this view.
+ * @return the allocation to the child
+ */
+ public Shape getChildAllocation(int index, Shape a) {
+ Shape ca = locator.getChildAllocation(index, a);
+ return ca;
+ }
+
+ /**
+ * Returns the child view index representing the given position in
+ * the model. By default a view has no children so this is implemented
+ * to return -1 to indicate there is no valid child index for any
+ * position.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ * @since 1.3
+ */
+ public int getViewIndex(int pos, Position.Bias b) {
+ return getViewIndexAtPosition(pos, b);
+ }
+
+ /**
+ * 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 >= 0
+ * @param a the allocated region to render into
+ * @param b the bias toward the previous character or the
+ * next character represented by the offset, in case the
+ * position is a boundary of two views.
+ * @return the bounding box of the given position is returned
+ * @exception BadLocationException if the given position does
+ * not represent a valid location in the associated document
+ * @exception IllegalArgumentException for an invalid bias argument
+ * @see View#viewToModel
+ */
+ public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
+ int index = getViewIndex(pos, b);
+ Shape ca = locator.getChildAllocation(index, a);
+
+ // forward to the child view, and make sure we don't
+ // interact with the layout thread by synchronizing
+ // on the child state.
+ ChildState cs = getChildState(index);
+ synchronized (cs) {
+ View cv = cs.getChildView();
+ Shape v = cv.modelToView(pos, ca, b);
+ return v;
+ }
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model. The biasReturn argument will be
+ * filled in to indicate that the point given is closer to the next
+ * character in the model or the previous character in the model.
+ * <p>
+ * This is expected to be called by the GUI thread, holding a
+ * read-lock on the associated model. It is implemented to
+ * locate the child view and determine it's allocation with a
+ * lock on the ChildLocator object, and to call viewToModel
+ * on the child view with a lock on the ChildState object
+ * to avoid interaction with the layout thread.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents the
+ * given point in the view >= 0. The biasReturn argument will be
+ * filled in to indicate that the point given is closer to the next
+ * character in the model or the previous character in the model.
+ */
+ public int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn) {
+ int pos; // return position
+ int index; // child index to forward to
+ Shape ca; // child allocation
+
+ // locate the child view and it's allocation so that
+ // we can forward to it. Make sure the layout thread
+ // doesn't change anything by trying to flush changes
+ // to the parent while the GUI thread is trying to
+ // find the child and it's allocation.
+ synchronized (locator) {
+ index = locator.getViewIndexAtPoint(x, y, a);
+ ca = locator.getChildAllocation(index, a);
+ }
+
+ // forward to the child view, and make sure we don't
+ // interact with the layout thread by synchronizing
+ // on the child state.
+ ChildState cs = getChildState(index);
+ synchronized (cs) {
+ View v = cs.getChildView();
+ pos = v.viewToModel(x, y, ca, biasReturn);
+ }
+ return pos;
+ }
+
+ /**
+ * Provides a way to determine the next visually represented model
+ * location that one might place a caret. Some views may not be visible,
+ * they might not be in the same order found in the model, or they just
+ * might not allow access to some of the locations in the model.
+ *
+ * @param pos the position to convert >= 0
+ * @param a the allocated region to render into
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard;
+ * this may be one of the following:
+ * <ul>
+ * <code>SwingConstants.WEST</code>
+ * <code>SwingConstants.EAST</code>
+ * <code>SwingConstants.NORTH</code>
+ * <code>SwingConstants.SOUTH</code>
+ * </ul>
+ * @param biasRet an array contain the bias that was checked
+ * @return the location within the model that best represents the next
+ * location visual position
+ * @exception BadLocationException
+ * @exception IllegalArgumentException if <code>direction</code> is invalid
+ */
+ public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a,
+ int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+ return Utilities.getNextVisualPositionFrom(
+ this, pos, b, a, direction, biasRet);
+ }
+
+ // --- variables -----------------------------------------
+
+ /**
+ * The major axis against which the children are
+ * tiled.
+ */
+ int axis;
+
+ /**
+ * The children and their layout statistics.
+ */
+ java.util.List stats;
+
+ /**
+ * Current span along the major axis. This
+ * is also the value returned by getMinimumSize,
+ * getPreferredSize, and getMaximumSize along
+ * the major axis.
+ */
+ float majorSpan;
+
+ /**
+ * Is the span along the major axis estimated?
+ */
+ boolean estimatedMajorSpan;
+
+ /**
+ * Current span along the minor axis. This
+ * is what layout was done against (i.e. things
+ * are flexible in this direction).
+ */
+ float minorSpan;
+
+ /**
+ * Object that manages the offsets of the
+ * children. All locking for management of
+ * child locations is on this object.
+ */
+ protected ChildLocator locator;
+
+ float topInset;
+ float bottomInset;
+ float leftInset;
+ float rightInset;
+
+ ChildState minRequest;
+ ChildState prefRequest;
+ boolean majorChanged;
+ boolean minorChanged;
+ Runnable flushTask;
+
+ /**
+ * Child that is actively changing size. This often
+ * causes a preferenceChanged, so this is a cache to
+ * possibly speed up the marking the state. It also
+ * helps flag an opportunity to avoid adding to flush
+ * task to the layout queue.
+ */
+ ChildState changing;
+
+ /**
+ * A class to manage the effective position of the
+ * child views in a localized area while changes are
+ * being made around the localized area. The AsyncBoxView
+ * may be continuously changing, but the visible area
+ * needs to remain fairly stable until the layout thread
+ * decides to publish an update to the parent.
+ * @since 1.3
+ */
+ public class ChildLocator {
+
+ /**
+ * construct a child locator.
+ */
+ public ChildLocator() {
+ lastAlloc = new Rectangle();
+ childAlloc = new Rectangle();
+ }
+
+ /**
+ * Notification that a child changed. This can effect
+ * whether or not new offset calculations are needed.
+ * This is called by a ChildState object that has
+ * changed it's major span. This can therefore be
+ * called by multiple threads.
+ */
+ public synchronized void childChanged(ChildState cs) {
+ if (lastValidOffset == null) {
+ lastValidOffset = cs;
+ } else if (cs.getChildView().getStartOffset() <
+ lastValidOffset.getChildView().getStartOffset()) {
+ lastValidOffset = cs;
+ }
+ }
+
+ /**
+ * Paint the children that intersect the clip area.
+ */
+ public synchronized void paintChildren(Graphics g) {
+ Rectangle clip = g.getClipBounds();
+ float targetOffset = (axis == X_AXIS) ?
+ clip.x - lastAlloc.x : clip.y - lastAlloc.y;
+ int index = getViewIndexAtVisualOffset(targetOffset);
+ int n = getViewCount();
+ float offs = getChildState(index).getMajorOffset();
+ for (int i = index; i < n; i++) {
+ ChildState cs = getChildState(i);
+ cs.setMajorOffset(offs);
+ Shape ca = getChildAllocation(i);
+ if (intersectsClip(ca, clip)) {
+ synchronized (cs) {
+ View v = cs.getChildView();
+ v.paint(g, ca);
+ }
+ } else {
+ // done painting intersection
+ break;
+ }
+ offs += cs.getMajorSpan();
+ }
+ }
+
+ /**
+ * Fetch the allocation to use for a child view.
+ * This will update the offsets for all children
+ * not yet updated before the given index.
+ */
+ public synchronized Shape getChildAllocation(int index, Shape a) {
+ if (a == null) {
+ return null;
+ }
+ setAllocation(a);
+ ChildState cs = getChildState(index);
+ if (lastValidOffset == null) {
+ lastValidOffset = getChildState(0);
+ }
+ if (cs.getChildView().getStartOffset() >
+ lastValidOffset.getChildView().getStartOffset()) {
+ // offsets need to be updated
+ updateChildOffsetsToIndex(index);
+ }
+ Shape ca = getChildAllocation(index);
+ return ca;
+ }
+
+ /**
+ * Fetches the child view index at the given point.
+ * This is called by the various View methods that
+ * need to calculate which child to forward a message
+ * to. This should be called by a block synchronized
+ * on this object, and would typically be followed
+ * with one or more calls to getChildAllocation that
+ * should also be in the synchronized block.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param a the allocation to the View
+ * @return the nearest child index
+ */
+ public int getViewIndexAtPoint(float x, float y, Shape a) {
+ setAllocation(a);
+ float targetOffset = (axis == X_AXIS) ? x - lastAlloc.x : y - lastAlloc.y;
+ int index = getViewIndexAtVisualOffset(targetOffset);
+ return index;
+ }
+
+ /**
+ * Fetch the allocation to use for a child view.
+ * <em>This does not update the offsets in the ChildState
+ * records.</em>
+ */
+ protected Shape getChildAllocation(int index) {
+ ChildState cs = getChildState(index);
+ if (! cs.isLayoutValid()) {
+ cs.run();
+ }
+ if (axis == X_AXIS) {
+ childAlloc.x = lastAlloc.x + (int) cs.getMajorOffset();
+ childAlloc.y = lastAlloc.y + (int) cs.getMinorOffset();
+ childAlloc.width = (int) cs.getMajorSpan();
+ childAlloc.height = (int) cs.getMinorSpan();
+ } else {
+ childAlloc.y = lastAlloc.y + (int) cs.getMajorOffset();
+ childAlloc.x = lastAlloc.x + (int) cs.getMinorOffset();
+ childAlloc.height = (int) cs.getMajorSpan();
+ childAlloc.width = (int) cs.getMinorSpan();
+ }
+ childAlloc.x += (int)getLeftInset();
+ childAlloc.y += (int)getRightInset();
+ return childAlloc;
+ }
+
+ /**
+ * Copy the currently allocated shape into the Rectangle
+ * used to store the current allocation. This would be
+ * a floating point rectangle in a Java2D-specific implmentation.
+ */
+ protected void setAllocation(Shape a) {
+ if (a instanceof Rectangle) {
+ lastAlloc.setBounds((Rectangle) a);
+ } else {
+ lastAlloc.setBounds(a.getBounds());
+ }
+ setSize(lastAlloc.width, lastAlloc.height);
+ }
+
+ /**
+ * Locate the view responsible for an offset into the box
+ * along the major axis. Make sure that offsets are set
+ * on the ChildState objects up to the given target span
+ * past the desired offset.
+ *
+ * @return index of the view representing the given visual
+ * location (targetOffset), or -1 if no view represents
+ * that location
+ */
+ protected int getViewIndexAtVisualOffset(float targetOffset) {
+ int n = getViewCount();
+ if (n > 0) {
+ boolean lastValid = (lastValidOffset != null);
+
+ if (lastValidOffset == null) {
+ lastValidOffset = getChildState(0);
+ }
+ if (targetOffset > majorSpan) {
+ // should only get here on the first time display.
+ if (!lastValid) {
+ return 0;
+ }
+ int pos = lastValidOffset.getChildView().getStartOffset();
+ int index = getViewIndex(pos, Position.Bias.Forward);
+ return index;
+ } else if (targetOffset > lastValidOffset.getMajorOffset()) {
+ // roll offset calculations forward
+ return updateChildOffsets(targetOffset);
+ } else {
+ // no changes prior to the needed offset
+ // this should be a binary search
+ float offs = 0f;
+ for (int i = 0; i < n; i++) {
+ ChildState cs = getChildState(i);
+ float nextOffs = offs + cs.getMajorSpan();
+ if (targetOffset < nextOffs) {
+ return i;
+ }
+ offs = nextOffs;
+ }
+ }
+ }
+ return n - 1;
+ }
+
+ /**
+ * Move the location of the last offset calculation forward
+ * to the desired offset.
+ */
+ int updateChildOffsets(float targetOffset) {
+ int n = getViewCount();
+ int targetIndex = n - 1;;
+ int pos = lastValidOffset.getChildView().getStartOffset();
+ int startIndex = getViewIndex(pos, Position.Bias.Forward);
+ float start = lastValidOffset.getMajorOffset();
+ float lastOffset = start;
+ for (int i = startIndex; i < n; i++) {
+ ChildState cs = getChildState(i);
+ cs.setMajorOffset(lastOffset);
+ lastOffset += cs.getMajorSpan();
+ if (targetOffset < lastOffset) {
+ targetIndex = i;
+ lastValidOffset = cs;
+ break;
+ }
+ }
+
+ return targetIndex;
+ }
+
+ /**
+ * Move the location of the last offset calculation forward
+ * to the desired index.
+ */
+ void updateChildOffsetsToIndex(int index) {
+ int pos = lastValidOffset.getChildView().getStartOffset();
+ int startIndex = getViewIndex(pos, Position.Bias.Forward);
+ float lastOffset = lastValidOffset.getMajorOffset();
+ for (int i = startIndex; i <= index; i++) {
+ ChildState cs = getChildState(i);
+ cs.setMajorOffset(lastOffset);
+ lastOffset += cs.getMajorSpan();
+ }
+ }
+
+ boolean intersectsClip(Shape childAlloc, Rectangle clip) {
+ Rectangle cs = (childAlloc instanceof Rectangle) ?
+ (Rectangle) childAlloc : childAlloc.getBounds();
+ if (cs.intersects(clip)) {
+ // Make sure that lastAlloc also contains childAlloc,
+ // this will be false if haven't yet flushed changes.
+ return lastAlloc.intersects(cs);
+ }
+ return false;
+ }
+
+ /**
+ * The location of the last offset calculation
+ * that is valid.
+ */
+ protected ChildState lastValidOffset;
+
+ /**
+ * The last seen allocation (for repainting when changes
+ * are flushed upward).
+ */
+ protected Rectangle lastAlloc;
+
+ /**
+ * A shape to use for the child allocation to avoid
+ * creating a lot of garbage.
+ */
+ protected Rectangle childAlloc;
+ }
+
+ /**
+ * A record representing the layout state of a
+ * child view. It is runnable as a task on another
+ * thread. All access to the child view that is
+ * based upon a read-lock on the model should synchronize
+ * on this object (i.e. The layout thread and the GUI
+ * thread can both have a read lock on the model at the
+ * same time and are not protected from each other).
+ * Access to a child view hierarchy is serialized via
+ * synchronization on the ChildState instance.
+ * @since 1.3
+ */
+ public class ChildState implements Runnable {
+
+ /**
+ * Construct a child status. This needs to start
+ * out as fairly large so we don't falsely begin with
+ * the idea that all of the children are visible.
+ * @since 1.4
+ */
+ public ChildState(View v) {
+ child = v;
+ minorValid = false;
+ majorValid = false;
+ childSizeValid = false;
+ child.setParent(AsyncBoxView.this);
+ }
+
+ /**
+ * Fetch the child view this record represents
+ */
+ public View getChildView() {
+ return child;
+ }
+
+ /**
+ * Update the child state. This should be
+ * called by the thread that desires to spend
+ * time updating the child state (intended to
+ * be the layout thread).
+ * <p>
+ * This aquires a read lock on the associated
+ * document for the duration of the update to
+ * ensure the model is not changed while it is
+ * operating. The first thing to do would be
+ * to see if any work actually needs to be done.
+ * The following could have conceivably happened
+ * while the state was waiting to be updated:
+ * <ol>
+ * <li>The child may have been removed from the
+ * view hierarchy.
+ * <li>The child may have been updated by a
+ * higher priority operation (i.e. the child
+ * may have become visible).
+ * </ol>
+ */
+ public void run () {
+ AbstractDocument doc = (AbstractDocument) getDocument();
+ try {
+ doc.readLock();
+ if (minorValid && majorValid && childSizeValid) {
+ // nothing to do
+ return;
+ }
+ if (child.getParent() == AsyncBoxView.this) {
+ // this may overwrite anothers threads cached
+ // value for actively changing... but that just
+ // means it won't use the cache if there is an
+ // overwrite.
+ synchronized(AsyncBoxView.this) {
+ changing = this;
+ }
+ updateChild();
+ synchronized(AsyncBoxView.this) {
+ changing = null;
+ }
+
+ // setting the child size on the minor axis
+ // may have caused it to change it's preference
+ // along the major axis.
+ updateChild();
+ }
+ } finally {
+ doc.readUnlock();
+ }
+ }
+
+ void updateChild() {
+ boolean minorUpdated = false;
+ synchronized(this) {
+ if (! minorValid) {
+ int minorAxis = getMinorAxis();
+ min = child.getMinimumSpan(minorAxis);
+ pref = child.getPreferredSpan(minorAxis);
+ max = child.getMaximumSpan(minorAxis);
+ minorValid = true;
+ minorUpdated = true;
+ }
+ }
+ if (minorUpdated) {
+ minorRequirementChange(this);
+ }
+
+ boolean majorUpdated = false;
+ float delta = 0.0f;
+ synchronized(this) {
+ if (! majorValid) {
+ float old = span;
+ span = child.getPreferredSpan(axis);
+ delta = span - old;
+ majorValid = true;
+ majorUpdated = true;
+ }
+ }
+ if (majorUpdated) {
+ majorRequirementChange(this, delta);
+ locator.childChanged(this);
+ }
+
+ synchronized(this) {
+ if (! childSizeValid) {
+ float w;
+ float h;
+ if (axis == X_AXIS) {
+ w = span;
+ h = getMinorSpan();
+ } else {
+ w = getMinorSpan();
+ h = span;
+ }
+ childSizeValid = true;
+ child.setSize(w, h);
+ }
+ }
+
+ }
+
+ /**
+ * What is the span along the minor axis.
+ */
+ public float getMinorSpan() {
+ if (max < minorSpan) {
+ return max;
+ }
+ // make it the target width, or as small as it can get.
+ return Math.max(min, minorSpan);
+ }
+
+ /**
+ * What is the offset along the minor axis
+ */
+ public float getMinorOffset() {
+ if (max < minorSpan) {
+ // can't make the child this wide, align it
+ float align = child.getAlignment(getMinorAxis());
+ return ((minorSpan - max) * align);
+ }
+ return 0f;
+ }
+
+ /**
+ * What is the span along the major axis.
+ */
+ public float getMajorSpan() {
+ return span;
+ }
+
+ /**
+ * Get the offset along the major axis
+ */
+ public float getMajorOffset() {
+ return offset;
+ }
+
+ /**
+ * This method should only be called by the ChildLocator,
+ * it is simply a convenient place to hold the cached
+ * location.
+ */
+ public void setMajorOffset(float offs) {
+ offset = offs;
+ }
+
+ /**
+ * Mark preferences changed for this child.
+ *
+ * @param width true if the width preference has changed
+ * @param height true if the height preference has changed
+ * @see javax.swing.JComponent#revalidate
+ */
+ public void preferenceChanged(boolean width, boolean height) {
+ if (axis == X_AXIS) {
+ if (width) {
+ majorValid = false;
+ }
+ if (height) {
+ minorValid = false;
+ }
+ } else {
+ if (width) {
+ minorValid = false;
+ }
+ if (height) {
+ majorValid = false;
+ }
+ }
+ childSizeValid = false;
+ }
+
+ /**
+ * Has the child view been laid out.
+ */
+ public boolean isLayoutValid() {
+ return (minorValid && majorValid && childSizeValid);
+ }
+
+ // minor axis
+ private float min;
+ private float pref;
+ private float max;
+ private float align;
+ private boolean minorValid;
+
+ // major axis
+ private float span;
+ private float offset;
+ private boolean majorValid;
+
+ private View child;
+ private boolean childSizeValid;
+ }
+
+ /**
+ * Task to flush requirement changes upward
+ */
+ class FlushTask implements Runnable {
+
+ public void run() {
+ flushRequirementChanges();
+ }
+
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/AttributeSet.java b/src/share/classes/javax/swing/text/AttributeSet.java
new file mode 100644
index 000000000..b20d3bcfd
--- /dev/null
+++ b/src/share/classes/javax/swing/text/AttributeSet.java
@@ -0,0 +1,193 @@
+/*
+ * 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;
+
+import java.util.Enumeration;
+
+/**
+ * A collection of unique attributes. This is a read-only,
+ * immutable interface. An attribute is basically a key and
+ * a value assigned to the key. The collection may represent
+ * something like a style run, a logical style, etc. These
+ * are generally used to describe features that will contribute
+ * to some graphical representation such as a font. The
+ * set of possible keys is unbounded and can be anything.
+ * Typically View implementations will respond to attribute
+ * definitions and render something to represent the attributes.
+ * <p>
+ * Attributes can potentially resolve in a hierarchy. If a
+ * key doesn't resolve locally, and a resolving parent
+ * exists, the key will be resolved through the parent.
+ *
+ * @author Timothy Prinzing
+ * @see MutableAttributeSet
+ */
+public interface AttributeSet {
+
+ /**
+ * This interface is the type signature that is expected
+ * to be present on any attribute key that contributes to
+ * the determination of what font to use to render some
+ * text. This is not considered to be a closed set, the
+ * definition can change across version of the platform and can
+ * be amended by additional user added entries that
+ * correspond to logical settings that are specific to
+ * some type of content.
+ */
+ public interface FontAttribute {
+ }
+
+ /**
+ * This interface is the type signature that is expected
+ * to be present on any attribute key that contributes to
+ * presentation of color.
+ */
+ public interface ColorAttribute {
+ }
+
+ /**
+ * This interface is the type signature that is expected
+ * to be present on any attribute key that contributes to
+ * character level presentation. This would be any attribute
+ * that applies to a so-called <term>run</term> of
+ * style.
+ */
+ public interface CharacterAttribute {
+ }
+
+ /**
+ * This interface is the type signature that is expected
+ * to be present on any attribute key that contributes to
+ * the paragraph level presentation.
+ */
+ public interface ParagraphAttribute {
+ }
+
+ /**
+ * Returns the number of attributes that are defined locally in this set.
+ * Attributes that are defined in the parent set are not included.
+ *
+ * @return the number of attributes >= 0
+ */
+ public int getAttributeCount();
+
+ /**
+ * Checks whether the named attribute has a value specified in
+ * the set without resolving through another attribute
+ * set.
+ *
+ * @param attrName the attribute name
+ * @return true if the attribute has a value specified
+ */
+ public boolean isDefined(Object attrName);
+
+ /**
+ * Determines if the two attribute sets are equivalent.
+ *
+ * @param attr an attribute set
+ * @return true if the sets are equivalent
+ */
+ public boolean isEqual(AttributeSet attr);
+
+ /**
+ * Returns an attribute set that is guaranteed not
+ * to change over time.
+ *
+ * @return a copy of the attribute set
+ */
+ public AttributeSet copyAttributes();
+
+ /**
+ * Fetches the value of the given attribute. If the value is not found
+ * locally, the search is continued upward through the resolving
+ * parent (if one exists) until the value is either
+ * found or there are no more parents. If the value is not found,
+ * null is returned.
+ *
+ * @param key the non-null key of the attribute binding
+ * @return the value of the attribute, or {@code null} if not found
+ */
+ public Object getAttribute(Object key);
+
+ /**
+ * Returns an enumeration over the names of the attributes that are
+ * defined locally in the set. Names of attributes defined in the
+ * resolving parent, if any, are not included. The values of the
+ * <code>Enumeration</code> may be anything and are not constrained to
+ * a particular <code>Object</code> type.
+ * <p>
+ * This method never returns {@code null}. For a set with no attributes, it
+ * returns an empty {@code Enumeration}.
+ *
+ * @return the names
+ */
+ public Enumeration<?> getAttributeNames();
+
+ /**
+ * Returns {@code true} if this set defines an attribute with the same
+ * name and an equal value. If such an attribute is not found locally,
+ * it is searched through in the resolving parent hierarchy.
+ *
+ * @param name the non-null attribute name
+ * @param value the value
+ * @return {@code true} if the set defines the attribute with an
+ * equal value, either locally or through its resolving parent
+ * @throws NullPointerException if either {@code name} or
+ * {@code value} is {@code null}
+ */
+ public boolean containsAttribute(Object name, Object value);
+
+ /**
+ * Returns {@code true} if this set defines all the attributes from the
+ * given set with equal values. If an attribute is not found locally,
+ * it is searched through in the resolving parent hierarchy.
+ *
+ * @param attributes the set of attributes to check against
+ * @return {@code true} if this set defines all the attributes with equal
+ * values, either locally or through its resolving parent
+ * @throws NullPointerException if {@code attributes} is {@code null}
+ */
+ public boolean containsAttributes(AttributeSet attributes);
+
+ /**
+ * Gets the resolving parent.
+ *
+ * @return the parent
+ */
+ public AttributeSet getResolveParent();
+
+ /**
+ * Attribute name used to name the collection of
+ * attributes.
+ */
+ public static final Object NameAttribute = StyleConstants.NameAttribute;
+
+ /**
+ * Attribute name used to identify the resolving parent
+ * set of attributes, if one is defined.
+ */
+ public static final Object ResolveAttribute = StyleConstants.ResolveAttribute;
+
+}
diff --git a/src/share/classes/javax/swing/text/BadLocationException.java b/src/share/classes/javax/swing/text/BadLocationException.java
new file mode 100644
index 000000000..abfa05fbd
--- /dev/null
+++ b/src/share/classes/javax/swing/text/BadLocationException.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 1997-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;
+
+/**
+ * This exception is to report bad locations within a document model
+ * (that is, attempts to reference a location that doesn't exist).
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Timothy Prinzing
+ */
+public class BadLocationException extends Exception
+{
+ /**
+ * Creates a new BadLocationException object.
+ *
+ * @param s a string indicating what was wrong with the arguments
+ * @param offs offset within the document that was requested >= 0
+ */
+ public BadLocationException(String s, int offs) {
+ super(s);
+ this.offs = offs;
+ }
+
+ /**
+ * Returns the offset into the document that was not legal.
+ *
+ * @return the offset >= 0
+ */
+ public int offsetRequested() {
+ return offs;
+ }
+
+ private int offs;
+}
diff --git a/src/share/classes/javax/swing/text/BoxView.java b/src/share/classes/javax/swing/text/BoxView.java
new file mode 100644
index 000000000..422c5ad54
--- /dev/null
+++ b/src/share/classes/javax/swing/text/BoxView.java
@@ -0,0 +1,1188 @@
+/*
+ * 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;
+
+import java.io.PrintStream;
+import java.util.Vector;
+import java.awt.*;
+import javax.swing.event.DocumentEvent;
+import javax.swing.SizeRequirements;
+
+/**
+ * A view that arranges its children into a box shape by tiling
+ * its children along an axis. The box is somewhat like that
+ * found in TeX where there is alignment of the
+ * children, flexibility of the children is considered, etc.
+ * This is a building block that might be useful to represent
+ * things like a collection of lines, paragraphs,
+ * lists, columns, pages, etc. The axis along which the children are tiled is
+ * considered the major axis. The orthoginal axis is the minor axis.
+ * <p>
+ * Layout for each axis is handled separately by the methods
+ * <code>layoutMajorAxis</code> and <code>layoutMinorAxis</code>.
+ * Subclasses can change the layout algorithm by
+ * reimplementing these methods. These methods will be called
+ * as necessary depending upon whether or not there is cached
+ * layout information and the cache is considered
+ * valid. These methods are typically called if the given size
+ * along the axis changes, or if <code>layoutChanged</code> is
+ * called to force an updated layout. The <code>layoutChanged</code>
+ * method invalidates cached layout information, if there is any.
+ * The requirements published to the parent view are calculated by
+ * the methods <code>calculateMajorAxisRequirements</code>
+ * and <code>calculateMinorAxisRequirements</code>.
+ * If the layout algorithm is changed, these methods will
+ * likely need to be reimplemented.
+ *
+ * @author Timothy Prinzing
+ */
+public class BoxView extends CompositeView {
+
+ /**
+ * Constructs a <code>BoxView</code>.
+ *
+ * @param elem the element this view is responsible for
+ * @param axis either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
+ */
+ public BoxView(Element elem, int axis) {
+ super(elem);
+ tempRect = new Rectangle();
+ this.majorAxis = axis;
+
+ majorOffsets = new int[0];
+ majorSpans = new int[0];
+ majorReqValid = false;
+ majorAllocValid = false;
+ minorOffsets = new int[0];
+ minorSpans = new int[0];
+ minorReqValid = false;
+ minorAllocValid = false;
+ }
+
+ /**
+ * Fetches the tile axis property. This is the axis along which
+ * the child views are tiled.
+ *
+ * @return the major axis of the box, either
+ * <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
+ *
+ * @since 1.3
+ */
+ public int getAxis() {
+ return majorAxis;
+ }
+
+ /**
+ * Sets the tile axis property. This is the axis along which
+ * the child views are tiled.
+ *
+ * @param axis either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
+ *
+ * @since 1.3
+ */
+ public void setAxis(int axis) {
+ boolean axisChanged = (axis != majorAxis);
+ majorAxis = axis;
+ if (axisChanged) {
+ preferenceChanged(null, true, true);
+ }
+ }
+
+ /**
+ * Invalidates the layout along an axis. This happens
+ * automatically if the preferences have changed for
+ * any of the child views. In some cases the layout
+ * may need to be recalculated when the preferences
+ * have not changed. The layout can be marked as
+ * invalid by calling this method. The layout will
+ * be updated the next time the <code>setSize</code> method
+ * is called on this view (typically in paint).
+ *
+ * @param axis either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
+ *
+ * @since 1.3
+ */
+ public void layoutChanged(int axis) {
+ if (axis == majorAxis) {
+ majorAllocValid = false;
+ } else {
+ minorAllocValid = false;
+ }
+ }
+
+ /**
+ * Determines if the layout is valid along the given axis.
+ *
+ * @param axis either <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
+ *
+ * @since 1.4
+ */
+ protected boolean isLayoutValid(int axis) {
+ if (axis == majorAxis) {
+ return majorAllocValid;
+ } else {
+ return minorAllocValid;
+ }
+ }
+
+ /**
+ * Paints a child. 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 paint into
+ * @param index the child index, >= 0 && < getViewCount()
+ */
+ protected void paintChild(Graphics g, Rectangle alloc, int index) {
+ View child = getView(index);
+ child.paint(g, alloc);
+ }
+
+ // --- View methods ---------------------------------------------
+
+ /**
+ * Invalidates the layout and resizes the cache of
+ * requests/allocations. The child allocations can still
+ * be accessed for the old layout, but the new children
+ * will have an offset and span of 0.
+ *
+ * @param index the starting index into the child views to insert
+ * the new views; this should be a value >= 0 and <= getViewCount
+ * @param length the number of existing child views to remove;
+ * This should be a value >= 0 and <= (getViewCount() - offset)
+ * @param elems the child views to add; this value can be
+ * <code>null</code>to indicate no children are being added
+ * (useful to remove)
+ */
+ public void replace(int index, int length, View[] elems) {
+ super.replace(index, length, elems);
+
+ // invalidate cache
+ int nInserted = (elems != null) ? elems.length : 0;
+ majorOffsets = updateLayoutArray(majorOffsets, index, nInserted);
+ majorSpans = updateLayoutArray(majorSpans, index, nInserted);
+ majorReqValid = false;
+ majorAllocValid = false;
+ minorOffsets = updateLayoutArray(minorOffsets, index, nInserted);
+ minorSpans = updateLayoutArray(minorSpans, index, nInserted);
+ minorReqValid = false;
+ minorAllocValid = false;
+ }
+
+ /**
+ * Resizes the given layout array to match the new number of
+ * child views. The current number of child views are used to
+ * produce the new array. The contents of the old array are
+ * inserted into the new array at the appropriate places so that
+ * the old layout information is transferred to the new array.
+ *
+ * @param oldArray the original layout array
+ * @param offset location where new views will be inserted
+ * @param nInserted the number of child views being inserted;
+ * therefore the number of blank spaces to leave in the
+ * new array at location <code>offset</code>
+ * @return the new layout array
+ */
+ int[] updateLayoutArray(int[] oldArray, int offset, int nInserted) {
+ int n = getViewCount();
+ int[] newArray = new int[n];
+
+ System.arraycopy(oldArray, 0, newArray, 0, offset);
+ System.arraycopy(oldArray, offset,
+ newArray, offset + nInserted, n - nInserted - offset);
+ return newArray;
+ }
+
+ /**
+ * Forwards the given <code>DocumentEvent</code> to the child views
+ * that need to be notified of the change to the model.
+ * If a child changed its requirements and the allocation
+ * was valid prior to forwarding the portion of the box
+ * from the starting child to the end of the box will
+ * be repainted.
+ *
+ * @param ec changes to the element this view is responsible
+ * for (may be <code>null</code> if there were no changes)
+ * @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 #insertUpdate
+ * @see #removeUpdate
+ * @see #changedUpdate
+ * @since 1.3
+ */
+ protected void forwardUpdate(DocumentEvent.ElementChange ec,
+ DocumentEvent e, Shape a, ViewFactory f) {
+ boolean wasValid = isLayoutValid(majorAxis);
+ super.forwardUpdate(ec, e, a, f);
+
+ // determine if a repaint is needed
+ if (wasValid && (! isLayoutValid(majorAxis))) {
+ // Repaint is needed because one of the tiled children
+ // have changed their span along the major axis. If there
+ // is a hosting component and an allocated shape we repaint.
+ Component c = getContainer();
+ if ((a != null) && (c != null)) {
+ int pos = e.getOffset();
+ int index = getViewIndexAtPosition(pos);
+ Rectangle alloc = getInsideAllocation(a);
+ if (majorAxis == X_AXIS) {
+ alloc.x += majorOffsets[index];
+ alloc.width -= majorOffsets[index];
+ } else {
+ alloc.y += minorOffsets[index];
+ alloc.height -= minorOffsets[index];
+ }
+ c.repaint(alloc.x, alloc.y, alloc.width, alloc.height);
+ }
+ }
+ }
+
+ /**
+ * This is called by a child to indicate its
+ * preferred span has changed. This is implemented to
+ * throw away cached layout information so that new
+ * calculations will be done the next time the children
+ * need an allocation.
+ *
+ * @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) {
+ boolean majorChanged = (majorAxis == X_AXIS) ? width : height;
+ boolean minorChanged = (majorAxis == X_AXIS) ? height : width;
+ if (majorChanged) {
+ majorReqValid = false;
+ majorAllocValid = false;
+ }
+ if (minorChanged) {
+ minorReqValid = false;
+ minorAllocValid = false;
+ }
+ super.preferenceChanged(child, width, height);
+ }
+
+ /**
+ * Gets the resize weight. A value of 0 or less is not resizable.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @return the weight
+ * @exception IllegalArgumentException for an invalid axis
+ */
+ public int getResizeWeight(int axis) {
+ checkRequests(axis);
+ if (axis == majorAxis) {
+ if ((majorRequest.preferred != majorRequest.minimum) ||
+ (majorRequest.preferred != majorRequest.maximum)) {
+ return 1;
+ }
+ } else {
+ if ((minorRequest.preferred != minorRequest.minimum) ||
+ (minorRequest.preferred != minorRequest.maximum)) {
+ return 1;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Sets the size of the view along an axis. This should cause
+ * layout of the view along the given axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @param span the span to layout to >= 0
+ */
+ void setSpanOnAxis(int axis, float span) {
+ if (axis == majorAxis) {
+ if (majorSpan != (int) span) {
+ majorAllocValid = false;
+ }
+ if (! majorAllocValid) {
+ // layout the major axis
+ majorSpan = (int) span;
+ checkRequests(majorAxis);
+ layoutMajorAxis(majorSpan, axis, majorOffsets, majorSpans);
+ majorAllocValid = true;
+
+ // flush changes to the children
+ updateChildSizes();
+ }
+ } else {
+ if (((int) span) != minorSpan) {
+ minorAllocValid = false;
+ }
+ if (! minorAllocValid) {
+ // layout the minor axis
+ minorSpan = (int) span;
+ checkRequests(axis);
+ layoutMinorAxis(minorSpan, axis, minorOffsets, minorSpans);
+ minorAllocValid = true;
+
+ // flush changes to the children
+ updateChildSizes();
+ }
+ }
+ }
+
+ /**
+ * Propagates the current allocations to the child views.
+ */
+ void updateChildSizes() {
+ int n = getViewCount();
+ if (majorAxis == X_AXIS) {
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ v.setSize((float) majorSpans[i], (float) minorSpans[i]);
+ }
+ } else {
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ v.setSize((float) minorSpans[i], (float) majorSpans[i]);
+ }
+ }
+ }
+
+ /**
+ * Returns the size of the view along an axis. This is implemented
+ * to return zero.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @return the current span of the view along the given axis, >= 0
+ */
+ float getSpanOnAxis(int axis) {
+ if (axis == majorAxis) {
+ return majorSpan;
+ } else {
+ return minorSpan;
+ }
+ }
+
+ /**
+ * Sets the size of the view. This should cause
+ * layout of the view if the view caches any layout
+ * information. This is implemented to call the
+ * layout method with the sizes inside of the insets.
+ *
+ * @param width the width >= 0
+ * @param height the height >= 0
+ */
+ public void setSize(float width, float height) {
+ layout(Math.max(0, (int)(width - getLeftInset() - getRightInset())),
+ Math.max(0, (int)(height - getTopInset() - getBottomInset())));
+ }
+
+ /**
+ * Renders the <code>BoxView</code> using the given
+ * rendering surface and area
+ * on that surface. Only the children that intersect
+ * the clip bounds of the given <code>Graphics</code>
+ * will be rendered.
+ *
+ * @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 alloc = (allocation instanceof Rectangle) ?
+ (Rectangle)allocation : allocation.getBounds();
+ int n = getViewCount();
+ int x = alloc.x + getLeftInset();
+ int y = alloc.y + getTopInset();
+ Rectangle clip = g.getClipBounds();
+ for (int i = 0; i < n; i++) {
+ tempRect.x = x + getOffset(X_AXIS, i);
+ tempRect.y = y + getOffset(Y_AXIS, i);
+ tempRect.width = getSpan(X_AXIS, i);
+ tempRect.height = getSpan(Y_AXIS, i);
+ int trx0 = tempRect.x, trx1 = trx0 + tempRect.width;
+ int try0 = tempRect.y, try1 = try0 + tempRect.height;
+ int crx0 = clip.x, crx1 = crx0 + clip.width;
+ int cry0 = clip.y, cry1 = cry0 + clip.height;
+ // We should paint views that intersect with clipping region
+ // even if the intersection has no inside points (is a line).
+ // This is needed for supporting views that have zero width, like
+ // views that contain only combining marks.
+ if ((trx1 >= crx0) && (try1 >= cry0) && (crx1 >= trx0) && (cry1 >= try0)) {
+ paintChild(g, tempRect, i);
+ }
+ }
+ }
+
+ /**
+ * Fetches the allocation for the given child view.
+ * This enables finding out where various views
+ * are located. This is implemented to return
+ * <code>null</code> if the layout is invalid,
+ * otherwise the superclass behavior is executed.
+ *
+ * @param index the index of the child, >= 0 && < getViewCount()
+ * @param a the allocation to this view
+ * @return the allocation to the child; or <code>null</code>
+ * if <code>a</code> is <code>null</code>;
+ * or <code>null</code> if the layout is invalid
+ */
+ public Shape getChildAllocation(int index, Shape a) {
+ if (a != null) {
+ Shape ca = super.getChildAllocation(index, a);
+ if ((ca != null) && (! isAllocationValid())) {
+ // The child allocation may not have been set yet.
+ Rectangle r = (ca instanceof Rectangle) ?
+ (Rectangle) ca : ca.getBounds();
+ if ((r.width == 0) && (r.height == 0)) {
+ return null;
+ }
+ }
+ return ca;
+ }
+ return null;
+ }
+
+ /**
+ * Provides a mapping from the document model coordinate space
+ * to the coordinate space of the view mapped to it. This makes
+ * sure the allocation is valid before calling the superclass.
+ *
+ * @param pos the position to convert >= 0
+ * @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 {
+ if (! isAllocationValid()) {
+ Rectangle alloc = a.getBounds();
+ setSize(alloc.width, alloc.height);
+ }
+ return super.modelToView(pos, a, b);
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param x x coordinate of the view location to convert >= 0
+ * @param y y coordinate of the view location to convert >= 0
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents the
+ * given point in the view >= 0
+ * @see View#viewToModel
+ */
+ public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
+ if (! isAllocationValid()) {
+ Rectangle alloc = a.getBounds();
+ setSize(alloc.width, alloc.height);
+ }
+ return super.viewToModel(x, y, a, bias);
+ }
+
+ /**
+ * Determines the desired alignment for this view along an
+ * axis. This is implemented to give the total alignment
+ * needed to position the children with the alignment points
+ * lined up along the axis orthoginal to the axis that is
+ * being tiled. The axis being tiled will request to be
+ * centered (i.e. 0.5f).
+ *
+ * @param axis may be either <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @return the desired alignment >= 0.0f && <= 1.0f; 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
+ * @exception IllegalArgumentException for an invalid axis
+ */
+ public float getAlignment(int axis) {
+ checkRequests(axis);
+ if (axis == majorAxis) {
+ return majorRequest.alignment;
+ } else {
+ return minorRequest.alignment;
+ }
+ }
+
+ /**
+ * Determines the preferred span for this view along an
+ * axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @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) {
+ checkRequests(axis);
+ float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset() :
+ getTopInset() + getBottomInset();
+ if (axis == majorAxis) {
+ return ((float)majorRequest.preferred) + marginSpan;
+ } else {
+ return ((float)minorRequest.preferred) + marginSpan;
+ }
+ }
+
+ /**
+ * Determines the minimum span for this view along an
+ * axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @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) {
+ checkRequests(axis);
+ float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset() :
+ getTopInset() + getBottomInset();
+ if (axis == majorAxis) {
+ return ((float)majorRequest.minimum) + marginSpan;
+ } else {
+ return ((float)minorRequest.minimum) + marginSpan;
+ }
+ }
+
+ /**
+ * Determines the maximum span for this view along an
+ * axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @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) {
+ checkRequests(axis);
+ float marginSpan = (axis == X_AXIS) ? getLeftInset() + getRightInset() :
+ getTopInset() + getBottomInset();
+ if (axis == majorAxis) {
+ return ((float)majorRequest.maximum) + marginSpan;
+ } else {
+ return ((float)minorRequest.maximum) + marginSpan;
+ }
+ }
+
+ // --- local methods ----------------------------------------------------
+
+ /**
+ * Are the allocations for the children still
+ * valid?
+ *
+ * @return true if allocations still valid
+ */
+ protected boolean isAllocationValid() {
+ return (majorAllocValid && minorAllocValid);
+ }
+
+ /**
+ * Determines if a point falls before an allocated region.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param innerAlloc the allocated region; this is the area
+ * inside of the insets
+ * @return true if the point lies before the region else false
+ */
+ protected boolean isBefore(int x, int y, Rectangle innerAlloc) {
+ if (majorAxis == View.X_AXIS) {
+ return (x < innerAlloc.x);
+ } else {
+ return (y < innerAlloc.y);
+ }
+ }
+
+ /**
+ * Determines if a point falls after an allocated region.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param innerAlloc the allocated region; this is the area
+ * inside of the insets
+ * @return true if the point lies after the region else false
+ */
+ protected boolean isAfter(int x, int y, Rectangle innerAlloc) {
+ if (majorAxis == View.X_AXIS) {
+ return (x > (innerAlloc.width + innerAlloc.x));
+ } else {
+ return (y > (innerAlloc.height + innerAlloc.y));
+ }
+ }
+
+ /**
+ * Fetches the child view at the given coordinates.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param alloc the parents inner allocation on entry, which should
+ * be changed to the childs allocation on exit
+ * @return the view
+ */
+ protected View getViewAtPoint(int x, int y, Rectangle alloc) {
+ int n = getViewCount();
+ if (majorAxis == View.X_AXIS) {
+ if (x < (alloc.x + majorOffsets[0])) {
+ childAllocation(0, alloc);
+ return getView(0);
+ }
+ for (int i = 0; i < n; i++) {
+ if (x < (alloc.x + majorOffsets[i])) {
+ childAllocation(i - 1, alloc);
+ return getView(i - 1);
+ }
+ }
+ childAllocation(n - 1, alloc);
+ return getView(n - 1);
+ } else {
+ if (y < (alloc.y + majorOffsets[0])) {
+ childAllocation(0, alloc);
+ return getView(0);
+ }
+ for (int i = 0; i < n; i++) {
+ if (y < (alloc.y + majorOffsets[i])) {
+ childAllocation(i - 1, alloc);
+ return getView(i - 1);
+ }
+ }
+ childAllocation(n - 1, alloc);
+ return getView(n - 1);
+ }
+ }
+
+ /**
+ * Allocates a region for a child view.
+ *
+ * @param index the index of the child view to
+ * allocate, >= 0 && < getViewCount()
+ * @param alloc the allocated region
+ */
+ protected void childAllocation(int index, Rectangle alloc) {
+ alloc.x += getOffset(X_AXIS, index);
+ alloc.y += getOffset(Y_AXIS, index);
+ alloc.width = getSpan(X_AXIS, index);
+ alloc.height = getSpan(Y_AXIS, index);
+ }
+
+ /**
+ * Perform layout on the box
+ *
+ * @param width the width (inside of the insets) >= 0
+ * @param height the height (inside of the insets) >= 0
+ */
+ protected void layout(int width, int height) {
+ setSpanOnAxis(X_AXIS, width);
+ setSpanOnAxis(Y_AXIS, height);
+ }
+
+ /**
+ * Returns the current width of the box. This is the width that
+ * it was last allocated.
+ * @return the current width of the box
+ */
+ public int getWidth() {
+ int span;
+ if (majorAxis == X_AXIS) {
+ span = majorSpan;
+ } else {
+ span = minorSpan;
+ }
+ span += getLeftInset() - getRightInset();
+ return span;
+ }
+
+ /**
+ * Returns the current height of the box. This is the height that
+ * it was last allocated.
+ * @return the current height of the box
+ */
+ public int getHeight() {
+ int span;
+ if (majorAxis == Y_AXIS) {
+ span = majorSpan;
+ } else {
+ span = minorSpan;
+ }
+ span += getTopInset() - getBottomInset();
+ return span;
+ }
+
+ /**
+ * Performs layout for the major axis of the box (i.e. 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 major axis.
+ *
+ * @param targetSpan the total span given to the view, which
+ * would 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
+ */
+ protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
+ /*
+ * first pass, calculate the preferred sizes
+ * and the flexibility to adjust the sizes.
+ */
+ long preferred = 0;
+ int n = getViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ spans[i] = (int) v.getPreferredSpan(axis);
+ preferred += spans[i];
+ }
+
+ /*
+ * Second pass, expand or contract by as much as possible to reach
+ * the target span.
+ */
+
+ // determine the adjustment to be made
+ long desiredAdjustment = targetSpan - preferred;
+ float adjustmentFactor = 0.0f;
+ int[] diffs = null;
+
+ if (desiredAdjustment != 0) {
+ long totalSpan = 0;
+ diffs = new int[n];
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ int tmp;
+ if (desiredAdjustment < 0) {
+ tmp = (int)v.getMinimumSpan(axis);
+ diffs[i] = spans[i] - tmp;
+ } else {
+ tmp = (int)v.getMaximumSpan(axis);
+ diffs[i] = tmp - spans[i];
+ }
+ totalSpan += tmp;
+ }
+
+ float maximumAdjustment = Math.abs(totalSpan - preferred);
+ adjustmentFactor = desiredAdjustment / maximumAdjustment;
+ adjustmentFactor = Math.min(adjustmentFactor, 1.0f);
+ adjustmentFactor = Math.max(adjustmentFactor, -1.0f);
+ }
+
+ // make the adjustments
+ int totalOffset = 0;
+ for (int i = 0; i < n; i++) {
+ offsets[i] = totalOffset;
+ if (desiredAdjustment != 0) {
+ float adjF = adjustmentFactor * diffs[i];
+ spans[i] += Math.round(adjF);
+ }
+ totalOffset = (int) Math.min((long) totalOffset + (long) spans[i], Integer.MAX_VALUE);
+ }
+ }
+
+ /**
+ * 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
+ * would 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
+ */
+ protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
+ int n = getViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ int max = (int) v.getMaximumSpan(axis);
+ 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.
+ int min = (int)v.getMinimumSpan(axis);
+ offsets[i] = 0;
+ spans[i] = Math.max(min, targetSpan);
+ }
+ }
+ }
+
+ /**
+ * Calculates the size requirements for the major axis
+ * <code>axis</code>.
+ *
+ * @param axis the axis being studied
+ * @param r the <code>SizeRequirements</code> object;
+ * if <code>null</code> one will be created
+ * @return the newly initialized <code>SizeRequirements</code> object
+ * @see javax.swing.SizeRequirements
+ */
+ protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) {
+ // calculate tiled request
+ float min = 0;
+ float pref = 0;
+ float max = 0;
+
+ int n = getViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ min += v.getMinimumSpan(axis);
+ pref += v.getPreferredSpan(axis);
+ max += v.getMaximumSpan(axis);
+ }
+
+ if (r == null) {
+ r = new SizeRequirements();
+ }
+ r.alignment = 0.5f;
+ r.minimum = (int) min;
+ r.preferred = (int) pref;
+ r.maximum = (int) max;
+ return r;
+ }
+
+ /**
+ * Calculates the size requirements for the minor axis
+ * <code>axis</code>.
+ *
+ * @param axis the axis being studied
+ * @param r the <code>SizeRequirements</code> object;
+ * if <code>null</code> one will be created
+ * @return the newly initialized <code>SizeRequirements</code> object
+ * @see javax.swing.SizeRequirements
+ */
+ protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) {
+ int min = 0;
+ long pref = 0;
+ int max = Integer.MAX_VALUE;
+ 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);
+ max = Math.max((int) v.getMaximumSpan(axis), max);
+ }
+
+ if (r == null) {
+ r = new SizeRequirements();
+ r.alignment = 0.5f;
+ }
+ r.preferred = (int) pref;
+ r.minimum = min;
+ r.maximum = max;
+ return r;
+ }
+
+ /**
+ * Checks the request cache and update if needed.
+ * @param axis the axis being studied
+ * @exception IllegalArgumentException if <code>axis</code> is
+ * neither <code>View.X_AXIS</code> nor <code>View.Y_AXIS</code>
+ */
+ void checkRequests(int axis) {
+ if ((axis != X_AXIS) && (axis != Y_AXIS)) {
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ if (axis == majorAxis) {
+ if (!majorReqValid) {
+ majorRequest = calculateMajorAxisRequirements(axis,
+ majorRequest);
+ majorReqValid = true;
+ }
+ } else if (! minorReqValid) {
+ minorRequest = calculateMinorAxisRequirements(axis, minorRequest);
+ minorReqValid = true;
+ }
+ }
+
+ /**
+ * Computes the location and extent of each child view
+ * in this <code>BoxView</code> given the <code>targetSpan</code>,
+ * which is the width (or height) of the region we have to
+ * work with.
+ *
+ * @param targetSpan the total span given to the view, which
+ * would be used to layout the children
+ * @param axis the axis being studied, either
+ * <code>View.X_AXIS</code> or <code>View.Y_AXIS</code>
+ * @param offsets an empty array filled by this method with
+ * values specifying the location of each child view
+ * @param spans an empty array filled by this method with
+ * values specifying the extent of each child view
+ */
+ protected void baselineLayout(int targetSpan, int axis, int[] offsets, int[] spans) {
+ int totalAscent = (int)(targetSpan * getAlignment(axis));
+ int totalDescent = targetSpan - totalAscent;
+
+ int n = getViewCount();
+
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ float align = v.getAlignment(axis);
+ float viewSpan;
+
+ if (v.getResizeWeight(axis) > 0) {
+ // if resizable then resize to the best fit
+
+ // the smallest span possible
+ float minSpan = v.getMinimumSpan(axis);
+ // the largest span possible
+ float maxSpan = v.getMaximumSpan(axis);
+
+ if (align == 0.0f) {
+ // if the alignment is 0 then we need to fit into the descent
+ viewSpan = Math.max(Math.min(maxSpan, totalDescent), minSpan);
+ } else if (align == 1.0f) {
+ // if the alignment is 1 then we need to fit into the ascent
+ viewSpan = Math.max(Math.min(maxSpan, totalAscent), minSpan);
+ } else {
+ // figure out the span that we must fit into
+ float fitSpan = Math.min(totalAscent / align,
+ totalDescent / (1.0f - align));
+ // fit into the calculated span
+ viewSpan = Math.max(Math.min(maxSpan, fitSpan), minSpan);
+ }
+ } else {
+ // otherwise use the preferred spans
+ viewSpan = v.getPreferredSpan(axis);
+ }
+
+ offsets[i] = totalAscent - (int)(viewSpan * align);
+ spans[i] = (int)viewSpan;
+ }
+ }
+
+ /**
+ * Calculates the size requirements for this <code>BoxView</code>
+ * by examining the size of each child view.
+ *
+ * @param axis the axis being studied
+ * @param r the <code>SizeRequirements</code> object;
+ * if <code>null</code> one will be created
+ * @return the newly initialized <code>SizeRequirements</code> object
+ */
+ protected SizeRequirements baselineRequirements(int axis, SizeRequirements r) {
+ SizeRequirements totalAscent = new SizeRequirements();
+ SizeRequirements totalDescent = new SizeRequirements();
+
+ if (r == null) {
+ r = new SizeRequirements();
+ }
+
+ r.alignment = 0.5f;
+
+ int n = getViewCount();
+
+ // loop through all children calculating the max of all their ascents and
+ // descents at minimum, preferred, and maximum sizes
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ float align = v.getAlignment(axis);
+ float span;
+ int ascent;
+ int descent;
+
+ // find the maximum of the preferred ascents and descents
+ span = v.getPreferredSpan(axis);
+ ascent = (int)(align * span);
+ descent = (int)(span - ascent);
+ totalAscent.preferred = Math.max(ascent, totalAscent.preferred);
+ totalDescent.preferred = Math.max(descent, totalDescent.preferred);
+
+ if (v.getResizeWeight(axis) > 0) {
+ // if the view is resizable then do the same for the minimum and
+ // maximum ascents and descents
+ span = v.getMinimumSpan(axis);
+ ascent = (int)(align * span);
+ descent = (int)(span - ascent);
+ totalAscent.minimum = Math.max(ascent, totalAscent.minimum);
+ totalDescent.minimum = Math.max(descent, totalDescent.minimum);
+
+ span = v.getMaximumSpan(axis);
+ ascent = (int)(align * span);
+ descent = (int)(span - ascent);
+ totalAscent.maximum = Math.max(ascent, totalAscent.maximum);
+ totalDescent.maximum = Math.max(descent, totalDescent.maximum);
+ } else {
+ // otherwise use the preferred
+ totalAscent.minimum = Math.max(ascent, totalAscent.minimum);
+ totalDescent.minimum = Math.max(descent, totalDescent.minimum);
+ totalAscent.maximum = Math.max(ascent, totalAscent.maximum);
+ totalDescent.maximum = Math.max(descent, totalDescent.maximum);
+ }
+ }
+
+ // we now have an overall preferred, minimum, and maximum ascent and descent
+
+ // calculate the preferred span as the sum of the preferred ascent and preferred descent
+ r.preferred = (int)Math.min((long)totalAscent.preferred + (long)totalDescent.preferred,
+ Integer.MAX_VALUE);
+
+ // calculate the preferred alignment as the preferred ascent divided by the preferred span
+ if (r.preferred > 0) {
+ r.alignment = (float)totalAscent.preferred / r.preferred;
+ }
+
+
+ if (r.alignment == 0.0f) {
+ // if the preferred alignment is 0 then the minimum and maximum spans are simply
+ // the minimum and maximum descents since there's nothing above the baseline
+ r.minimum = totalDescent.minimum;
+ r.maximum = totalDescent.maximum;
+ } else if (r.alignment == 1.0f) {
+ // if the preferred alignment is 1 then the minimum and maximum spans are simply
+ // the minimum and maximum ascents since there's nothing below the baseline
+ r.minimum = totalAscent.minimum;
+ r.maximum = totalAscent.maximum;
+ } else {
+ // we want to honor the preferred alignment so we calculate two possible minimum
+ // span values using 1) the minimum ascent and the alignment, and 2) the minimum
+ // descent and the alignment. We'll choose the larger of these two numbers.
+ r.minimum = Math.round(Math.max(totalAscent.minimum / r.alignment,
+ totalDescent.minimum / (1.0f - r.alignment)));
+ // a similar calculation is made for the maximum but we choose the smaller number.
+ r.maximum = Math.round(Math.min(totalAscent.maximum / r.alignment,
+ totalDescent.maximum / (1.0f - r.alignment)));
+ }
+
+ return r;
+ }
+
+ /**
+ * Fetches the offset of a particular child's current layout.
+ * @param axis the axis being studied
+ * @param childIndex the index of the requested child
+ * @return the offset (location) for the specified child
+ */
+ protected int getOffset(int axis, int childIndex) {
+ int[] offsets = (axis == majorAxis) ? majorOffsets : minorOffsets;
+ return offsets[childIndex];
+ }
+
+ /**
+ * Fetches the span of a particular childs current layout.
+ * @param axis the axis being studied
+ * @param childIndex the index of the requested child
+ * @return the span (width or height) of the specified child
+ */
+ protected int getSpan(int axis, int childIndex) {
+ int[] spans = (axis == majorAxis) ? majorSpans : minorSpans;
+ return spans[childIndex];
+ }
+
+ /**
+ * Determines in which direction the next view lays.
+ * Consider the View at index n. Typically the <code>View</code>s
+ * are layed out from left to right, so that the <code>View</code>
+ * to the EAST will be at index n + 1, and the <code>View</code>
+ * to the WEST will be at index n - 1. In certain situations,
+ * such as with bidirectional text, it is possible
+ * that the <code>View</code> to EAST is not at index n + 1,
+ * but rather at index n - 1, or that the <code>View</code>
+ * to the WEST is not at index n - 1, but index n + 1.
+ * In this case this method would return true,
+ * indicating the <code>View</code>s are layed out in
+ * descending order. Otherwise the method would return false
+ * indicating the <code>View</code>s are layed out in ascending order.
+ * <p>
+ * If the receiver is laying its <code>View</code>s along the
+ * <code>Y_AXIS</code>, this will will return the value from
+ * invoking the same method on the <code>View</code>
+ * responsible for rendering <code>position</code> and
+ * <code>bias</code>. Otherwise this will return false.
+ *
+ * @param position position into the model
+ * @param bias either <code>Position.Bias.Forward</code> or
+ * <code>Position.Bias.Backward</code>
+ * @return true if the <code>View</code>s surrounding the
+ * <code>View</code> responding for rendering
+ * <code>position</code> and <code>bias</code>
+ * are layed out in descending order; otherwise false
+ */
+ protected boolean flipEastAndWestAtEnds(int position,
+ Position.Bias bias) {
+ if(majorAxis == Y_AXIS) {
+ int testPos = (bias == Position.Bias.Backward) ?
+ Math.max(0, position - 1) : position;
+ int index = getViewIndexAtPosition(testPos);
+ if(index != -1) {
+ View v = getView(index);
+ if(v != null && v instanceof CompositeView) {
+ return ((CompositeView)v).flipEastAndWestAtEnds(position,
+ bias);
+ }
+ }
+ }
+ return false;
+ }
+
+ // --- variables ------------------------------------------------
+
+ int majorAxis;
+
+ int majorSpan;
+ int minorSpan;
+
+ /*
+ * Request cache
+ */
+ boolean majorReqValid;
+ boolean minorReqValid;
+ SizeRequirements majorRequest;
+ SizeRequirements minorRequest;
+
+ /*
+ * Allocation cache
+ */
+ boolean majorAllocValid;
+ int[] majorOffsets;
+ int[] majorSpans;
+ boolean minorAllocValid;
+ int[] minorOffsets;
+ int[] minorSpans;
+
+ /** used in paint. */
+ Rectangle tempRect;
+}
diff --git a/src/share/classes/javax/swing/text/Caret.java b/src/share/classes/javax/swing/text/Caret.java
new file mode 100644
index 000000000..f3290deb9
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Caret.java
@@ -0,0 +1,204 @@
+/*
+ * 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;
+
+import java.awt.Graphics;
+import java.awt.Point;
+import javax.swing.Action;
+import javax.swing.event.ChangeListener;
+
+/**
+ * A place within a document view that represents where
+ * things can be inserted into the document model. A caret
+ * has a position in the document referred to as a dot.
+ * The dot is where the caret is currently located in the
+ * model. There is
+ * a second position maintained by the caret that represents
+ * the other end of a selection called mark. If there is
+ * no selection the dot and mark will be equal. If a selection
+ * exists, the two values will be different.
+ * <p>
+ * The dot can be placed by either calling
+ * <code>setDot</code> or <code>moveDot</code>. Setting
+ * the dot has the effect of removing any selection that may
+ * have previously existed. The dot and mark will be equal.
+ * Moving the dot has the effect of creating a selection as
+ * the mark is left at whatever position it previously had.
+ *
+ * @author Timothy Prinzing
+ */
+public interface Caret {
+
+ /**
+ * Called when the UI is being installed into the
+ * interface of a JTextComponent. This can be used
+ * to gain access to the model that is being navigated
+ * by the implementation of this interface.
+ *
+ * @param c the JTextComponent
+ */
+ public void install(JTextComponent c);
+
+ /**
+ * Called when the UI is being removed from the
+ * interface of a JTextComponent. This is used to
+ * unregister any listeners that were attached.
+ *
+ * @param c the JTextComponent
+ */
+ public void deinstall(JTextComponent c);
+
+ /**
+ * Renders the caret. This method is called by UI classes.
+ *
+ * @param g the graphics context
+ */
+ public void paint(Graphics g);
+
+ /**
+ * Adds a listener to track whenever the caret position
+ * has been changed.
+ *
+ * @param l the change listener
+ */
+ public void addChangeListener(ChangeListener l);
+
+ /**
+ * Removes a listener that was tracking caret position changes.
+ *
+ * @param l the change listener
+ */
+ public void removeChangeListener(ChangeListener l);
+
+ /**
+ * Determines if the caret is currently visible.
+ *
+ * @return true if the caret is visible else false
+ */
+ public boolean isVisible();
+
+ /**
+ * Sets the visibility of the caret.
+ *
+ * @param v true if the caret should be shown,
+ * and false if the caret should be hidden
+ */
+ public void setVisible(boolean v);
+
+ /**
+ * Determines if the selection is currently visible.
+ *
+ * @return true if the caret is visible else false
+ */
+ public boolean isSelectionVisible();
+
+ /**
+ * Sets the visibility of the selection
+ *
+ * @param v true if the caret should be shown,
+ * and false if the caret should be hidden
+ */
+ public void setSelectionVisible(boolean v);
+
+ /**
+ * Set the current caret visual location. This can be used when
+ * moving between lines that have uneven end positions (such as
+ * when caret up or down actions occur). If text flows
+ * left-to-right or right-to-left the x-coordinate will indicate
+ * the desired navigation location for vertical movement. If
+ * the text flow is top-to-bottom, the y-coordinate will indicate
+ * the desired navigation location for horizontal movement.
+ *
+ * @param p the Point to use for the saved position. This
+ * can be null to indicate there is no visual location.
+ */
+ public void setMagicCaretPosition(Point p);
+
+ /**
+ * Gets the current caret visual location.
+ *
+ * @return the visual position.
+ * @see #setMagicCaretPosition
+ */
+ public Point getMagicCaretPosition();
+
+ /**
+ * Sets the blink rate of the caret. This determines if
+ * and how fast the caret blinks, commonly used as one
+ * way to attract attention to the caret.
+ *
+ * @param rate the delay in milliseconds >= 0. If this is
+ * zero the caret will not blink.
+ */
+ public void setBlinkRate(int rate);
+
+ /**
+ * Gets the blink rate of the caret. This determines if
+ * and how fast the caret blinks, commonly used as one
+ * way to attract attention to the caret.
+ *
+ * @return the delay in milliseconds >= 0. If this is
+ * zero the caret will not blink.
+ */
+ public int getBlinkRate();
+
+ /**
+ * Fetches the current position of the caret.
+ *
+ * @return the position >= 0
+ */
+ public int getDot();
+
+ /**
+ * Fetches the current position of the mark. If there
+ * is a selection, the mark will not be the same as
+ * the dot.
+ *
+ * @return the position >= 0
+ */
+ public int getMark();
+
+ /**
+ * Sets the caret position to some position. This
+ * causes the mark to become the same as the dot,
+ * effectively setting the selection range to zero.
+ * <p>
+ * If the parameter is negative or beyond the length of the document,
+ * the caret is placed at the beginning or at the end, respectively.
+ *
+ * @param dot the new position to set the caret to
+ */
+ public void setDot(int dot);
+
+ /**
+ * Moves the caret position (dot) to some other position,
+ * leaving behind the mark. This is useful for
+ * making selections.
+ *
+ * @param dot the new position to move the caret to >= 0
+ */
+ public void moveDot(int dot);
+
+};
diff --git a/src/share/classes/javax/swing/text/ChangedCharSetException.java b/src/share/classes/javax/swing/text/ChangedCharSetException.java
new file mode 100644
index 000000000..7336a5184
--- /dev/null
+++ b/src/share/classes/javax/swing/text/ChangedCharSetException.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+/**
+ * ChangedCharSetException as the name indicates is an exception
+ * thrown when the charset is changed.
+ *
+ * @author Sunita Mani
+ */
+public class ChangedCharSetException extends IOException {
+
+ String charSetSpec;
+ boolean charSetKey;
+
+ public ChangedCharSetException(String charSetSpec, boolean charSetKey) {
+ this.charSetSpec = charSetSpec;
+ this.charSetKey = charSetKey;
+ }
+
+ public String getCharSetSpec() {
+ return charSetSpec;
+ }
+
+ public boolean keyEqualsCharSet() {
+ return charSetKey;
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/ComponentView.java b/src/share/classes/javax/swing/text/ComponentView.java
new file mode 100644
index 000000000..62e4bd02a
--- /dev/null
+++ b/src/share/classes/javax/swing/text/ComponentView.java
@@ -0,0 +1,503 @@
+/*
+ * 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;
+
+import java.awt.*;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import javax.swing.SwingUtilities;
+import javax.swing.event.*;
+
+/**
+ * Component decorator that implements the view interface. The
+ * entire element is used to represent the component. This acts
+ * as a gateway from the display-only View implementations to
+ * interactive lightweight components (ie it allows components
+ * to be embedded into the View hierarchy).
+ * <p>
+ * The component is placed relative to the text baseline
+ * according to the value returned by
+ * <code>Component.getAlignmentY</code>. For Swing components
+ * this value can be conveniently set using the method
+ * <code>JComponent.setAlignmentY</code>. For example, setting
+ * a value of <code>0.75</code> will cause 75 percent of the
+ * component to be above the baseline, and 25 percent of the
+ * component to be below the baseline.
+ * <p>
+ * This class is implemented to do the extra work necessary to
+ * work properly in the presence of multiple threads (i.e. from
+ * asynchronous notification of model changes for example) by
+ * ensuring that all component access is done on the event thread.
+ * <p>
+ * The component used is determined by the return value of the
+ * createComponent method. The default implementation of this
+ * method is to return the component held as an attribute of
+ * the element (by calling StyleConstants.getComponent). A
+ * limitation of this behavior is that the component cannot
+ * be used by more than one text component (i.e. with a shared
+ * model). Subclasses can remove this constraint by implementing
+ * the createComponent to actually create a component based upon
+ * some kind of specification contained in the attributes. The
+ * ObjectView class in the html package is an example of a
+ * ComponentView implementation that supports multiple component
+ * views of a shared model.
+ *
+ * @author Timothy Prinzing
+ */
+public class ComponentView extends View {
+
+ /**
+ * Creates a new ComponentView object.
+ *
+ * @param elem the element to decorate
+ */
+ public ComponentView(Element elem) {
+ super(elem);
+ }
+
+ /**
+ * Create the component that is associated with
+ * this view. This will be called when it has
+ * been determined that a new component is needed.
+ * This would result from a call to setParent or
+ * as a result of being notified that attributes
+ * have changed.
+ */
+ protected Component createComponent() {
+ AttributeSet attr = getElement().getAttributes();
+ Component comp = StyleConstants.getComponent(attr);
+ return comp;
+ }
+
+ /**
+ * Fetch the component associated with the view.
+ */
+ public final Component getComponent() {
+ return createdC;
+ }
+
+ // --- View methods ---------------------------------------------
+
+ /**
+ * The real paint behavior occurs naturally from the association
+ * that the component has with its parent container (the same
+ * container hosting this view). This is implemented to do nothing.
+ *
+ * @param g the graphics context
+ * @param a the shape
+ * @see View#paint
+ */
+ public void paint(Graphics g, Shape a) {
+ if (c != null) {
+ Rectangle alloc = (a instanceof Rectangle) ?
+ (Rectangle) a : a.getBounds();
+ c.setBounds(alloc.x, alloc.y, alloc.width, alloc.height);
+ }
+ }
+
+ /**
+ * Determines the preferred span for this view along an
+ * axis. This is implemented to return the value
+ * returned by Component.getPreferredSize 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 getPreferredSpan(int axis) {
+ if ((axis != X_AXIS) && (axis != Y_AXIS)) {
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ if (c != null) {
+ Dimension size = c.getPreferredSize();
+ if (axis == View.X_AXIS) {
+ return size.width;
+ } else {
+ return size.height;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Determines the minimum span for this view along an
+ * axis. This is implemented to return the value
+ * returned by Component.getMinimumSize 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 getMinimumSpan(int axis) {
+ if ((axis != X_AXIS) && (axis != Y_AXIS)) {
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ if (c != null) {
+ Dimension size = c.getMinimumSize();
+ if (axis == View.X_AXIS) {
+ return size.width;
+ } else {
+ return size.height;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Determines the maximum span for this view along an
+ * axis. This is implemented to 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) {
+ if ((axis != X_AXIS) && (axis != Y_AXIS)) {
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ if (c != null) {
+ Dimension size = c.getMaximumSize();
+ if (axis == View.X_AXIS) {
+ return size.width;
+ } else {
+ return size.height;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Determines the desired alignment for this view along an
+ * axis. This is implemented to give the alignment of the
+ * embedded component.
+ *
+ * @param axis may be either View.X_AXIS or View.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) {
+ if (c != null) {
+ switch (axis) {
+ case View.X_AXIS:
+ return c.getAlignmentX();
+ case View.Y_AXIS:
+ return c.getAlignmentY();
+ }
+ }
+ return super.getAlignment(axis);
+ }
+
+ /**
+ * Sets the parent for a child view.
+ * The parent calls this on the child to tell it who its
+ * parent is, giving the view access to things like
+ * the hosting Container. The superclass behavior is
+ * executed, followed by a call to createComponent if
+ * the parent view parameter is non-null and a component
+ * has not yet been created. The embedded components parent
+ * is then set to the value returned by <code>getContainer</code>.
+ * If the parent view parameter is null, this view is being
+ * cleaned up, thus the component is removed from its parent.
+ * <p>
+ * The changing of the component hierarchy will
+ * touch the component lock, which is the one thing
+ * that is not safe from the View hierarchy. Therefore,
+ * this functionality is executed immediately if on the
+ * event thread, or is queued on the event queue if
+ * called from another thread (notification of change
+ * from an asynchronous update).
+ *
+ * @param p the parent
+ */
+ public void setParent(View p) {
+ super.setParent(p);
+ if (SwingUtilities.isEventDispatchThread()) {
+ setComponentParent();
+ } else {
+ Runnable callSetComponentParent = new Runnable() {
+ public void run() {
+ Document doc = getDocument();
+ try {
+ if (doc instanceof AbstractDocument) {
+ ((AbstractDocument)doc).readLock();
+ }
+ setComponentParent();
+ Container host = getContainer();
+ if (host != null) {
+ preferenceChanged(null, true, true);
+ host.repaint();
+ }
+ } finally {
+ if (doc instanceof AbstractDocument) {
+ ((AbstractDocument)doc).readUnlock();
+ }
+ }
+ }
+ };
+ SwingUtilities.invokeLater(callSetComponentParent);
+ }
+ }
+
+ /**
+ * Set the parent of the embedded component
+ * with assurance that it is thread-safe.
+ */
+ void setComponentParent() {
+ View p = getParent();
+ if (p != null) {
+ Container parent = getContainer();
+ if (parent != null) {
+ if (c == null) {
+ // try to build a component
+ Component comp = createComponent();
+ if (comp != null) {
+ createdC = comp;
+ c = new Invalidator(comp);
+ }
+ }
+ if (c != null) {
+ if (c.getParent() == null) {
+ // components associated with the View tree are added
+ // to the hosting container with the View as a constraint.
+ parent.add(c, this);
+ parent.addPropertyChangeListener("enabled", c);
+ }
+ }
+ }
+ } else {
+ if (c != null) {
+ Container parent = c.getParent();
+ if (parent != null) {
+ // remove the component from its hosting container
+ parent.remove(c);
+ parent.removePropertyChangeListener("enabled", c);
+ }
+ }
+ }
+ }
+
+ /**
+ * Provides a mapping from the coordinate space of the model to
+ * that of the view.
+ *
+ * @param pos the position to convert >= 0
+ * @param a the allocated region to render into
+ * @return the bounding box of the given position is returned
+ * @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;
+ }
+ throw new BadLocationException(pos + " not in range " + p0 + "," + p1, pos);
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents
+ * the given point in the 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();
+ }
+
+ // --- member variables ------------------------------------------------
+
+ private Component createdC;
+ private Invalidator c;
+
+ /**
+ * This class feeds the invalidate back to the
+ * hosting View. This is needed to get the View
+ * hierarchy to consider giving the component
+ * a different size (i.e. layout may have been
+ * cached between the associated view and the
+ * container hosting this component).
+ */
+ class Invalidator extends Container implements PropertyChangeListener {
+
+ // NOTE: When we remove this class we are going to have to some
+ // how enforce setting of the focus traversal keys on the children
+ // so that they don't inherit them from the JEditorPane. We need
+ // to do this as JEditorPane has abnormal bindings (it is a focus cycle
+ // root) and the children typically don't want these bindings as well.
+
+ Invalidator(Component child) {
+ setLayout(null);
+ add(child);
+ cacheChildSizes();
+ }
+
+ /**
+ * The components invalid layout needs
+ * to be propagated through the view hierarchy
+ * so the views (which position the component)
+ * can have their layout recomputed.
+ */
+ public void invalidate() {
+ super.invalidate();
+ if (getParent() != null) {
+ preferenceChanged(null, true, true);
+ }
+ }
+
+ public void doLayout() {
+ cacheChildSizes();
+ }
+
+ public void setBounds(int x, int y, int w, int h) {
+ super.setBounds(x, y, w, h);
+ if (getComponentCount() > 0) {
+ getComponent(0).setSize(w, h);
+ }
+ cacheChildSizes();
+ }
+
+ public void validateIfNecessary() {
+ if (!isValid()) {
+ validate();
+ }
+ }
+
+ private void cacheChildSizes() {
+ if (getComponentCount() > 0) {
+ Component child = getComponent(0);
+ min = child.getMinimumSize();
+ pref = child.getPreferredSize();
+ max = child.getMaximumSize();
+ yalign = child.getAlignmentY();
+ xalign = child.getAlignmentX();
+ } else {
+ min = pref = max = new Dimension(0, 0);
+ }
+ }
+
+ /**
+ * Shows or hides this component depending on the value of parameter
+ * <code>b</code>.
+ * @param <code>b</code> If <code>true</code>, shows this component;
+ * otherwise, hides this component.
+ * @see #isVisible
+ * @since JDK1.1
+ */
+ public void setVisible(boolean b) {
+ super.setVisible(b);
+ if (getComponentCount() > 0) {
+ getComponent(0).setVisible(b);
+ }
+ }
+
+ /**
+ * Overridden to fix 4759054. Must return true so that content
+ * is painted when inside a CellRendererPane which is normally
+ * invisible.
+ */
+ public boolean isShowing() {
+ return true;
+ }
+
+ public Dimension getMinimumSize() {
+ validateIfNecessary();
+ return min;
+ }
+
+ public Dimension getPreferredSize() {
+ validateIfNecessary();
+ return pref;
+ }
+
+ public Dimension getMaximumSize() {
+ validateIfNecessary();
+ return max;
+ }
+
+ public float getAlignmentX() {
+ validateIfNecessary();
+ return xalign;
+ }
+
+ public float getAlignmentY() {
+ validateIfNecessary();
+ return yalign;
+ }
+
+ public java.util.Set getFocusTraversalKeys(int id) {
+ return KeyboardFocusManager.getCurrentKeyboardFocusManager().
+ getDefaultFocusTraversalKeys(id);
+ }
+
+ public void propertyChange(PropertyChangeEvent ev) {
+ Boolean enable = (Boolean) ev.getNewValue();
+ if (getComponentCount() > 0) {
+ getComponent(0).setEnabled(enable);
+ }
+ }
+
+ Dimension min;
+ Dimension pref;
+ Dimension max;
+ float yalign;
+ float xalign;
+
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/CompositeView.java b/src/share/classes/javax/swing/text/CompositeView.java
new file mode 100644
index 000000000..099fc8358
--- /dev/null
+++ b/src/share/classes/javax/swing/text/CompositeView.java
@@ -0,0 +1,794 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import java.awt.*;
+import javax.swing.event.*;
+import javax.swing.SwingConstants;
+
+/**
+ * <code>CompositeView</code> is an abstract <code>View</code>
+ * implementation which manages one or more child views.
+ * (Note that <code>CompositeView</code> is intended
+ * for managing relatively small numbers of child views.)
+ * <code>CompositeView</code> is intended to be used as
+ * a starting point for <code>View</code> implementations,
+ * such as <code>BoxView</code>, that will contain child
+ * <code>View</code>s. Subclasses that wish to manage the
+ * collection of child <code>View</code>s should use the
+ * {@link #replace} method. As <code>View</code> invokes
+ * <code>replace</code> during <code>DocumentListener</code>
+ * notification, you normally won't need to directly
+ * invoke <code>replace</code>.
+ *
+ * <p>While <code>CompositeView</code>
+ * does not impose a layout policy on its child <code>View</code>s,
+ * it does allow for inseting the child <code>View</code>s
+ * it will contain. The insets can be set by either
+ * {@link #setInsets} or {@link #setParagraphInsets}.
+ *
+ * <p>In addition to the abstract methods of
+ * {@link javax.swing.text.View},
+ * subclasses of <code>CompositeView</code> will need to
+ * override:
+ * <ul>
+ * <li>{@link #isBefore} - Used to test if a given
+ * <code>View</code> location is before the visual space
+ * of the <code>CompositeView</code>.
+ * <li>{@link #isAfter} - Used to test if a given
+ * <code>View</code> location is after the visual space
+ * of the <code>CompositeView</code>.
+ * <li>{@link #getViewAtPoint} - Returns the view at
+ * a given visual location.
+ * <li>{@link #childAllocation} - Returns the bounds of
+ * a particular child <code>View</code>.
+ * <code>getChildAllocation</code> will invoke
+ * <code>childAllocation</code> after offseting
+ * the bounds by the <code>Inset</code>s of the
+ * <code>CompositeView</code>.
+ * </ul>
+ *
+ * @author Timothy Prinzing
+ */
+public abstract class CompositeView extends View {
+
+ /**
+ * Constructs a <code>CompositeView</code> for the given element.
+ *
+ * @param elem the element this view is responsible for
+ */
+ public CompositeView(Element elem) {
+ super(elem);
+ children = new View[1];
+ nchildren = 0;
+ childAlloc = new Rectangle();
+ }
+
+ /**
+ * Loads all of the children to initialize the view.
+ * This is called by the {@link #setParent}
+ * method. Subclasses can reimplement this to initialize
+ * their child views in a different manner. The default
+ * implementation creates a child view for each
+ * child element.
+ *
+ * @param f the view factory
+ * @see #setParent
+ */
+ protected void loadChildren(ViewFactory f) {
+ if (f == null) {
+ // No factory. This most likely indicates the parent view
+ // has changed out from under us, bail!
+ return;
+ }
+ Element e = getElement();
+ int n = e.getElementCount();
+ if (n > 0) {
+ View[] added = new View[n];
+ for (int i = 0; i < n; i++) {
+ added[i] = f.create(e.getElement(i));
+ }
+ replace(0, 0, added);
+ }
+ }
+
+ // --- View methods ---------------------------------------------
+
+ /**
+ * Sets the parent of the view.
+ * This is reimplemented to provide the superclass
+ * behavior as well as calling the <code>loadChildren</code>
+ * method if this view does not already have children.
+ * The children should not be loaded in the
+ * constructor because the act of setting the parent
+ * may cause them to try to search up the hierarchy
+ * (to get the hosting <code>Container</code> for example).
+ * If this view has children (the view is being moved
+ * from one place in the view hierarchy to another),
+ * the <code>loadChildren</code> method will not be called.
+ *
+ * @param parent the parent of the view, <code>null</code> if none
+ */
+ public void setParent(View parent) {
+ super.setParent(parent);
+ if ((parent != null) && (nchildren == 0)) {
+ ViewFactory f = getViewFactory();
+ loadChildren(f);
+ }
+ }
+
+ /**
+ * Returns the number of child views of this view.
+ *
+ * @return the number of views >= 0
+ * @see #getView
+ */
+ public int getViewCount() {
+ return nchildren;
+ }
+
+ /**
+ * Returns the n-th view in this container.
+ *
+ * @param n the number of the desired view, >= 0 && < getViewCount()
+ * @return the view at index <code>n</code>
+ */
+ public View getView(int n) {
+ return children[n];
+ }
+
+ /**
+ * Replaces child views. If there are no views to remove
+ * this acts as an insert. If there are no views to
+ * add this acts as a remove. Views being removed will
+ * have the parent set to <code>null</code>,
+ * and the internal reference to them removed so that they
+ * may be garbage collected.
+ *
+ * @param offset the starting index into the child views to insert
+ * the new views; >= 0 and <= getViewCount
+ * @param length the number of existing child views to remove;
+ * this should be a value >= 0 and <= (getViewCount() - offset)
+ * @param views the child views to add; this value can be
+ * <code>null</code>
+ * to indicate no children are being added (useful to remove)
+ */
+ public void replace(int offset, int length, View[] views) {
+ // make sure an array exists
+ if (views == null) {
+ views = ZERO;
+ }
+
+ // update parent reference on removed views
+ for (int i = offset; i < offset + length; i++) {
+ if (children[i].getParent() == this) {
+ // in FlowView.java view might be referenced
+ // from two super-views as a child. see logicalView
+ children[i].setParent(null);
+ }
+ children[i] = null;
+ }
+
+ // update the array
+ int delta = views.length - length;
+ int src = offset + length;
+ int nmove = nchildren - src;
+ int dest = src + delta;
+ if ((nchildren + delta) >= children.length) {
+ // need to grow the array
+ int newLength = Math.max(2*children.length, nchildren + delta);
+ View[] newChildren = new View[newLength];
+ System.arraycopy(children, 0, newChildren, 0, offset);
+ System.arraycopy(views, 0, newChildren, offset, views.length);
+ System.arraycopy(children, src, newChildren, dest, nmove);
+ children = newChildren;
+ } else {
+ // patch the existing array
+ System.arraycopy(children, src, children, dest, nmove);
+ System.arraycopy(views, 0, children, offset, views.length);
+ }
+ nchildren = nchildren + delta;
+
+ // update parent reference on added views
+ for (int i = 0; i < views.length; i++) {
+ views[i].setParent(this);
+ }
+ }
+
+ /**
+ * Fetches the allocation for the given child view to
+ * render into. This enables finding out where various views
+ * are located.
+ *
+ * @param index the index of the child, >= 0 && < getViewCount()
+ * @param a the allocation to this view
+ * @return the allocation to the child
+ */
+ public Shape getChildAllocation(int index, Shape a) {
+ Rectangle alloc = getInsideAllocation(a);
+ childAllocation(index, alloc);
+ return alloc;
+ }
+
+ /**
+ * 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 >= 0
+ * @param a the allocated region to render into
+ * @param b a bias value of either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * @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 {
+ boolean isBackward = (b == Position.Bias.Backward);
+ int testPos = (isBackward) ? Math.max(0, pos - 1) : pos;
+ if(isBackward && testPos < getStartOffset()) {
+ return null;
+ }
+ int vIndex = getViewIndexAtPosition(testPos);
+ if ((vIndex != -1) && (vIndex < getViewCount())) {
+ View v = getView(vIndex);
+ if(v != null && testPos >= v.getStartOffset() &&
+ testPos < v.getEndOffset()) {
+ Shape childShape = getChildAllocation(vIndex, a);
+ if (childShape == null) {
+ // We are likely invalid, fail.
+ return null;
+ }
+ Shape retShape = v.modelToView(pos, childShape, b);
+ if(retShape == null && v.getEndOffset() == pos) {
+ if(++vIndex < getViewCount()) {
+ v = getView(vIndex);
+ retShape = v.modelToView(pos, getChildAllocation(vIndex, a), b);
+ }
+ }
+ return retShape;
+ }
+ }
+ throw new BadLocationException("Position not represented by view",
+ pos);
+ }
+
+ /**
+ * Provides a mapping from the document model coordinate space
+ * to the coordinate space of the view mapped to it.
+ *
+ * @param p0 the position to convert >= 0
+ * @param b0 the bias toward the previous character or the
+ * next character represented by p0, in case the
+ * position is a boundary of two views; either
+ * <code>Position.Bias.Forward</code> or
+ * <code>Position.Bias.Backward</code>
+ * @param p1 the position to convert >= 0
+ * @param b1 the bias toward the previous character or the
+ * next character represented by p1, in case the
+ * position is a boundary of two views
+ * @param a the allocated region to render into
+ * @return the bounding box of the given position is returned
+ * @exception BadLocationException if the given position does
+ * not represent a valid location in the associated document
+ * @exception IllegalArgumentException for an invalid bias argument
+ * @see View#viewToModel
+ */
+ public Shape modelToView(int p0, Position.Bias b0, int p1, Position.Bias b1, Shape a) throws BadLocationException {
+ if (p0 == getStartOffset() && p1 == getEndOffset()) {
+ return a;
+ }
+ Rectangle alloc = getInsideAllocation(a);
+ Rectangle r0 = new Rectangle(alloc);
+ View v0 = getViewAtPosition((b0 == Position.Bias.Backward) ?
+ Math.max(0, p0 - 1) : p0, r0);
+ Rectangle r1 = new Rectangle(alloc);
+ View v1 = getViewAtPosition((b1 == Position.Bias.Backward) ?
+ Math.max(0, p1 - 1) : p1, r1);
+ if (v0 == v1) {
+ if (v0 == null) {
+ return a;
+ }
+ // Range contained in one view
+ return v0.modelToView(p0, b0, p1, b1, r0);
+ }
+ // Straddles some views.
+ int viewCount = getViewCount();
+ int counter = 0;
+ while (counter < viewCount) {
+ View v;
+ // Views may not be in same order as model.
+ // v0 or v1 may be null if there is a gap in the range this
+ // view contains.
+ if ((v = getView(counter)) == v0 || v == v1) {
+ View endView;
+ Rectangle retRect;
+ Rectangle tempRect = new Rectangle();
+ if (v == v0) {
+ retRect = v0.modelToView(p0, b0, v0.getEndOffset(),
+ Position.Bias.Backward, r0).
+ getBounds();
+ endView = v1;
+ }
+ else {
+ retRect = v1.modelToView(v1.getStartOffset(),
+ Position.Bias.Forward,
+ p1, b1, r1).getBounds();
+ endView = v0;
+ }
+
+ // Views entirely covered by range.
+ while (++counter < viewCount &&
+ (v = getView(counter)) != endView) {
+ tempRect.setBounds(alloc);
+ childAllocation(counter, tempRect);
+ retRect.add(tempRect);
+ }
+
+ // End view.
+ if (endView != null) {
+ Shape endShape;
+ if (endView == v1) {
+ endShape = v1.modelToView(v1.getStartOffset(),
+ Position.Bias.Forward,
+ p1, b1, r1);
+ }
+ else {
+ endShape = v0.modelToView(p0, b0, v0.getEndOffset(),
+ Position.Bias.Backward, r0);
+ }
+ if (endShape instanceof Rectangle) {
+ retRect.add((Rectangle)endShape);
+ }
+ else {
+ retRect.add(endShape.getBounds());
+ }
+ }
+ return retRect;
+ }
+ counter++;
+ }
+ throw new BadLocationException("Position not represented by view", p0);
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param x x coordinate of the view location to convert >= 0
+ * @param y y coordinate of the view location to convert >= 0
+ * @param a the allocated region to render into
+ * @param bias either <code>Position.Bias.Forward</code> or
+ * <code>Position.Bias.Backward</code>
+ * @return the location within the model that best represents the
+ * given point in the view >= 0
+ * @see View#viewToModel
+ */
+ public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
+ Rectangle alloc = getInsideAllocation(a);
+ if (isBefore((int) x, (int) y, alloc)) {
+ // point is before the range represented
+ int retValue = -1;
+
+ try {
+ retValue = getNextVisualPositionFrom(-1, Position.Bias.Forward,
+ a, EAST, bias);
+ } catch (BadLocationException ble) { }
+ catch (IllegalArgumentException iae) { }
+ if(retValue == -1) {
+ retValue = getStartOffset();
+ bias[0] = Position.Bias.Forward;
+ }
+ return retValue;
+ } else if (isAfter((int) x, (int) y, alloc)) {
+ // point is after the range represented.
+ int retValue = -1;
+ try {
+ retValue = getNextVisualPositionFrom(-1, Position.Bias.Forward,
+ a, WEST, bias);
+ } catch (BadLocationException ble) { }
+ catch (IllegalArgumentException iae) { }
+
+ if(retValue == -1) {
+ // NOTE: this could actually use end offset with backward.
+ retValue = getEndOffset() - 1;
+ bias[0] = Position.Bias.Forward;
+ }
+ return retValue;
+ } else {
+ // locate the child and pass along the request
+ View v = getViewAtPoint((int) x, (int) y, alloc);
+ if (v != null) {
+ return v.viewToModel(x, y, alloc, bias);
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Provides a way to determine the next visually represented model
+ * location that one might place a caret. Some views may not be visible,
+ * they might not be in the same order found in the model, or they just
+ * might not allow access to some of the locations in the model.
+ * This is a convenience method for {@link #getNextNorthSouthVisualPositionFrom}
+ * and {@link #getNextEastWestVisualPositionFrom}.
+ *
+ * @param pos the position to convert >= 0
+ * @param b a bias value of either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * @param a the allocated region to render into
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard;
+ * this may be one of the following:
+ * <ul>
+ * <li><code>SwingConstants.WEST</code>
+ * <li><code>SwingConstants.EAST</code>
+ * <li><code>SwingConstants.NORTH</code>
+ * <li><code>SwingConstants.SOUTH</code>
+ * </ul>
+ * @param biasRet an array containing the bias that was checked
+ * @return the location within the model that best represents the next
+ * location visual position
+ * @exception BadLocationException
+ * @exception IllegalArgumentException if <code>direction</code> is invalid
+ */
+ public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a,
+ int direction, Position.Bias[] biasRet)
+ throws BadLocationException {
+ Rectangle alloc = getInsideAllocation(a);
+
+ switch (direction) {
+ case NORTH:
+ return getNextNorthSouthVisualPositionFrom(pos, b, a, direction,
+ biasRet);
+ case SOUTH:
+ return getNextNorthSouthVisualPositionFrom(pos, b, a, direction,
+ biasRet);
+ case EAST:
+ return getNextEastWestVisualPositionFrom(pos, b, a, direction,
+ biasRet);
+ case WEST:
+ return getNextEastWestVisualPositionFrom(pos, b, a, direction,
+ biasRet);
+ default:
+ throw new IllegalArgumentException("Bad direction: " + direction);
+ }
+ }
+
+ /**
+ * Returns the child view index representing the given
+ * position in the model. This is implemented to call the
+ * <code>getViewIndexByPosition</code>
+ * method for backward compatibility.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ * @since 1.3
+ */
+ public int getViewIndex(int pos, Position.Bias b) {
+ if(b == Position.Bias.Backward) {
+ pos -= 1;
+ }
+ if ((pos >= getStartOffset()) && (pos < getEndOffset())) {
+ return getViewIndexAtPosition(pos);
+ }
+ return -1;
+ }
+
+ // --- local methods ----------------------------------------------------
+
+
+ /**
+ * Tests whether a point lies before the rectangle range.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param alloc the rectangle
+ * @return true if the point is before the specified range
+ */
+ protected abstract boolean isBefore(int x, int y, Rectangle alloc);
+
+ /**
+ * Tests whether a point lies after the rectangle range.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param alloc the rectangle
+ * @return true if the point is after the specified range
+ */
+ protected abstract boolean isAfter(int x, int y, Rectangle alloc);
+
+ /**
+ * Fetches the child view at the given coordinates.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param alloc the parent's allocation on entry, which should
+ * be changed to the child's allocation on exit
+ * @return the child view
+ */
+ protected abstract View getViewAtPoint(int x, int y, Rectangle alloc);
+
+ /**
+ * Returns the allocation for a given child.
+ *
+ * @param index the index of the child, >= 0 && < getViewCount()
+ * @param a the allocation to the interior of the box on entry,
+ * and the allocation of the child view at the index on exit.
+ */
+ protected abstract void childAllocation(int index, Rectangle a);
+
+ /**
+ * Fetches the child view that represents the given position in
+ * the model. This is implemented to fetch the view in the case
+ * where there is a child view for each child element.
+ *
+ * @param pos the position >= 0
+ * @param a the allocation to the interior of the box on entry,
+ * and the allocation of the view containing the position on exit
+ * @return the view representing the given position, or
+ * <code>null</code> if there isn't one
+ */
+ protected View getViewAtPosition(int pos, Rectangle a) {
+ int index = getViewIndexAtPosition(pos);
+ if ((index >= 0) && (index < getViewCount())) {
+ View v = getView(index);
+ if (a != null) {
+ childAllocation(index, a);
+ }
+ return v;
+ }
+ return null;
+ }
+
+ /**
+ * Fetches the child view index representing the given position in
+ * the model. This is implemented to fetch the view in the case
+ * where there is a child view for each child element.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ */
+ protected int getViewIndexAtPosition(int pos) {
+ Element elem = getElement();
+ return elem.getElementIndex(pos);
+ }
+
+ /**
+ * Translates the immutable allocation given to the view
+ * to a mutable allocation that represents the interior
+ * allocation (i.e. the bounds of the given allocation
+ * with the top, left, bottom, and right insets removed.
+ * It is expected that the returned value would be further
+ * mutated to represent an allocation to a child view.
+ * This is implemented to reuse an instance variable so
+ * it avoids creating excessive Rectangles. Typically
+ * the result of calling this method would be fed to
+ * the <code>childAllocation</code> method.
+ *
+ * @param a the allocation given to the view
+ * @return the allocation that represents the inside of the
+ * view after the margins have all been removed; if the
+ * given allocation was <code>null</code>,
+ * the return value is <code>null</code>
+ */
+ protected Rectangle getInsideAllocation(Shape a) {
+ if (a != null) {
+ // get the bounds, hopefully without allocating
+ // a new rectangle. The Shape argument should
+ // not be modified... we copy it into the
+ // child allocation.
+ Rectangle alloc;
+ if (a instanceof Rectangle) {
+ alloc = (Rectangle) a;
+ } else {
+ alloc = a.getBounds();
+ }
+
+ childAlloc.setBounds(alloc);
+ childAlloc.x += getLeftInset();
+ childAlloc.y += getTopInset();
+ childAlloc.width -= getLeftInset() + getRightInset();
+ childAlloc.height -= getTopInset() + getBottomInset();
+ return childAlloc;
+ }
+ return null;
+ }
+
+ /**
+ * Sets the insets from the paragraph attributes specified in
+ * the given attributes.
+ *
+ * @param attr the attributes
+ */
+ protected void setParagraphInsets(AttributeSet attr) {
+ // Since version 1.1 doesn't have scaling and assumes
+ // a pixel is equal to a point, we just cast the point
+ // sizes to integers.
+ top = (short) StyleConstants.getSpaceAbove(attr);
+ left = (short) StyleConstants.getLeftIndent(attr);
+ bottom = (short) StyleConstants.getSpaceBelow(attr);
+ right = (short) StyleConstants.getRightIndent(attr);
+ }
+
+ /**
+ * Sets the insets for the view.
+ *
+ * @param top the top inset >= 0
+ * @param left the left inset >= 0
+ * @param bottom the bottom inset >= 0
+ * @param right the right inset >= 0
+ */
+ protected void setInsets(short top, short left, short bottom, short right) {
+ this.top = top;
+ this.left = left;
+ this.right = right;
+ this.bottom = bottom;
+ }
+
+ /**
+ * Gets the left inset.
+ *
+ * @return the inset >= 0
+ */
+ protected short getLeftInset() {
+ return left;
+ }
+
+ /**
+ * Gets the right inset.
+ *
+ * @return the inset >= 0
+ */
+ protected short getRightInset() {
+ return right;
+ }
+
+ /**
+ * Gets the top inset.
+ *
+ * @return the inset >= 0
+ */
+ protected short getTopInset() {
+ return top;
+ }
+
+ /**
+ * Gets the bottom inset.
+ *
+ * @return the inset >= 0
+ */
+ protected short getBottomInset() {
+ return bottom;
+ }
+
+ /**
+ * Returns the next visual position for the cursor, in either the
+ * north or south direction.
+ *
+ * @param pos the position to convert >= 0
+ * @param b a bias value of either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * @param a the allocated region to render into
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard;
+ * this may be one of the following:
+ * <ul>
+ * <li><code>SwingConstants.NORTH</code>
+ * <li><code>SwingConstants.SOUTH</code>
+ * </ul>
+ * @param biasRet an array containing the bias that was checked
+ * @return the location within the model that best represents the next
+ * north or south location
+ * @exception BadLocationException
+ * @exception IllegalArgumentException if <code>direction</code> is invalid
+ * @see #getNextVisualPositionFrom
+ *
+ * @return the next position west of the passed in position
+ */
+ protected int getNextNorthSouthVisualPositionFrom(int pos, Position.Bias b,
+ Shape a, int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+ return Utilities.getNextVisualPositionFrom(
+ this, pos, b, a, direction, biasRet);
+ }
+
+ /**
+ * Returns the next visual position for the cursor, in either the
+ * east or west direction.
+ *
+ * @param pos the position to convert >= 0
+ * @param b a bias value of either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * @param a the allocated region to render into
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard;
+ * this may be one of the following:
+ * <ul>
+ * <li><code>SwingConstants.WEST</code>
+ * <li><code>SwingConstants.EAST</code>
+ * </ul>
+ * @param biasRet an array containing the bias that was checked
+ * @return the location within the model that best represents the next
+ * west or east location
+ * @exception BadLocationException
+ * @exception IllegalArgumentException if <code>direction</code> is invalid
+ * @see #getNextVisualPositionFrom
+ */
+ protected int getNextEastWestVisualPositionFrom(int pos, Position.Bias b,
+ Shape a,
+ int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+ return Utilities.getNextVisualPositionFrom(
+ this, pos, b, a, direction, biasRet);
+ }
+
+ /**
+ * Determines in which direction the next view lays.
+ * Consider the <code>View</code> at index n. Typically the
+ * <code>View</code>s are layed out from left to right,
+ * so that the <code>View</code> to the EAST will be
+ * at index n + 1, and the <code>View</code> to the WEST
+ * will be at index n - 1. In certain situations,
+ * such as with bidirectional text, it is possible
+ * that the <code>View</code> to EAST is not at index n + 1,
+ * but rather at index n - 1, or that the <code>View</code>
+ * to the WEST is not at index n - 1, but index n + 1.
+ * In this case this method would return true, indicating the
+ * <code>View</code>s are layed out in descending order.
+ * <p>
+ * This unconditionally returns false, subclasses should override this
+ * method if there is the possibility for laying <code>View</code>s in
+ * descending order.
+ *
+ * @param position position into the model
+ * @param bias either <code>Position.Bias.Forward</code> or
+ * <code>Position.Bias.Backward</code>
+ * @return false
+ */
+ protected boolean flipEastAndWestAtEnds(int position,
+ Position.Bias bias) {
+ return false;
+ }
+
+
+ // ---- member variables ---------------------------------------------
+
+
+ private static View[] ZERO = new View[0];
+
+ private View[] children;
+ private int nchildren;
+ private short left;
+ private short right;
+ private short top;
+ private short bottom;
+ private Rectangle childAlloc;
+}
diff --git a/src/share/classes/javax/swing/text/DateFormatter.java b/src/share/classes/javax/swing/text/DateFormatter.java
new file mode 100644
index 000000000..6ea0ec231
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DateFormatter.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2000-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;
+
+import java.awt.event.*;
+import java.text.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.text.*;
+
+/**
+ * DateFormatter is an <code>InternationalFormatter</code> that does its
+ * formatting by way of an instance of <code>java.text.DateFormat</code>.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see java.text.DateFormat
+ *
+ * @since 1.4
+ */
+public class DateFormatter extends InternationalFormatter {
+ /**
+ * This is shorthand for
+ * <code>new DateFormatter(DateFormat.getDateInstance())</code>.
+ */
+ public DateFormatter() {
+ this(DateFormat.getDateInstance());
+ }
+
+ /**
+ * Returns a DateFormatter configured with the specified
+ * <code>Format</code> instance.
+ *
+ * @param format Format used to dictate legal values
+ */
+ public DateFormatter(DateFormat format) {
+ super(format);
+ setFormat(format);
+ }
+
+ /**
+ * Sets the format that dictates the legal values that can be edited
+ * and displayed.
+ * <p>
+ * If you have used the nullary constructor the value of this property
+ * will be determined for the current locale by way of the
+ * <code>Dateformat.getDateInstance()</code> method.
+ *
+ * @param format DateFormat instance used for converting from/to Strings
+ */
+ public void setFormat(DateFormat format) {
+ super.setFormat(format);
+ }
+
+ /**
+ * Returns the Calendar that <code>DateFormat</code> is associated with,
+ * or if the <code>Format</code> is not a <code>DateFormat</code>
+ * <code>Calendar.getInstance</code> is returned.
+ */
+ private Calendar getCalendar() {
+ Format f = getFormat();
+
+ if (f instanceof DateFormat) {
+ return ((DateFormat)f).getCalendar();
+ }
+ return Calendar.getInstance();
+ }
+
+
+ /**
+ * Returns true, as DateFormatterFilter will support
+ * incrementing/decrementing of the value.
+ */
+ boolean getSupportsIncrement() {
+ return true;
+ }
+
+ /**
+ * Returns the field that will be adjusted by adjustValue.
+ */
+ Object getAdjustField(int start, Map attributes) {
+ Iterator attrs = attributes.keySet().iterator();
+
+ while (attrs.hasNext()) {
+ Object key = attrs.next();
+
+ if ((key instanceof DateFormat.Field) &&
+ (key == DateFormat.Field.HOUR1 ||
+ ((DateFormat.Field)key).getCalendarField() != -1)) {
+ return key;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adjusts the Date if FieldPosition identifies a known calendar
+ * field.
+ */
+ Object adjustValue(Object value, Map attributes, Object key,
+ int direction) throws
+ BadLocationException, ParseException {
+ if (key != null) {
+ int field;
+
+ // HOUR1 has no corresponding calendar field, thus, map
+ // it to HOUR0 which will give the correct behavior.
+ if (key == DateFormat.Field.HOUR1) {
+ key = DateFormat.Field.HOUR0;
+ }
+ field = ((DateFormat.Field)key).getCalendarField();
+
+ Calendar calendar = getCalendar();
+
+ if (calendar != null) {
+ calendar.setTime((Date)value);
+
+ int fieldValue = calendar.get(field);
+
+ try {
+ calendar.add(field, direction);
+ value = calendar.getTime();
+ } catch (Throwable th) {
+ value = null;
+ }
+ return value;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/DefaultCaret.java b/src/share/classes/javax/swing/text/DefaultCaret.java
new file mode 100644
index 000000000..22ec280ba
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DefaultCaret.java
@@ -0,0 +1,1917 @@
+/*
+ * 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;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.datatransfer.*;
+import java.beans.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.plaf.*;
+import java.util.EventListener;
+import sun.swing.SwingUtilities2;
+
+/**
+ * A default implementation of Caret. The caret is rendered as
+ * a vertical line in the color specified by the CaretColor property
+ * of the associated JTextComponent. It can blink at the rate specified
+ * by the BlinkRate property.
+ * <p>
+ * This implementation expects two sources of asynchronous notification.
+ * The timer thread fires asynchronously, and causes the caret to simply
+ * repaint the most recent bounding box. The caret also tracks change
+ * as the document is modified. Typically this will happen on the
+ * event dispatch thread as a result of some mouse or keyboard event.
+ * The caret behavior on both synchronous and asynchronous documents updates
+ * is controlled by <code>UpdatePolicy</code> property. The repaint of the
+ * new caret location will occur on the event thread in any case, as calls to
+ * <code>modelToView</code> are only safe on the event thread.
+ * <p>
+ * The caret acts as a mouse and focus listener on the text component
+ * it has been installed in, and defines the caret semantics based upon
+ * those events. The listener methods can be reimplemented to change the
+ * semantics.
+ * By default, the first mouse button will be used to set focus and caret
+ * position. Dragging the mouse pointer with the first mouse button will
+ * sweep out a selection that is contiguous in the model. If the associated
+ * text component is editable, the caret will become visible when focus
+ * is gained, and invisible when focus is lost.
+ * <p>
+ * The Highlighter bound to the associated text component is used to
+ * render the selection by default.
+ * Selection appearance can be customized by supplying a
+ * painter to use for the highlights. By default a painter is used that
+ * will render a solid color as specified in the associated text component
+ * in the <code>SelectionColor</code> property. This can easily be changed
+ * by reimplementing the
+ * <a href="#getSelectionHighlighter">getSelectionHighlighter</a>
+ * method.
+ * <p>
+ * A customized caret appearance can be achieved by reimplementing
+ * the paint method. If the paint method is changed, the damage method
+ * should also be reimplemented to cause a repaint for the area needed
+ * to render the caret. The caret extends the Rectangle class which
+ * is used to hold the bounding box for where the caret was last rendered.
+ * This enables the caret to repaint in a thread-safe manner when the
+ * caret moves without making a call to modelToView which is unstable
+ * between model updates and view repair (i.e. the order of delivery
+ * to DocumentListeners is not guaranteed).
+ * <p>
+ * The magic caret position is set to null when the caret position changes.
+ * A timer is used to determine the new location (after the caret change).
+ * When the timer fires, if the magic caret position is still null it is
+ * reset to the current caret position. Any actions that change
+ * the caret position and want the magic caret position to remain the
+ * same, must remember the magic caret position, change the cursor, and
+ * then set the magic caret position to its original value. This has the
+ * benefit that only actions that want the magic caret position to persist
+ * (such as open/down) need to know about it.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Timothy Prinzing
+ * @see Caret
+ */
+public class DefaultCaret extends Rectangle implements Caret, FocusListener, MouseListener, MouseMotionListener {
+
+ /**
+ * Indicates that the caret position is to be updated only when
+ * document changes are performed on the Event Dispatching Thread.
+ * @see #setUpdatePolicy
+ * @see #getUpdatePolicy
+ * @since 1.5
+ */
+ public static final int UPDATE_WHEN_ON_EDT = 0;
+
+ /**
+ * Indicates that the caret should remain at the same
+ * absolute position in the document regardless of any document
+ * updates, except when the document length becomes less than
+ * the current caret position due to removal. In that case the caret
+ * position is adjusted to the end of the document.
+ *
+ * @see #setUpdatePolicy
+ * @see #getUpdatePolicy
+ * @since 1.5
+ */
+ public static final int NEVER_UPDATE = 1;
+
+ /**
+ * Indicates that the caret position is to be <b>always</b>
+ * updated accordingly to the document changes regardless whether
+ * the document updates are performed on the Event Dispatching Thread
+ * or not.
+ *
+ * @see #setUpdatePolicy
+ * @see #getUpdatePolicy
+ * @since 1.5
+ */
+ public static final int ALWAYS_UPDATE = 2;
+
+ /**
+ * Constructs a default caret.
+ */
+ public DefaultCaret() {
+ }
+
+ /**
+ * Sets the caret movement policy on the document updates. Normally
+ * the caret updates its absolute position within the document on
+ * insertions occurred before or at the caret position and
+ * on removals before the caret position. 'Absolute position'
+ * means here the position relative to the start of the document.
+ * For example if
+ * a character is typed within editable text component it is inserted
+ * at the caret position and the caret moves to the next absolute
+ * position within the document due to insertion and if
+ * <code>BACKSPACE</code> is typed then caret decreases its absolute
+ * position due to removal of a character before it. Sometimes
+ * it may be useful to turn off the caret position updates so that
+ * the caret stays at the same absolute position within the
+ * document position regardless of any document updates.
+ * <p>
+ * The following update policies are allowed:
+ * <ul>
+ * <li><code>NEVER_UPDATE</code>: the caret stays at the same
+ * absolute position in the document regardless of any document
+ * updates, except when document length becomes less than
+ * the current caret position due to removal. In that case caret
+ * position is adjusted to the end of the document.
+ * The caret doesn't try to keep itself visible by scrolling
+ * the associated view when using this policy. </li>
+ * <li><code>ALWAYS_UPDATE</code>: the caret always tracks document
+ * changes. For regular changes it increases its position
+ * if an insertion occurs before or at its current position,
+ * and decreases position if a removal occurs before
+ * its current position. For undo/redo updates it is always
+ * moved to the position where update occurred. The caret
+ * also tries to keep itself visible by calling
+ * <code>adjustVisibility</code> method.</li>
+ * <li><code>UPDATE_WHEN_ON_EDT</code>: acts like <code>ALWAYS_UPDATE</code>
+ * if the document updates are performed on the Event Dispatching Thread
+ * and like <code>NEVER_UPDATE</code> if updates are performed on
+ * other thread. </li>
+ * </ul> <p>
+ * The default property value is <code>UPDATE_WHEN_ON_EDT</code>.
+ *
+ * @param policy one of the following values : <code>UPDATE_WHEN_ON_EDT</code>,
+ * <code>NEVER_UPDATE</code>, <code>ALWAYS_UPDATE</code>
+ * @throws IllegalArgumentException if invalid value is passed
+ *
+ * @see #getUpdatePolicy
+ * @see #adjustVisibility
+ * @see #UPDATE_WHEN_ON_EDT
+ * @see #NEVER_UPDATE
+ * @see #ALWAYS_UPDATE
+ *
+ * @since 1.5
+ */
+ public void setUpdatePolicy(int policy) {
+ updatePolicy = policy;
+ }
+
+ /**
+ * Gets the caret movement policy on document updates.
+ *
+ * @return one of the following values : <code>UPDATE_WHEN_ON_EDT</code>,
+ * <code>NEVER_UPDATE</code>, <code>ALWAYS_UPDATE</code>
+ *
+ * @see #setUpdatePolicy
+ * @see #UPDATE_WHEN_ON_EDT
+ * @see #NEVER_UPDATE
+ * @see #ALWAYS_UPDATE
+ *
+ * @since 1.5
+ */
+ public int getUpdatePolicy() {
+ return updatePolicy;
+ }
+
+ /**
+ * Gets the text editor component that this caret is
+ * is bound to.
+ *
+ * @return the component
+ */
+ protected final JTextComponent getComponent() {
+ return component;
+ }
+
+ /**
+ * Cause the caret to be painted. The repaint
+ * area is the bounding box of the caret (i.e.
+ * the caret rectangle or <em>this</em>).
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ */
+ protected final synchronized void repaint() {
+ if (component != null) {
+ component.repaint(x, y, width, height);
+ }
+ }
+
+ /**
+ * Damages the area surrounding the caret to cause
+ * it to be repainted in a new location. If paint()
+ * is reimplemented, this method should also be
+ * reimplemented. This method should update the
+ * caret bounds (x, y, width, and height).
+ *
+ * @param r the current location of the caret
+ * @see #paint
+ */
+ protected synchronized void damage(Rectangle r) {
+ if (r != null) {
+ int damageWidth = getCaretWidth(r.height);
+ x = r.x - 4 - (damageWidth >> 1);
+ y = r.y;
+ width = 9 + damageWidth;
+ height = r.height;
+ repaint();
+ }
+ }
+
+ /**
+ * Scrolls the associated view (if necessary) to make
+ * the caret visible. Since how this should be done
+ * is somewhat of a policy, this method can be
+ * reimplemented to change the behavior. By default
+ * the scrollRectToVisible method is called on the
+ * associated component.
+ *
+ * @param nloc the new position to scroll to
+ */
+ protected void adjustVisibility(Rectangle nloc) {
+ if(component == null) {
+ return;
+ }
+ if (SwingUtilities.isEventDispatchThread()) {
+ component.scrollRectToVisible(nloc);
+ } else {
+ SwingUtilities.invokeLater(new SafeScroller(nloc));
+ }
+ }
+
+ /**
+ * Gets the painter for the Highlighter.
+ *
+ * @return the painter
+ */
+ protected Highlighter.HighlightPainter getSelectionPainter() {
+ return DefaultHighlighter.DefaultPainter;
+ }
+
+ /**
+ * Tries to set the position of the caret from
+ * the coordinates of a mouse event, using viewToModel().
+ *
+ * @param e the mouse event
+ */
+ protected void positionCaret(MouseEvent e) {
+ Point pt = new Point(e.getX(), e.getY());
+ Position.Bias[] biasRet = new Position.Bias[1];
+ int pos = component.getUI().viewToModel(component, pt, biasRet);
+ if(biasRet[0] == null)
+ biasRet[0] = Position.Bias.Forward;
+ if (pos >= 0) {
+ setDot(pos, biasRet[0]);
+ }
+ }
+
+ /**
+ * Tries to move the position of the caret from
+ * the coordinates of a mouse event, using viewToModel().
+ * This will cause a selection if the dot and mark
+ * are different.
+ *
+ * @param e the mouse event
+ */
+ protected void moveCaret(MouseEvent e) {
+ Point pt = new Point(e.getX(), e.getY());
+ Position.Bias[] biasRet = new Position.Bias[1];
+ int pos = component.getUI().viewToModel(component, pt, biasRet);
+ if(biasRet[0] == null)
+ biasRet[0] = Position.Bias.Forward;
+ if (pos >= 0) {
+ moveDot(pos, biasRet[0]);
+ }
+ }
+
+ // --- FocusListener methods --------------------------
+
+ /**
+ * Called when the component containing the caret gains
+ * focus. This is implemented to set the caret to visible
+ * if the component is editable.
+ *
+ * @param e the focus event
+ * @see FocusListener#focusGained
+ */
+ public void focusGained(FocusEvent e) {
+ if (component.isEnabled()) {
+ if (component.isEditable()) {
+ setVisible(true);
+ }
+ setSelectionVisible(true);
+ }
+ }
+
+ /**
+ * Called when the component containing the caret loses
+ * focus. This is implemented to set the caret to visibility
+ * to false.
+ *
+ * @param e the focus event
+ * @see FocusListener#focusLost
+ */
+ public void focusLost(FocusEvent e) {
+ setVisible(false);
+ setSelectionVisible(ownsSelection || e.isTemporary());
+ }
+
+
+ /**
+ * Selects word based on the MouseEvent
+ */
+ private void selectWord(MouseEvent e) {
+ if (selectedWordEvent != null
+ && selectedWordEvent.getX() == e.getX()
+ && selectedWordEvent.getY() == e.getY()) {
+ //we already done selection for this
+ return;
+ }
+ Action a = null;
+ ActionMap map = getComponent().getActionMap();
+ if (map != null) {
+ a = map.get(DefaultEditorKit.selectWordAction);
+ }
+ if (a == null) {
+ if (selectWord == null) {
+ selectWord = new DefaultEditorKit.SelectWordAction();
+ }
+ a = selectWord;
+ }
+ a.actionPerformed(new ActionEvent(getComponent(),
+ ActionEvent.ACTION_PERFORMED, null, e.getWhen(), e.getModifiers()));
+ selectedWordEvent = e;
+ }
+
+ // --- MouseListener methods -----------------------------------
+
+ /**
+ * Called when the mouse is clicked. If the click was generated
+ * from button1, a double click selects a word,
+ * and a triple click the current line.
+ *
+ * @param e the mouse event
+ * @see MouseListener#mouseClicked
+ */
+ public void mouseClicked(MouseEvent e) {
+ int nclicks = SwingUtilities2.getAdjustedClickCount(getComponent(), e);
+
+ if (! e.isConsumed()) {
+ if (SwingUtilities.isLeftMouseButton(e)) {
+ // mouse 1 behavior
+ if(nclicks == 1) {
+ selectedWordEvent = null;
+ } else if(nclicks == 2
+ && SwingUtilities2.canEventAccessSystemClipboard(e)) {
+ selectWord(e);
+ selectedWordEvent = null;
+ } else if(nclicks == 3
+ && SwingUtilities2.canEventAccessSystemClipboard(e)) {
+ Action a = null;
+ ActionMap map = getComponent().getActionMap();
+ if (map != null) {
+ a = map.get(DefaultEditorKit.selectLineAction);
+ }
+ if (a == null) {
+ if (selectLine == null) {
+ selectLine = new DefaultEditorKit.SelectLineAction();
+ }
+ a = selectLine;
+ }
+ a.actionPerformed(new ActionEvent(getComponent(),
+ ActionEvent.ACTION_PERFORMED, null, e.getWhen(), e.getModifiers()));
+ }
+ } else if (SwingUtilities.isMiddleMouseButton(e)) {
+ // mouse 2 behavior
+ if (nclicks == 1 && component.isEditable() && component.isEnabled()
+ && SwingUtilities2.canEventAccessSystemClipboard(e)) {
+ // paste system selection, if it exists
+ JTextComponent c = (JTextComponent) e.getSource();
+ if (c != null) {
+ try {
+ Toolkit tk = c.getToolkit();
+ Clipboard buffer = tk.getSystemSelection();
+ if (buffer != null) {
+ // platform supports system selections, update it.
+ adjustCaret(e);
+ TransferHandler th = c.getTransferHandler();
+ if (th != null) {
+ Transferable trans = null;
+
+ try {
+ trans = buffer.getContents(null);
+ } catch (IllegalStateException ise) {
+ // clipboard was unavailable
+ UIManager.getLookAndFeel().provideErrorFeedback(c);
+ }
+
+ if (trans != null) {
+ th.importData(c, trans);
+ }
+ }
+ adjustFocus(true);
+ }
+ } catch (HeadlessException he) {
+ // do nothing... there is no system clipboard
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * If button 1 is pressed, this is implemented to
+ * request focus on the associated text component,
+ * and to set the caret position. If the shift key is held down,
+ * the caret will be moved, potentially resulting in a selection,
+ * otherwise the
+ * caret position will be set to the new location. If the component
+ * is not enabled, there will be no request for focus.
+ *
+ * @param e the mouse event
+ * @see MouseListener#mousePressed
+ */
+ public void mousePressed(MouseEvent e) {
+ int nclicks = SwingUtilities2.getAdjustedClickCount(getComponent(), e);
+
+ if (SwingUtilities.isLeftMouseButton(e)) {
+ if (e.isConsumed()) {
+ shouldHandleRelease = true;
+ } else {
+ shouldHandleRelease = false;
+ adjustCaretAndFocus(e);
+ if (nclicks == 2
+ && SwingUtilities2.canEventAccessSystemClipboard(e)) {
+ selectWord(e);
+ }
+ }
+ }
+ }
+
+ void adjustCaretAndFocus(MouseEvent e) {
+ adjustCaret(e);
+ adjustFocus(false);
+ }
+
+ /**
+ * Adjusts the caret location based on the MouseEvent.
+ */
+ private void adjustCaret(MouseEvent e) {
+ if ((e.getModifiers() & ActionEvent.SHIFT_MASK) != 0 &&
+ getDot() != -1) {
+ moveCaret(e);
+ } else {
+ positionCaret(e);
+ }
+ }
+
+ /**
+ * Adjusts the focus, if necessary.
+ *
+ * @param inWindow if true indicates requestFocusInWindow should be used
+ */
+ private void adjustFocus(boolean inWindow) {
+ if ((component != null) && component.isEnabled() &&
+ component.isRequestFocusEnabled()) {
+ if (inWindow) {
+ component.requestFocusInWindow();
+ }
+ else {
+ component.requestFocus();
+ }
+ }
+ }
+
+ /**
+ * Called when the mouse is released.
+ *
+ * @param e the mouse event
+ * @see MouseListener#mouseReleased
+ */
+ public void mouseReleased(MouseEvent e) {
+ if (!e.isConsumed()
+ && shouldHandleRelease
+ && SwingUtilities.isLeftMouseButton(e)) {
+
+ adjustCaretAndFocus(e);
+ }
+ }
+
+ /**
+ * Called when the mouse enters a region.
+ *
+ * @param e the mouse event
+ * @see MouseListener#mouseEntered
+ */
+ public void mouseEntered(MouseEvent e) {
+ }
+
+ /**
+ * Called when the mouse exits a region.
+ *
+ * @param e the mouse event
+ * @see MouseListener#mouseExited
+ */
+ public void mouseExited(MouseEvent e) {
+ }
+
+ // --- MouseMotionListener methods -------------------------
+
+ /**
+ * Moves the caret position
+ * according to the mouse pointer's current
+ * location. This effectively extends the
+ * selection. By default, this is only done
+ * for mouse button 1.
+ *
+ * @param e the mouse event
+ * @see MouseMotionListener#mouseDragged
+ */
+ public void mouseDragged(MouseEvent e) {
+ if ((! e.isConsumed()) && SwingUtilities.isLeftMouseButton(e)) {
+ moveCaret(e);
+ }
+ }
+
+ /**
+ * Called when the mouse is moved.
+ *
+ * @param e the mouse event
+ * @see MouseMotionListener#mouseMoved
+ */
+ public void mouseMoved(MouseEvent e) {
+ }
+
+ // ---- Caret methods ---------------------------------
+
+ /**
+ * Renders the caret as a vertical line. If this is reimplemented
+ * the damage method should also be reimplemented as it assumes the
+ * shape of the caret is a vertical line. Sets the caret color to
+ * the value returned by getCaretColor().
+ * <p>
+ * If there are multiple text directions present in the associated
+ * document, a flag indicating the caret bias will be rendered.
+ * This will occur only if the associated document is a subclass
+ * of AbstractDocument and there are multiple bidi levels present
+ * in the bidi element structure (i.e. the text has multiple
+ * directions associated with it).
+ *
+ * @param g the graphics context
+ * @see #damage
+ */
+ public void paint(Graphics g) {
+ if(isVisible()) {
+ try {
+ TextUI mapper = component.getUI();
+ Rectangle r = mapper.modelToView(component, dot, dotBias);
+
+ if ((r == null) || ((r.width == 0) && (r.height == 0))) {
+ return;
+ }
+ if (width > 0 && height > 0 &&
+ !this._contains(r.x, r.y, r.width, r.height)) {
+ // We seem to have gotten out of sync and no longer
+ // contain the right location, adjust accordingly.
+ Rectangle clip = g.getClipBounds();
+
+ if (clip != null && !clip.contains(this)) {
+ // Clip doesn't contain the old location, force it
+ // to be repainted lest we leave a caret around.
+ repaint();
+ }
+ // This will potentially cause a repaint of something
+ // we're already repainting, but without changing the
+ // semantics of damage we can't really get around this.
+ damage(r);
+ }
+ g.setColor(component.getCaretColor());
+ int paintWidth = getCaretWidth(r.height);
+ r.x -= paintWidth >> 1;
+ g.fillRect(r.x, r.y, paintWidth, r.height);
+
+ // see if we should paint a flag to indicate the bias
+ // of the caret.
+ // PENDING(prinz) this should be done through
+ // protected methods so that alternative LAF
+ // will show bidi information.
+ Document doc = component.getDocument();
+ if (doc instanceof AbstractDocument) {
+ Element bidi = ((AbstractDocument)doc).getBidiRootElement();
+ if ((bidi != null) && (bidi.getElementCount() > 1)) {
+ // there are multiple directions present.
+ flagXPoints[0] = r.x + ((dotLTR) ? paintWidth : 0);
+ flagYPoints[0] = r.y;
+ flagXPoints[1] = flagXPoints[0];
+ flagYPoints[1] = flagYPoints[0] + 4;
+ flagXPoints[2] = flagXPoints[0] + ((dotLTR) ? 4 : -4);
+ flagYPoints[2] = flagYPoints[0];
+ g.fillPolygon(flagXPoints, flagYPoints, 3);
+ }
+ }
+ } catch (BadLocationException e) {
+ // can't render I guess
+ //System.err.println("Can't render cursor");
+ }
+ }
+ }
+
+ /**
+ * Called when the UI is being installed into the
+ * interface of a JTextComponent. This can be used
+ * to gain access to the model that is being navigated
+ * by the implementation of this interface. Sets the dot
+ * and mark to 0, and establishes document, property change,
+ * focus, mouse, and mouse motion listeners.
+ *
+ * @param c the component
+ * @see Caret#install
+ */
+ public void install(JTextComponent c) {
+ component = c;
+ Document doc = c.getDocument();
+ dot = mark = 0;
+ dotLTR = markLTR = true;
+ dotBias = markBias = Position.Bias.Forward;
+ if (doc != null) {
+ doc.addDocumentListener(handler);
+ }
+ c.addPropertyChangeListener(handler);
+ c.addFocusListener(this);
+ c.addMouseListener(this);
+ c.addMouseMotionListener(this);
+
+ // if the component already has focus, it won't
+ // be notified.
+ if (component.hasFocus()) {
+ focusGained(null);
+ }
+
+ Number ratio = (Number) c.getClientProperty("caretAspectRatio");
+ if (ratio != null) {
+ aspectRatio = ratio.floatValue();
+ } else {
+ aspectRatio = -1;
+ }
+
+ Integer width = (Integer) c.getClientProperty("caretWidth");
+ if (width != null) {
+ caretWidth = width.intValue();
+ } else {
+ caretWidth = -1;
+ }
+ }
+
+ /**
+ * Called when the UI is being removed from the
+ * interface of a JTextComponent. This is used to
+ * unregister any listeners that were attached.
+ *
+ * @param c the component
+ * @see Caret#deinstall
+ */
+ public void deinstall(JTextComponent c) {
+ c.removeMouseListener(this);
+ c.removeMouseMotionListener(this);
+ c.removeFocusListener(this);
+ c.removePropertyChangeListener(handler);
+ Document doc = c.getDocument();
+ if (doc != null) {
+ doc.removeDocumentListener(handler);
+ }
+ synchronized(this) {
+ component = null;
+ }
+ if (flasher != null) {
+ flasher.stop();
+ }
+
+
+ }
+
+ /**
+ * Adds a listener to track whenever the caret position has
+ * been changed.
+ *
+ * @param l the listener
+ * @see Caret#addChangeListener
+ */
+ public void addChangeListener(ChangeListener l) {
+ listenerList.add(ChangeListener.class, l);
+ }
+
+ /**
+ * Removes a listener that was tracking caret position changes.
+ *
+ * @param l the listener
+ * @see Caret#removeChangeListener
+ */
+ public void removeChangeListener(ChangeListener l) {
+ listenerList.remove(ChangeListener.class, l);
+ }
+
+ /**
+ * Returns an array of all the change listeners
+ * registered on this caret.
+ *
+ * @return all of this caret's <code>ChangeListener</code>s
+ * or an empty
+ * array if no change listeners are currently registered
+ *
+ * @see #addChangeListener
+ * @see #removeChangeListener
+ *
+ * @since 1.4
+ */
+ public ChangeListener[] getChangeListeners() {
+ return (ChangeListener[])listenerList.getListeners(
+ ChangeListener.class);
+ }
+
+ /**
+ * 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. The listener list is processed last to first.
+ *
+ * @see EventListenerList
+ */
+ protected void fireStateChanged() {
+ // Guaranteed to return a non-null array
+ Object[] listeners = listenerList.getListenerList();
+ // Process the listeners last to first, notifying
+ // those that are interested in this event
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==ChangeListener.class) {
+ // Lazily create the event:
+ if (changeEvent == null)
+ changeEvent = new ChangeEvent(this);
+ ((ChangeListener)listeners[i+1]).stateChanged(changeEvent);
+ }
+ }
+ }
+
+ /**
+ * Returns an array of all the objects currently registered
+ * as <code><em>Foo</em>Listener</code>s
+ * upon this caret.
+ * <code><em>Foo</em>Listener</code>s are registered using the
+ * <code>add<em>Foo</em>Listener</code> method.
+ *
+ * <p>
+ *
+ * You can specify the <code>listenerType</code> argument
+ * with a class literal,
+ * such as
+ * <code><em>Foo</em>Listener.class</code>.
+ * For example, you can query a
+ * <code>DefaultCaret</code> <code>c</code>
+ * for its change listeners with the following code:
+ *
+ * <pre>ChangeListener[] cls = (ChangeListener[])(c.getListeners(ChangeListener.class));</pre>
+ *
+ * If no such listeners exist, this method returns an empty array.
+ *
+ * @param listenerType the type of listeners requested; this parameter
+ * should specify an interface that descends from
+ * <code>java.util.EventListener</code>
+ * @return an array of all objects registered as
+ * <code><em>Foo</em>Listener</code>s on this component,
+ * or an empty array if no such
+ * listeners have been added
+ * @exception ClassCastException if <code>listenerType</code>
+ * doesn't specify a class or interface that implements
+ * <code>java.util.EventListener</code>
+ *
+ * @see #getChangeListeners
+ *
+ * @since 1.3
+ */
+ public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
+ return listenerList.getListeners(listenerType);
+ }
+
+ /**
+ * Changes the selection visibility.
+ *
+ * @param vis the new visibility
+ */
+ public void setSelectionVisible(boolean vis) {
+ if (vis != selectionVisible) {
+ selectionVisible = vis;
+ if (selectionVisible) {
+ // show
+ Highlighter h = component.getHighlighter();
+ if ((dot != mark) && (h != null) && (selectionTag == null)) {
+ int p0 = Math.min(dot, mark);
+ int p1 = Math.max(dot, mark);
+ Highlighter.HighlightPainter p = getSelectionPainter();
+ try {
+ selectionTag = h.addHighlight(p0, p1, p);
+ } catch (BadLocationException bl) {
+ selectionTag = null;
+ }
+ }
+ } else {
+ // hide
+ if (selectionTag != null) {
+ Highlighter h = component.getHighlighter();
+ h.removeHighlight(selectionTag);
+ selectionTag = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks whether the current selection is visible.
+ *
+ * @return true if the selection is visible
+ */
+ public boolean isSelectionVisible() {
+ return selectionVisible;
+ }
+
+ /**
+ * Determines if the caret is currently active.
+ * <p>
+ * This method returns whether or not the <code>Caret</code>
+ * is currently in a blinking state. It does not provide
+ * information as to whether it is currently blinked on or off.
+ * To determine if the caret is currently painted use the
+ * <code>isVisible</code> method.
+ *
+ * @return <code>true</code> if active else <code>false</code>
+ * @see #isVisible
+ *
+ * @since 1.5
+ */
+ public boolean isActive() {
+ return active;
+ }
+
+ /**
+ * Indicates whether or not the caret is currently visible. As the
+ * caret flashes on and off the return value of this will change
+ * between true, when the caret is painted, and false, when the
+ * caret is not painted. <code>isActive</code> indicates whether
+ * or not the caret is in a blinking state, such that it <b>can</b>
+ * be visible, and <code>isVisible</code> indicates whether or not
+ * the caret <b>is</b> actually visible.
+ * <p>
+ * Subclasses that wish to render a different flashing caret
+ * should override paint and only paint the caret if this method
+ * returns true.
+ *
+ * @return true if visible else false
+ * @see Caret#isVisible
+ * @see #isActive
+ */
+ public boolean isVisible() {
+ return visible;
+ }
+
+ /**
+ * Sets the caret visibility, and repaints the caret.
+ * It is important to understand the relationship between this method,
+ * <code>isVisible</code> and <code>isActive</code>.
+ * Calling this method with a value of <code>true</code> activates the
+ * caret blinking. Setting it to <code>false</code> turns it completely off.
+ * To determine whether the blinking is active, you should call
+ * <code>isActive</code>. In effect, <code>isActive</code> is an
+ * appropriate corresponding "getter" method for this one.
+ * <code>isVisible</code> can be used to fetch the current
+ * visibility status of the caret, meaning whether or not it is currently
+ * painted. This status will change as the caret blinks on and off.
+ * <p>
+ * Here's a list showing the potential return values of both
+ * <code>isActive</code> and <code>isVisible</code>
+ * after calling this method:
+ * <p>
+ * <b><code>setVisible(true)</code></b>:
+ * <ul>
+ * <li>isActive(): true</li>
+ * <li>isVisible(): true or false depending on whether
+ * or not the caret is blinked on or off</li>
+ * </ul>
+ * <p>
+ * <b><code>setVisible(false)</code></b>:
+ * <ul>
+ * <li>isActive(): false</li>
+ * <li>isVisible(): false</li>
+ * </ul>
+ *
+ * @param e the visibility specifier
+ * @see #isActive
+ * @see Caret#setVisible
+ */
+ public void setVisible(boolean e) {
+ // focus lost notification can come in later after the
+ // caret has been deinstalled, in which case the component
+ // will be null.
+ if (component != null) {
+ active = e;
+ TextUI mapper = component.getUI();
+ if (visible != e) {
+ visible = e;
+ // repaint the caret
+ try {
+ Rectangle loc = mapper.modelToView(component, dot,dotBias);
+ damage(loc);
+ } catch (BadLocationException badloc) {
+ // hmm... not legally positioned
+ }
+ }
+ }
+ if (flasher != null) {
+ if (visible) {
+ flasher.start();
+ } else {
+ flasher.stop();
+ }
+ }
+ }
+
+ /**
+ * Sets the caret blink rate.
+ *
+ * @param rate the rate in milliseconds, 0 to stop blinking
+ * @see Caret#setBlinkRate
+ */
+ public void setBlinkRate(int rate) {
+ if (rate != 0) {
+ if (flasher == null) {
+ flasher = new Timer(rate, handler);
+ }
+ flasher.setDelay(rate);
+ } else {
+ if (flasher != null) {
+ flasher.stop();
+ flasher.removeActionListener(handler);
+ flasher = null;
+ }
+ }
+ }
+
+ /**
+ * Gets the caret blink rate.
+ *
+ * @return the delay in milliseconds. If this is
+ * zero the caret will not blink.
+ * @see Caret#getBlinkRate
+ */
+ public int getBlinkRate() {
+ return (flasher == null) ? 0 : flasher.getDelay();
+ }
+
+ /**
+ * Fetches the current position of the caret.
+ *
+ * @return the position &gt;= 0
+ * @see Caret#getDot
+ */
+ public int getDot() {
+ return dot;
+ }
+
+ /**
+ * Fetches the current position of the mark. If there is a selection,
+ * the dot and mark will not be the same.
+ *
+ * @return the position &gt;= 0
+ * @see Caret#getMark
+ */
+ public int getMark() {
+ return mark;
+ }
+
+ /**
+ * Sets the caret position and mark to the specified position,
+ * with a forward bias. This implicitly sets the
+ * selection range to zero.
+ *
+ * @param dot the position &gt;= 0
+ * @see #setDot(int, Position.Bias)
+ * @see Caret#setDot
+ */
+ public void setDot(int dot) {
+ setDot(dot, Position.Bias.Forward);
+ }
+
+ /**
+ * Moves the caret position to the specified position,
+ * with a forward bias.
+ *
+ * @param dot the position &gt;= 0
+ * @see #moveDot(int, javax.swing.text.Position.Bias)
+ * @see Caret#moveDot
+ */
+ public void moveDot(int dot) {
+ moveDot(dot, Position.Bias.Forward);
+ }
+
+ // ---- Bidi methods (we could put these in a subclass)
+
+ /**
+ * Moves the caret position to the specified position, with the
+ * specified bias.
+ *
+ * @param dot the position &gt;= 0
+ * @param dotBias the bias for this position, not <code>null</code>
+ * @throws IllegalArgumentException if the bias is <code>null</code>
+ * @see Caret#moveDot
+ * @since 1.6
+ */
+ public void moveDot(int dot, Position.Bias dotBias) {
+ if (dotBias == null) {
+ throw new IllegalArgumentException("null bias");
+ }
+
+ if (! component.isEnabled()) {
+ // don't allow selection on disabled components.
+ setDot(dot, dotBias);
+ return;
+ }
+ if (dot != this.dot) {
+ NavigationFilter filter = component.getNavigationFilter();
+
+ if (filter != null) {
+ filter.moveDot(getFilterBypass(), dot, dotBias);
+ }
+ else {
+ handleMoveDot(dot, dotBias);
+ }
+ }
+ }
+
+ void handleMoveDot(int dot, Position.Bias dotBias) {
+ changeCaretPosition(dot, dotBias);
+
+ if (selectionVisible) {
+ Highlighter h = component.getHighlighter();
+ if (h != null) {
+ int p0 = Math.min(dot, mark);
+ int p1 = Math.max(dot, mark);
+
+ // if p0 == p1 then there should be no highlight, remove it if necessary
+ if (p0 == p1) {
+ if (selectionTag != null) {
+ h.removeHighlight(selectionTag);
+ selectionTag = null;
+ }
+ // otherwise, change or add the highlight
+ } else {
+ try {
+ if (selectionTag != null) {
+ h.changeHighlight(selectionTag, p0, p1);
+ } else {
+ Highlighter.HighlightPainter p = getSelectionPainter();
+ selectionTag = h.addHighlight(p0, p1, p);
+ }
+ } catch (BadLocationException e) {
+ throw new StateInvariantError("Bad caret position");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the caret position and mark to the specified position, with the
+ * specified bias. This implicitly sets the selection range
+ * to zero.
+ *
+ * @param dot the position &gt;= 0
+ * @param dotBias the bias for this position, not <code>null</code>
+ * @throws IllegalArgumentException if the bias is <code>null</code>
+ * @see Caret#setDot
+ * @since 1.6
+ */
+ public void setDot(int dot, Position.Bias dotBias) {
+ if (dotBias == null) {
+ throw new IllegalArgumentException("null bias");
+ }
+
+ NavigationFilter filter = component.getNavigationFilter();
+
+ if (filter != null) {
+ filter.setDot(getFilterBypass(), dot, dotBias);
+ }
+ else {
+ handleSetDot(dot, dotBias);
+ }
+ }
+
+ void handleSetDot(int dot, Position.Bias dotBias) {
+ // move dot, if it changed
+ Document doc = component.getDocument();
+ if (doc != null) {
+ dot = Math.min(dot, doc.getLength());
+ }
+ dot = Math.max(dot, 0);
+
+ // The position (0,Backward) is out of range so disallow it.
+ if( dot == 0 )
+ dotBias = Position.Bias.Forward;
+
+ mark = dot;
+ if (this.dot != dot || this.dotBias != dotBias ||
+ selectionTag != null || forceCaretPositionChange) {
+ changeCaretPosition(dot, dotBias);
+ }
+ this.markBias = this.dotBias;
+ this.markLTR = dotLTR;
+ Highlighter h = component.getHighlighter();
+ if ((h != null) && (selectionTag != null)) {
+ h.removeHighlight(selectionTag);
+ selectionTag = null;
+ }
+ }
+
+ /**
+ * Returns the bias of the caret position.
+ *
+ * @return the bias of the caret position
+ * @since 1.6
+ */
+ public Position.Bias getDotBias() {
+ return dotBias;
+ }
+
+ /**
+ * Returns the bias of the mark.
+ *
+ * @return the bias of the mark
+ * @since 1.6
+ */
+ public Position.Bias getMarkBias() {
+ return markBias;
+ }
+
+ boolean isDotLeftToRight() {
+ return dotLTR;
+ }
+
+ boolean isMarkLeftToRight() {
+ return markLTR;
+ }
+
+ boolean isPositionLTR(int position, Position.Bias bias) {
+ Document doc = component.getDocument();
+ if(doc instanceof AbstractDocument ) {
+ if(bias == Position.Bias.Backward && --position < 0)
+ position = 0;
+ return ((AbstractDocument)doc).isLeftToRight(position, position);
+ }
+ return true;
+ }
+
+ Position.Bias guessBiasForOffset(int offset, Position.Bias lastBias,
+ boolean lastLTR) {
+ // There is an abiguous case here. That if your model looks like:
+ // abAB with the cursor at abB]A (visual representation of
+ // 3 forward) deleting could either become abB] or
+ // ab[B. I'ld actually prefer abB]. But, if I implement that
+ // a delete at abBA] would result in aBA] vs a[BA which I
+ // think is totally wrong. To get this right we need to know what
+ // was deleted. And we could get this from the bidi structure
+ // in the change event. So:
+ // PENDING: base this off what was deleted.
+ if(lastLTR != isPositionLTR(offset, lastBias)) {
+ lastBias = Position.Bias.Backward;
+ }
+ else if(lastBias != Position.Bias.Backward &&
+ lastLTR != isPositionLTR(offset, Position.Bias.Backward)) {
+ lastBias = Position.Bias.Backward;
+ }
+ if (lastBias == Position.Bias.Backward && offset > 0) {
+ try {
+ Segment s = new Segment();
+ component.getDocument().getText(offset - 1, 1, s);
+ if (s.count > 0 && s.array[s.offset] == '\n') {
+ lastBias = Position.Bias.Forward;
+ }
+ }
+ catch (BadLocationException ble) {}
+ }
+ return lastBias;
+ }
+
+ // ---- local methods --------------------------------------------
+
+ /**
+ * Sets the caret position (dot) to a new location. This
+ * causes the old and new location to be repainted. It
+ * also makes sure that the caret is within the visible
+ * region of the view, if the view is scrollable.
+ */
+ void changeCaretPosition(int dot, Position.Bias dotBias) {
+ // repaint the old position and set the new value of
+ // the dot.
+ repaint();
+
+
+ // Make sure the caret is visible if this window has the focus.
+ if (flasher != null && flasher.isRunning()) {
+ visible = true;
+ flasher.restart();
+ }
+
+ // notify listeners at the caret moved
+ this.dot = dot;
+ this.dotBias = dotBias;
+ dotLTR = isPositionLTR(dot, dotBias);
+ fireStateChanged();
+
+ updateSystemSelection();
+
+ setMagicCaretPosition(null);
+
+ // We try to repaint the caret later, since things
+ // may be unstable at the time this is called
+ // (i.e. we don't want to depend upon notification
+ // order or the fact that this might happen on
+ // an unsafe thread).
+ Runnable callRepaintNewCaret = new Runnable() {
+ public void run() {
+ repaintNewCaret();
+ }
+ };
+ SwingUtilities.invokeLater(callRepaintNewCaret);
+ }
+
+ /**
+ * Repaints the new caret position, with the
+ * assumption that this is happening on the
+ * event thread so that calling <code>modelToView</code>
+ * is safe.
+ */
+ void repaintNewCaret() {
+ if (component != null) {
+ TextUI mapper = component.getUI();
+ Document doc = component.getDocument();
+ if ((mapper != null) && (doc != null)) {
+ // determine the new location and scroll if
+ // not visible.
+ Rectangle newLoc;
+ try {
+ newLoc = mapper.modelToView(component, this.dot, this.dotBias);
+ } catch (BadLocationException e) {
+ newLoc = null;
+ }
+ if (newLoc != null) {
+ adjustVisibility(newLoc);
+ // If there is no magic caret position, make one
+ if (getMagicCaretPosition() == null) {
+ setMagicCaretPosition(new Point(newLoc.x, newLoc.y));
+ }
+ }
+
+ // repaint the new position
+ damage(newLoc);
+ }
+ }
+ }
+
+ private void updateSystemSelection() {
+ if ( ! SwingUtilities2.canCurrentEventAccessSystemClipboard() ) {
+ return;
+ }
+ if (this.dot != this.mark && component != null) {
+ Clipboard clip = getSystemSelection();
+ if (clip != null) {
+ String selectedText = null;
+ if (component instanceof JPasswordField
+ && component.getClientProperty("JPasswordField.cutCopyAllowed") !=
+ Boolean.TRUE) {
+ //fix for 4793761
+ StringBuffer txt = null;
+ char echoChar = ((JPasswordField)component).getEchoChar();
+ int p0 = Math.min(getDot(), getMark());
+ int p1 = Math.max(getDot(), getMark());
+ for (int i = p0; i < p1; i++) {
+ if (txt == null) {
+ txt = new StringBuffer();
+ }
+ txt.append(echoChar);
+ }
+ selectedText = (txt != null) ? txt.toString() : null;
+ } else {
+ selectedText = component.getSelectedText();
+ }
+ try {
+ clip.setContents(
+ new StringSelection(selectedText), getClipboardOwner());
+
+ ownsSelection = true;
+ } catch (IllegalStateException ise) {
+ // clipboard was unavailable
+ // no need to provide error feedback to user since updating
+ // the system selection is not a user invoked action
+ }
+ }
+ }
+ }
+
+ private Clipboard getSystemSelection() {
+ try {
+ return component.getToolkit().getSystemSelection();
+ } catch (HeadlessException he) {
+ // do nothing... there is no system clipboard
+ } catch (SecurityException se) {
+ // do nothing... there is no allowed system clipboard
+ }
+ return null;
+ }
+
+ private ClipboardOwner getClipboardOwner() {
+ return handler;
+ }
+
+ /**
+ * This is invoked after the document changes to verify the current
+ * dot/mark is valid. We do this in case the <code>NavigationFilter</code>
+ * changed where to position the dot, that resulted in the current location
+ * being bogus.
+ */
+ private void ensureValidPosition() {
+ int length = component.getDocument().getLength();
+ if (dot > length || mark > length) {
+ // Current location is bogus and filter likely vetoed the
+ // change, force the reset without giving the filter a
+ // chance at changing it.
+ handleSetDot(length, Position.Bias.Forward);
+ }
+ }
+
+
+ /**
+ * Saves the current caret position. This is used when
+ * caret up/down actions occur, moving between lines
+ * that have uneven end positions.
+ *
+ * @param p the position
+ * @see #getMagicCaretPosition
+ */
+ public void setMagicCaretPosition(Point p) {
+ magicCaretPosition = p;
+ }
+
+ /**
+ * Gets the saved caret position.
+ *
+ * @return the position
+ * see #setMagicCaretPosition
+ */
+ public Point getMagicCaretPosition() {
+ return magicCaretPosition;
+ }
+
+ /**
+ * Compares this object to the specified object.
+ * The superclass behavior of comparing rectangles
+ * is not desired, so this is changed to the Object
+ * behavior.
+ *
+ * @param obj the object to compare this font with
+ * @return <code>true</code> if the objects are equal;
+ * <code>false</code> otherwise
+ */
+ public boolean equals(Object obj) {
+ return (this == obj);
+ }
+
+ public String toString() {
+ String s = "Dot=(" + dot + ", " + dotBias + ")";
+ s += " Mark=(" + mark + ", " + markBias + ")";
+ return s;
+ }
+
+ private NavigationFilter.FilterBypass getFilterBypass() {
+ if (filterBypass == null) {
+ filterBypass = new DefaultFilterBypass();
+ }
+ return filterBypass;
+ }
+
+ // Rectangle.contains returns false if passed a rect with a w or h == 0,
+ // this won't (assuming X,Y are contained with this rectangle).
+ private boolean _contains(int X, int Y, int W, int H) {
+ int w = this.width;
+ int h = this.height;
+ if ((w | h | W | H) < 0) {
+ // At least one of the dimensions is negative...
+ return false;
+ }
+ // Note: if any dimension is zero, tests below must return false...
+ int x = this.x;
+ int y = this.y;
+ if (X < x || Y < y) {
+ return false;
+ }
+ if (W > 0) {
+ w += x;
+ W += X;
+ if (W <= X) {
+ // X+W overflowed or W was zero, return false if...
+ // either original w or W was zero or
+ // x+w did not overflow or
+ // the overflowed x+w is smaller than the overflowed X+W
+ if (w >= x || W > w) return false;
+ } else {
+ // X+W did not overflow and W was not zero, return false if...
+ // original w was zero or
+ // x+w did not overflow and x+w is smaller than X+W
+ if (w >= x && W > w) return false;
+ }
+ }
+ else if ((x + w) < X) {
+ return false;
+ }
+ if (H > 0) {
+ h += y;
+ H += Y;
+ if (H <= Y) {
+ if (h >= y || H > h) return false;
+ } else {
+ if (h >= y && H > h) return false;
+ }
+ }
+ else if ((y + h) < Y) {
+ return false;
+ }
+ return true;
+ }
+
+ int getCaretWidth(int height) {
+ if (aspectRatio > -1) {
+ return (int) (aspectRatio * height) + 1;
+ }
+
+ if (caretWidth > -1) {
+ return caretWidth;
+ }
+
+ return 1;
+ }
+
+ // --- serialization ---------------------------------------------
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException
+ {
+ s.defaultReadObject();
+ handler = new Handler();
+ if (!s.readBoolean()) {
+ dotBias = Position.Bias.Forward;
+ }
+ else {
+ dotBias = Position.Bias.Backward;
+ }
+ if (!s.readBoolean()) {
+ markBias = Position.Bias.Forward;
+ }
+ else {
+ markBias = Position.Bias.Backward;
+ }
+ }
+
+ private void writeObject(ObjectOutputStream s) throws IOException {
+ s.defaultWriteObject();
+ s.writeBoolean((dotBias == Position.Bias.Backward));
+ s.writeBoolean((markBias == Position.Bias.Backward));
+ }
+
+ // ---- member variables ------------------------------------------
+
+ /**
+ * The event listener list.
+ */
+ protected EventListenerList listenerList = new EventListenerList();
+
+ /**
+ * The change event for the model.
+ * Only one ChangeEvent is needed per model instance since the
+ * event's only (read-only) state is the source property. The source
+ * of events generated here is always "this".
+ */
+ protected transient ChangeEvent changeEvent = null;
+
+ // package-private to avoid inner classes private member
+ // access bug
+ JTextComponent component;
+
+ int updatePolicy = UPDATE_WHEN_ON_EDT;
+ boolean visible;
+ boolean active;
+ int dot;
+ int mark;
+ Object selectionTag;
+ boolean selectionVisible;
+ Timer flasher;
+ Point magicCaretPosition;
+ transient Position.Bias dotBias;
+ transient Position.Bias markBias;
+ boolean dotLTR;
+ boolean markLTR;
+ transient Handler handler = new Handler();
+ transient private int[] flagXPoints = new int[3];
+ transient private int[] flagYPoints = new int[3];
+ private transient NavigationFilter.FilterBypass filterBypass;
+ static private transient Action selectWord = null;
+ static private transient Action selectLine = null;
+ /**
+ * This is used to indicate if the caret currently owns the selection.
+ * This is always false if the system does not support the system
+ * clipboard.
+ */
+ private boolean ownsSelection;
+
+ /**
+ * If this is true, the location of the dot is updated regardless of
+ * the current location. This is set in the DocumentListener
+ * such that even if the model location of dot hasn't changed (perhaps do
+ * to a forward delete) the visual location is updated.
+ */
+ private boolean forceCaretPositionChange;
+
+ /**
+ * Whether or not mouseReleased should adjust the caret and focus.
+ * This flag is set by mousePressed if it wanted to adjust the caret
+ * and focus but couldn't because of a possible DnD operation.
+ */
+ private transient boolean shouldHandleRelease;
+
+
+ /**
+ * holds last MouseEvent which caused the word selection
+ */
+ private transient MouseEvent selectedWordEvent = null;
+
+ /**
+ * The width of the caret in pixels.
+ */
+ private int caretWidth = -1;
+ private float aspectRatio = -1;
+
+ class SafeScroller implements Runnable {
+
+ SafeScroller(Rectangle r) {
+ this.r = r;
+ }
+
+ public void run() {
+ if (component != null) {
+ component.scrollRectToVisible(r);
+ }
+ }
+
+ Rectangle r;
+ }
+
+
+ class Handler implements PropertyChangeListener, DocumentListener, ActionListener, ClipboardOwner {
+
+ // --- ActionListener methods ----------------------------------
+
+ /**
+ * Invoked when the blink timer fires. This is called
+ * asynchronously. The simply changes the visibility
+ * and repaints the rectangle that last bounded the caret.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ if (width == 0 || height == 0) {
+ // setVisible(true) will cause a scroll, only do this if the
+ // new location is really valid.
+ if (component != null) {
+ TextUI mapper = component.getUI();
+ try {
+ Rectangle r = mapper.modelToView(component, dot,
+ dotBias);
+ if (r != null && r.width != 0 && r.height != 0) {
+ damage(r);
+ }
+ } catch (BadLocationException ble) {
+ }
+ }
+ }
+ visible = !visible;
+ repaint();
+ }
+
+ // --- DocumentListener methods --------------------------------
+
+ /**
+ * Updates the dot and mark if they were changed by
+ * the insertion.
+ *
+ * @param e the document event
+ * @see DocumentListener#insertUpdate
+ */
+ public void insertUpdate(DocumentEvent e) {
+ if (getUpdatePolicy() == NEVER_UPDATE ||
+ (getUpdatePolicy() == UPDATE_WHEN_ON_EDT &&
+ !SwingUtilities.isEventDispatchThread())) {
+
+ if ((e.getOffset() <= dot || e.getOffset() <= mark)
+ && selectionTag != null) {
+ try {
+ component.getHighlighter().changeHighlight(selectionTag,
+ Math.min(dot, mark), Math.max(dot, mark));
+ } catch (BadLocationException e1) {
+ e1.printStackTrace();
+ }
+ }
+ return;
+ }
+ int adjust = 0;
+ int offset = e.getOffset();
+ int length = e.getLength();
+ int newDot = dot;
+ short changed = 0;
+
+ if (e instanceof AbstractDocument.UndoRedoDocumentEvent) {
+ setDot(offset + length);
+ return;
+ }
+ if (newDot >= offset) {
+ newDot += length;
+ changed |= 1;
+ }
+ int newMark = mark;
+ if (newMark >= offset) {
+ newMark += length;
+ changed |= 2;
+ }
+
+ if (changed != 0) {
+ Position.Bias dotBias = DefaultCaret.this.dotBias;
+ if (dot == offset) {
+ Document doc = component.getDocument();
+ boolean isNewline;
+ try {
+ Segment s = new Segment();
+ doc.getText(newDot - 1, 1, s);
+ isNewline = (s.count > 0 &&
+ s.array[s.offset] == '\n');
+ } catch (BadLocationException ble) {
+ isNewline = false;
+ }
+ if (isNewline) {
+ dotBias = Position.Bias.Forward;
+ } else {
+ dotBias = Position.Bias.Backward;
+ }
+ }
+ if (newMark == newDot) {
+ setDot(newDot, dotBias);
+ ensureValidPosition();
+ }
+ else {
+ setDot(newMark, markBias);
+ if (getDot() == newMark) {
+ // Due this test in case the filter vetoed the
+ // change in which case this probably won't be
+ // valid either.
+ moveDot(newDot, dotBias);
+ }
+ ensureValidPosition();
+ }
+ }
+ }
+
+ /**
+ * Updates the dot and mark if they were changed
+ * by the removal.
+ *
+ * @param e the document event
+ * @see DocumentListener#removeUpdate
+ */
+ public void removeUpdate(DocumentEvent e) {
+ if (getUpdatePolicy() == NEVER_UPDATE ||
+ (getUpdatePolicy() == UPDATE_WHEN_ON_EDT &&
+ !SwingUtilities.isEventDispatchThread())) {
+
+ int length = component.getDocument().getLength();
+ dot = Math.min(dot, length);
+ mark = Math.min(mark, length);
+ if ((e.getOffset() < dot || e.getOffset() < mark)
+ && selectionTag != null) {
+ try {
+ component.getHighlighter().changeHighlight(selectionTag,
+ Math.min(dot, mark), Math.max(dot, mark));
+ } catch (BadLocationException e1) {
+ e1.printStackTrace();
+ }
+ }
+ return;
+ }
+ int offs0 = e.getOffset();
+ int offs1 = offs0 + e.getLength();
+ int adjust = 0;
+ int newDot = dot;
+ boolean adjustDotBias = false;
+ int newMark = mark;
+ boolean adjustMarkBias = false;
+
+ if(e instanceof AbstractDocument.UndoRedoDocumentEvent) {
+ setDot(offs0);
+ return;
+ }
+ if (newDot >= offs1) {
+ newDot -= (offs1 - offs0);
+ if(newDot == offs1) {
+ adjustDotBias = true;
+ }
+ } else if (newDot >= offs0) {
+ newDot = offs0;
+ adjustDotBias = true;
+ }
+ if (newMark >= offs1) {
+ newMark -= (offs1 - offs0);
+ if(newMark == offs1) {
+ adjustMarkBias = true;
+ }
+ } else if (newMark >= offs0) {
+ newMark = offs0;
+ adjustMarkBias = true;
+ }
+ if (newMark == newDot) {
+ forceCaretPositionChange = true;
+ try {
+ setDot(newDot, guessBiasForOffset(newDot, dotBias,
+ dotLTR));
+ } finally {
+ forceCaretPositionChange = false;
+ }
+ ensureValidPosition();
+ } else {
+ Position.Bias dotBias = DefaultCaret.this.dotBias;
+ Position.Bias markBias = DefaultCaret.this.markBias;
+ if(adjustDotBias) {
+ dotBias = guessBiasForOffset(newDot, dotBias, dotLTR);
+ }
+ if(adjustMarkBias) {
+ markBias = guessBiasForOffset(mark, markBias, markLTR);
+ }
+ setDot(newMark, markBias);
+ if (getDot() == newMark) {
+ // Due this test in case the filter vetoed the change
+ // in which case this probably won't be valid either.
+ moveDot(newDot, dotBias);
+ }
+ ensureValidPosition();
+ }
+ }
+
+ /**
+ * Gives notification that an attribute or set of attributes changed.
+ *
+ * @param e the document event
+ * @see DocumentListener#changedUpdate
+ */
+ public void changedUpdate(DocumentEvent e) {
+ if (getUpdatePolicy() == NEVER_UPDATE ||
+ (getUpdatePolicy() == UPDATE_WHEN_ON_EDT &&
+ !SwingUtilities.isEventDispatchThread())) {
+ return;
+ }
+ if(e instanceof AbstractDocument.UndoRedoDocumentEvent) {
+ setDot(e.getOffset() + e.getLength());
+ }
+ }
+
+ // --- PropertyChangeListener methods -----------------------
+
+ /**
+ * This method gets called when a bound property is changed.
+ * We are looking for document changes on the editor.
+ */
+ public void propertyChange(PropertyChangeEvent evt) {
+ Object oldValue = evt.getOldValue();
+ Object newValue = evt.getNewValue();
+ if ((oldValue instanceof Document) || (newValue instanceof Document)) {
+ setDot(0);
+ if (oldValue != null) {
+ ((Document)oldValue).removeDocumentListener(this);
+ }
+ if (newValue != null) {
+ ((Document)newValue).addDocumentListener(this);
+ }
+ } else if("enabled".equals(evt.getPropertyName())) {
+ Boolean enabled = (Boolean) evt.getNewValue();
+ if(component.isFocusOwner()) {
+ if(enabled == Boolean.TRUE) {
+ if(component.isEditable()) {
+ setVisible(true);
+ }
+ setSelectionVisible(true);
+ } else {
+ setVisible(false);
+ setSelectionVisible(false);
+ }
+ }
+ } else if("caretWidth".equals(evt.getPropertyName())) {
+ Integer newWidth = (Integer) evt.getNewValue();
+ if (newWidth != null) {
+ caretWidth = newWidth.intValue();
+ } else {
+ caretWidth = -1;
+ }
+ repaint();
+ } else if("caretAspectRatio".equals(evt.getPropertyName())) {
+ Number newRatio = (Number) evt.getNewValue();
+ if (newRatio != null) {
+ aspectRatio = newRatio.floatValue();
+ } else {
+ aspectRatio = -1;
+ }
+ repaint();
+ }
+ }
+
+
+ //
+ // ClipboardOwner
+ //
+ /**
+ * Toggles the visibility of the selection when ownership is lost.
+ */
+ public void lostOwnership(Clipboard clipboard,
+ Transferable contents) {
+ if (ownsSelection) {
+ ownsSelection = false;
+ if (component != null && !component.hasFocus()) {
+ setSelectionVisible(false);
+ }
+ }
+ }
+ }
+
+
+ private class DefaultFilterBypass extends NavigationFilter.FilterBypass {
+ public Caret getCaret() {
+ return DefaultCaret.this;
+ }
+
+ public void setDot(int dot, Position.Bias bias) {
+ handleSetDot(dot, bias);
+ }
+
+ public void moveDot(int dot, Position.Bias bias) {
+ handleMoveDot(dot, bias);
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/DefaultEditorKit.java b/src/share/classes/javax/swing/text/DefaultEditorKit.java
new file mode 100644
index 000000000..32c5c1fda
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DefaultEditorKit.java
@@ -0,0 +1,2306 @@
+/*
+ * 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;
+
+import java.io.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.text.*;
+import javax.swing.Action;
+import javax.swing.KeyStroke;
+import javax.swing.SwingConstants;
+import javax.swing.UIManager;
+
+/**
+ * This is the set of things needed by a text component
+ * to be a reasonably functioning editor for some <em>type</em>
+ * of text document. This implementation provides a default
+ * implementation which treats text as plain text and
+ * provides a minimal set of actions for a simple editor.
+ * <p>
+ * <dl>
+ * <dt><b><font size=+1>Newlines</font></b>
+ * <dd>
+ * There are two properties which deal with newlines. The
+ * system property, <code>line.separator</code>, is defined to be
+ * platform-dependent, either "\n", "\r", or "\r\n". There is also
+ * a property defined in <code>DefaultEditorKit</code>, called
+ * <a href=#EndOfLineStringProperty><code>EndOfLineStringProperty</code></a>,
+ * which is defined automatically when a document is loaded, to be
+ * the first occurrence of any of the newline characters.
+ * When a document is loaded, <code>EndOfLineStringProperty</code>
+ * is set appropriately, and when the document is written back out, the
+ * <code>EndOfLineStringProperty</code> is used. But while the document
+ * is in memory, the "\n" character is used to define a
+ * newline, regardless of how the newline is defined when
+ * the document is on disk. Therefore, for searching purposes,
+ * "\n" should always be used. When a new document is created,
+ * and the <code>EndOfLineStringProperty</code> has not been defined,
+ * it will use the System property when writing out the
+ * document.
+ * <p>Note that <code>EndOfLineStringProperty</code> is set
+ * on the <code>Document</code> using the <code>get/putProperty</code>
+ * methods. Subclasses may override this behavior.
+ *
+ * </dl>
+ *
+ * @author Timothy Prinzing
+ */
+public class DefaultEditorKit extends EditorKit {
+
+ /**
+ * default constructor for DefaultEditorKit
+ */
+ public DefaultEditorKit() {
+ }
+
+ /**
+ * Gets the MIME type of the data that this
+ * kit represents support for. The default
+ * is <code>text/plain</code>.
+ *
+ * @return the type
+ */
+ public String getContentType() {
+ return "text/plain";
+ }
+
+ /**
+ * Fetches a factory that is suitable for producing
+ * views of any models that are produced by this
+ * kit. The default is to have the UI produce the
+ * factory, so this method has no implementation.
+ *
+ * @return the view factory
+ */
+ public ViewFactory getViewFactory() {
+ return null;
+ }
+
+ /**
+ * Fetches the set of commands that can be used
+ * on a text component that is using a model and
+ * view produced by this kit.
+ *
+ * @return the command list
+ */
+ public Action[] getActions() {
+ return defaultActions;
+ }
+
+ /**
+ * Fetches a caret that can navigate through views
+ * produced by the associated ViewFactory.
+ *
+ * @return the caret
+ */
+ public Caret createCaret() {
+ return null;
+ }
+
+ /**
+ * Creates an uninitialized text storage model (PlainDocument)
+ * that is appropriate for this type of editor.
+ *
+ * @return the model
+ */
+ public Document createDefaultDocument() {
+ return new PlainDocument();
+ }
+
+ /**
+ * Inserts content from the given stream which is expected
+ * to be in a format appropriate for this kind of content
+ * handler.
+ *
+ * @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 >= 0.
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document.
+ */
+ public void read(InputStream in, Document doc, int pos)
+ throws IOException, BadLocationException {
+
+ read(new InputStreamReader(in), doc, pos);
+ }
+
+ /**
+ * Writes 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 >= 0.
+ * @param len The amount to write out >= 0.
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document.
+ */
+ public void write(OutputStream out, Document doc, int pos, int len)
+ throws IOException, BadLocationException {
+ OutputStreamWriter osw = new OutputStreamWriter(out);
+
+ write(osw, doc, pos, len);
+ osw.flush();
+ }
+
+ /**
+ * Gets the input attributes for the pane. This method exists for
+ * the benefit of StyledEditorKit so that the read method will
+ * pick up the correct attributes to apply to inserted text.
+ * This class's implementation simply returns null.
+ *
+ * @return null
+ */
+ MutableAttributeSet getInputAttributes() {
+ return null;
+ }
+
+ /**
+ * Inserts content from the given stream, which will be
+ * treated as plain text.
+ *
+ * @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 >= 0.
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document.
+ */
+ public void read(Reader in, Document doc, int pos)
+ throws IOException, BadLocationException {
+
+ char[] buff = new char[4096];
+ int nch;
+ boolean lastWasCR = false;
+ boolean isCRLF = false;
+ boolean isCR = false;
+ int last;
+ boolean wasEmpty = (doc.getLength() == 0);
+ AttributeSet attr = getInputAttributes();
+
+ // Read in a block at a time, mapping \r\n to \n, as well as single
+ // \r's to \n's. If a \r\n is encountered, \r\n will be set as the
+ // newline string for the document, if \r is encountered it will
+ // be set as the newline character, otherwise the newline property
+ // for the document will be removed.
+ while ((nch = in.read(buff, 0, buff.length)) != -1) {
+ last = 0;
+ for(int counter = 0; counter < nch; counter++) {
+ switch(buff[counter]) {
+ case '\r':
+ if (lastWasCR) {
+ isCR = true;
+ if (counter == 0) {
+ doc.insertString(pos, "\n", attr);
+ pos++;
+ }
+ else {
+ buff[counter - 1] = '\n';
+ }
+ }
+ else {
+ lastWasCR = true;
+ }
+ break;
+ case '\n':
+ if (lastWasCR) {
+ if (counter > (last + 1)) {
+ doc.insertString(pos, new String(buff, last,
+ counter - last - 1), attr);
+ pos += (counter - last - 1);
+ }
+ // else nothing to do, can skip \r, next write will
+ // write \n
+ lastWasCR = false;
+ last = counter;
+ isCRLF = true;
+ }
+ break;
+ default:
+ if (lastWasCR) {
+ isCR = true;
+ if (counter == 0) {
+ doc.insertString(pos, "\n", attr);
+ pos++;
+ }
+ else {
+ buff[counter - 1] = '\n';
+ }
+ lastWasCR = false;
+ }
+ break;
+ }
+ }
+ if (last < nch) {
+ if(lastWasCR) {
+ if (last < (nch - 1)) {
+ doc.insertString(pos, new String(buff, last,
+ nch - last - 1), attr);
+ pos += (nch - last - 1);
+ }
+ }
+ else {
+ doc.insertString(pos, new String(buff, last,
+ nch - last), attr);
+ pos += (nch - last);
+ }
+ }
+ }
+ if (lastWasCR) {
+ doc.insertString(pos, "\n", attr);
+ isCR = true;
+ }
+ if (wasEmpty) {
+ if (isCRLF) {
+ doc.putProperty(EndOfLineStringProperty, "\r\n");
+ }
+ else if (isCR) {
+ doc.putProperty(EndOfLineStringProperty, "\r");
+ }
+ else {
+ doc.putProperty(EndOfLineStringProperty, "\n");
+ }
+ }
+ }
+
+ /**
+ * Writes content from a document to the given stream
+ * as plain text.
+ *
+ * @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 from >= 0.
+ * @param len The amount to write out >= 0.
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos is not within 0 and
+ * the length of the document.
+ */
+ public void write(Writer out, Document doc, int pos, int len)
+ throws IOException, BadLocationException {
+
+ if ((pos < 0) || ((pos + len) > doc.getLength())) {
+ throw new BadLocationException("DefaultEditorKit.write", pos);
+ }
+ Segment data = new Segment();
+ int nleft = len;
+ int offs = pos;
+ Object endOfLineProperty = doc.getProperty(EndOfLineStringProperty);
+ if (endOfLineProperty == null) {
+ try {
+ endOfLineProperty = System.getProperty("line.separator");
+ } catch (SecurityException se) { }
+ }
+ String endOfLine;
+ if (endOfLineProperty instanceof String) {
+ endOfLine = (String)endOfLineProperty;
+ }
+ else {
+ endOfLine = null;
+ }
+ if (endOfLineProperty != null && !endOfLine.equals("\n")) {
+ // There is an end of line string that isn't \n, have to iterate
+ // through and find all \n's and translate to end of line string.
+ while (nleft > 0) {
+ int n = Math.min(nleft, 4096);
+ doc.getText(offs, n, data);
+ int last = data.offset;
+ char[] array = data.array;
+ int maxCounter = last + data.count;
+ for (int counter = last; counter < maxCounter; counter++) {
+ if (array[counter] == '\n') {
+ if (counter > last) {
+ out.write(array, last, counter - last);
+ }
+ out.write(endOfLine);
+ last = counter + 1;
+ }
+ }
+ if (maxCounter > last) {
+ out.write(array, last, maxCounter - last);
+ }
+ offs += n;
+ nleft -= n;
+ }
+ }
+ else {
+ // Just write out text, will already have \n, no mapping to
+ // do.
+ while (nleft > 0) {
+ int n = Math.min(nleft, 4096);
+ doc.getText(offs, n, data);
+ out.write(data.array, data.offset, data.count);
+ offs += n;
+ nleft -= n;
+ }
+ }
+ out.flush();
+ }
+
+
+ /**
+ * When reading a document if a CRLF is encountered a property
+ * with this name is added and the value will be "\r\n".
+ */
+ public static final String EndOfLineStringProperty = "__EndOfLine__";
+
+ // --- names of well-known actions ---------------------------
+
+ /**
+ * Name of the action to place content into the associated
+ * document. If there is a selection, it is removed before
+ * the new content is added.
+ * @see #getActions
+ */
+ public static final String insertContentAction = "insert-content";
+
+ /**
+ * Name of the action to place a line/paragraph break into
+ * the document. If there is a selection, it is removed before
+ * the break is added.
+ * @see #getActions
+ */
+ public static final String insertBreakAction = "insert-break";
+
+ /**
+ * Name of the action to place a tab character into
+ * the document. If there is a selection, it is removed before
+ * the tab is added.
+ * @see #getActions
+ */
+ public static final String insertTabAction = "insert-tab";
+
+ /**
+ * Name of the action to delete the character of content that
+ * precedes the current caret position.
+ * @see #getActions
+ */
+ public static final String deletePrevCharAction = "delete-previous";
+
+ /**
+ * Name of the action to delete the character of content that
+ * follows the current caret position.
+ * @see #getActions
+ */
+ public static final String deleteNextCharAction = "delete-next";
+
+ /**
+ * Name of the action to delete the word that
+ * follows the beginning of the selection.
+ * @see #getActions
+ * @see JTextComponent#getSelectionStart
+ * @since 1.6
+ */
+ public static final String deleteNextWordAction = "delete-next-word";
+
+ /**
+ * Name of the action to delete the word that
+ * precedes the beginning of the selection.
+ * @see #getActions
+ * @see JTextComponent#getSelectionStart
+ * @since 1.6
+ */
+ public static final String deletePrevWordAction = "delete-previous-word";
+
+ /**
+ * Name of the action to set the editor into read-only
+ * mode.
+ * @see #getActions
+ */
+ public static final String readOnlyAction = "set-read-only";
+
+ /**
+ * Name of the action to set the editor into writeable
+ * mode.
+ * @see #getActions
+ */
+ public static final String writableAction = "set-writable";
+
+ /**
+ * Name of the action to cut the selected region
+ * and place the contents into the system clipboard.
+ * @see JTextComponent#cut
+ * @see #getActions
+ */
+ public static final String cutAction = "cut-to-clipboard";
+
+ /**
+ * Name of the action to copy the selected region
+ * and place the contents into the system clipboard.
+ * @see JTextComponent#copy
+ * @see #getActions
+ */
+ public static final String copyAction = "copy-to-clipboard";
+
+ /**
+ * Name of the action to paste the contents of the
+ * system clipboard into the selected region, or before the
+ * caret if nothing is selected.
+ * @see JTextComponent#paste
+ * @see #getActions
+ */
+ public static final String pasteAction = "paste-from-clipboard";
+
+ /**
+ * Name of the action to create a beep.
+ * @see #getActions
+ */
+ public static final String beepAction = "beep";
+
+ /**
+ * Name of the action to page up vertically.
+ * @see #getActions
+ */
+ public static final String pageUpAction = "page-up";
+
+ /**
+ * Name of the action to page down vertically.
+ * @see #getActions
+ */
+ public static final String pageDownAction = "page-down";
+
+ /**
+ * Name of the action to page up vertically, and move the
+ * selection.
+ * @see #getActions
+ */
+ /*public*/ static final String selectionPageUpAction = "selection-page-up";
+
+ /**
+ * Name of the action to page down vertically, and move the
+ * selection.
+ * @see #getActions
+ */
+ /*public*/ static final String selectionPageDownAction = "selection-page-down";
+
+ /**
+ * Name of the action to page left horizontally, and move the
+ * selection.
+ * @see #getActions
+ */
+ /*public*/ static final String selectionPageLeftAction = "selection-page-left";
+
+ /**
+ * Name of the action to page right horizontally, and move the
+ * selection.
+ * @see #getActions
+ */
+ /*public*/ static final String selectionPageRightAction = "selection-page-right";
+
+ /**
+ * Name of the Action for moving the caret
+ * logically forward one position.
+ * @see #getActions
+ */
+ public static final String forwardAction = "caret-forward";
+
+ /**
+ * Name of the Action for moving the caret
+ * logically backward one position.
+ * @see #getActions
+ */
+ public static final String backwardAction = "caret-backward";
+
+ /**
+ * Name of the Action for extending the selection
+ * by moving the caret logically forward one position.
+ * @see #getActions
+ */
+ public static final String selectionForwardAction = "selection-forward";
+
+ /**
+ * Name of the Action for extending the selection
+ * by moving the caret logically backward one position.
+ * @see #getActions
+ */
+ public static final String selectionBackwardAction = "selection-backward";
+
+ /**
+ * Name of the Action for moving the caret
+ * logically upward one position.
+ * @see #getActions
+ */
+ public static final String upAction = "caret-up";
+
+ /**
+ * Name of the Action for moving the caret
+ * logically downward one position.
+ * @see #getActions
+ */
+ public static final String downAction = "caret-down";
+
+ /**
+ * Name of the Action for moving the caret
+ * logically upward one position, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionUpAction = "selection-up";
+
+ /**
+ * Name of the Action for moving the caret
+ * logically downward one position, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionDownAction = "selection-down";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the beginning of a word.
+ * @see #getActions
+ */
+ public static final String beginWordAction = "caret-begin-word";
+
+ /**
+ * Name of the Action for moving the caret
+ * to the end of a word.
+ * @see #getActions
+ */
+ public static final String endWordAction = "caret-end-word";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the beginning of a word, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionBeginWordAction = "selection-begin-word";
+
+ /**
+ * Name of the Action for moving the caret
+ * to the end of a word, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionEndWordAction = "selection-end-word";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret to the
+ * beginning of the previous word.
+ * @see #getActions
+ */
+ public static final String previousWordAction = "caret-previous-word";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret to the
+ * beginning of the next word.
+ * @see #getActions
+ */
+ public static final String nextWordAction = "caret-next-word";
+
+ /**
+ * Name of the <code>Action</code> for moving the selection to the
+ * beginning of the previous word, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionPreviousWordAction = "selection-previous-word";
+
+ /**
+ * Name of the <code>Action</code> for moving the selection to the
+ * beginning of the next word, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionNextWordAction = "selection-next-word";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the beginning of a line.
+ * @see #getActions
+ */
+ public static final String beginLineAction = "caret-begin-line";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the end of a line.
+ * @see #getActions
+ */
+ public static final String endLineAction = "caret-end-line";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the beginning of a line, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionBeginLineAction = "selection-begin-line";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the end of a line, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionEndLineAction = "selection-end-line";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the beginning of a paragraph.
+ * @see #getActions
+ */
+ public static final String beginParagraphAction = "caret-begin-paragraph";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the end of a paragraph.
+ * @see #getActions
+ */
+ public static final String endParagraphAction = "caret-end-paragraph";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the beginning of a paragraph, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionBeginParagraphAction = "selection-begin-paragraph";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the end of a paragraph, extending the selection.
+ * @see #getActions
+ */
+ public static final String selectionEndParagraphAction = "selection-end-paragraph";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the beginning of the document.
+ * @see #getActions
+ */
+ public static final String beginAction = "caret-begin";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the end of the document.
+ * @see #getActions
+ */
+ public static final String endAction = "caret-end";
+
+ /**
+ * Name of the <code>Action</code> for moving the caret
+ * to the beginning of the document.
+ * @see #getActions
+ */
+ public static final String selectionBeginAction = "selection-begin";
+
+ /**
+ * Name of the Action for moving the caret
+ * to the end of the document.
+ * @see #getActions
+ */
+ public static final String selectionEndAction = "selection-end";
+
+ /**
+ * Name of the Action for selecting a word around the caret.
+ * @see #getActions
+ */
+ public static final String selectWordAction = "select-word";
+
+ /**
+ * Name of the Action for selecting a line around the caret.
+ * @see #getActions
+ */
+ public static final String selectLineAction = "select-line";
+
+ /**
+ * Name of the Action for selecting a paragraph around the caret.
+ * @see #getActions
+ */
+ public static final String selectParagraphAction = "select-paragraph";
+
+ /**
+ * Name of the Action for selecting the entire document
+ * @see #getActions
+ */
+ public static final String selectAllAction = "select-all";
+
+ /**
+ * Name of the Action for removing selection
+ * @see #getActions
+ */
+ /*public*/ static final String unselectAction = "unselect";
+
+ /**
+ * Name of the Action for toggling the component's orientation.
+ * @see #getActions
+ */
+ /*public*/ static final String toggleComponentOrientationAction
+ = "toggle-componentOrientation";
+
+ /**
+ * Name of the action that is executed by default if
+ * a <em>key typed event</em> is received and there
+ * is no keymap entry.
+ * @see #getActions
+ */
+ public static final String defaultKeyTypedAction = "default-typed";
+
+ // --- Action implementations ---------------------------------
+
+ private static final Action[] defaultActions = {
+ new InsertContentAction(), new DeletePrevCharAction(),
+ new DeleteNextCharAction(), new ReadOnlyAction(),
+ new DeleteWordAction(deletePrevWordAction),
+ new DeleteWordAction(deleteNextWordAction),
+ new WritableAction(), new CutAction(),
+ new CopyAction(), new PasteAction(),
+ new VerticalPageAction(pageUpAction, -1, false),
+ new VerticalPageAction(pageDownAction, 1, false),
+ new VerticalPageAction(selectionPageUpAction, -1, true),
+ new VerticalPageAction(selectionPageDownAction, 1, true),
+ new PageAction(selectionPageLeftAction, true, true),
+ new PageAction(selectionPageRightAction, false, true),
+ new InsertBreakAction(), new BeepAction(),
+ new NextVisualPositionAction(forwardAction, false,
+ SwingConstants.EAST),
+ new NextVisualPositionAction(backwardAction, false,
+ SwingConstants.WEST),
+ new NextVisualPositionAction(selectionForwardAction, true,
+ SwingConstants.EAST),
+ new NextVisualPositionAction(selectionBackwardAction, true,
+ SwingConstants.WEST),
+ new NextVisualPositionAction(upAction, false,
+ SwingConstants.NORTH),
+ new NextVisualPositionAction(downAction, false,
+ SwingConstants.SOUTH),
+ new NextVisualPositionAction(selectionUpAction, true,
+ SwingConstants.NORTH),
+ new NextVisualPositionAction(selectionDownAction, true,
+ SwingConstants.SOUTH),
+ new BeginWordAction(beginWordAction, false),
+ new EndWordAction(endWordAction, false),
+ new BeginWordAction(selectionBeginWordAction, true),
+ new EndWordAction(selectionEndWordAction, true),
+ new PreviousWordAction(previousWordAction, false),
+ new NextWordAction(nextWordAction, false),
+ new PreviousWordAction(selectionPreviousWordAction, true),
+ new NextWordAction(selectionNextWordAction, true),
+ new BeginLineAction(beginLineAction, false),
+ new EndLineAction(endLineAction, false),
+ new BeginLineAction(selectionBeginLineAction, true),
+ new EndLineAction(selectionEndLineAction, true),
+ new BeginParagraphAction(beginParagraphAction, false),
+ new EndParagraphAction(endParagraphAction, false),
+ new BeginParagraphAction(selectionBeginParagraphAction, true),
+ new EndParagraphAction(selectionEndParagraphAction, true),
+ new BeginAction(beginAction, false),
+ new EndAction(endAction, false),
+ new BeginAction(selectionBeginAction, true),
+ new EndAction(selectionEndAction, true),
+ new DefaultKeyTypedAction(), new InsertTabAction(),
+ new SelectWordAction(), new SelectLineAction(),
+ new SelectParagraphAction(), new SelectAllAction(),
+ new UnselectAction(), new ToggleComponentOrientationAction(),
+ new DumpModelAction()
+ };
+
+ /**
+ * The action that is executed by default if
+ * a <em>key typed event</em> is received and there
+ * is no keymap entry. There is a variation across
+ * different VM's in what gets sent as a <em>key typed</em>
+ * event, and this action tries to filter out the undesired
+ * events. This filters the control characters and those
+ * with the ALT modifier. It allows Control-Alt sequences
+ * through as these form legitimate unicode characters on
+ * some PC keyboards.
+ * <p>
+ * If the event doesn't get filtered, it will try to insert
+ * content into the text editor. The content is fetched
+ * from the command string of the ActionEvent. The text
+ * entry is done through the <code>replaceSelection</code>
+ * method on the target text component. This is the
+ * action that will be fired for most text entry tasks.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see DefaultEditorKit#defaultKeyTypedAction
+ * @see DefaultEditorKit#getActions
+ * @see Keymap#setDefaultAction
+ * @see Keymap#getDefaultAction
+ */
+ public static class DefaultKeyTypedAction extends TextAction {
+
+ /**
+ * Creates this object with the appropriate identifier.
+ */
+ public DefaultKeyTypedAction() {
+ super(defaultKeyTypedAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if ((target != null) && (e != null)) {
+ if ((! target.isEditable()) || (! target.isEnabled())) {
+ return;
+ }
+ String content = e.getActionCommand();
+ int mod = e.getModifiers();
+ if ((content != null) && (content.length() > 0) &&
+ ((mod & ActionEvent.ALT_MASK) == (mod & ActionEvent.CTRL_MASK))) {
+ char c = content.charAt(0);
+ if ((c >= 0x20) && (c != 0x7F)) {
+ target.replaceSelection(content);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Places content into the associated document.
+ * If there is a selection, it is removed before
+ * the new content is added.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see DefaultEditorKit#insertContentAction
+ * @see DefaultEditorKit#getActions
+ */
+ public static class InsertContentAction extends TextAction {
+
+ /**
+ * Creates this object with the appropriate identifier.
+ */
+ public InsertContentAction() {
+ super(insertContentAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if ((target != null) && (e != null)) {
+ if ((! target.isEditable()) || (! target.isEnabled())) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ return;
+ }
+ String content = e.getActionCommand();
+ if (content != null) {
+ target.replaceSelection(content);
+ } else {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+ }
+
+ /**
+ * Places a line/paragraph break into the document.
+ * If there is a selection, it is removed before
+ * the break is added.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see DefaultEditorKit#insertBreakAction
+ * @see DefaultEditorKit#getActions
+ */
+ public static class InsertBreakAction extends TextAction {
+
+ /**
+ * Creates this object with the appropriate identifier.
+ */
+ public InsertBreakAction() {
+ super(insertBreakAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ if ((! target.isEditable()) || (! target.isEnabled())) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ return;
+ }
+ target.replaceSelection("\n");
+ }
+ }
+ }
+
+ /**
+ * Places a tab character into the document. If there
+ * is a selection, it is removed before the tab is added.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see DefaultEditorKit#insertTabAction
+ * @see DefaultEditorKit#getActions
+ */
+ public static class InsertTabAction extends TextAction {
+
+ /**
+ * Creates this object with the appropriate identifier.
+ */
+ public InsertTabAction() {
+ super(insertTabAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ if ((! target.isEditable()) || (! target.isEnabled())) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ return;
+ }
+ target.replaceSelection("\t");
+ }
+ }
+ }
+
+ /*
+ * Deletes the character of content that precedes the
+ * current caret position.
+ * @see DefaultEditorKit#deletePrevCharAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class DeletePrevCharAction extends TextAction {
+
+ /**
+ * Creates this object with the appropriate identifier.
+ */
+ DeletePrevCharAction() {
+ super(deletePrevCharAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ boolean beep = true;
+ if ((target != null) && (target.isEditable())) {
+ try {
+ Document doc = target.getDocument();
+ Caret caret = target.getCaret();
+ int dot = caret.getDot();
+ int mark = caret.getMark();
+ if (dot != mark) {
+ doc.remove(Math.min(dot, mark), Math.abs(dot - mark));
+ beep = false;
+ } else if (dot > 0) {
+ int delChars = 1;
+
+ if (dot > 1) {
+ String dotChars = doc.getText(dot - 2, 2);
+ char c0 = dotChars.charAt(0);
+ char c1 = dotChars.charAt(1);
+
+ if (c0 >= '\uD800' && c0 <= '\uDBFF' &&
+ c1 >= '\uDC00' && c1 <= '\uDFFF') {
+ delChars = 2;
+ }
+ }
+
+ doc.remove(dot - delChars, delChars);
+ beep = false;
+ }
+ } catch (BadLocationException bl) {
+ }
+ }
+ if (beep) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+
+ /*
+ * Deletes the character of content that follows the
+ * current caret position.
+ * @see DefaultEditorKit#deleteNextCharAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class DeleteNextCharAction extends TextAction {
+
+ /* Create this object with the appropriate identifier. */
+ DeleteNextCharAction() {
+ super(deleteNextCharAction);
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ boolean beep = true;
+ if ((target != null) && (target.isEditable())) {
+ try {
+ Document doc = target.getDocument();
+ Caret caret = target.getCaret();
+ int dot = caret.getDot();
+ int mark = caret.getMark();
+ if (dot != mark) {
+ doc.remove(Math.min(dot, mark), Math.abs(dot - mark));
+ beep = false;
+ } else if (dot < doc.getLength()) {
+ int delChars = 1;
+
+ if (dot < doc.getLength() - 1) {
+ String dotChars = doc.getText(dot, 2);
+ char c0 = dotChars.charAt(0);
+ char c1 = dotChars.charAt(1);
+
+ if (c0 >= '\uD800' && c0 <= '\uDBFF' &&
+ c1 >= '\uDC00' && c1 <= '\uDFFF') {
+ delChars = 2;
+ }
+ }
+
+ doc.remove(dot, delChars);
+ beep = false;
+ }
+ } catch (BadLocationException bl) {
+ }
+ }
+ if (beep) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+
+
+ /*
+ * Deletes the word that precedes/follows the beginning of the selection.
+ * @see DefaultEditorKit#getActions
+ */
+ static class DeleteWordAction extends TextAction {
+ DeleteWordAction(String name) {
+ super(name);
+ assert (name == deletePrevWordAction)
+ || (name == deleteNextWordAction);
+ }
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ final JTextComponent target = getTextComponent(e);
+ if ((target != null) && (e != null)) {
+ if ((! target.isEditable()) || (! target.isEnabled())) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ return;
+ }
+ boolean beep = true;
+ try {
+ final int start = target.getSelectionStart();
+ final Element line =
+ Utilities.getParagraphElement(target, start);
+ int end;
+ if (deleteNextWordAction == getValue(Action.NAME)) {
+ end = Utilities.
+ getNextWordInParagraph(target, line, start, false);
+ if (end == java.text.BreakIterator.DONE) {
+ //last word in the paragraph
+ final int endOfLine = line.getEndOffset();
+ if (start == endOfLine - 1) {
+ //for last position remove last \n
+ end = endOfLine;
+ } else {
+ //remove to the end of the paragraph
+ end = endOfLine - 1;
+ }
+ }
+ } else {
+ end = Utilities.
+ getPrevWordInParagraph(target, line, start);
+ if (end == java.text.BreakIterator.DONE) {
+ //there is no previous word in the paragraph
+ final int startOfLine = line.getStartOffset();
+ if (start == startOfLine) {
+ //for first position remove previous \n
+ end = startOfLine - 1;
+ } else {
+ //remove to the start of the paragraph
+ end = startOfLine;
+ }
+ }
+ }
+ int offs = Math.min(start, end);
+ int len = Math.abs(end - start);
+ if (offs >= 0) {
+ target.getDocument().remove(offs, len);
+ beep = false;
+ }
+ } catch (BadLocationException ignore) {
+ }
+ if (beep) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+ }
+
+
+ /*
+ * Sets the editor into read-only mode.
+ * @see DefaultEditorKit#readOnlyAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class ReadOnlyAction extends TextAction {
+
+ /* Create this object with the appropriate identifier. */
+ ReadOnlyAction() {
+ super(readOnlyAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ target.setEditable(false);
+ }
+ }
+ }
+
+ /*
+ * Sets the editor into writeable mode.
+ * @see DefaultEditorKit#writableAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class WritableAction extends TextAction {
+
+ /* Create this object with the appropriate identifier. */
+ WritableAction() {
+ super(writableAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ target.setEditable(true);
+ }
+ }
+ }
+
+ /**
+ * Cuts the selected region and place its contents
+ * into the system clipboard.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see DefaultEditorKit#cutAction
+ * @see DefaultEditorKit#getActions
+ */
+ public static class CutAction extends TextAction {
+
+ /** Create this object with the appropriate identifier. */
+ public CutAction() {
+ super(cutAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ target.cut();
+ }
+ }
+ }
+
+ /**
+ * Copies the selected region and place its contents
+ * into the system clipboard.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see DefaultEditorKit#copyAction
+ * @see DefaultEditorKit#getActions
+ */
+ public static class CopyAction extends TextAction {
+
+ /** Create this object with the appropriate identifier. */
+ public CopyAction() {
+ super(copyAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ target.copy();
+ }
+ }
+ }
+
+ /**
+ * Pastes the contents of the system clipboard into the
+ * selected region, or before the caret if nothing is
+ * selected.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see DefaultEditorKit#pasteAction
+ * @see DefaultEditorKit#getActions
+ */
+ public static class PasteAction extends TextAction {
+
+ /** Create this object with the appropriate identifier. */
+ public PasteAction() {
+ super(pasteAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ target.paste();
+ }
+ }
+ }
+
+ /**
+ * Creates a beep.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see DefaultEditorKit#beepAction
+ * @see DefaultEditorKit#getActions
+ */
+ public static class BeepAction extends TextAction {
+
+ /** Create this object with the appropriate identifier. */
+ public BeepAction() {
+ super(beepAction);
+ }
+
+ /**
+ * The operation to perform when this action is triggered.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+
+ /**
+ * Scrolls up/down vertically. The select version of this action extends
+ * the selection, instead of simply moving the caret.
+ *
+ * @see DefaultEditorKit#pageUpAction
+ * @see DefaultEditorKit#pageDownAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class VerticalPageAction extends TextAction {
+
+ /** Create this object with the appropriate identifier. */
+ public VerticalPageAction(String nm, int direction, boolean select) {
+ super(nm);
+ this.select = select;
+ this.direction = direction;
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ Rectangle visible = target.getVisibleRect();
+ Rectangle newVis = new Rectangle(visible);
+ int selectedIndex = target.getCaretPosition();
+ int scrollAmount = direction *
+ target.getScrollableBlockIncrement(
+ visible, SwingConstants.VERTICAL, direction);
+ int initialY = visible.y;
+ Caret caret = target.getCaret();
+ Point magicPosition = caret.getMagicCaretPosition();
+
+ if (selectedIndex != -1) {
+ try {
+ Rectangle dotBounds = target.modelToView(
+ selectedIndex);
+ int x = (magicPosition != null) ? magicPosition.x :
+ dotBounds.x;
+ int h = dotBounds.height;
+ if (h > 0) {
+ // We want to scroll by a multiple of caret height,
+ // rounding towards lower integer
+ scrollAmount = scrollAmount / h * h;
+ }
+ newVis.y = constrainY(target,
+ initialY + scrollAmount, visible.height);
+
+ int newIndex;
+
+ if (visible.contains(dotBounds.x, dotBounds.y)) {
+ // Dot is currently visible, base the new
+ // location off the old, or
+ newIndex = target.viewToModel(
+ new Point(x, constrainY(target,
+ dotBounds.y + scrollAmount, 0)));
+ }
+ else {
+ // Dot isn't visible, choose the top or the bottom
+ // for the new location.
+ if (direction == -1) {
+ newIndex = target.viewToModel(new Point(
+ x, newVis.y));
+ }
+ else {
+ newIndex = target.viewToModel(new Point(
+ x, newVis.y + visible.height));
+ }
+ }
+ newIndex = constrainOffset(target, newIndex);
+ if (newIndex != selectedIndex) {
+ // Make sure the new visible location contains
+ // the location of dot, otherwise Caret will
+ // cause an additional scroll.
+ adjustScrollIfNecessary(target, newVis, initialY,
+ newIndex);
+ if (select) {
+ target.moveCaretPosition(newIndex);
+ }
+ else {
+ target.setCaretPosition(newIndex);
+ }
+ }
+ } catch (BadLocationException ble) { }
+ } else {
+ newVis.y = constrainY(target,
+ initialY + scrollAmount, visible.height);
+ }
+ if (magicPosition != null) {
+ caret.setMagicCaretPosition(magicPosition);
+ }
+ target.scrollRectToVisible(newVis);
+ }
+ }
+
+ /**
+ * Makes sure <code>y</code> is a valid location in
+ * <code>target</code>.
+ */
+ private int constrainY(JTextComponent target, int y, int vis) {
+ if (y < 0) {
+ y = 0;
+ }
+ else if (y + vis > target.getHeight()) {
+ y = Math.max(0, target.getHeight() - vis);
+ }
+ return y;
+ }
+
+ /**
+ * Ensures that <code>offset</code> is a valid offset into the
+ * model for <code>text</code>.
+ */
+ private int constrainOffset(JTextComponent text, int offset) {
+ Document doc = text.getDocument();
+
+ if ((offset != 0) && (offset > doc.getLength())) {
+ offset = doc.getLength();
+ }
+ if (offset < 0) {
+ offset = 0;
+ }
+ return offset;
+ }
+
+ /**
+ * Adjusts the rectangle that indicates the location to scroll to
+ * after selecting <code>index</code>.
+ */
+ private void adjustScrollIfNecessary(JTextComponent text,
+ Rectangle visible, int initialY,
+ int index) {
+ try {
+ Rectangle dotBounds = text.modelToView(index);
+
+ if (dotBounds.y < visible.y ||
+ (dotBounds.y > (visible.y + visible.height)) ||
+ (dotBounds.y + dotBounds.height) >
+ (visible.y + visible.height)) {
+ int y;
+
+ if (dotBounds.y < visible.y) {
+ y = dotBounds.y;
+ }
+ else {
+ y = dotBounds.y + dotBounds.height - visible.height;
+ }
+ if ((direction == -1 && y < initialY) ||
+ (direction == 1 && y > initialY)) {
+ // Only adjust if won't cause scrolling upward.
+ visible.y = y;
+ }
+ }
+ } catch (BadLocationException ble) {}
+ }
+
+ /**
+ * Adjusts the Rectangle to contain the bounds of the character at
+ * <code>index</code> in response to a page up.
+ */
+ private boolean select;
+
+ /**
+ * Direction to scroll, 1 is down, -1 is up.
+ */
+ private int direction;
+ }
+
+
+ /**
+ * Pages one view to the left or right.
+ */
+ static class PageAction extends TextAction {
+
+ /** Create this object with the appropriate identifier. */
+ public PageAction(String nm, boolean left, boolean select) {
+ super(nm);
+ this.select = select;
+ this.left = left;
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ int selectedIndex;
+ Rectangle visible = new Rectangle();
+ target.computeVisibleRect(visible);
+ if (left) {
+ visible.x = Math.max(0, visible.x - visible.width);
+ }
+ else {
+ visible.x += visible.width;
+ }
+
+ selectedIndex = target.getCaretPosition();
+ if(selectedIndex != -1) {
+ if (left) {
+ selectedIndex = target.viewToModel
+ (new Point(visible.x, visible.y));
+ }
+ else {
+ selectedIndex = target.viewToModel
+ (new Point(visible.x + visible.width - 1,
+ visible.y + visible.height - 1));
+ }
+ Document doc = target.getDocument();
+ if ((selectedIndex != 0) &&
+ (selectedIndex > (doc.getLength()-1))) {
+ selectedIndex = doc.getLength()-1;
+ }
+ else if(selectedIndex < 0) {
+ selectedIndex = 0;
+ }
+ if (select)
+ target.moveCaretPosition(selectedIndex);
+ else
+ target.setCaretPosition(selectedIndex);
+ }
+ }
+ }
+
+ private boolean select;
+ private boolean left;
+ }
+
+ static class DumpModelAction extends TextAction {
+
+ DumpModelAction() {
+ super("dump-model");
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ Document d = target.getDocument();
+ if (d instanceof AbstractDocument) {
+ ((AbstractDocument) d).dump(System.err);
+ }
+ }
+ }
+ }
+
+ /*
+ * Action to move the selection by way of the
+ * getNextVisualPositionFrom method. Constructor indicates direction
+ * to use.
+ */
+ static class NextVisualPositionAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ NextVisualPositionAction(String nm, boolean select, int direction) {
+ super(nm);
+ this.select = select;
+ this.direction = direction;
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ Caret caret = target.getCaret();
+ DefaultCaret bidiCaret = (caret instanceof DefaultCaret) ?
+ (DefaultCaret)caret : null;
+ int dot = caret.getDot();
+ Position.Bias[] bias = new Position.Bias[1];
+ Point magicPosition = caret.getMagicCaretPosition();
+
+ try {
+ if(magicPosition == null &&
+ (direction == SwingConstants.NORTH ||
+ direction == SwingConstants.SOUTH)) {
+ Rectangle r = (bidiCaret != null) ?
+ target.getUI().modelToView(target, dot,
+ bidiCaret.getDotBias()) :
+ target.modelToView(dot);
+ magicPosition = new Point(r.x, r.y);
+ }
+
+ NavigationFilter filter = target.getNavigationFilter();
+
+ if (filter != null) {
+ dot = filter.getNextVisualPositionFrom
+ (target, dot, (bidiCaret != null) ?
+ bidiCaret.getDotBias() :
+ Position.Bias.Forward, direction, bias);
+ }
+ else {
+ dot = target.getUI().getNextVisualPositionFrom
+ (target, dot, (bidiCaret != null) ?
+ bidiCaret.getDotBias() :
+ Position.Bias.Forward, direction, bias);
+ }
+ if(bias[0] == null) {
+ bias[0] = Position.Bias.Forward;
+ }
+ if(bidiCaret != null) {
+ if (select) {
+ bidiCaret.moveDot(dot, bias[0]);
+ } else {
+ bidiCaret.setDot(dot, bias[0]);
+ }
+ }
+ else {
+ if (select) {
+ caret.moveDot(dot);
+ } else {
+ caret.setDot(dot);
+ }
+ }
+ if(magicPosition != null &&
+ (direction == SwingConstants.NORTH ||
+ direction == SwingConstants.SOUTH)) {
+ target.getCaret().setMagicCaretPosition(magicPosition);
+ }
+ } catch (BadLocationException ex) {
+ }
+ }
+ }
+
+ private boolean select;
+ private int direction;
+ }
+
+ /*
+ * Position the caret to the beginning of the word.
+ * @see DefaultEditorKit#beginWordAction
+ * @see DefaultEditorKit#selectBeginWordAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class BeginWordAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ BeginWordAction(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);
+ if (target != null) {
+ try {
+ int offs = target.getCaretPosition();
+ int begOffs = Utilities.getWordStart(target, offs);
+ if (select) {
+ target.moveCaretPosition(begOffs);
+ } else {
+ target.setCaretPosition(begOffs);
+ }
+ } catch (BadLocationException bl) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Position the caret to the end of the word.
+ * @see DefaultEditorKit#endWordAction
+ * @see DefaultEditorKit#selectEndWordAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class EndWordAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ EndWordAction(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);
+ if (target != null) {
+ try {
+ int offs = target.getCaretPosition();
+ int endOffs = Utilities.getWordEnd(target, offs);
+ if (select) {
+ target.moveCaretPosition(endOffs);
+ } else {
+ target.setCaretPosition(endOffs);
+ }
+ } catch (BadLocationException bl) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Position the caret to the beginning of the previous word.
+ * @see DefaultEditorKit#previousWordAction
+ * @see DefaultEditorKit#selectPreviousWordAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class PreviousWordAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ PreviousWordAction(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);
+ if (target != null) {
+ int offs = target.getCaretPosition();
+ boolean failed = false;
+ try {
+ Element curPara =
+ Utilities.getParagraphElement(target, offs);
+ offs = Utilities.getPreviousWord(target, offs);
+ if(offs < curPara.getStartOffset()) {
+ // we should first move to the end of the
+ // previous paragraph (bug #4278839)
+ offs = Utilities.getParagraphElement(target, offs).
+ getEndOffset() - 1;
+ }
+ } catch (BadLocationException bl) {
+ if (offs != 0) {
+ offs = 0;
+ }
+ else {
+ failed = true;
+ }
+ }
+ if (!failed) {
+ if (select) {
+ target.moveCaretPosition(offs);
+ } else {
+ target.setCaretPosition(offs);
+ }
+ }
+ else {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Position the caret to the next of the word.
+ * @see DefaultEditorKit#nextWordAction
+ * @see DefaultEditorKit#selectNextWordAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class NextWordAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ NextWordAction(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);
+ if (target != null) {
+ int offs = target.getCaretPosition();
+ boolean failed = false;
+ int oldOffs = offs;
+ Element curPara =
+ Utilities.getParagraphElement(target, offs);
+ try {
+ offs = Utilities.getNextWord(target, offs);
+ if(offs >= curPara.getEndOffset() &&
+ oldOffs != curPara.getEndOffset() - 1) {
+ // we should first move to the end of current
+ // paragraph (bug #4278839)
+ offs = curPara.getEndOffset() - 1;
+ }
+ } catch (BadLocationException bl) {
+ int end = target.getDocument().getLength();
+ if (offs != end) {
+ if(oldOffs != curPara.getEndOffset() - 1) {
+ offs = curPara.getEndOffset() - 1;
+ } else {
+ offs = end;
+ }
+ }
+ else {
+ failed = true;
+ }
+ }
+ if (!failed) {
+ if (select) {
+ target.moveCaretPosition(offs);
+ } else {
+ target.setCaretPosition(offs);
+ }
+ }
+ else {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Position the caret to the beginning of the line.
+ * @see DefaultEditorKit#beginLineAction
+ * @see DefaultEditorKit#selectBeginLineAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class BeginLineAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ BeginLineAction(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);
+ if (target != null) {
+ try {
+ int offs = target.getCaretPosition();
+ int begOffs = Utilities.getRowStart(target, offs);
+ if (select) {
+ target.moveCaretPosition(begOffs);
+ } else {
+ target.setCaretPosition(begOffs);
+ }
+ } catch (BadLocationException bl) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Position the caret to the end of the line.
+ * @see DefaultEditorKit#endLineAction
+ * @see DefaultEditorKit#selectEndLineAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class EndLineAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ EndLineAction(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);
+ if (target != null) {
+ try {
+ int offs = target.getCaretPosition();
+ int endOffs = Utilities.getRowEnd(target, offs);
+ if (select) {
+ target.moveCaretPosition(endOffs);
+ } else {
+ target.setCaretPosition(endOffs);
+ }
+ } catch (BadLocationException bl) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Position the caret to the beginning of the paragraph.
+ * @see DefaultEditorKit#beginParagraphAction
+ * @see DefaultEditorKit#selectBeginParagraphAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class BeginParagraphAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ BeginParagraphAction(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);
+ if (target != null) {
+ int offs = target.getCaretPosition();
+ Element elem = Utilities.getParagraphElement(target, offs);
+ offs = elem.getStartOffset();
+ if (select) {
+ target.moveCaretPosition(offs);
+ } else {
+ target.setCaretPosition(offs);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Position the caret to the end of the paragraph.
+ * @see DefaultEditorKit#endParagraphAction
+ * @see DefaultEditorKit#selectEndParagraphAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class EndParagraphAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ EndParagraphAction(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);
+ if (target != null) {
+ int offs = target.getCaretPosition();
+ Element elem = Utilities.getParagraphElement(target, offs);
+ offs = Math.min(target.getDocument().getLength(),
+ elem.getEndOffset());
+ if (select) {
+ target.moveCaretPosition(offs);
+ } else {
+ target.setCaretPosition(offs);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Move the caret to the beginning of the document.
+ * @see DefaultEditorKit#beginAction
+ * @see DefaultEditorKit#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);
+ if (target != null) {
+ if (select) {
+ target.moveCaretPosition(0);
+ } else {
+ target.setCaretPosition(0);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Move the caret to the end of the document.
+ * @see DefaultEditorKit#endAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class EndAction extends TextAction {
+
+ /* Create this object with the appropriate identifier. */
+ EndAction(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);
+ if (target != null) {
+ Document doc = target.getDocument();
+ int dot = doc.getLength();
+ if (select) {
+ target.moveCaretPosition(dot);
+ } else {
+ target.setCaretPosition(dot);
+ }
+ }
+ }
+
+ private boolean select;
+ }
+
+ /*
+ * Select the word around the caret
+ * @see DefaultEditorKit#endAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class SelectWordAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ SelectWordAction() {
+ super(selectWordAction);
+ start = new BeginWordAction("pigdog", false);
+ end = new EndWordAction("pigdog", true);
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ start.actionPerformed(e);
+ end.actionPerformed(e);
+ }
+
+ private Action start;
+ private Action end;
+ }
+
+ /*
+ * Select the line around the caret
+ * @see DefaultEditorKit#endAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class SelectLineAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ SelectLineAction() {
+ super(selectLineAction);
+ start = new BeginLineAction("pigdog", false);
+ end = new EndLineAction("pigdog", true);
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ start.actionPerformed(e);
+ end.actionPerformed(e);
+ }
+
+ private Action start;
+ private Action end;
+ }
+
+ /*
+ * Select the paragraph around the caret
+ * @see DefaultEditorKit#endAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class SelectParagraphAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ SelectParagraphAction() {
+ super(selectParagraphAction);
+ start = new BeginParagraphAction("pigdog", false);
+ end = new EndParagraphAction("pigdog", true);
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ start.actionPerformed(e);
+ end.actionPerformed(e);
+ }
+
+ private Action start;
+ private Action end;
+ }
+
+ /*
+ * Select the entire document
+ * @see DefaultEditorKit#endAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class SelectAllAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ * @param nm the name of the action, Action.NAME.
+ * @param select whether to extend the selection when
+ * changing the caret position.
+ */
+ SelectAllAction() {
+ super(selectAllAction);
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ Document doc = target.getDocument();
+ target.setCaretPosition(0);
+ target.moveCaretPosition(doc.getLength());
+ }
+ }
+
+ }
+
+ /*
+ * Remove the selection, if any.
+ * @see DefaultEditorKit#unselectAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class UnselectAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ */
+ UnselectAction() {
+ super(unselectAction);
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ target.setCaretPosition(target.getCaretPosition());
+ }
+ }
+
+ }
+
+ /*
+ * Toggles the ComponentOrientation of the text component.
+ * @see DefaultEditorKit#toggleComponentOrientationAction
+ * @see DefaultEditorKit#getActions
+ */
+ static class ToggleComponentOrientationAction extends TextAction {
+
+ /**
+ * Create this action with the appropriate identifier.
+ */
+ ToggleComponentOrientationAction() {
+ super(toggleComponentOrientationAction);
+ }
+
+ /** The operation to perform when this action is triggered. */
+ public void actionPerformed(ActionEvent e) {
+ JTextComponent target = getTextComponent(e);
+ if (target != null) {
+ ComponentOrientation last = target.getComponentOrientation();
+ ComponentOrientation next;
+ if( last == ComponentOrientation.RIGHT_TO_LEFT )
+ next = ComponentOrientation.LEFT_TO_RIGHT;
+ else
+ next = ComponentOrientation.RIGHT_TO_LEFT;
+ target.setComponentOrientation(next);
+ target.repaint();
+ }
+ }
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/DefaultFormatter.java b/src/share/classes/javax/swing/text/DefaultFormatter.java
new file mode 100644
index 000000000..794190472
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DefaultFormatter.java
@@ -0,0 +1,758 @@
+/*
+ * Copyright 2000-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;
+
+import java.io.Serializable;
+import java.lang.reflect.*;
+import java.text.ParseException;
+import javax.swing.*;
+import javax.swing.text.*;
+
+/**
+ * <code>DefaultFormatter</code> formats aribtrary objects. Formatting is done
+ * by invoking the <code>toString</code> method. In order to convert the
+ * value back to a String, your class must provide a constructor that
+ * takes a String argument. If no single argument constructor that takes a
+ * String is found, the returned value will be the String passed into
+ * <code>stringToValue</code>.
+ * <p>
+ * Instances of <code>DefaultFormatter</code> can not be used in multiple
+ * instances of <code>JFormattedTextField</code>. To obtain a copy of
+ * an already configured <code>DefaultFormatter</code>, use the
+ * <code>clone</code> method.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see javax.swing.JFormattedTextField.AbstractFormatter
+ *
+ * @since 1.4
+ */
+public class DefaultFormatter extends JFormattedTextField.AbstractFormatter
+ implements Cloneable, Serializable {
+ /** Indicates if the value being edited must match the mask. */
+ private boolean allowsInvalid;
+
+ /** If true, editing mode is in overwrite (or strikethough). */
+ private boolean overwriteMode;
+
+ /** If true, any time a valid edit happens commitEdit is invoked. */
+ private boolean commitOnEdit;
+
+ /** Class used to create new instances. */
+ private Class valueClass;
+
+ /** NavigationFilter that forwards calls back to DefaultFormatter. */
+ private NavigationFilter navigationFilter;
+
+ /** DocumentFilter that forwards calls back to DefaultFormatter. */
+ private DocumentFilter documentFilter;
+
+ /** Used during replace to track the region to replace. */
+ transient ReplaceHolder replaceHolder;
+
+
+ /**
+ * Creates a DefaultFormatter.
+ */
+ public DefaultFormatter() {
+ overwriteMode = true;
+ allowsInvalid = true;
+ }
+
+ /**
+ * Installs the <code>DefaultFormatter</code> onto a particular
+ * <code>JFormattedTextField</code>.
+ * This will invoke <code>valueToString</code> to convert the
+ * current value from the <code>JFormattedTextField</code> to
+ * a String. This will then install the <code>Action</code>s from
+ * <code>getActions</code>, the <code>DocumentFilter</code>
+ * returned from <code>getDocumentFilter</code> and the
+ * <code>NavigationFilter</code> returned from
+ * <code>getNavigationFilter</code> onto the
+ * <code>JFormattedTextField</code>.
+ * <p>
+ * Subclasses will typically only need to override this if they
+ * wish to install additional listeners on the
+ * <code>JFormattedTextField</code>.
+ * <p>
+ * If there is a <code>ParseException</code> in converting the
+ * current value to a String, this will set the text to an empty
+ * String, and mark the <code>JFormattedTextField</code> as being
+ * in an invalid state.
+ * <p>
+ * While this is a public method, this is typically only useful
+ * for subclassers of <code>JFormattedTextField</code>.
+ * <code>JFormattedTextField</code> will invoke this method at
+ * the appropriate times when the value changes, or its internal
+ * state changes.
+ *
+ * @param ftf JFormattedTextField to format for, may be null indicating
+ * uninstall from current JFormattedTextField.
+ */
+ public void install(JFormattedTextField ftf) {
+ super.install(ftf);
+ positionCursorAtInitialLocation();
+ }
+
+ /**
+ * Sets when edits are published back to the
+ * <code>JFormattedTextField</code>. If true, <code>commitEdit</code>
+ * is invoked after every valid edit (any time the text is edited). On
+ * the other hand, if this is false than the <code>DefaultFormatter</code>
+ * does not publish edits back to the <code>JFormattedTextField</code>.
+ * As such, the only time the value of the <code>JFormattedTextField</code>
+ * will change is when <code>commitEdit</code> is invoked on
+ * <code>JFormattedTextField</code>, typically when enter is pressed
+ * or focus leaves the <code>JFormattedTextField</code>.
+ *
+ * @param commit Used to indicate when edits are commited back to the
+ * JTextComponent
+ */
+ public void setCommitsOnValidEdit(boolean commit) {
+ commitOnEdit = commit;
+ }
+
+ /**
+ * Returns when edits are published back to the
+ * <code>JFormattedTextField</code>.
+ *
+ * @return true if edits are commited after evey valid edit
+ */
+ public boolean getCommitsOnValidEdit() {
+ return commitOnEdit;
+ }
+
+ /**
+ * Configures the behavior when inserting characters. If
+ * <code>overwriteMode</code> is true (the default), new characters
+ * overwrite existing characters in the model.
+ *
+ * @param overwriteMode Indicates if overwrite or overstrike mode is used
+ */
+ public void setOverwriteMode(boolean overwriteMode) {
+ this.overwriteMode = overwriteMode;
+ }
+
+ /**
+ * Returns the behavior when inserting characters.
+ *
+ * @return true if newly inserted characters overwrite existing characters
+ */
+ public boolean getOverwriteMode() {
+ return overwriteMode;
+ }
+
+ /**
+ * Sets whether or not the value being edited is allowed to be invalid
+ * for a length of time (that is, <code>stringToValue</code> throws
+ * a <code>ParseException</code>).
+ * It is often convenient to allow the user to temporarily input an
+ * invalid value.
+ *
+ * @param allowsInvalid Used to indicate if the edited value must always
+ * be valid
+ */
+ public void setAllowsInvalid(boolean allowsInvalid) {
+ this.allowsInvalid = allowsInvalid;
+ }
+
+ /**
+ * Returns whether or not the value being edited is allowed to be invalid
+ * for a length of time.
+ *
+ * @return false if the edited value must always be valid
+ */
+ public boolean getAllowsInvalid() {
+ return allowsInvalid;
+ }
+
+ /**
+ * Sets that class that is used to create new Objects. If the
+ * passed in class does not have a single argument constructor that
+ * takes a String, String values will be used.
+ *
+ * @param valueClass Class used to construct return value from
+ * stringToValue
+ */
+ public void setValueClass(Class<?> valueClass) {
+ this.valueClass = valueClass;
+ }
+
+ /**
+ * Returns that class that is used to create new Objects.
+ *
+ * @return Class used to constuct return value from stringToValue
+ */
+ public Class<?> getValueClass() {
+ return valueClass;
+ }
+
+ /**
+ * Converts the passed in String into an instance of
+ * <code>getValueClass</code> by way of the constructor that
+ * takes a String argument. If <code>getValueClass</code>
+ * returns null, the Class of the current value in the
+ * <code>JFormattedTextField</code> will be used. If this is null, a
+ * String will be returned. If the constructor thows an exception, a
+ * <code>ParseException</code> will be thrown. If there is no single
+ * argument String constructor, <code>string</code> will be returned.
+ *
+ * @throws ParseException if there is an error in the conversion
+ * @param string String to convert
+ * @return Object representation of text
+ */
+ public Object stringToValue(String string) throws ParseException {
+ Class vc = getValueClass();
+ JFormattedTextField ftf = getFormattedTextField();
+
+ if (vc == null && ftf != null) {
+ Object value = ftf.getValue();
+
+ if (value != null) {
+ vc = value.getClass();
+ }
+ }
+ if (vc != null) {
+ Constructor cons;
+
+ try {
+ cons = vc.getConstructor(new Class[] { String.class });
+
+ } catch (NoSuchMethodException nsme) {
+ cons = null;
+ }
+
+ if (cons != null) {
+ try {
+ return cons.newInstance(new Object[] { string });
+ } catch (Throwable ex) {
+ throw new ParseException("Error creating instance", 0);
+ }
+ }
+ }
+ return string;
+ }
+
+ /**
+ * Converts the passed in Object into a String by way of the
+ * <code>toString</code> method.
+ *
+ * @throws ParseException if there is an error in the conversion
+ * @param value Value to convert
+ * @return String representation of value
+ */
+ public String valueToString(Object value) throws ParseException {
+ if (value == null) {
+ return "";
+ }
+ return value.toString();
+ }
+
+ /**
+ * Returns the <code>DocumentFilter</code> used to restrict the characters
+ * that can be input into the <code>JFormattedTextField</code>.
+ *
+ * @return DocumentFilter to restrict edits
+ */
+ protected DocumentFilter getDocumentFilter() {
+ if (documentFilter == null) {
+ documentFilter = new DefaultDocumentFilter();
+ }
+ return documentFilter;
+ }
+
+ /**
+ * Returns the <code>NavigationFilter</code> used to restrict where the
+ * cursor can be placed.
+ *
+ * @return NavigationFilter to restrict navigation
+ */
+ protected NavigationFilter getNavigationFilter() {
+ if (navigationFilter == null) {
+ navigationFilter = new DefaultNavigationFilter();
+ }
+ return navigationFilter;
+ }
+
+ /**
+ * Creates a copy of the DefaultFormatter.
+ *
+ * @return copy of the DefaultFormatter
+ */
+ public Object clone() throws CloneNotSupportedException {
+ DefaultFormatter formatter = (DefaultFormatter)super.clone();
+
+ formatter.navigationFilter = null;
+ formatter.documentFilter = null;
+ formatter.replaceHolder = null;
+ return formatter;
+ }
+
+
+ /**
+ * Positions the cursor at the initial location.
+ */
+ void positionCursorAtInitialLocation() {
+ JFormattedTextField ftf = getFormattedTextField();
+ if (ftf != null) {
+ ftf.setCaretPosition(getInitialVisualPosition());
+ }
+ }
+
+ /**
+ * Returns the initial location to position the cursor at. This forwards
+ * the call to <code>getNextNavigatableChar</code>.
+ */
+ int getInitialVisualPosition() {
+ return getNextNavigatableChar(0, 1);
+ }
+
+ /**
+ * Subclasses should override this if they want cursor navigation
+ * to skip certain characters. A return value of false indicates
+ * the character at <code>offset</code> should be skipped when
+ * navigating throught the field.
+ */
+ boolean isNavigatable(int offset) {
+ return true;
+ }
+
+ /**
+ * Returns true if the text in <code>text</code> can be inserted. This
+ * does not mean the text will ultimately be inserted, it is used if
+ * text can trivially reject certain characters.
+ */
+ boolean isLegalInsertText(String text) {
+ return true;
+ }
+
+ /**
+ * Returns the next editable character starting at offset incrementing
+ * the offset by <code>direction</code>.
+ */
+ private int getNextNavigatableChar(int offset, int direction) {
+ int max = getFormattedTextField().getDocument().getLength();
+
+ while (offset >= 0 && offset < max) {
+ if (isNavigatable(offset)) {
+ return offset;
+ }
+ offset += direction;
+ }
+ return offset;
+ }
+
+ /**
+ * A convenience methods to return the result of deleting
+ * <code>deleteLength</code> characters at <code>offset</code>
+ * and inserting <code>replaceString</code> at <code>offset</code>
+ * in the current text field.
+ */
+ String getReplaceString(int offset, int deleteLength,
+ String replaceString) {
+ String string = getFormattedTextField().getText();
+ String result;
+
+ result = string.substring(0, offset);
+ if (replaceString != null) {
+ result += replaceString;
+ }
+ if (offset + deleteLength < string.length()) {
+ result += string.substring(offset + deleteLength);
+ }
+ return result;
+ }
+
+ /*
+ * Returns true if the operation described by <code>rh</code> will
+ * result in a legal edit. This may set the <code>value</code>
+ * field of <code>rh</code>.
+ */
+ boolean isValidEdit(ReplaceHolder rh) {
+ if (!getAllowsInvalid()) {
+ String newString = getReplaceString(rh.offset, rh.length, rh.text);
+
+ try {
+ rh.value = stringToValue(newString);
+
+ return true;
+ } catch (ParseException pe) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Invokes <code>commitEdit</code> on the JFormattedTextField.
+ */
+ void commitEdit() throws ParseException {
+ JFormattedTextField ftf = getFormattedTextField();
+
+ if (ftf != null) {
+ ftf.commitEdit();
+ }
+ }
+
+ /**
+ * Pushes the value to the JFormattedTextField if the current value
+ * is valid and invokes <code>setEditValid</code> based on the
+ * validity of the value.
+ */
+ void updateValue() {
+ updateValue(null);
+ }
+
+ /**
+ * Pushes the <code>value</code> to the editor if we are to
+ * commit on edits. If <code>value</code> is null, the current value
+ * will be obtained from the text component.
+ */
+ void updateValue(Object value) {
+ try {
+ if (value == null) {
+ String string = getFormattedTextField().getText();
+
+ value = stringToValue(string);
+ }
+
+ if (getCommitsOnValidEdit()) {
+ commitEdit();
+ }
+ setEditValid(true);
+ } catch (ParseException pe) {
+ setEditValid(false);
+ }
+ }
+
+ /**
+ * Returns the next cursor position from offset by incrementing
+ * <code>direction</code>. This uses
+ * <code>getNextNavigatableChar</code>
+ * as well as constraining the location to the max position.
+ */
+ int getNextCursorPosition(int offset, int direction) {
+ int newOffset = getNextNavigatableChar(offset, direction);
+ int max = getFormattedTextField().getDocument().getLength();
+
+ if (!getAllowsInvalid()) {
+ if (direction == -1 && offset == newOffset) {
+ // Case where hit backspace and only characters before
+ // offset are fixed.
+ newOffset = getNextNavigatableChar(newOffset, 1);
+ if (newOffset >= max) {
+ newOffset = offset;
+ }
+ }
+ else if (direction == 1 && newOffset >= max) {
+ // Don't go beyond last editable character.
+ newOffset = getNextNavigatableChar(max - 1, -1);
+ if (newOffset < max) {
+ newOffset++;
+ }
+ }
+ }
+ return newOffset;
+ }
+
+ /**
+ * Resets the cursor by using getNextCursorPosition.
+ */
+ void repositionCursor(int offset, int direction) {
+ getFormattedTextField().getCaret().setDot(getNextCursorPosition
+ (offset, direction));
+ }
+
+
+ /**
+ * Finds the next navigatable character.
+ */
+ int getNextVisualPositionFrom(JTextComponent text, int pos,
+ Position.Bias bias, int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+ int value = text.getUI().getNextVisualPositionFrom(text, pos, bias,
+ direction, biasRet);
+
+ if (value == -1) {
+ return -1;
+ }
+ if (!getAllowsInvalid() && (direction == SwingConstants.EAST ||
+ direction == SwingConstants.WEST)) {
+ int last = -1;
+
+ while (!isNavigatable(value) && value != last) {
+ last = value;
+ value = text.getUI().getNextVisualPositionFrom(
+ text, value, bias, direction,biasRet);
+ }
+ int max = getFormattedTextField().getDocument().getLength();
+ if (last == value || value == max) {
+ if (value == 0) {
+ biasRet[0] = Position.Bias.Forward;
+ value = getInitialVisualPosition();
+ }
+ if (value >= max && max > 0) {
+ // Pending: should not assume forward!
+ biasRet[0] = Position.Bias.Forward;
+ value = getNextNavigatableChar(max - 1, -1) + 1;
+ }
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Returns true if the edit described by <code>rh</code> will result
+ * in a legal value.
+ */
+ boolean canReplace(ReplaceHolder rh) {
+ return isValidEdit(rh);
+ }
+
+ /**
+ * DocumentFilter method, funnels into <code>replace</code>.
+ */
+ void replace(DocumentFilter.FilterBypass fb, int offset,
+ int length, String text,
+ AttributeSet attrs) throws BadLocationException {
+ ReplaceHolder rh = getReplaceHolder(fb, offset, length, text, attrs);
+
+ replace(rh);
+ }
+
+ /**
+ * If the edit described by <code>rh</code> is legal, this will
+ * return true, commit the edit (if necessary) and update the cursor
+ * position. This forwards to <code>canReplace</code> and
+ * <code>isLegalInsertText</code> as necessary to determine if
+ * the edit is in fact legal.
+ * <p>
+ * All of the DocumentFilter methods funnel into here, you should
+ * generally only have to override this.
+ */
+ boolean replace(ReplaceHolder rh) throws BadLocationException {
+ boolean valid = true;
+ int direction = 1;
+
+ if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
+ (getFormattedTextField().getSelectionStart() != rh.offset ||
+ rh.length > 1)) {
+ direction = -1;
+ }
+
+ if (getOverwriteMode() && rh.text != null) {
+ rh.length = Math.min(Math.max(rh.length, rh.text.length()),
+ rh.fb.getDocument().getLength() - rh.offset);
+ }
+ if ((rh.text != null && !isLegalInsertText(rh.text)) ||
+ !canReplace(rh) ||
+ (rh.length == 0 && (rh.text == null || rh.text.length() == 0))) {
+ valid = false;
+ }
+ if (valid) {
+ int cursor = rh.cursorPosition;
+
+ rh.fb.replace(rh.offset, rh.length, rh.text, rh.attrs);
+ if (cursor == -1) {
+ cursor = rh.offset;
+ if (direction == 1 && rh.text != null) {
+ cursor = rh.offset + rh.text.length();
+ }
+ }
+ updateValue(rh.value);
+ repositionCursor(cursor, direction);
+ return true;
+ }
+ else {
+ invalidEdit();
+ }
+ return false;
+ }
+
+ /**
+ * NavigationFilter method, subclasses that wish finer control should
+ * override this.
+ */
+ void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias){
+ fb.setDot(dot, bias);
+ }
+
+ /**
+ * NavigationFilter method, subclasses that wish finer control should
+ * override this.
+ */
+ void moveDot(NavigationFilter.FilterBypass fb, int dot,
+ Position.Bias bias) {
+ fb.moveDot(dot, bias);
+ }
+
+
+ /**
+ * Returns the ReplaceHolder to track the replace of the specified
+ * text.
+ */
+ ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset,
+ int length, String text,
+ AttributeSet attrs) {
+ if (replaceHolder == null) {
+ replaceHolder = new ReplaceHolder();
+ }
+ replaceHolder.reset(fb, offset, length, text, attrs);
+ return replaceHolder;
+ }
+
+
+ /**
+ * ReplaceHolder is used to track where insert/remove/replace is
+ * going to happen.
+ */
+ static class ReplaceHolder {
+ /** The FilterBypass that was passed to the DocumentFilter method. */
+ DocumentFilter.FilterBypass fb;
+ /** Offset where the remove/insert is going to occur. */
+ int offset;
+ /** Length of text to remove. */
+ int length;
+ /** The text to insert, may be null. */
+ String text;
+ /** AttributeSet to attach to text, may be null. */
+ AttributeSet attrs;
+ /** The resulting value, this may never be set. */
+ Object value;
+ /** Position the cursor should be adjusted from. If this is -1
+ * the cursor position will be adjusted based on the direction of
+ * the replace (-1: offset, 1: offset + text.length()), otherwise
+ * the cursor position is adusted from this position.
+ */
+ int cursorPosition;
+
+ void reset(DocumentFilter.FilterBypass fb, int offset, int length,
+ String text, AttributeSet attrs) {
+ this.fb = fb;
+ this.offset = offset;
+ this.length = length;
+ this.text = text;
+ this.attrs = attrs;
+ this.value = null;
+ cursorPosition = -1;
+ }
+ }
+
+
+ /**
+ * NavigationFilter implementation that calls back to methods with
+ * same name in DefaultFormatter.
+ */
+ private class DefaultNavigationFilter extends NavigationFilter
+ implements Serializable {
+ public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
+ JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
+ if (tc.composedTextExists()) {
+ // bypass the filter
+ fb.setDot(dot, bias);
+ } else {
+ DefaultFormatter.this.setDot(fb, dot, bias);
+ }
+ }
+
+ public void moveDot(FilterBypass fb, int dot, Position.Bias bias) {
+ JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
+ if (tc.composedTextExists()) {
+ // bypass the filter
+ fb.moveDot(dot, bias);
+ } else {
+ DefaultFormatter.this.moveDot(fb, dot, bias);
+ }
+ }
+
+ public int getNextVisualPositionFrom(JTextComponent text, int pos,
+ Position.Bias bias,
+ int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+ if (text.composedTextExists()) {
+ // forward the call to the UI directly
+ return text.getUI().getNextVisualPositionFrom(
+ text, pos, bias, direction, biasRet);
+ } else {
+ return DefaultFormatter.this.getNextVisualPositionFrom(
+ text, pos, bias, direction, biasRet);
+ }
+ }
+ }
+
+
+ /**
+ * DocumentFilter implementation that calls back to the replace
+ * method of DefaultFormatter.
+ */
+ private class DefaultDocumentFilter extends DocumentFilter implements
+ Serializable {
+ public void remove(FilterBypass fb, int offset, int length) throws
+ BadLocationException {
+ JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
+ if (tc.composedTextExists()) {
+ // bypass the filter
+ fb.remove(offset, length);
+ } else {
+ DefaultFormatter.this.replace(fb, offset, length, null, null);
+ }
+ }
+
+ public void insertString(FilterBypass fb, int offset,
+ String string, AttributeSet attr) throws
+ BadLocationException {
+ JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
+ if (tc.composedTextExists() ||
+ Utilities.isComposedTextAttributeDefined(attr)) {
+ // bypass the filter
+ fb.insertString(offset, string, attr);
+ } else {
+ DefaultFormatter.this.replace(fb, offset, 0, string, attr);
+ }
+ }
+
+ public void replace(FilterBypass fb, int offset, int length,
+ String text, AttributeSet attr) throws
+ BadLocationException {
+ JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
+ if (tc.composedTextExists() ||
+ Utilities.isComposedTextAttributeDefined(attr)) {
+ // bypass the filter
+ fb.replace(offset, length, text, attr);
+ } else {
+ DefaultFormatter.this.replace(fb, offset, length, text, attr);
+ }
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/DefaultFormatterFactory.java b/src/share/classes/javax/swing/text/DefaultFormatterFactory.java
new file mode 100644
index 000000000..169d6143c
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DefaultFormatterFactory.java
@@ -0,0 +1,313 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import javax.swing.JFormattedTextField;
+
+/**
+ * An implementation of
+ * <code>JFormattedTextField.AbstractFormatterFactory</code>.
+ * <code>DefaultFormatterFactory</code> allows specifying a number of
+ * different <code>JFormattedTextField.AbstractFormatter</code>s that are to
+ * be used.
+ * The most important one is the default one
+ * (<code>setDefaultFormatter</code>). The default formatter will be used
+ * if a more specific formatter could not be found. The following process
+ * is used to determine the appropriate formatter to use.
+ * <ol>
+ * <li>Is the passed in value null? Use the null formatter.
+ * <li>Does the <code>JFormattedTextField</code> have focus? Use the edit
+ * formatter.
+ * <li>Otherwise, use the display formatter.
+ * <li>If a non-null <code>AbstractFormatter</code> has not been found, use
+ * the default formatter.
+ * </ol>
+ * <p>
+ * The following code shows how to configure a
+ * <code>JFormattedTextField</code> with two
+ * <code>JFormattedTextField.AbstractFormatter</code>s, one for display and
+ * one for editing.
+ * <pre>
+ * JFormattedTextField.AbstractFormatter editFormatter = ...;
+ * JFormattedTextField.AbstractFormatter displayFormatter = ...;
+ * DefaultFormatterFactory factory = new DefaultFormatterFactory(
+ * displayFormatter, displayFormatter, editFormatter);
+ * JFormattedTextField tf = new JFormattedTextField(factory);
+ * </pre>
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see javax.swing.JFormattedTextField
+ *
+ * @since 1.4
+ */
+public class DefaultFormatterFactory extends JFormattedTextField.AbstractFormatterFactory implements Serializable {
+ /**
+ * Default <code>AbstractFormatter</code> to use if a more specific one has
+ * not been specified.
+ */
+ private JFormattedTextField.AbstractFormatter defaultFormat;
+
+ /**
+ * <code>JFormattedTextField.AbstractFormatter</code> to use for display.
+ */
+ private JFormattedTextField.AbstractFormatter displayFormat;
+
+ /**
+ * <code>JFormattedTextField.AbstractFormatter</code> to use for editing.
+ */
+ private JFormattedTextField.AbstractFormatter editFormat;
+
+ /**
+ * <code>JFormattedTextField.AbstractFormatter</code> to use if the value
+ * is null.
+ */
+ private JFormattedTextField.AbstractFormatter nullFormat;
+
+
+ public DefaultFormatterFactory() {
+ }
+
+ /**
+ * Creates a <code>DefaultFormatterFactory</code> with the specified
+ * <code>JFormattedTextField.AbstractFormatter</code>.
+ *
+ * @param defaultFormat JFormattedTextField.AbstractFormatter to be used
+ * if a more specific
+ * JFormattedTextField.AbstractFormatter can not be
+ * found.
+ */
+ public DefaultFormatterFactory(JFormattedTextField.
+ AbstractFormatter defaultFormat) {
+ this(defaultFormat, null);
+ }
+
+ /**
+ * Creates a <code>DefaultFormatterFactory</code> with the specified
+ * <code>JFormattedTextField.AbstractFormatter</code>s.
+ *
+ * @param defaultFormat JFormattedTextField.AbstractFormatter to be used
+ * if a more specific
+ * JFormattedTextField.AbstractFormatter can not be
+ * found.
+ * @param displayFormat JFormattedTextField.AbstractFormatter to be used
+ * when the JFormattedTextField does not have focus.
+ */
+ public DefaultFormatterFactory(
+ JFormattedTextField.AbstractFormatter defaultFormat,
+ JFormattedTextField.AbstractFormatter displayFormat) {
+ this(defaultFormat, displayFormat, null);
+ }
+
+ /**
+ * Creates a DefaultFormatterFactory with the specified
+ * JFormattedTextField.AbstractFormatters.
+ *
+ * @param defaultFormat JFormattedTextField.AbstractFormatter to be used
+ * if a more specific
+ * JFormattedTextField.AbstractFormatter can not be
+ * found.
+ * @param displayFormat JFormattedTextField.AbstractFormatter to be used
+ * when the JFormattedTextField does not have focus.
+ * @param editFormat JFormattedTextField.AbstractFormatter to be used
+ * when the JFormattedTextField has focus.
+ */
+ public DefaultFormatterFactory(
+ JFormattedTextField.AbstractFormatter defaultFormat,
+ JFormattedTextField.AbstractFormatter displayFormat,
+ JFormattedTextField.AbstractFormatter editFormat) {
+ this(defaultFormat, displayFormat, editFormat, null);
+ }
+
+ /**
+ * Creates a DefaultFormatterFactory with the specified
+ * JFormattedTextField.AbstractFormatters.
+ *
+ * @param defaultFormat JFormattedTextField.AbstractFormatter to be used
+ * if a more specific
+ * JFormattedTextField.AbstractFormatter can not be
+ * found.
+ * @param displayFormat JFormattedTextField.AbstractFormatter to be used
+ * when the JFormattedTextField does not have focus.
+ * @param editFormat JFormattedTextField.AbstractFormatter to be used
+ * when the JFormattedTextField has focus.
+ * @param nullFormat JFormattedTextField.AbstractFormatter to be used
+ * when the JFormattedTextField has a null value.
+ */
+ public DefaultFormatterFactory(
+ JFormattedTextField.AbstractFormatter defaultFormat,
+ JFormattedTextField.AbstractFormatter displayFormat,
+ JFormattedTextField.AbstractFormatter editFormat,
+ JFormattedTextField.AbstractFormatter nullFormat) {
+ this.defaultFormat = defaultFormat;
+ this.displayFormat = displayFormat;
+ this.editFormat = editFormat;
+ this.nullFormat = nullFormat;
+ }
+
+ /**
+ * Sets the <code>JFormattedTextField.AbstractFormatter</code> to use as
+ * a last resort, eg in case a display, edit or null
+ * <code>JFormattedTextField.AbstractFormatter</code> has not been
+ * specified.
+ *
+ * @param atf JFormattedTextField.AbstractFormatter used if a more
+ * specific is not specified
+ */
+ public void setDefaultFormatter(JFormattedTextField.AbstractFormatter atf){
+ defaultFormat = atf;
+ }
+
+ /**
+ * Returns the <code>JFormattedTextField.AbstractFormatter</code> to use
+ * as a last resort, eg in case a display, edit or null
+ * <code>JFormattedTextField.AbstractFormatter</code>
+ * has not been specified.
+ *
+ * @return JFormattedTextField.AbstractFormatter used if a more specific
+ * one is not specified.
+ */
+ public JFormattedTextField.AbstractFormatter getDefaultFormatter() {
+ return defaultFormat;
+ }
+
+ /**
+ * Sets the <code>JFormattedTextField.AbstractFormatter</code> to use if
+ * the <code>JFormattedTextField</code> is not being edited and either
+ * the value is not-null, or the value is null and a null formatter has
+ * has not been specified.
+ *
+ * @param atf JFormattedTextField.AbstractFormatter to use when the
+ * JFormattedTextField does not have focus
+ */
+ public void setDisplayFormatter(JFormattedTextField.AbstractFormatter atf){
+ displayFormat = atf;
+ }
+
+ /**
+ * Returns the <code>JFormattedTextField.AbstractFormatter</code> to use
+ * if the <code>JFormattedTextField</code> is not being edited and either
+ * the value is not-null, or the value is null and a null formatter has
+ * has not been specified.
+ *
+ * @return JFormattedTextField.AbstractFormatter to use when the
+ * JFormattedTextField does not have focus
+ */
+ public JFormattedTextField.AbstractFormatter getDisplayFormatter() {
+ return displayFormat;
+ }
+
+ /**
+ * Sets the <code>JFormattedTextField.AbstractFormatter</code> to use if
+ * the <code>JFormattedTextField</code> is being edited and either
+ * the value is not-null, or the value is null and a null formatter has
+ * has not been specified.
+ *
+ * @param atf JFormattedTextField.AbstractFormatter to use when the
+ * component has focus
+ */
+ public void setEditFormatter(JFormattedTextField.AbstractFormatter atf) {
+ editFormat = atf;
+ }
+
+ /**
+ * Returns the <code>JFormattedTextField.AbstractFormatter</code> to use
+ * if the <code>JFormattedTextField</code> is being edited and either
+ * the value is not-null, or the value is null and a null formatter has
+ * has not been specified.
+ *
+ * @return JFormattedTextField.AbstractFormatter to use when the
+ * component has focus
+ */
+ public JFormattedTextField.AbstractFormatter getEditFormatter() {
+ return editFormat;
+ }
+
+ /**
+ * Sets the formatter to use if the value of the JFormattedTextField is
+ * null.
+ *
+ * @param atf JFormattedTextField.AbstractFormatter to use when
+ * the value of the JFormattedTextField is null.
+ */
+ public void setNullFormatter(JFormattedTextField.AbstractFormatter atf) {
+ nullFormat = atf;
+ }
+
+ /**
+ * Returns the formatter to use if the value is null.
+ *
+ * @return JFormattedTextField.AbstractFormatter to use when the value is
+ * null
+ */
+ public JFormattedTextField.AbstractFormatter getNullFormatter() {
+ return nullFormat;
+ }
+
+ /**
+ * Returns either the default formatter, display formatter, editor
+ * formatter or null formatter based on the state of the
+ * JFormattedTextField.
+ *
+ * @param source JFormattedTextField requesting
+ * JFormattedTextField.AbstractFormatter
+ * @return JFormattedTextField.AbstractFormatter to handle
+ * formatting duties.
+ */
+ public JFormattedTextField.AbstractFormatter getFormatter(
+ JFormattedTextField source) {
+ JFormattedTextField.AbstractFormatter format = null;
+
+ if (source == null) {
+ return null;
+ }
+ Object value = source.getValue();
+
+ if (value == null) {
+ format = getNullFormatter();
+ }
+ if (format == null) {
+ if (source.hasFocus()) {
+ format = getEditFormatter();
+ }
+ else {
+ format = getDisplayFormatter();
+ }
+ if (format == null) {
+ format = getDefaultFormatter();
+ }
+ }
+ return format;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/DefaultHighlighter.java b/src/share/classes/javax/swing/text/DefaultHighlighter.java
new file mode 100644
index 000000000..9e3202130
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DefaultHighlighter.java
@@ -0,0 +1,638 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import java.awt.*;
+import javax.swing.plaf.*;
+import javax.swing.*;
+
+/**
+ * Implements the Highlighter interfaces. Implements a simple highlight
+ * painter that renders in a solid color.
+ *
+ * @author Timothy Prinzing
+ * @see Highlighter
+ */
+public class DefaultHighlighter extends LayeredHighlighter {
+
+ /**
+ * Creates a new DefaultHighlighther object.
+ */
+ public DefaultHighlighter() {
+ drawsLayeredHighlights = true;
+ }
+
+ // ---- Highlighter methods ----------------------------------------------
+
+ /**
+ * Renders the highlights.
+ *
+ * @param g the graphics context
+ */
+ public void paint(Graphics g) {
+ // PENDING(prinz) - should cull ranges not visible
+ int len = highlights.size();
+ for (int i = 0; i < len; i++) {
+ HighlightInfo info = (HighlightInfo) highlights.elementAt(i);
+ if (!(info instanceof LayeredHighlightInfo)) {
+ // Avoid allocing unless we need it.
+ Rectangle a = component.getBounds();
+ Insets insets = component.getInsets();
+ a.x = insets.left;
+ a.y = insets.top;
+ a.width -= insets.left + insets.right;
+ a.height -= insets.top + insets.bottom;
+ for (; i < len; i++) {
+ info = (HighlightInfo)highlights.elementAt(i);
+ if (!(info instanceof LayeredHighlightInfo)) {
+ Highlighter.HighlightPainter p = info.getPainter();
+ p.paint(g, info.getStartOffset(), info.getEndOffset(),
+ a, component);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when the UI is being installed into the
+ * interface of a JTextComponent. Installs the editor, and
+ * removes any existing highlights.
+ *
+ * @param c the editor component
+ * @see Highlighter#install
+ */
+ public void install(JTextComponent c) {
+ component = c;
+ removeAllHighlights();
+ }
+
+ /**
+ * Called when the UI is being removed from the interface of
+ * a JTextComponent.
+ *
+ * @param c the component
+ * @see Highlighter#deinstall
+ */
+ public void deinstall(JTextComponent c) {
+ component = null;
+ }
+
+ /**
+ * Adds a highlight to the view. Returns a tag that can be used
+ * to refer to the highlight.
+ *
+ * @param p0 the start offset of the range to highlight >= 0
+ * @param p1 the end offset of the range to highlight >= p0
+ * @param p the painter to use to actually render the highlight
+ * @return an object that can be used as a tag
+ * to refer to the highlight
+ * @exception BadLocationException if the specified location is invalid
+ */
+ public Object addHighlight(int p0, int p1, Highlighter.HighlightPainter p) throws BadLocationException {
+ Document doc = component.getDocument();
+ HighlightInfo i = (getDrawsLayeredHighlights() &&
+ (p instanceof LayeredHighlighter.LayerPainter)) ?
+ new LayeredHighlightInfo() : new HighlightInfo();
+ i.painter = p;
+ i.p0 = doc.createPosition(p0);
+ i.p1 = doc.createPosition(p1);
+ highlights.addElement(i);
+ safeDamageRange(p0, p1);
+ return i;
+ }
+
+ /**
+ * Removes a highlight from the view.
+ *
+ * @param tag the reference to the highlight
+ */
+ public void removeHighlight(Object tag) {
+ if (tag instanceof LayeredHighlightInfo) {
+ LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
+ if (lhi.width > 0 && lhi.height > 0) {
+ component.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
+ }
+ }
+ else {
+ HighlightInfo info = (HighlightInfo) tag;
+ safeDamageRange(info.p0, info.p1);
+ }
+ highlights.removeElement(tag);
+ }
+
+ /**
+ * Removes all highlights.
+ */
+ public void removeAllHighlights() {
+ TextUI mapper = component.getUI();
+ if (getDrawsLayeredHighlights()) {
+ int len = highlights.size();
+ if (len != 0) {
+ int minX = 0;
+ int minY = 0;
+ int maxX = 0;
+ int maxY = 0;
+ int p0 = -1;
+ int p1 = -1;
+ for (int i = 0; i < len; i++) {
+ HighlightInfo hi = (HighlightInfo)highlights.elementAt(i);
+ if (hi instanceof LayeredHighlightInfo) {
+ LayeredHighlightInfo info = (LayeredHighlightInfo)hi;
+ minX = Math.min(minX, info.x);
+ minY = Math.min(minY, info.y);
+ maxX = Math.max(maxX, info.x + info.width);
+ maxY = Math.max(maxY, info.y + info.height);
+ }
+ else {
+ if (p0 == -1) {
+ p0 = hi.p0.getOffset();
+ p1 = hi.p1.getOffset();
+ }
+ else {
+ p0 = Math.min(p0, hi.p0.getOffset());
+ p1 = Math.max(p1, hi.p1.getOffset());
+ }
+ }
+ }
+ if (minX != maxX && minY != maxY) {
+ component.repaint(minX, minY, maxX - minX, maxY - minY);
+ }
+ if (p0 != -1) {
+ try {
+ safeDamageRange(p0, p1);
+ } catch (BadLocationException e) {}
+ }
+ highlights.removeAllElements();
+ }
+ }
+ else if (mapper != null) {
+ int len = highlights.size();
+ if (len != 0) {
+ int p0 = Integer.MAX_VALUE;
+ int p1 = 0;
+ for (int i = 0; i < len; i++) {
+ HighlightInfo info = (HighlightInfo) highlights.elementAt(i);
+ p0 = Math.min(p0, info.p0.getOffset());
+ p1 = Math.max(p1, info.p1.getOffset());
+ }
+ try {
+ safeDamageRange(p0, p1);
+ } catch (BadLocationException e) {}
+
+ highlights.removeAllElements();
+ }
+ }
+ }
+
+ /**
+ * Changes a highlight.
+ *
+ * @param tag the highlight tag
+ * @param p0 the beginning of the range >= 0
+ * @param p1 the end of the range >= p0
+ * @exception BadLocationException if the specified location is invalid
+ */
+ public void changeHighlight(Object tag, int p0, int p1) throws BadLocationException {
+ Document doc = component.getDocument();
+ if (tag instanceof LayeredHighlightInfo) {
+ LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
+ if (lhi.width > 0 && lhi.height > 0) {
+ component.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
+ }
+ // Mark the highlights region as invalid, it will reset itself
+ // next time asked to paint.
+ lhi.width = lhi.height = 0;
+ lhi.p0 = doc.createPosition(p0);
+ lhi.p1 = doc.createPosition(p1);
+ safeDamageRange(Math.min(p0, p1), Math.max(p0, p1));
+ }
+ else {
+ HighlightInfo info = (HighlightInfo) tag;
+ int oldP0 = info.p0.getOffset();
+ int oldP1 = info.p1.getOffset();
+ if (p0 == oldP0) {
+ safeDamageRange(Math.min(oldP1, p1),
+ Math.max(oldP1, p1));
+ } else if (p1 == oldP1) {
+ safeDamageRange(Math.min(p0, oldP0),
+ Math.max(p0, oldP0));
+ } else {
+ safeDamageRange(oldP0, oldP1);
+ safeDamageRange(p0, p1);
+ }
+ info.p0 = doc.createPosition(p0);
+ info.p1 = doc.createPosition(p1);
+ }
+ }
+
+ /**
+ * Makes a copy of the highlights. Does not actually clone each highlight,
+ * but only makes references to them.
+ *
+ * @return the copy
+ * @see Highlighter#getHighlights
+ */
+ public Highlighter.Highlight[] getHighlights() {
+ int size = highlights.size();
+ if (size == 0) {
+ return noHighlights;
+ }
+ Highlighter.Highlight[] h = new Highlighter.Highlight[size];
+ highlights.copyInto(h);
+ return h;
+ }
+
+ /**
+ * When leaf Views (such as LabelView) are rendering they should
+ * call into this method. If a highlight is in the given region it will
+ * be drawn immediately.
+ *
+ * @param g Graphics used to draw
+ * @param p0 starting offset of view
+ * @param p1 ending offset of view
+ * @param viewBounds Bounds of View
+ * @param editor JTextComponent
+ * @param view View instance being rendered
+ */
+ public void paintLayeredHighlights(Graphics g, int p0, int p1,
+ Shape viewBounds,
+ JTextComponent editor, View view) {
+ for (int counter = highlights.size() - 1; counter >= 0; counter--) {
+ Object tag = highlights.elementAt(counter);
+ if (tag instanceof LayeredHighlightInfo) {
+ LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
+ int start = lhi.getStartOffset();
+ int end = lhi.getEndOffset();
+ if ((p0 < start && p1 > start) ||
+ (p0 >= start && p0 < end)) {
+ lhi.paintLayeredHighlights(g, p0, p1, viewBounds,
+ editor, view);
+ }
+ }
+ }
+ }
+
+ /**
+ * Queues damageRange() call into event dispatch thread
+ * to be sure that views are in consistent state.
+ */
+ private void safeDamageRange(final Position p0, final Position p1) {
+ safeDamager.damageRange(p0, p1);
+ }
+
+ /**
+ * Queues damageRange() call into event dispatch thread
+ * to be sure that views are in consistent state.
+ */
+ private void safeDamageRange(int a0, int a1) throws BadLocationException {
+ Document doc = component.getDocument();
+ safeDamageRange(doc.createPosition(a0), doc.createPosition(a1));
+ }
+
+ /**
+ * If true, highlights are drawn as the Views draw the text. That is
+ * the Views will call into <code>paintLayeredHighlight</code> which
+ * will result in a rectangle being drawn before the text is drawn
+ * (if the offsets are in a highlighted region that is). For this to
+ * work the painter supplied must be an instance of
+ * LayeredHighlightPainter.
+ */
+ public void setDrawsLayeredHighlights(boolean newValue) {
+ drawsLayeredHighlights = newValue;
+ }
+
+ public boolean getDrawsLayeredHighlights() {
+ return drawsLayeredHighlights;
+ }
+
+ // ---- member variables --------------------------------------------
+
+ private final static Highlighter.Highlight[] noHighlights =
+ new Highlighter.Highlight[0];
+ private Vector highlights = new Vector(); // Vector<HighlightInfo>
+ private JTextComponent component;
+ private boolean drawsLayeredHighlights;
+ private SafeDamager safeDamager = new SafeDamager();
+
+
+ /**
+ * Default implementation of LayeredHighlighter.LayerPainter that can
+ * be used for painting highlights.
+ * <p>
+ * As of 1.4 this field is final.
+ */
+ public static final LayeredHighlighter.LayerPainter DefaultPainter = new DefaultHighlightPainter(null);
+
+
+ /**
+ * Simple highlight painter that fills a highlighted area with
+ * a solid color.
+ */
+ public static class DefaultHighlightPainter extends LayeredHighlighter.LayerPainter {
+
+ /**
+ * Constructs a new highlight painter. If <code>c</code> is null,
+ * the JTextComponent will be queried for its selection color.
+ *
+ * @param c the color for the highlight
+ */
+ public DefaultHighlightPainter(Color c) {
+ color = c;
+ }
+
+ /**
+ * Returns the color of the highlight.
+ *
+ * @return the color
+ */
+ public Color getColor() {
+ return color;
+ }
+
+ // --- HighlightPainter methods ---------------------------------------
+
+ /**
+ * Paints 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 for the highlight
+ * @param c the editor
+ */
+ public void paint(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) {
+ Rectangle alloc = bounds.getBounds();
+ try {
+ // --- determine locations ---
+ TextUI mapper = c.getUI();
+ Rectangle p0 = mapper.modelToView(c, offs0);
+ Rectangle p1 = mapper.modelToView(c, offs1);
+
+ // --- render ---
+ Color color = getColor();
+
+ if (color == null) {
+ g.setColor(c.getSelectionColor());
+ }
+ else {
+ g.setColor(color);
+ }
+ if (p0.y == p1.y) {
+ // same line, render a rectangle
+ Rectangle r = p0.union(p1);
+ g.fillRect(r.x, r.y, r.width, r.height);
+ } else {
+ // different lines
+ int p0ToMarginWidth = alloc.x + alloc.width - p0.x;
+ g.fillRect(p0.x, p0.y, p0ToMarginWidth, p0.height);
+ if ((p0.y + p0.height) != p1.y) {
+ g.fillRect(alloc.x, p0.y + p0.height, alloc.width,
+ p1.y - (p0.y + p0.height));
+ }
+ g.fillRect(alloc.x, p1.y, (p1.x - alloc.x), p1.height);
+ }
+ } catch (BadLocationException e) {
+ // can't render
+ }
+ }
+
+ // --- LayerPainter methods ----------------------------
+ /**
+ * 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 drawing occured in
+ */
+ 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);
+ }
+
+ Rectangle r;
+
+ if (offs0 == view.getStartOffset() &&
+ offs1 == view.getEndOffset()) {
+ // Contained in view, can just use bounds.
+ if (bounds instanceof Rectangle) {
+ r = (Rectangle) bounds;
+ }
+ else {
+ r = bounds.getBounds();
+ }
+ }
+ else {
+ // Should only render part of View.
+ try {
+ // --- determine locations ---
+ Shape shape = view.modelToView(offs0, Position.Bias.Forward,
+ offs1,Position.Bias.Backward,
+ bounds);
+ r = (shape instanceof Rectangle) ?
+ (Rectangle)shape : shape.getBounds();
+ } catch (BadLocationException e) {
+ // can't render
+ r = null;
+ }
+ }
+
+ if (r != null) {
+ // If we are asked to highlight, we should draw something even
+ // if the model-to-view projection is of zero width (6340106).
+ r.width = Math.max(r.width, 1);
+
+ g.fillRect(r.x, r.y, r.width, r.height);
+ }
+
+ return r;
+ }
+
+ private Color color;
+
+ }
+
+
+ class HighlightInfo implements Highlighter.Highlight {
+
+ public int getStartOffset() {
+ return p0.getOffset();
+ }
+
+ public int getEndOffset() {
+ return p1.getOffset();
+ }
+
+ public Highlighter.HighlightPainter getPainter() {
+ return painter;
+ }
+
+ Position p0;
+ Position p1;
+ Highlighter.HighlightPainter painter;
+ }
+
+
+ /**
+ * LayeredHighlightPainter is used when a drawsLayeredHighlights is
+ * true. It maintains a rectangle of the region to paint.
+ */
+ class LayeredHighlightInfo extends HighlightInfo {
+
+ void union(Shape bounds) {
+ if (bounds == null)
+ return;
+
+ Rectangle alloc;
+ if (bounds instanceof Rectangle) {
+ alloc = (Rectangle)bounds;
+ }
+ else {
+ alloc = bounds.getBounds();
+ }
+ if (width == 0 || height == 0) {
+ x = alloc.x;
+ y = alloc.y;
+ width = alloc.width;
+ height = alloc.height;
+ }
+ else {
+ width = Math.max(x + width, alloc.x + alloc.width);
+ height = Math.max(y + height, alloc.y + alloc.height);
+ x = Math.min(x, alloc.x);
+ width -= x;
+ y = Math.min(y, alloc.y);
+ height -= y;
+ }
+ }
+
+ /**
+ * Restricts the region based on the receivers offsets and messages
+ * the painter to paint the region.
+ */
+ void paintLayeredHighlights(Graphics g, int p0, int p1,
+ Shape viewBounds, JTextComponent editor,
+ View view) {
+ int start = getStartOffset();
+ int end = getEndOffset();
+ // Restrict the region to what we represent
+ p0 = Math.max(start, p0);
+ p1 = Math.min(end, p1);
+ // Paint the appropriate region using the painter and union
+ // the effected region with our bounds.
+ union(((LayeredHighlighter.LayerPainter)painter).paintLayer
+ (g, p0, p1, viewBounds, editor, view));
+ }
+
+ int x;
+ int y;
+ int width;
+ int height;
+ }
+
+ /**
+ * This class invokes <code>mapper.damageRange</code> in
+ * EventDispatchThread. The only one instance per Highlighter
+ * is cretaed. When a number of ranges should be damaged
+ * it collects them into queue and damages
+ * them in consecutive order in <code>run</code>
+ * call.
+ */
+ class SafeDamager implements Runnable {
+ private Vector p0 = new Vector(10);
+ private Vector p1 = new Vector(10);
+ private Document lastDoc = null;
+
+ /**
+ * Executes range(s) damage and cleans range queue.
+ */
+ public synchronized void run() {
+ if (component != null) {
+ TextUI mapper = component.getUI();
+ if (mapper != null && lastDoc == component.getDocument()) {
+ // the Document should be the same to properly
+ // display highlights
+ int len = p0.size();
+ for (int i = 0; i < len; i++){
+ mapper.damageRange(component,
+ ((Position)p0.get(i)).getOffset(),
+ ((Position)p1.get(i)).getOffset());
+ }
+ }
+ }
+ p0.clear();
+ p1.clear();
+
+ // release reference
+ lastDoc = null;
+ }
+
+ /**
+ * Adds the range to be damaged into the range queue. If the
+ * range queue is empty (the first call or run() was already
+ * invoked) then adds this class instance into EventDispatch
+ * queue.
+ *
+ * The method also tracks if the current document changed or
+ * component is null. In this case it removes all ranges added
+ * before from range queue.
+ */
+ public synchronized void damageRange(Position pos0, Position pos1) {
+ if (component == null) {
+ p0.clear();
+ lastDoc = null;
+ return;
+ }
+
+ boolean addToQueue = p0.isEmpty();
+ Document curDoc = component.getDocument();
+ if (curDoc != lastDoc) {
+ if (!p0.isEmpty()) {
+ p0.clear();
+ p1.clear();
+ }
+ lastDoc = curDoc;
+ }
+ p0.add(pos0);
+ p1.add(pos1);
+
+ if (addToQueue) {
+ SwingUtilities.invokeLater(this);
+ }
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/DefaultStyledDocument.java b/src/share/classes/javax/swing/text/DefaultStyledDocument.java
new file mode 100644
index 000000000..4939a7615
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DefaultStyledDocument.java
@@ -0,0 +1,2742 @@
+/*
+ * 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;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.font.TextAttribute;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.Vector;
+import java.util.ArrayList;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import javax.swing.Icon;
+import javax.swing.event.*;
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+import javax.swing.undo.UndoableEdit;
+import javax.swing.SwingUtilities;
+
+/**
+ * A document that can be marked up with character and paragraph
+ * styles in a manner similar to the Rich Text Format. The element
+ * structure for this document represents style crossings for
+ * style runs. These style runs are mapped into a paragraph element
+ * structure (which may reside in some other structure). The
+ * style runs break at paragraph boundaries since logical styles are
+ * assigned to paragraph boundaries.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Timothy Prinzing
+ * @see Document
+ * @see AbstractDocument
+ */
+public class DefaultStyledDocument extends AbstractDocument implements StyledDocument {
+
+ /**
+ * Constructs a styled document.
+ *
+ * @param c the container for the content
+ * @param styles resources and style definitions which may
+ * be shared across documents
+ */
+ public DefaultStyledDocument(Content c, StyleContext styles) {
+ super(c, styles);
+ listeningStyles = new Vector();
+ buffer = new ElementBuffer(createDefaultRoot());
+ Style defaultStyle = styles.getStyle(StyleContext.DEFAULT_STYLE);
+ setLogicalStyle(0, defaultStyle);
+ }
+
+ /**
+ * Constructs a styled document with the default content
+ * storage implementation and a shared set of styles.
+ *
+ * @param styles the styles
+ */
+ public DefaultStyledDocument(StyleContext styles) {
+ this(new GapContent(BUFFER_SIZE_DEFAULT), styles);
+ }
+
+ /**
+ * Constructs a default styled document. This buffers
+ * input content by a size of <em>BUFFER_SIZE_DEFAULT</em>
+ * and has a style context that is scoped by the lifetime
+ * of the document and is not shared with other documents.
+ */
+ public DefaultStyledDocument() {
+ this(new GapContent(BUFFER_SIZE_DEFAULT), new StyleContext());
+ }
+
+ /**
+ * Gets the default root element.
+ *
+ * @return the root
+ * @see Document#getDefaultRootElement
+ */
+ public Element getDefaultRootElement() {
+ return buffer.getRootElement();
+ }
+
+ /**
+ * Initialize the document to reflect the given element
+ * structure (i.e. the structure reported by the
+ * <code>getDefaultRootElement</code> method. If the
+ * document contained any data it will first be removed.
+ */
+ protected void create(ElementSpec[] data) {
+ try {
+ if (getLength() != 0) {
+ remove(0, getLength());
+ }
+ writeLock();
+
+ // install the content
+ Content c = getContent();
+ int n = data.length;
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < n; i++) {
+ ElementSpec es = data[i];
+ if (es.getLength() > 0) {
+ sb.append(es.getArray(), es.getOffset(), es.getLength());
+ }
+ }
+ UndoableEdit cEdit = c.insertString(0, sb.toString());
+
+ // build the event and element structure
+ int length = sb.length();
+ DefaultDocumentEvent evnt =
+ new DefaultDocumentEvent(0, length, DocumentEvent.EventType.INSERT);
+ evnt.addEdit(cEdit);
+ buffer.create(length, data, evnt);
+
+ // update bidi (possibly)
+ super.insertUpdate(evnt, null);
+
+ // notify the listeners
+ evnt.end();
+ fireInsertUpdate(evnt);
+ fireUndoableEditUpdate(new UndoableEditEvent(this, evnt));
+ } catch (BadLocationException ble) {
+ throw new StateInvariantError("problem initializing");
+ } finally {
+ writeUnlock();
+ }
+
+ }
+
+ /**
+ * Inserts new elements in bulk. This is useful to allow
+ * parsing with the document in an unlocked state and
+ * prepare an element structure modification. This method
+ * takes an array of tokens that describe how to update an
+ * element structure so the time within a write lock can
+ * be greatly reduced in an asynchronous update situation.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param offset the starting offset >= 0
+ * @param data the element data
+ * @exception BadLocationException for an invalid starting offset
+ */
+ protected void insert(int offset, ElementSpec[] data) throws BadLocationException {
+ if (data == null || data.length == 0) {
+ return;
+ }
+
+ try {
+ writeLock();
+
+ // install the content
+ Content c = getContent();
+ int n = data.length;
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < n; i++) {
+ ElementSpec es = data[i];
+ if (es.getLength() > 0) {
+ sb.append(es.getArray(), es.getOffset(), es.getLength());
+ }
+ }
+ if (sb.length() == 0) {
+ // Nothing to insert, bail.
+ return;
+ }
+ UndoableEdit cEdit = c.insertString(offset, sb.toString());
+
+ // create event and build the element structure
+ int length = sb.length();
+ DefaultDocumentEvent evnt =
+ new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.INSERT);
+ evnt.addEdit(cEdit);
+ buffer.insert(offset, length, data, evnt);
+
+ // update bidi (possibly)
+ super.insertUpdate(evnt, null);
+
+ // notify the listeners
+ evnt.end();
+ fireInsertUpdate(evnt);
+ fireUndoableEditUpdate(new UndoableEditEvent(this, evnt));
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ /**
+ * Removes an element from this document.
+ *
+ * <p>The element is removed from its parent element, as well as
+ * the text in the range identified by the element. If the
+ * element isn't associated with the document, {@code
+ * IllegalArgumentException} is thrown.</p>
+ *
+ * <p>As empty branch elements are not allowed in the document, if the
+ * element is the sole child, its parent element is removed as well,
+ * recursively. This means that when replacing all the children of a
+ * particular element, new children should be added <em>before</em>
+ * removing old children.
+ *
+ * <p>Element removal results in two events being fired, the
+ * {@code DocumentEvent} for changes in element structure and {@code
+ * UndoableEditEvent} for changes in document content.</p>
+ *
+ * <p>If the element contains end-of-content mark (the last {@code
+ * "\n"} character in document), this character is not removed;
+ * instead, preceding leaf element is extended to cover the
+ * character. If the last leaf already ends with {@code "\n",} it is
+ * included in content removal.</p>
+ *
+ * <p>If the element is {@code null,} {@code NullPointerException} is
+ * thrown. If the element structure would become invalid after the removal,
+ * for example if the element is the document root element, {@code
+ * IllegalArgumentException} is thrown. If the current element structure is
+ * invalid, {@code IllegalStateException} is thrown.</p>
+ *
+ * @param elem the element to remove
+ * @throws NullPointerException if the element is {@code null}
+ * @throws IllegalArgumentException if the element could not be removed
+ * @throws IllegalStateException if the element structure is invalid
+ *
+ * @since 1.7
+ */
+ public void removeElement(Element elem) {
+ try {
+ writeLock();
+ removeElementImpl(elem);
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ private void removeElementImpl(Element elem) {
+ if (elem.getDocument() != this) {
+ throw new IllegalArgumentException("element doesn't belong to document");
+ }
+ BranchElement parent = (BranchElement) elem.getParentElement();
+ if (parent == null) {
+ throw new IllegalArgumentException("can't remove the root element");
+ }
+
+ int startOffset = elem.getStartOffset();
+ int removeFrom = startOffset;
+ int endOffset = elem.getEndOffset();
+ int removeTo = endOffset;
+ int lastEndOffset = getLength() + 1;
+ Content content = getContent();
+ boolean atEnd = false;
+ boolean isComposedText = Utilities.isComposedTextElement(elem);
+
+ if (endOffset >= lastEndOffset) {
+ // element includes the last "\n" character, needs special handling
+ if (startOffset <= 0) {
+ throw new IllegalArgumentException("can't remove the whole content");
+ }
+ removeTo = lastEndOffset - 1; // last "\n" must not be removed
+ try {
+ if (content.getString(startOffset - 1, 1).charAt(0) == '\n') {
+ removeFrom--; // preceding leaf ends with "\n", remove it
+ }
+ } catch (BadLocationException ble) { // can't happen
+ throw new IllegalStateException(ble);
+ }
+ atEnd = true;
+ }
+ int length = removeTo - removeFrom;
+
+ DefaultDocumentEvent dde = new DefaultDocumentEvent(removeFrom,
+ length, DefaultDocumentEvent.EventType.REMOVE);
+ UndoableEdit ue = null;
+ // do not leave empty branch elements
+ while (parent.getElementCount() == 1) {
+ elem = parent;
+ parent = (BranchElement) parent.getParentElement();
+ if (parent == null) { // shouldn't happen
+ throw new IllegalStateException("invalid element structure");
+ }
+ }
+ Element[] removed = { elem };
+ Element[] added = {};
+ int index = parent.getElementIndex(startOffset);
+ parent.replace(index, 1, added);
+ dde.addEdit(new ElementEdit(parent, index, removed, added));
+ if (length > 0) {
+ try {
+ ue = content.remove(removeFrom, length);
+ if (ue != null) {
+ dde.addEdit(ue);
+ }
+ } catch (BadLocationException ble) {
+ // can only happen if the element structure is severely broken
+ throw new IllegalStateException(ble);
+ }
+ lastEndOffset -= length;
+ }
+
+ if (atEnd) {
+ // preceding leaf element should be extended to cover orphaned "\n"
+ Element prevLeaf = parent.getElement(parent.getElementCount() - 1);
+ while ((prevLeaf != null) && !prevLeaf.isLeaf()) {
+ prevLeaf = prevLeaf.getElement(prevLeaf.getElementCount() - 1);
+ }
+ if (prevLeaf == null) { // shouldn't happen
+ throw new IllegalStateException("invalid element structure");
+ }
+ int prevStartOffset = prevLeaf.getStartOffset();
+ BranchElement prevParent = (BranchElement) prevLeaf.getParentElement();
+ int prevIndex = prevParent.getElementIndex(prevStartOffset);
+ Element newElem = null;
+ newElem = createLeafElement(prevParent, prevLeaf.getAttributes(),
+ prevStartOffset, lastEndOffset);
+ Element[] prevRemoved = { prevLeaf };
+ Element[] prevAdded = { newElem };
+ prevParent.replace(prevIndex, 1, prevAdded);
+ dde.addEdit(new ElementEdit(prevParent, prevIndex,
+ prevRemoved, prevAdded));
+ }
+
+ postRemoveUpdate(dde);
+ dde.end();
+ fireRemoveUpdate(dde);
+ if (! (isComposedText && (ue != null))) {
+ // do not fire UndoabeEdit event for composed text edit (unsupported)
+ fireUndoableEditUpdate(new UndoableEditEvent(this, dde));
+ }
+ }
+
+ /**
+ * Adds a new style into the logical style hierarchy. Style attributes
+ * resolve from bottom up so an attribute specified in a child
+ * will override an attribute specified in the parent.
+ *
+ * @param nm the name of the style (must be unique within the
+ * collection of named styles). The name may be null if the style
+ * is unnamed, but the caller is responsible
+ * for managing the reference returned as an unnamed style can't
+ * be fetched by name. An unnamed style may be useful for things
+ * like character attribute overrides such as found in a style
+ * run.
+ * @param parent the parent style. This may be null if unspecified
+ * attributes need not be resolved in some other style.
+ * @return the style
+ */
+ public Style addStyle(String nm, Style parent) {
+ StyleContext styles = (StyleContext) getAttributeContext();
+ return styles.addStyle(nm, parent);
+ }
+
+ /**
+ * Removes a named style previously added to the document.
+ *
+ * @param nm the name of the style to remove
+ */
+ public void removeStyle(String nm) {
+ StyleContext styles = (StyleContext) getAttributeContext();
+ styles.removeStyle(nm);
+ }
+
+ /**
+ * Fetches a named style previously added.
+ *
+ * @param nm the name of the style
+ * @return the style
+ */
+ public Style getStyle(String nm) {
+ StyleContext styles = (StyleContext) getAttributeContext();
+ return styles.getStyle(nm);
+ }
+
+
+ /**
+ * Fetches the list of of style names.
+ *
+ * @return all the style names
+ */
+ public Enumeration<?> getStyleNames() {
+ return ((StyleContext) getAttributeContext()).getStyleNames();
+ }
+
+ /**
+ * Sets the logical style to use for the paragraph at the
+ * given position. If attributes aren't explicitly set
+ * for character and paragraph attributes they will resolve
+ * through the logical style assigned to the paragraph, which
+ * in turn may resolve through some hierarchy completely
+ * independent of the element hierarchy in the document.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param pos the offset from the start of the document >= 0
+ * @param s the logical style to assign to the paragraph, null if none
+ */
+ public void setLogicalStyle(int pos, Style s) {
+ Element paragraph = getParagraphElement(pos);
+ if ((paragraph != null) && (paragraph instanceof AbstractElement)) {
+ try {
+ writeLock();
+ StyleChangeUndoableEdit edit = new StyleChangeUndoableEdit((AbstractElement)paragraph, s);
+ ((AbstractElement)paragraph).setResolveParent(s);
+ int p0 = paragraph.getStartOffset();
+ int p1 = paragraph.getEndOffset();
+ DefaultDocumentEvent e =
+ new DefaultDocumentEvent(p0, p1 - p0, DocumentEvent.EventType.CHANGE);
+ e.addEdit(edit);
+ e.end();
+ fireChangedUpdate(e);
+ fireUndoableEditUpdate(new UndoableEditEvent(this, e));
+ } finally {
+ writeUnlock();
+ }
+ }
+ }
+
+ /**
+ * Fetches the logical style assigned to the paragraph
+ * represented by the given position.
+ *
+ * @param p the location to translate to a paragraph
+ * and determine the logical style assigned >= 0. This
+ * is an offset from the start of the document.
+ * @return the style, null if none
+ */
+ public Style getLogicalStyle(int p) {
+ Style s = null;
+ Element paragraph = getParagraphElement(p);
+ if (paragraph != null) {
+ AttributeSet a = paragraph.getAttributes();
+ AttributeSet parent = a.getResolveParent();
+ if (parent instanceof Style) {
+ s = (Style) parent;
+ }
+ }
+ return s;
+ }
+
+ /**
+ * Sets attributes for some part of the document.
+ * A write lock is held by this operation while changes
+ * are being made, and a DocumentEvent is sent to the listeners
+ * after the change has been successfully completed.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param offset the offset in the document >= 0
+ * @param length the length >= 0
+ * @param s the attributes
+ * @param replace true if the previous attributes should be replaced
+ * before setting the new attributes
+ */
+ public void setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace) {
+ if (length == 0) {
+ return;
+ }
+ try {
+ writeLock();
+ DefaultDocumentEvent changes =
+ new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
+
+ // split elements that need it
+ buffer.change(offset, length, changes);
+
+ AttributeSet sCopy = s.copyAttributes();
+
+ // PENDING(prinz) - this isn't a very efficient way to iterate
+ int lastEnd = Integer.MAX_VALUE;
+ for (int pos = offset; pos < (offset + length); pos = lastEnd) {
+ Element run = getCharacterElement(pos);
+ lastEnd = run.getEndOffset();
+ if (pos == lastEnd) {
+ // offset + length beyond length of document, bail.
+ break;
+ }
+ MutableAttributeSet attr = (MutableAttributeSet) run.getAttributes();
+ changes.addEdit(new AttributeUndoableEdit(run, sCopy, replace));
+ if (replace) {
+ attr.removeAttributes(attr);
+ }
+ attr.addAttributes(s);
+ }
+ changes.end();
+ fireChangedUpdate(changes);
+ fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
+ } finally {
+ writeUnlock();
+ }
+
+ }
+
+ /**
+ * Sets attributes for a paragraph.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param offset the offset into the paragraph >= 0
+ * @param length the number of characters affected >= 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();
+ DefaultDocumentEvent changes =
+ new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
+
+ AttributeSet sCopy = s.copyAttributes();
+
+ // PENDING(prinz) - this assumes a particular element structure
+ Element section = getDefaultRootElement();
+ int index0 = section.getElementIndex(offset);
+ int index1 = section.getElementIndex(offset + ((length > 0) ? length - 1 : 0));
+ boolean isI18N = Boolean.TRUE.equals(getProperty(I18NProperty));
+ boolean hasRuns = false;
+ for (int i = index0; i <= index1; i++) {
+ Element paragraph = section.getElement(i);
+ MutableAttributeSet attr = (MutableAttributeSet) paragraph.getAttributes();
+ changes.addEdit(new AttributeUndoableEdit(paragraph, sCopy, replace));
+ if (replace) {
+ attr.removeAttributes(attr);
+ }
+ attr.addAttributes(s);
+ if (isI18N && !hasRuns) {
+ hasRuns = (attr.getAttribute(TextAttribute.RUN_DIRECTION) != null);
+ }
+ }
+
+ if (hasRuns) {
+ updateBidi( changes );
+ }
+
+ changes.end();
+ fireChangedUpdate(changes);
+ fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
+ } finally {
+ writeUnlock();
+ }
+ }
+
+ /**
+ * Gets the paragraph element at the offset <code>pos</code>.
+ * A paragraph consists of at least one child Element, which is usually
+ * a leaf.
+ *
+ * @param pos the starting offset >= 0
+ * @return the element
+ */
+ public Element getParagraphElement(int pos) {
+ Element e = null;
+ for (e = getDefaultRootElement(); ! e.isLeaf(); ) {
+ int index = e.getElementIndex(pos);
+ e = e.getElement(index);
+ }
+ if(e != null)
+ return e.getParentElement();
+ return e;
+ }
+
+ /**
+ * Gets a character element based on a position.
+ *
+ * @param pos the position in the document >= 0
+ * @return the element
+ */
+ public Element getCharacterElement(int pos) {
+ Element e = null;
+ for (e = getDefaultRootElement(); ! e.isLeaf(); ) {
+ int index = e.getElementIndex(pos);
+ e = e.getElement(index);
+ }
+ return e;
+ }
+
+ // --- local methods -------------------------------------------------
+
+ /**
+ * 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) {
+ int offset = chng.getOffset();
+ int length = chng.getLength();
+ if (attr == null) {
+ attr = SimpleAttributeSet.EMPTY;
+ }
+
+ // Paragraph attributes should come from point after insertion.
+ // You really only notice this when inserting at a paragraph
+ // boundary.
+ Element paragraph = getParagraphElement(offset + length);
+ AttributeSet pattr = paragraph.getAttributes();
+ // Character attributes should come from actual insertion point.
+ Element pParagraph = getParagraphElement(offset);
+ Element run = pParagraph.getElement(pParagraph.getElementIndex
+ (offset));
+ int endOffset = offset + length;
+ boolean insertingAtBoundry = (run.getEndOffset() == endOffset);
+ AttributeSet cattr = run.getAttributes();
+
+ try {
+ Segment s = new Segment();
+ Vector parseBuffer = new Vector();
+ ElementSpec lastStartSpec = null;
+ boolean insertingAfterNewline = false;
+ short lastStartDirection = ElementSpec.OriginateDirection;
+ // Check if the previous character was a newline.
+ if (offset > 0) {
+ getText(offset - 1, 1, s);
+ if (s.array[s.offset] == '\n') {
+ // Inserting after a newline.
+ insertingAfterNewline = true;
+ lastStartDirection = createSpecsForInsertAfterNewline
+ (paragraph, pParagraph, pattr, parseBuffer,
+ offset, endOffset);
+ for(int counter = parseBuffer.size() - 1; counter >= 0;
+ counter--) {
+ ElementSpec spec = (ElementSpec)parseBuffer.
+ elementAt(counter);
+ if(spec.getType() == ElementSpec.StartTagType) {
+ lastStartSpec = spec;
+ break;
+ }
+ }
+ }
+ }
+ // If not inserting after a new line, pull the attributes for
+ // new paragraphs from the paragraph under the insertion point.
+ if(!insertingAfterNewline)
+ pattr = pParagraph.getAttributes();
+
+ getText(offset, length, s);
+ char[] txt = s.array;
+ int n = s.offset + s.count;
+ int lastOffset = s.offset;
+
+ for (int i = s.offset; i < n; i++) {
+ if (txt[i] == '\n') {
+ int breakOffset = i + 1;
+ parseBuffer.addElement(
+ new ElementSpec(attr, ElementSpec.ContentType,
+ breakOffset - lastOffset));
+ parseBuffer.addElement(
+ new ElementSpec(null, ElementSpec.EndTagType));
+ lastStartSpec = new ElementSpec(pattr, ElementSpec.
+ StartTagType);
+ parseBuffer.addElement(lastStartSpec);
+ lastOffset = breakOffset;
+ }
+ }
+ if (lastOffset < n) {
+ parseBuffer.addElement(
+ new ElementSpec(attr, ElementSpec.ContentType,
+ n - lastOffset));
+ }
+
+ ElementSpec first = (ElementSpec) parseBuffer.firstElement();
+
+ int docLength = getLength();
+
+ // Check for join previous of first content.
+ if(first.getType() == ElementSpec.ContentType &&
+ cattr.isEqual(attr)) {
+ first.setDirection(ElementSpec.JoinPreviousDirection);
+ }
+
+ // Do a join fracture/next for last start spec if necessary.
+ if(lastStartSpec != null) {
+ if(insertingAfterNewline) {
+ lastStartSpec.setDirection(lastStartDirection);
+ }
+ // Join to the fracture if NOT inserting at the end
+ // (fracture only happens when not inserting at end of
+ // paragraph).
+ else if(pParagraph.getEndOffset() != endOffset) {
+ lastStartSpec.setDirection(ElementSpec.
+ JoinFractureDirection);
+ }
+ // Join to next if parent of pParagraph has another
+ // element after pParagraph, and it isn't a leaf.
+ else {
+ Element parent = pParagraph.getParentElement();
+ int pParagraphIndex = parent.getElementIndex(offset);
+ if((pParagraphIndex + 1) < parent.getElementCount() &&
+ !parent.getElement(pParagraphIndex + 1).isLeaf()) {
+ lastStartSpec.setDirection(ElementSpec.
+ JoinNextDirection);
+ }
+ }
+ }
+
+ // Do a JoinNext for last spec if it is content, it doesn't
+ // already have a direction set, no new paragraphs have been
+ // inserted or a new paragraph has been inserted and its join
+ // direction isn't originate, and the element at endOffset
+ // is a leaf.
+ if(insertingAtBoundry && endOffset < docLength) {
+ ElementSpec last = (ElementSpec) parseBuffer.lastElement();
+ if(last.getType() == ElementSpec.ContentType &&
+ last.getDirection() != ElementSpec.JoinPreviousDirection &&
+ ((lastStartSpec == null && (paragraph == pParagraph ||
+ insertingAfterNewline)) ||
+ (lastStartSpec != null && lastStartSpec.getDirection() !=
+ ElementSpec.OriginateDirection))) {
+ Element nextRun = paragraph.getElement(paragraph.
+ getElementIndex(endOffset));
+ // Don't try joining to a branch!
+ if(nextRun.isLeaf() &&
+ attr.isEqual(nextRun.getAttributes())) {
+ last.setDirection(ElementSpec.JoinNextDirection);
+ }
+ }
+ }
+ // If not inserting at boundary and there is going to be a
+ // fracture, then can join next on last content if cattr
+ // matches the new attributes.
+ else if(!insertingAtBoundry && lastStartSpec != null &&
+ lastStartSpec.getDirection() ==
+ ElementSpec.JoinFractureDirection) {
+ ElementSpec last = (ElementSpec) parseBuffer.lastElement();
+ if(last.getType() == ElementSpec.ContentType &&
+ last.getDirection() != ElementSpec.JoinPreviousDirection &&
+ attr.isEqual(cattr)) {
+ last.setDirection(ElementSpec.JoinNextDirection);
+ }
+ }
+
+ // Check for the composed text element. If it is, merge the character attributes
+ // into this element as well.
+ if (Utilities.isComposedTextAttributeDefined(attr)) {
+ ((MutableAttributeSet)attr).addAttributes(cattr);
+ ((MutableAttributeSet)attr).addAttribute(AbstractDocument.ElementNameAttribute,
+ AbstractDocument.ContentElementName);
+ }
+
+ ElementSpec[] spec = new ElementSpec[parseBuffer.size()];
+ parseBuffer.copyInto(spec);
+ buffer.insert(offset, length, spec, chng);
+ } catch (BadLocationException bl) {
+ }
+
+ super.insertUpdate( chng, attr );
+ }
+
+ /**
+ * This is called by insertUpdate when inserting after a new line.
+ * It generates, in <code>parseBuffer</code>, ElementSpecs that will
+ * position the stack in <code>paragraph</code>.<p>
+ * It returns the direction the last StartSpec should have (this don't
+ * necessarily create the last start spec).
+ */
+ short createSpecsForInsertAfterNewline(Element paragraph,
+ Element pParagraph, AttributeSet pattr, Vector parseBuffer,
+ int offset, int endOffset) {
+ // Need to find the common parent of pParagraph and paragraph.
+ if(paragraph.getParentElement() == pParagraph.getParentElement()) {
+ // The simple (and common) case that pParagraph and
+ // paragraph have the same parent.
+ ElementSpec spec = new ElementSpec(pattr, ElementSpec.EndTagType);
+ parseBuffer.addElement(spec);
+ spec = new ElementSpec(pattr, ElementSpec.StartTagType);
+ parseBuffer.addElement(spec);
+ if(pParagraph.getEndOffset() != endOffset)
+ return ElementSpec.JoinFractureDirection;
+
+ Element parent = pParagraph.getParentElement();
+ if((parent.getElementIndex(offset) + 1) < parent.getElementCount())
+ return ElementSpec.JoinNextDirection;
+ }
+ else {
+ // Will only happen for text with more than 2 levels.
+ // Find the common parent of a paragraph and pParagraph
+ Vector leftParents = new Vector();
+ Vector rightParents = new Vector();
+ Element e = pParagraph;
+ while(e != null) {
+ leftParents.addElement(e);
+ e = e.getParentElement();
+ }
+ e = paragraph;
+ int leftIndex = -1;
+ while(e != null && (leftIndex = leftParents.indexOf(e)) == -1) {
+ rightParents.addElement(e);
+ e = e.getParentElement();
+ }
+ if(e != null) {
+ // e identifies the common parent.
+ // Build the ends.
+ for(int counter = 0; counter < leftIndex;
+ counter++) {
+ parseBuffer.addElement(new ElementSpec
+ (null, ElementSpec.EndTagType));
+ }
+ // And the starts.
+ ElementSpec spec = null;
+ for(int counter = rightParents.size() - 1;
+ counter >= 0; counter--) {
+ spec = new ElementSpec(((Element)rightParents.
+ elementAt(counter)).getAttributes(),
+ ElementSpec.StartTagType);
+ if(counter > 0)
+ spec.setDirection(ElementSpec.JoinNextDirection);
+ parseBuffer.addElement(spec);
+ }
+ // If there are right parents, then we generated starts
+ // down the right subtree and there will be an element to
+ // join to.
+ if(rightParents.size() > 0)
+ return ElementSpec.JoinNextDirection;
+ // No right subtree, e.getElement(endOffset) is a
+ // leaf. There will be a facture.
+ return ElementSpec.JoinFractureDirection;
+ }
+ // else: Could throw an exception here, but should never get here!
+ }
+ return ElementSpec.OriginateDirection;
+ }
+
+ /**
+ * Updates document structure as a result of text removal.
+ *
+ * @param chng a description of the document change
+ */
+ protected void removeUpdate(DefaultDocumentEvent chng) {
+ super.removeUpdate(chng);
+ buffer.remove(chng.getOffset(), chng.getLength(), chng);
+ }
+
+ /**
+ * 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();
+ BranchElement section = new SectionElement();
+ BranchElement paragraph = new BranchElement(section, null);
+
+ LeafElement brk = new LeafElement(paragraph, null, 0, 1);
+ Element[] buff = new Element[1];
+ buff[0] = brk;
+ paragraph.replace(0, 0, buff);
+
+ buff[0] = paragraph;
+ section.replace(0, 0, buff);
+ writeUnlock();
+ return section;
+ }
+
+ /**
+ * Gets the foreground color from an attribute set.
+ *
+ * @param attr the attribute set
+ * @return the color
+ */
+ public Color getForeground(AttributeSet attr) {
+ StyleContext styles = (StyleContext) getAttributeContext();
+ return styles.getForeground(attr);
+ }
+
+ /**
+ * Gets the background color from an attribute set.
+ *
+ * @param attr the attribute set
+ * @return the color
+ */
+ public Color getBackground(AttributeSet attr) {
+ StyleContext styles = (StyleContext) getAttributeContext();
+ return styles.getBackground(attr);
+ }
+
+ /**
+ * Gets the font from an attribute set.
+ *
+ * @param attr the attribute set
+ * @return the font
+ */
+ public Font getFont(AttributeSet attr) {
+ StyleContext styles = (StyleContext) getAttributeContext();
+ return styles.getFont(attr);
+ }
+
+ /**
+ * Called when any of this document's styles have changed.
+ * Subclasses may wish to be intelligent about what gets damaged.
+ *
+ * @param style The Style that has changed.
+ */
+ protected void styleChanged(Style style) {
+ // Only propagate change updated if have content
+ if (getLength() != 0) {
+ // lazily create a ChangeUpdateRunnable
+ if (updateRunnable == null) {
+ updateRunnable = new ChangeUpdateRunnable();
+ }
+
+ // We may get a whole batch of these at once, so only
+ // queue the runnable if it is not already pending
+ synchronized(updateRunnable) {
+ if (!updateRunnable.isPending) {
+ SwingUtilities.invokeLater(updateRunnable);
+ updateRunnable.isPending = true;
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a document listener for notification of any changes.
+ *
+ * @param listener the listener
+ * @see Document#addDocumentListener
+ */
+ public void addDocumentListener(DocumentListener listener) {
+ synchronized(listeningStyles) {
+ int oldDLCount = listenerList.getListenerCount
+ (DocumentListener.class);
+ super.addDocumentListener(listener);
+ if (oldDLCount == 0) {
+ if (styleContextChangeListener == null) {
+ styleContextChangeListener =
+ createStyleContextChangeListener();
+ }
+ if (styleContextChangeListener != null) {
+ StyleContext styles = (StyleContext)getAttributeContext();
+ List<ChangeListener> staleListeners =
+ AbstractChangeHandler.getStaleListeners(styleContextChangeListener);
+ for (ChangeListener l: staleListeners) {
+ styles.removeChangeListener(l);
+ }
+ styles.addChangeListener(styleContextChangeListener);
+ }
+ updateStylesListeningTo();
+ }
+ }
+ }
+
+ /**
+ * Removes a document listener.
+ *
+ * @param listener the listener
+ * @see Document#removeDocumentListener
+ */
+ public void removeDocumentListener(DocumentListener listener) {
+ synchronized(listeningStyles) {
+ super.removeDocumentListener(listener);
+ if (listenerList.getListenerCount(DocumentListener.class) == 0) {
+ for (int counter = listeningStyles.size() - 1; counter >= 0;
+ counter--) {
+ ((Style)listeningStyles.elementAt(counter)).
+ removeChangeListener(styleChangeListener);
+ }
+ listeningStyles.removeAllElements();
+ if (styleContextChangeListener != null) {
+ StyleContext styles = (StyleContext)getAttributeContext();
+ styles.removeChangeListener(styleContextChangeListener);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a new instance of StyleChangeHandler.
+ */
+ ChangeListener createStyleChangeListener() {
+ return new StyleChangeHandler(this);
+ }
+
+ /**
+ * Returns a new instance of StyleContextChangeHandler.
+ */
+ ChangeListener createStyleContextChangeListener() {
+ return new StyleContextChangeHandler(this);
+ }
+
+ /**
+ * Adds a ChangeListener to new styles, and removes ChangeListener from
+ * old styles.
+ */
+ void updateStylesListeningTo() {
+ synchronized(listeningStyles) {
+ StyleContext styles = (StyleContext)getAttributeContext();
+ if (styleChangeListener == null) {
+ styleChangeListener = createStyleChangeListener();
+ }
+ if (styleChangeListener != null && styles != null) {
+ Enumeration styleNames = styles.getStyleNames();
+ Vector v = (Vector)listeningStyles.clone();
+ listeningStyles.removeAllElements();
+ List<ChangeListener> staleListeners =
+ AbstractChangeHandler.getStaleListeners(styleChangeListener);
+ while (styleNames.hasMoreElements()) {
+ String name = (String)styleNames.nextElement();
+ Style aStyle = styles.getStyle(name);
+ int index = v.indexOf(aStyle);
+ listeningStyles.addElement(aStyle);
+ if (index == -1) {
+ for (ChangeListener l: staleListeners) {
+ aStyle.removeChangeListener(l);
+ }
+ aStyle.addChangeListener(styleChangeListener);
+ }
+ else {
+ v.removeElementAt(index);
+ }
+ }
+ for (int counter = v.size() - 1; counter >= 0; counter--) {
+ Style aStyle = (Style)v.elementAt(counter);
+ aStyle.removeChangeListener(styleChangeListener);
+ }
+ if (listeningStyles.size() == 0) {
+ styleChangeListener = null;
+ }
+ }
+ }
+ }
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException {
+ listeningStyles = new Vector();
+ s.defaultReadObject();
+ // Reinstall style listeners.
+ if (styleContextChangeListener == null &&
+ listenerList.getListenerCount(DocumentListener.class) > 0) {
+ styleContextChangeListener = createStyleContextChangeListener();
+ if (styleContextChangeListener != null) {
+ StyleContext styles = (StyleContext)getAttributeContext();
+ styles.addChangeListener(styleContextChangeListener);
+ }
+ updateStylesListeningTo();
+ }
+ }
+
+ // --- member variables -----------------------------------------------------------
+
+ /**
+ * The default size of the initial content buffer.
+ */
+ public static final int BUFFER_SIZE_DEFAULT = 4096;
+
+ protected ElementBuffer buffer;
+
+ /** Styles listening to. */
+ private transient Vector listeningStyles;
+
+ /** Listens to Styles. */
+ private transient ChangeListener styleChangeListener;
+
+ /** Listens to Styles. */
+ private transient ChangeListener styleContextChangeListener;
+
+ /** Run to create a change event for the document */
+ private transient ChangeUpdateRunnable updateRunnable;
+
+ /**
+ * Default root element for a document... maps out the
+ * paragraphs/lines contained.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ protected class SectionElement extends BranchElement {
+
+ /**
+ * Creates a new SectionElement.
+ */
+ public SectionElement() {
+ super(null, null);
+ }
+
+ /**
+ * Gets the name of the element.
+ *
+ * @return the name
+ */
+ public String getName() {
+ return SectionElementName;
+ }
+ }
+
+ /**
+ * Specification for building elements.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class ElementSpec {
+
+ /**
+ * A possible value for getType. This specifies
+ * that this record type is a start tag and
+ * represents markup that specifies the start
+ * of an element.
+ */
+ public static final short StartTagType = 1;
+
+ /**
+ * A possible value for getType. This specifies
+ * that this record type is a end tag and
+ * represents markup that specifies the end
+ * of an element.
+ */
+ public static final short EndTagType = 2;
+
+ /**
+ * A possible value for getType. This specifies
+ * that this record type represents content.
+ */
+ public static final short ContentType = 3;
+
+ /**
+ * A possible value for getDirection. This specifies
+ * that the data associated with this record should
+ * be joined to what precedes it.
+ */
+ public static final short JoinPreviousDirection = 4;
+
+ /**
+ * A possible value for getDirection. This specifies
+ * that the data associated with this record should
+ * be joined to what follows it.
+ */
+ public static final short JoinNextDirection = 5;
+
+ /**
+ * A possible value for getDirection. This specifies
+ * that the data associated with this record should
+ * be used to originate a new element. This would be
+ * the normal value.
+ */
+ public static final short OriginateDirection = 6;
+
+ /**
+ * A possible value for getDirection. This specifies
+ * that the data associated with this record should
+ * be joined to the fractured element.
+ */
+ public static final short JoinFractureDirection = 7;
+
+
+ /**
+ * Constructor useful for markup when the markup will not
+ * be stored in the document.
+ *
+ * @param a the attributes for the element
+ * @param type the type of the element (StartTagType, EndTagType,
+ * ContentType)
+ */
+ public ElementSpec(AttributeSet a, short type) {
+ this(a, type, null, 0, 0);
+ }
+
+ /**
+ * Constructor for parsing inside the document when
+ * the data has already been added, but len information
+ * is needed.
+ *
+ * @param a the attributes for the element
+ * @param type the type of the element (StartTagType, EndTagType,
+ * ContentType)
+ * @param len the length >= 0
+ */
+ public ElementSpec(AttributeSet a, short type, int len) {
+ this(a, type, null, 0, len);
+ }
+
+ /**
+ * Constructor for creating a spec externally for batch
+ * input of content and markup into the document.
+ *
+ * @param a the attributes for the element
+ * @param type the type of the element (StartTagType, EndTagType,
+ * ContentType)
+ * @param txt the text for the element
+ * @param offs the offset into the text >= 0
+ * @param len the length of the text >= 0
+ */
+ public ElementSpec(AttributeSet a, short type, char[] txt,
+ int offs, int len) {
+ attr = a;
+ this.type = type;
+ this.data = txt;
+ this.offs = offs;
+ this.len = len;
+ this.direction = OriginateDirection;
+ }
+
+ /**
+ * Sets the element type.
+ *
+ * @param type the type of the element (StartTagType, EndTagType,
+ * ContentType)
+ */
+ public void setType(short type) {
+ this.type = type;
+ }
+
+ /**
+ * Gets the element type.
+ *
+ * @return the type of the element (StartTagType, EndTagType,
+ * ContentType)
+ */
+ public short getType() {
+ return type;
+ }
+
+ /**
+ * Sets the direction.
+ *
+ * @param direction the direction (JoinPreviousDirection,
+ * JoinNextDirection)
+ */
+ public void setDirection(short direction) {
+ this.direction = direction;
+ }
+
+ /**
+ * Gets the direction.
+ *
+ * @return the direction (JoinPreviousDirection, JoinNextDirection)
+ */
+ public short getDirection() {
+ return direction;
+ }
+
+ /**
+ * Gets the element attributes.
+ *
+ * @return the attribute set
+ */
+ public AttributeSet getAttributes() {
+ return attr;
+ }
+
+ /**
+ * Gets the array of characters.
+ *
+ * @return the array
+ */
+ public char[] getArray() {
+ return data;
+ }
+
+
+ /**
+ * Gets the starting offset.
+ *
+ * @return the offset >= 0
+ */
+ public int getOffset() {
+ return offs;
+ }
+
+ /**
+ * Gets the length.
+ *
+ * @return the length >= 0
+ */
+ public int getLength() {
+ return len;
+ }
+
+ /**
+ * Converts the element to a string.
+ *
+ * @return the string
+ */
+ public String toString() {
+ String tlbl = "??";
+ String plbl = "??";
+ switch(type) {
+ case StartTagType:
+ tlbl = "StartTag";
+ break;
+ case ContentType:
+ tlbl = "Content";
+ break;
+ case EndTagType:
+ tlbl = "EndTag";
+ break;
+ }
+ switch(direction) {
+ case JoinPreviousDirection:
+ plbl = "JoinPrevious";
+ break;
+ case JoinNextDirection:
+ plbl = "JoinNext";
+ break;
+ case OriginateDirection:
+ plbl = "Originate";
+ break;
+ case JoinFractureDirection:
+ plbl = "Fracture";
+ break;
+ }
+ return tlbl + ":" + plbl + ":" + getLength();
+ }
+
+ private AttributeSet attr;
+ private int len;
+ private short type;
+ private short direction;
+
+ private int offs;
+ private char[] data;
+ }
+
+ /**
+ * Class to manage changes to the element
+ * hierarchy.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public class ElementBuffer implements Serializable {
+
+ /**
+ * Creates a new ElementBuffer.
+ *
+ * @param root the root element
+ * @since 1.4
+ */
+ public ElementBuffer(Element root) {
+ this.root = root;
+ changes = new Vector();
+ path = new Stack();
+ }
+
+ /**
+ * Gets the root element.
+ *
+ * @return the root element
+ */
+ public Element getRootElement() {
+ return root;
+ }
+
+ /**
+ * Inserts new content.
+ *
+ * @param offset the starting offset >= 0
+ * @param length the length >= 0
+ * @param data the data to insert
+ * @param de the event capturing this edit
+ */
+ public void insert(int offset, int length, ElementSpec[] data,
+ DefaultDocumentEvent de) {
+ if (length == 0) {
+ // Nothing was inserted, no structure change.
+ return;
+ }
+ insertOp = true;
+ beginEdits(offset, length);
+ insertUpdate(data);
+ endEdits(de);
+
+ insertOp = false;
+ }
+
+ void create(int length, ElementSpec[] data, DefaultDocumentEvent de) {
+ insertOp = true;
+ beginEdits(offset, length);
+
+ // PENDING(prinz) this needs to be fixed to create a new
+ // root element as well, but requires changes to the
+ // DocumentEvent to inform the views that there is a new
+ // root element.
+
+ // Recreate the ending fake element to have the correct offsets.
+ Element elem = root;
+ int index = elem.getElementIndex(0);
+ while (! elem.isLeaf()) {
+ Element child = elem.getElement(index);
+ push(elem, index);
+ elem = child;
+ index = elem.getElementIndex(0);
+ }
+ ElemChanges ec = (ElemChanges) path.peek();
+ Element child = ec.parent.getElement(ec.index);
+ ec.added.addElement(createLeafElement(ec.parent,
+ child.getAttributes(), getLength(),
+ child.getEndOffset()));
+ ec.removed.addElement(child);
+ while (path.size() > 1) {
+ pop();
+ }
+
+ int n = data.length;
+
+ // Reset the root elements attributes.
+ AttributeSet newAttrs = null;
+ if (n > 0 && data[0].getType() == ElementSpec.StartTagType) {
+ newAttrs = data[0].getAttributes();
+ }
+ if (newAttrs == null) {
+ newAttrs = SimpleAttributeSet.EMPTY;
+ }
+ MutableAttributeSet attr = (MutableAttributeSet)root.
+ getAttributes();
+ de.addEdit(new AttributeUndoableEdit(root, newAttrs, true));
+ attr.removeAttributes(attr);
+ attr.addAttributes(newAttrs);
+
+ // fold in the specified subtree
+ for (int i = 1; i < n; i++) {
+ insertElement(data[i]);
+ }
+
+ // pop the remaining path
+ while (path.size() != 0) {
+ pop();
+ }
+
+ endEdits(de);
+ insertOp = false;
+ }
+
+ /**
+ * Removes content.
+ *
+ * @param offset the starting offset >= 0
+ * @param length the length >= 0
+ * @param de the event capturing this edit
+ */
+ public void remove(int offset, int length, DefaultDocumentEvent de) {
+ beginEdits(offset, length);
+ removeUpdate();
+ endEdits(de);
+ }
+
+ /**
+ * Changes content.
+ *
+ * @param offset the starting offset >= 0
+ * @param length the length >= 0
+ * @param de the event capturing this edit
+ */
+ public void change(int offset, int length, DefaultDocumentEvent de) {
+ beginEdits(offset, length);
+ changeUpdate();
+ endEdits(de);
+ }
+
+ /**
+ * Inserts an update into the document.
+ *
+ * @param data the elements to insert
+ */
+ protected void insertUpdate(ElementSpec[] data) {
+ // push the path
+ Element elem = root;
+ int index = elem.getElementIndex(offset);
+ while (! elem.isLeaf()) {
+ Element child = elem.getElement(index);
+ push(elem, (child.isLeaf() ? index : index+1));
+ elem = child;
+ index = elem.getElementIndex(offset);
+ }
+
+ // Build a copy of the original path.
+ insertPath = new ElemChanges[path.size()];
+ path.copyInto(insertPath);
+
+ // Haven't created the fracture yet.
+ createdFracture = false;
+
+ // Insert the first content.
+ int i;
+
+ recreateLeafs = false;
+ if(data[0].getType() == ElementSpec.ContentType) {
+ insertFirstContent(data);
+ pos += data[0].getLength();
+ i = 1;
+ }
+ else {
+ fractureDeepestLeaf(data);
+ i = 0;
+ }
+
+ // fold in the specified subtree
+ int n = data.length;
+ for (; i < n; i++) {
+ insertElement(data[i]);
+ }
+
+ // Fracture, if we haven't yet.
+ if(!createdFracture)
+ fracture(-1);
+
+ // pop the remaining path
+ while (path.size() != 0) {
+ pop();
+ }
+
+ // Offset the last index if necessary.
+ if(offsetLastIndex && offsetLastIndexOnReplace) {
+ insertPath[insertPath.length - 1].index++;
+ }
+
+ // Make sure an edit is going to be created for each of the
+ // original path items that have a change.
+ for(int counter = insertPath.length - 1; counter >= 0;
+ counter--) {
+ ElemChanges change = insertPath[counter];
+ if(change.parent == fracturedParent)
+ change.added.addElement(fracturedChild);
+ if((change.added.size() > 0 ||
+ change.removed.size() > 0) && !changes.contains(change)) {
+ // PENDING(sky): Do I need to worry about order here?
+ changes.addElement(change);
+ }
+ }
+
+ // An insert at 0 with an initial end implies some elements
+ // will have no children (the bottomost leaf would have length 0)
+ // this will find what element need to be removed and remove it.
+ if (offset == 0 && fracturedParent != null &&
+ data[0].getType() == ElementSpec.EndTagType) {
+ int counter = 0;
+ while (counter < data.length &&
+ data[counter].getType() == ElementSpec.EndTagType) {
+ counter++;
+ }
+ ElemChanges change = insertPath[insertPath.length -
+ counter - 1];
+ change.removed.insertElementAt(change.parent.getElement
+ (--change.index), 0);
+ }
+ }
+
+ /**
+ * Updates the element structure in response to a removal from the
+ * associated sequence in the document. Any elements consumed by the
+ * span of the removal are removed.
+ */
+ protected void removeUpdate() {
+ removeElements(root, offset, offset + length);
+ }
+
+ /**
+ * Updates the element structure in response to a change in the
+ * document.
+ */
+ protected void changeUpdate() {
+ boolean didEnd = split(offset, length);
+ if (! didEnd) {
+ // need to do the other end
+ while (path.size() != 0) {
+ pop();
+ }
+ split(offset + length, 0);
+ }
+ while (path.size() != 0) {
+ pop();
+ }
+ }
+
+ boolean split(int offs, int len) {
+ boolean splitEnd = false;
+ // push the path
+ Element e = root;
+ int index = e.getElementIndex(offs);
+ while (! e.isLeaf()) {
+ push(e, index);
+ e = e.getElement(index);
+ index = e.getElementIndex(offs);
+ }
+
+ ElemChanges ec = (ElemChanges) path.peek();
+ Element child = ec.parent.getElement(ec.index);
+ // make sure there is something to do... if the
+ // offset is already at a boundary then there is
+ // nothing to do.
+ if (child.getStartOffset() < offs && offs < child.getEndOffset()) {
+ // we need to split, now see if the other end is within
+ // the same parent.
+ int index0 = ec.index;
+ int index1 = index0;
+ if (((offs + len) < ec.parent.getEndOffset()) && (len != 0)) {
+ // it's a range split in the same parent
+ index1 = ec.parent.getElementIndex(offs+len);
+ if (index1 == index0) {
+ // it's a three-way split
+ ec.removed.addElement(child);
+ e = createLeafElement(ec.parent, child.getAttributes(),
+ child.getStartOffset(), offs);
+ ec.added.addElement(e);
+ e = createLeafElement(ec.parent, child.getAttributes(),
+ offs, offs + len);
+ ec.added.addElement(e);
+ e = createLeafElement(ec.parent, child.getAttributes(),
+ offs + len, child.getEndOffset());
+ ec.added.addElement(e);
+ return true;
+ } else {
+ child = ec.parent.getElement(index1);
+ if ((offs + len) == child.getStartOffset()) {
+ // end is already on a boundary
+ index1 = index0;
+ }
+ }
+ splitEnd = true;
+ }
+
+ // split the first location
+ pos = offs;
+ child = ec.parent.getElement(index0);
+ ec.removed.addElement(child);
+ e = createLeafElement(ec.parent, child.getAttributes(),
+ child.getStartOffset(), pos);
+ ec.added.addElement(e);
+ e = createLeafElement(ec.parent, child.getAttributes(),
+ pos, child.getEndOffset());
+ ec.added.addElement(e);
+
+ // pick up things in the middle
+ for (int i = index0 + 1; i < index1; i++) {
+ child = ec.parent.getElement(i);
+ ec.removed.addElement(child);
+ ec.added.addElement(child);
+ }
+
+ if (index1 != index0) {
+ child = ec.parent.getElement(index1);
+ pos = offs + len;
+ ec.removed.addElement(child);
+ e = createLeafElement(ec.parent, child.getAttributes(),
+ child.getStartOffset(), pos);
+ ec.added.addElement(e);
+ e = createLeafElement(ec.parent, child.getAttributes(),
+ pos, child.getEndOffset());
+ ec.added.addElement(e);
+ }
+ }
+ return splitEnd;
+ }
+
+ /**
+ * Creates the UndoableEdit record for the edits made
+ * in the buffer.
+ */
+ void endEdits(DefaultDocumentEvent de) {
+ int n = changes.size();
+ for (int i = 0; i < n; i++) {
+ ElemChanges ec = (ElemChanges) changes.elementAt(i);
+ Element[] removed = new Element[ec.removed.size()];
+ ec.removed.copyInto(removed);
+ Element[] added = new Element[ec.added.size()];
+ ec.added.copyInto(added);
+ int index = ec.index;
+ ((BranchElement) ec.parent).replace(index, removed.length, added);
+ ElementEdit ee = new ElementEdit((BranchElement) ec.parent,
+ index, removed, added);
+ de.addEdit(ee);
+ }
+
+ changes.removeAllElements();
+ path.removeAllElements();
+
+ /*
+ for (int i = 0; i < n; i++) {
+ ElemChanges ec = (ElemChanges) changes.elementAt(i);
+ System.err.print("edited: " + ec.parent + " at: " + ec.index +
+ " removed " + ec.removed.size());
+ if (ec.removed.size() > 0) {
+ int r0 = ((Element) ec.removed.firstElement()).getStartOffset();
+ int r1 = ((Element) ec.removed.lastElement()).getEndOffset();
+ System.err.print("[" + r0 + "," + r1 + "]");
+ }
+ System.err.print(" added " + ec.added.size());
+ if (ec.added.size() > 0) {
+ int p0 = ((Element) ec.added.firstElement()).getStartOffset();
+ int p1 = ((Element) ec.added.lastElement()).getEndOffset();
+ System.err.print("[" + p0 + "," + p1 + "]");
+ }
+ System.err.println("");
+ }
+ */
+ }
+
+ /**
+ * Initialize the buffer
+ */
+ void beginEdits(int offset, int length) {
+ this.offset = offset;
+ this.length = length;
+ this.endOffset = offset + length;
+ pos = offset;
+ if (changes == null) {
+ changes = new Vector();
+ } else {
+ changes.removeAllElements();
+ }
+ if (path == null) {
+ path = new Stack();
+ } else {
+ path.removeAllElements();
+ }
+ fracturedParent = null;
+ fracturedChild = null;
+ offsetLastIndex = offsetLastIndexOnReplace = false;
+ }
+
+ /**
+ * Pushes a new element onto the stack that represents
+ * the current path.
+ * @param record Whether or not the push should be
+ * recorded as an element change or not.
+ * @param isFracture true if pushing on an element that was created
+ * as the result of a fracture.
+ */
+ void push(Element e, int index, boolean isFracture) {
+ ElemChanges ec = new ElemChanges(e, index, isFracture);
+ path.push(ec);
+ }
+
+ void push(Element e, int index) {
+ push(e, index, false);
+ }
+
+ void pop() {
+ ElemChanges ec = (ElemChanges) path.peek();
+ path.pop();
+ if ((ec.added.size() > 0) || (ec.removed.size() > 0)) {
+ changes.addElement(ec);
+ } else if (! path.isEmpty()) {
+ Element e = ec.parent;
+ if(e.getElementCount() == 0) {
+ // if we pushed a branch element that didn't get
+ // used, make sure its not marked as having been added.
+ ec = (ElemChanges) path.peek();
+ ec.added.removeElement(e);
+ }
+ }
+ }
+
+ /**
+ * move the current offset forward by n.
+ */
+ void advance(int n) {
+ pos += n;
+ }
+
+ void insertElement(ElementSpec es) {
+ ElemChanges ec = (ElemChanges) path.peek();
+ switch(es.getType()) {
+ case ElementSpec.StartTagType:
+ switch(es.getDirection()) {
+ case ElementSpec.JoinNextDirection:
+ // Don't create a new element, use the existing one
+ // at the specified location.
+ Element parent = ec.parent.getElement(ec.index);
+
+ if(parent.isLeaf()) {
+ // This happens if inserting into a leaf, followed
+ // by a join next where next sibling is not a leaf.
+ if((ec.index + 1) < ec.parent.getElementCount())
+ parent = ec.parent.getElement(ec.index + 1);
+ else
+ throw new StateInvariantError("Join next to leaf");
+ }
+ // Not really a fracture, but need to treat it like
+ // one so that content join next will work correctly.
+ // We can do this because there will never be a join
+ // next followed by a join fracture.
+ push(parent, 0, true);
+ break;
+ case ElementSpec.JoinFractureDirection:
+ if(!createdFracture) {
+ // Should always be something on the stack!
+ fracture(path.size() - 1);
+ }
+ // If parent isn't a fracture, fracture will be
+ // fracturedChild.
+ if(!ec.isFracture) {
+ push(fracturedChild, 0, true);
+ }
+ else
+ // Parent is a fracture, use 1st element.
+ push(ec.parent.getElement(0), 0, true);
+ break;
+ default:
+ Element belem = createBranchElement(ec.parent,
+ es.getAttributes());
+ ec.added.addElement(belem);
+ push(belem, 0);
+ break;
+ }
+ break;
+ case ElementSpec.EndTagType:
+ pop();
+ break;
+ case ElementSpec.ContentType:
+ int len = es.getLength();
+ if (es.getDirection() != ElementSpec.JoinNextDirection) {
+ Element leaf = createLeafElement(ec.parent, es.getAttributes(),
+ pos, pos + len);
+ ec.added.addElement(leaf);
+ }
+ else {
+ // JoinNext on tail is only applicable if last element
+ // and attributes come from that of first element.
+ // With a little extra testing it would be possible
+ // to NOT due this again, as more than likely fracture()
+ // created this element.
+ if(!ec.isFracture) {
+ Element first = null;
+ if(insertPath != null) {
+ for(int counter = insertPath.length - 1;
+ counter >= 0; counter--) {
+ if(insertPath[counter] == ec) {
+ if(counter != (insertPath.length - 1))
+ first = ec.parent.getElement(ec.index);
+ break;
+ }
+ }
+ }
+ if(first == null)
+ first = ec.parent.getElement(ec.index + 1);
+ Element leaf = createLeafElement(ec.parent, first.
+ getAttributes(), pos, first.getEndOffset());
+ ec.added.addElement(leaf);
+ ec.removed.addElement(first);
+ }
+ else {
+ // Parent was fractured element.
+ Element first = ec.parent.getElement(0);
+ Element leaf = createLeafElement(ec.parent, first.
+ getAttributes(), pos, first.getEndOffset());
+ ec.added.addElement(leaf);
+ ec.removed.addElement(first);
+ }
+ }
+ pos += len;
+ break;
+ }
+ }
+
+ /**
+ * Remove the elements from <code>elem</code> in range
+ * <code>rmOffs0</code>, <code>rmOffs1</code>. This uses
+ * <code>canJoin</code> and <code>join</code> to handle joining
+ * the endpoints of the insertion.
+ *
+ * @return true if elem will no longer have any elements.
+ */
+ boolean removeElements(Element elem, int rmOffs0, int rmOffs1) {
+ if (! elem.isLeaf()) {
+ // update path for changes
+ int index0 = elem.getElementIndex(rmOffs0);
+ int index1 = elem.getElementIndex(rmOffs1);
+ push(elem, index0);
+ ElemChanges ec = (ElemChanges)path.peek();
+
+ // if the range is contained by one element,
+ // we just forward the request
+ if (index0 == index1) {
+ Element child0 = elem.getElement(index0);
+ if(rmOffs0 <= child0.getStartOffset() &&
+ rmOffs1 >= child0.getEndOffset()) {
+ // Element totally removed.
+ ec.removed.addElement(child0);
+ }
+ else if(removeElements(child0, rmOffs0, rmOffs1)) {
+ ec.removed.addElement(child0);
+ }
+ } else {
+ // the removal range spans elements. If we can join
+ // the two endpoints, do it. Otherwise we remove the
+ // interior and forward to the endpoints.
+ Element child0 = elem.getElement(index0);
+ Element child1 = elem.getElement(index1);
+ boolean containsOffs1 = (rmOffs1 < elem.getEndOffset());
+ if (containsOffs1 && canJoin(child0, child1)) {
+ // remove and join
+ for (int i = index0; i <= index1; i++) {
+ ec.removed.addElement(elem.getElement(i));
+ }
+ Element e = join(elem, child0, child1, rmOffs0, rmOffs1);
+ ec.added.addElement(e);
+ } else {
+ // remove interior and forward
+ int rmIndex0 = index0 + 1;
+ int rmIndex1 = index1 - 1;
+ if (child0.getStartOffset() == rmOffs0 ||
+ (index0 == 0 &&
+ child0.getStartOffset() > rmOffs0 &&
+ child0.getEndOffset() <= rmOffs1)) {
+ // start element completely consumed
+ child0 = null;
+ rmIndex0 = index0;
+ }
+ if (!containsOffs1) {
+ child1 = null;
+ rmIndex1++;
+ }
+ else if (child1.getStartOffset() == rmOffs1) {
+ // end element not touched
+ child1 = null;
+ }
+ if (rmIndex0 <= rmIndex1) {
+ ec.index = rmIndex0;
+ }
+ for (int i = rmIndex0; i <= rmIndex1; i++) {
+ ec.removed.addElement(elem.getElement(i));
+ }
+ if (child0 != null) {
+ if(removeElements(child0, rmOffs0, rmOffs1)) {
+ ec.removed.insertElementAt(child0, 0);
+ ec.index = index0;
+ }
+ }
+ if (child1 != null) {
+ if(removeElements(child1, rmOffs0, rmOffs1)) {
+ ec.removed.addElement(child1);
+ }
+ }
+ }
+ }
+
+ // publish changes
+ pop();
+
+ // Return true if we no longer have any children.
+ if(elem.getElementCount() == (ec.removed.size() -
+ ec.added.size())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Can the two given elements be coelesced together
+ * into one element?
+ */
+ boolean canJoin(Element e0, Element e1) {
+ if ((e0 == null) || (e1 == null)) {
+ return false;
+ }
+ // Don't join a leaf to a branch.
+ boolean leaf0 = e0.isLeaf();
+ boolean leaf1 = e1.isLeaf();
+ if(leaf0 != leaf1) {
+ return false;
+ }
+ if (leaf0) {
+ // Only join leaves if the attributes match, otherwise
+ // style information will be lost.
+ return e0.getAttributes().isEqual(e1.getAttributes());
+ }
+ // Only join non-leafs if the names are equal. This may result
+ // in loss of style information, but this is typically acceptable
+ // for non-leafs.
+ String name0 = e0.getName();
+ String name1 = e1.getName();
+ if (name0 != null) {
+ return name0.equals(name1);
+ }
+ if (name1 != null) {
+ return name1.equals(name0);
+ }
+ // Both names null, treat as equal.
+ return true;
+ }
+
+ /**
+ * Joins the two elements carving out a hole for the
+ * given removed range.
+ */
+ Element join(Element p, Element left, Element right, int rmOffs0, int rmOffs1) {
+ if (left.isLeaf() && right.isLeaf()) {
+ return createLeafElement(p, left.getAttributes(), left.getStartOffset(),
+ right.getEndOffset());
+ } else if ((!left.isLeaf()) && (!right.isLeaf())) {
+ // join two branch elements. This copies the children before
+ // the removal range on the left element, and after the removal
+ // range on the right element. The two elements on the edge
+ // are joined if possible and needed.
+ Element to = createBranchElement(p, left.getAttributes());
+ int ljIndex = left.getElementIndex(rmOffs0);
+ int rjIndex = right.getElementIndex(rmOffs1);
+ Element lj = left.getElement(ljIndex);
+ if (lj.getStartOffset() >= rmOffs0) {
+ lj = null;
+ }
+ Element rj = right.getElement(rjIndex);
+ if (rj.getStartOffset() == rmOffs1) {
+ rj = null;
+ }
+ Vector children = new Vector();
+
+ // transfer the left
+ for (int i = 0; i < ljIndex; i++) {
+ children.addElement(clone(to, left.getElement(i)));
+ }
+
+ // transfer the join/middle
+ if (canJoin(lj, rj)) {
+ Element e = join(to, lj, rj, rmOffs0, rmOffs1);
+ children.addElement(e);
+ } else {
+ if (lj != null) {
+ children.addElement(cloneAsNecessary(to, lj, rmOffs0, rmOffs1));
+ }
+ if (rj != null) {
+ children.addElement(cloneAsNecessary(to, rj, rmOffs0, rmOffs1));
+ }
+ }
+
+ // transfer the right
+ int n = right.getElementCount();
+ for (int i = (rj == null) ? rjIndex : rjIndex + 1; i < n; i++) {
+ children.addElement(clone(to, right.getElement(i)));
+ }
+
+ // install the children
+ Element[] c = new Element[children.size()];
+ children.copyInto(c);
+ ((BranchElement)to).replace(0, 0, c);
+ return to;
+ } else {
+ throw new StateInvariantError(
+ "No support to join leaf element with non-leaf element");
+ }
+ }
+
+ /**
+ * Creates a copy of this element, with a different
+ * parent.
+ *
+ * @param parent the parent element
+ * @param clonee the element to be cloned
+ * @return the copy
+ */
+ public Element clone(Element parent, Element clonee) {
+ if (clonee.isLeaf()) {
+ return createLeafElement(parent, clonee.getAttributes(),
+ clonee.getStartOffset(),
+ clonee.getEndOffset());
+ }
+ Element e = createBranchElement(parent, clonee.getAttributes());
+ int n = clonee.getElementCount();
+ Element[] children = new Element[n];
+ for (int i = 0; i < n; i++) {
+ children[i] = clone(e, clonee.getElement(i));
+ }
+ ((BranchElement)e).replace(0, 0, children);
+ return e;
+ }
+
+ /**
+ * Creates a copy of this element, with a different
+ * parent. Children of this element included in the
+ * removal range will be discarded.
+ */
+ Element cloneAsNecessary(Element parent, Element clonee, int rmOffs0, int rmOffs1) {
+ if (clonee.isLeaf()) {
+ return createLeafElement(parent, clonee.getAttributes(),
+ clonee.getStartOffset(),
+ clonee.getEndOffset());
+ }
+ Element e = createBranchElement(parent, clonee.getAttributes());
+ int n = clonee.getElementCount();
+ ArrayList childrenList = new ArrayList(n);
+ for (int i = 0; i < n; i++) {
+ Element elem = clonee.getElement(i);
+ if (elem.getStartOffset() < rmOffs0 || elem.getEndOffset() > rmOffs1) {
+ childrenList.add(cloneAsNecessary(e, elem, rmOffs0, rmOffs1));
+ }
+ }
+ Element[] children = new Element[childrenList.size()];
+ children = (Element[])childrenList.toArray(children);
+ ((BranchElement)e).replace(0, 0, children);
+ return e;
+ }
+
+ /**
+ * Determines if a fracture needs to be performed. A fracture
+ * can be thought of as moving the right part of a tree to a
+ * new location, where the right part is determined by what has
+ * been inserted. <code>depth</code> is used to indicate a
+ * JoinToFracture is needed to an element at a depth
+ * of <code>depth</code>. Where the root is 0, 1 is the children
+ * of the root...
+ * <p>This will invoke <code>fractureFrom</code> if it is determined
+ * a fracture needs to happen.
+ */
+ void fracture(int depth) {
+ int cLength = insertPath.length;
+ int lastIndex = -1;
+ boolean needRecreate = recreateLeafs;
+ ElemChanges lastChange = insertPath[cLength - 1];
+ // Use childAltered to determine when a child has been altered,
+ // that is the point of insertion is less than the element count.
+ boolean childAltered = ((lastChange.index + 1) <
+ lastChange.parent.getElementCount());
+ int deepestAlteredIndex = (needRecreate) ? cLength : -1;
+ int lastAlteredIndex = cLength - 1;
+
+ createdFracture = true;
+ // Determine where to start recreating from.
+ // Start at - 2, as first one is indicated by recreateLeafs and
+ // childAltered.
+ for(int counter = cLength - 2; counter >= 0; counter--) {
+ ElemChanges change = insertPath[counter];
+ if(change.added.size() > 0 || counter == depth) {
+ lastIndex = counter;
+ if(!needRecreate && childAltered) {
+ needRecreate = true;
+ if(deepestAlteredIndex == -1)
+ deepestAlteredIndex = lastAlteredIndex + 1;
+ }
+ }
+ if(!childAltered && change.index <
+ change.parent.getElementCount()) {
+ childAltered = true;
+ lastAlteredIndex = counter;
+ }
+ }
+ if(needRecreate) {
+ // Recreate all children to right of parent starting
+ // at lastIndex.
+ if(lastIndex == -1)
+ lastIndex = cLength - 1;
+ fractureFrom(insertPath, lastIndex, deepestAlteredIndex);
+ }
+ }
+
+ /**
+ * Recreates the elements to the right of the insertion point.
+ * This starts at <code>startIndex</code> in <code>changed</code>,
+ * and calls duplicate to duplicate existing elements.
+ * This will also duplicate the elements along the insertion
+ * point, until a depth of <code>endFractureIndex</code> is
+ * reached, at which point only the elements to the right of
+ * the insertion point are duplicated.
+ */
+ void fractureFrom(ElemChanges[] changed, int startIndex,
+ int endFractureIndex) {
+ // Recreate the element representing the inserted index.
+ ElemChanges change = changed[startIndex];
+ Element child;
+ Element newChild;
+ int changeLength = changed.length;
+
+ if((startIndex + 1) == changeLength)
+ child = change.parent.getElement(change.index);
+ else
+ child = change.parent.getElement(change.index - 1);
+ if(child.isLeaf()) {
+ newChild = createLeafElement(change.parent,
+ child.getAttributes(), Math.max(endOffset,
+ child.getStartOffset()), child.getEndOffset());
+ }
+ else {
+ newChild = createBranchElement(change.parent,
+ child.getAttributes());
+ }
+ fracturedParent = change.parent;
+ fracturedChild = newChild;
+
+ // Recreate all the elements to the right of the
+ // insertion point.
+ Element parent = newChild;
+
+ while(++startIndex < endFractureIndex) {
+ boolean isEnd = ((startIndex + 1) == endFractureIndex);
+ boolean isEndLeaf = ((startIndex + 1) == changeLength);
+
+ // Create the newChild, a duplicate of the elment at
+ // index. This isn't done if isEnd and offsetLastIndex are true
+ // indicating a join previous was done.
+ change = changed[startIndex];
+
+ // Determine the child to duplicate, won't have to duplicate
+ // if at end of fracture, or offseting index.
+ if(isEnd) {
+ if(offsetLastIndex || !isEndLeaf)
+ child = null;
+ else
+ child = change.parent.getElement(change.index);
+ }
+ else {
+ child = change.parent.getElement(change.index - 1);
+ }
+ // Duplicate it.
+ if(child != null) {
+ if(child.isLeaf()) {
+ newChild = createLeafElement(parent,
+ child.getAttributes(), Math.max(endOffset,
+ child.getStartOffset()), child.getEndOffset());
+ }
+ else {
+ newChild = createBranchElement(parent,
+ child.getAttributes());
+ }
+ }
+ else
+ newChild = null;
+
+ // Recreate the remaining children (there may be none).
+ int kidsToMove = change.parent.getElementCount() -
+ change.index;
+ Element[] kids;
+ int moveStartIndex;
+ int kidStartIndex = 1;
+
+ if(newChild == null) {
+ // Last part of fracture.
+ if(isEndLeaf) {
+ kidsToMove--;
+ moveStartIndex = change.index + 1;
+ }
+ else {
+ moveStartIndex = change.index;
+ }
+ kidStartIndex = 0;
+ kids = new Element[kidsToMove];
+ }
+ else {
+ if(!isEnd) {
+ // Branch.
+ kidsToMove++;
+ moveStartIndex = change.index;
+ }
+ else {
+ // Last leaf, need to recreate part of it.
+ moveStartIndex = change.index + 1;
+ }
+ kids = new Element[kidsToMove];
+ kids[0] = newChild;
+ }
+
+ for(int counter = kidStartIndex; counter < kidsToMove;
+ counter++) {
+ Element toMove =change.parent.getElement(moveStartIndex++);
+ kids[counter] = recreateFracturedElement(parent, toMove);
+ change.removed.addElement(toMove);
+ }
+ ((BranchElement)parent).replace(0, 0, kids);
+ parent = newChild;
+ }
+ }
+
+ /**
+ * Recreates <code>toDuplicate</code>. This is called when an
+ * element needs to be created as the result of an insertion. This
+ * will recurse and create all the children. This is similiar to
+ * <code>clone</code>, but deteremines the offsets differently.
+ */
+ Element recreateFracturedElement(Element parent, Element toDuplicate) {
+ if(toDuplicate.isLeaf()) {
+ return createLeafElement(parent, toDuplicate.getAttributes(),
+ Math.max(toDuplicate.getStartOffset(),
+ endOffset),
+ toDuplicate.getEndOffset());
+ }
+ // Not a leaf
+ Element newParent = createBranchElement(parent, toDuplicate.
+ getAttributes());
+ int childCount = toDuplicate.getElementCount();
+ Element[] newKids = new Element[childCount];
+ for(int counter = 0; counter < childCount; counter++) {
+ newKids[counter] = recreateFracturedElement(newParent,
+ toDuplicate.getElement(counter));
+ }
+ ((BranchElement)newParent).replace(0, 0, newKids);
+ return newParent;
+ }
+
+ /**
+ * Splits the bottommost leaf in <code>path</code>.
+ * This is called from insert when the first element is NOT content.
+ */
+ void fractureDeepestLeaf(ElementSpec[] specs) {
+ // Split the bottommost leaf. It will be recreated elsewhere.
+ ElemChanges ec = (ElemChanges) path.peek();
+ Element child = ec.parent.getElement(ec.index);
+ // Inserts at offset 0 do not need to recreate child (it would
+ // have a length of 0!).
+ if (offset != 0) {
+ Element newChild = createLeafElement(ec.parent,
+ child.getAttributes(),
+ child.getStartOffset(),
+ offset);
+
+ ec.added.addElement(newChild);
+ }
+ ec.removed.addElement(child);
+ if(child.getEndOffset() != endOffset)
+ recreateLeafs = true;
+ else
+ offsetLastIndex = true;
+ }
+
+ /**
+ * Inserts the first content. This needs to be separate to handle
+ * joining.
+ */
+ void insertFirstContent(ElementSpec[] specs) {
+ ElementSpec firstSpec = specs[0];
+ ElemChanges ec = (ElemChanges) path.peek();
+ Element child = ec.parent.getElement(ec.index);
+ int firstEndOffset = offset + firstSpec.getLength();
+ boolean isOnlyContent = (specs.length == 1);
+
+ switch(firstSpec.getDirection()) {
+ case ElementSpec.JoinPreviousDirection:
+ if(child.getEndOffset() != firstEndOffset &&
+ !isOnlyContent) {
+ // Create the left split part containing new content.
+ Element newE = createLeafElement(ec.parent,
+ child.getAttributes(), child.getStartOffset(),
+ firstEndOffset);
+ ec.added.addElement(newE);
+ ec.removed.addElement(child);
+ // Remainder will be created later.
+ if(child.getEndOffset() != endOffset)
+ recreateLeafs = true;
+ else
+ offsetLastIndex = true;
+ }
+ else {
+ offsetLastIndex = true;
+ offsetLastIndexOnReplace = true;
+ }
+ // else Inserted at end, and is total length.
+ // Update index incase something added/removed.
+ break;
+ case ElementSpec.JoinNextDirection:
+ if(offset != 0) {
+ // Recreate the first element, its offset will have
+ // changed.
+ Element newE = createLeafElement(ec.parent,
+ child.getAttributes(), child.getStartOffset(),
+ offset);
+ ec.added.addElement(newE);
+ // Recreate the second, merge part. We do no checking
+ // to see if JoinNextDirection is valid here!
+ Element nextChild = ec.parent.getElement(ec.index + 1);
+ if(isOnlyContent)
+ newE = createLeafElement(ec.parent, nextChild.
+ getAttributes(), offset, nextChild.getEndOffset());
+ else
+ newE = createLeafElement(ec.parent, nextChild.
+ getAttributes(), offset, firstEndOffset);
+ ec.added.addElement(newE);
+ ec.removed.addElement(child);
+ ec.removed.addElement(nextChild);
+ }
+ // else nothin to do.
+ // PENDING: if !isOnlyContent could raise here!
+ break;
+ default:
+ // Inserted into middle, need to recreate split left
+ // new content, and split right.
+ if(child.getStartOffset() != offset) {
+ Element newE = createLeafElement(ec.parent,
+ child.getAttributes(), child.getStartOffset(),
+ offset);
+ ec.added.addElement(newE);
+ }
+ ec.removed.addElement(child);
+ // new content
+ Element newE = createLeafElement(ec.parent,
+ firstSpec.getAttributes(),
+ offset, firstEndOffset);
+ ec.added.addElement(newE);
+ if(child.getEndOffset() != endOffset) {
+ // Signals need to recreate right split later.
+ recreateLeafs = true;
+ }
+ else {
+ offsetLastIndex = true;
+ }
+ break;
+ }
+ }
+
+ Element root;
+ transient int pos; // current position
+ transient int offset;
+ transient int length;
+ transient int endOffset;
+ transient Vector changes; // Vector<ElemChanges>
+ transient Stack path; // Stack<ElemChanges>
+ transient boolean insertOp;
+
+ transient boolean recreateLeafs; // For insert.
+
+ /** For insert, path to inserted elements. */
+ transient ElemChanges[] insertPath;
+ /** Only for insert, set to true when the fracture has been created. */
+ transient boolean createdFracture;
+ /** Parent that contains the fractured child. */
+ transient Element fracturedParent;
+ /** Fractured child. */
+ transient Element fracturedChild;
+ /** Used to indicate when fracturing that the last leaf should be
+ * skipped. */
+ transient boolean offsetLastIndex;
+ /** Used to indicate that the parent of the deepest leaf should
+ * offset the index by 1 when adding/removing elements in an
+ * insert. */
+ transient boolean offsetLastIndexOnReplace;
+
+ /*
+ * Internal record used to hold element change specifications
+ */
+ class ElemChanges {
+
+ ElemChanges(Element parent, int index, boolean isFracture) {
+ this.parent = parent;
+ this.index = index;
+ this.isFracture = isFracture;
+ added = new Vector();
+ removed = new Vector();
+ }
+
+ public String toString() {
+ return "added: " + added + "\nremoved: " + removed + "\n";
+ }
+
+ Element parent;
+ int index;
+ Vector added;
+ Vector removed;
+ boolean isFracture;
+ }
+
+ }
+
+ /**
+ * An UndoableEdit used to remember AttributeSet changes to an
+ * Element.
+ */
+ public static class AttributeUndoableEdit extends AbstractUndoableEdit {
+ public AttributeUndoableEdit(Element element, AttributeSet newAttributes,
+ boolean isReplacing) {
+ super();
+ this.element = element;
+ this.newAttributes = newAttributes;
+ this.isReplacing = isReplacing;
+ // If not replacing, it may be more efficient to only copy the
+ // changed values...
+ copy = element.getAttributes().copyAttributes();
+ }
+
+ /**
+ * Redoes a change.
+ *
+ * @exception CannotRedoException if the change cannot be redone
+ */
+ public void redo() throws CannotRedoException {
+ super.redo();
+ MutableAttributeSet as = (MutableAttributeSet)element
+ .getAttributes();
+ if(isReplacing)
+ as.removeAttributes(as);
+ as.addAttributes(newAttributes);
+ }
+
+ /**
+ * Undoes a change.
+ *
+ * @exception CannotUndoException if the change cannot be undone
+ */
+ public void undo() throws CannotUndoException {
+ super.undo();
+ MutableAttributeSet as = (MutableAttributeSet)element.getAttributes();
+ as.removeAttributes(as);
+ as.addAttributes(copy);
+ }
+
+ // AttributeSet containing additional entries, must be non-mutable!
+ protected AttributeSet newAttributes;
+ // Copy of the AttributeSet the Element contained.
+ protected AttributeSet copy;
+ // true if all the attributes in the element were removed first.
+ protected boolean isReplacing;
+ // Efected Element.
+ protected Element element;
+ }
+
+ /**
+ * UndoableEdit for changing the resolve parent of an Element.
+ */
+ static class StyleChangeUndoableEdit extends AbstractUndoableEdit {
+ public StyleChangeUndoableEdit(AbstractElement element,
+ Style newStyle) {
+ super();
+ this.element = element;
+ this.newStyle = newStyle;
+ oldStyle = element.getResolveParent();
+ }
+
+ /**
+ * Redoes a change.
+ *
+ * @exception CannotRedoException if the change cannot be redone
+ */
+ public void redo() throws CannotRedoException {
+ super.redo();
+ element.setResolveParent(newStyle);
+ }
+
+ /**
+ * Undoes a change.
+ *
+ * @exception CannotUndoException if the change cannot be undone
+ */
+ public void undo() throws CannotUndoException {
+ super.undo();
+ element.setResolveParent(oldStyle);
+ }
+
+ /** Element to change resolve parent of. */
+ protected AbstractElement element;
+ /** New style. */
+ protected Style newStyle;
+ /** Old style, before setting newStyle. */
+ protected AttributeSet oldStyle;
+ }
+
+ /**
+ * Base class for style change handlers with support for stale objects detection.
+ */
+ abstract static class AbstractChangeHandler implements ChangeListener {
+
+ /* This has an implicit reference to the handler object. */
+ private class DocReference extends WeakReference<DefaultStyledDocument> {
+
+ DocReference(DefaultStyledDocument d, ReferenceQueue q) {
+ super(d, q);
+ }
+
+ /**
+ * Return a reference to the style change handler object.
+ */
+ ChangeListener getListener() {
+ return AbstractChangeHandler.this;
+ }
+ }
+
+ /** Class-specific reference queues. */
+ private final static Map<Class, ReferenceQueue> queueMap
+ = new HashMap<Class, ReferenceQueue>();
+
+ /** A weak reference to the document object. */
+ private DocReference doc;
+
+ AbstractChangeHandler(DefaultStyledDocument d) {
+ Class c = getClass();
+ ReferenceQueue q;
+ synchronized (queueMap) {
+ q = queueMap.get(c);
+ if (q == null) {
+ q = new ReferenceQueue();
+ queueMap.put(c, q);
+ }
+ }
+ doc = new DocReference(d, q);
+ }
+
+ /**
+ * Return a list of stale change listeners.
+ *
+ * A change listener becomes "stale" when its document is cleaned by GC.
+ */
+ static List<ChangeListener> getStaleListeners(ChangeListener l) {
+ List<ChangeListener> staleListeners = new ArrayList<ChangeListener>();
+ ReferenceQueue q = queueMap.get(l.getClass());
+
+ if (q != null) {
+ DocReference r;
+ synchronized (q) {
+ while ((r = (DocReference) q.poll()) != null) {
+ staleListeners.add(r.getListener());
+ }
+ }
+ }
+
+ return staleListeners;
+ }
+
+ /**
+ * The ChangeListener wrapper which guards against dead documents.
+ */
+ public void stateChanged(ChangeEvent e) {
+ DefaultStyledDocument d = doc.get();
+ if (d != null) {
+ fireStateChanged(d, e);
+ }
+ }
+
+ /** Run the actual class-specific stateChanged() method. */
+ abstract void fireStateChanged(DefaultStyledDocument d, ChangeEvent e);
+ }
+
+ /**
+ * Added to all the Styles. When instances of this receive a
+ * stateChanged method, styleChanged is invoked.
+ */
+ static class StyleChangeHandler extends AbstractChangeHandler {
+
+ StyleChangeHandler(DefaultStyledDocument d) {
+ super(d);
+ }
+
+ void fireStateChanged(DefaultStyledDocument d, ChangeEvent e) {
+ Object source = e.getSource();
+ if (source instanceof Style) {
+ d.styleChanged((Style) source);
+ } else {
+ d.styleChanged(null);
+ }
+ }
+ }
+
+
+ /**
+ * Added to the StyleContext. When the StyleContext changes, this invokes
+ * <code>updateStylesListeningTo</code>.
+ */
+ static class StyleContextChangeHandler extends AbstractChangeHandler {
+
+ StyleContextChangeHandler(DefaultStyledDocument d) {
+ super(d);
+ }
+
+ void fireStateChanged(DefaultStyledDocument d, ChangeEvent e) {
+ d.updateStylesListeningTo();
+ }
+ }
+
+
+ /**
+ * When run this creates a change event for the complete document
+ * and fires it.
+ */
+ class ChangeUpdateRunnable implements Runnable {
+ boolean isPending = false;
+
+ public void run() {
+ synchronized(this) {
+ isPending = false;
+ }
+
+ try {
+ writeLock();
+ DefaultDocumentEvent dde = new DefaultDocumentEvent(0,
+ getLength(),
+ DocumentEvent.EventType.CHANGE);
+ dde.end();
+ fireChangedUpdate(dde);
+ } finally {
+ writeUnlock();
+ }
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/DefaultTextUI.java b/src/share/classes/javax/swing/text/DefaultTextUI.java
new file mode 100644
index 000000000..8173d3e69
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DefaultTextUI.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 1997-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;
+
+import javax.swing.plaf.basic.BasicTextUI;
+
+/**
+ * <p>
+ * This class has been deprecated and should no longer be used.
+ * The basis of the various TextUI implementations can be found
+ * in the javax.swing.plaf.basic package and the class
+ * BasicTextUI replaces this class.
+ *
+ * @deprecated
+ */
+@Deprecated
+public abstract class DefaultTextUI extends BasicTextUI {
+
+
+}
diff --git a/src/share/classes/javax/swing/text/Document.java b/src/share/classes/javax/swing/text/Document.java
new file mode 100644
index 000000000..b98d6e372
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Document.java
@@ -0,0 +1,476 @@
+/*
+ * 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;
+
+import javax.swing.event.*;
+
+/**
+ * <p>
+ * The <code>Document</code> is a container for text that serves
+ * as the model for swing text components. The goal for this
+ * interface is to scale from very simple needs (a plain text textfield)
+ * to complex needs (an HTML or XML document, for example).
+ *
+ * <p><b><font size=+1>Content</font></b>
+ * <p>
+ * At the simplest level, text can be
+ * modeled as a linear sequence of characters. To support
+ * internationalization, the Swing text model uses
+ * <a href="http://www.unicode.org/">unicode</a> characters.
+ * The sequence of characters displayed in a text component is
+ * generally referred to as the component's <em>content</em>.
+ * <p>
+ * To refer to locations within the sequence, the coordinates
+ * used are the location between two characters. As the diagram
+ * below shows, a location in a text document can be referred to
+ * as a position, or an offset. This position is zero-based.
+ * <p align=center><img src="doc-files/Document-coord.gif"
+ * alt="The following text describes this graphic.">
+ * <p>
+ * In the example, if the content of a document is the
+ * sequence "The quick brown fox," as shown in the preceding diagram,
+ * the location just before the word "The" is 0, and the location after
+ * the word "The" and before the whitespace that follows it is 3.
+ * The entire sequence of characters in the sequence "The" is called a
+ * <em>range</em>.
+ * <p>The following methods give access to the character data
+ * that makes up the content.
+ * <ul>
+ * <li><a href="#getLength()">getLength()</a>
+ * <li><a href="#getText(int, int)">getText(int, int)</a>
+ * <li><a href="#getText(int, int, javax.swing.text.Segment)">getText(int, int, Segment)</a>
+ * </ul>
+ * <p><b><font size=+1>Structure</font></b>
+ * <p>
+ * Text is rarely represented simply as featureless content. Rather,
+ * text typically has some sort of structure associated with it.
+ * Exactly what structure is modeled is up to a particular Document
+ * implementation. It might be as simple as no structure (i.e. a
+ * simple text field), or it might be something like diagram below.
+ * <p align=center><img src="doc-files/Document-structure.gif"
+ * alt="Diagram shows Book->Chapter->Paragraph">
+ * <p>
+ * The unit of structure (i.e. a node of the tree) is referred to
+ * by the <a href="Element.html">Element</a> interface. Each Element
+ * can be tagged with a set of attributes. These attributes
+ * (name/value pairs) are defined by the
+ * <a href="AttributeSet.html">AttributeSet</a> interface.
+ * <p>The following methods give access to the document structure.
+ * <ul>
+ * <li><a href="#getDefaultRootElement()">getDefaultRootElement</a>
+ * <li><a href="#getRootElements()">getRootElements</a>
+ * </ul>
+ *
+ * <p><b><font size=+1>Mutations</font></b>
+ * <p>
+ * All documents need to be able to add and remove simple text.
+ * Typically, text is inserted and removed via gestures from
+ * a keyboard or a mouse. What effect the insertion or removal
+ * has upon the document structure is entirely up to the
+ * implementation of the document.
+ * <p>The following methods are related to mutation of the
+ * document content:
+ * <ul>
+ * <li><a href="#insertString(int, java.lang.String, javax.swing.text.AttributeSet)">insertString(int, String, AttributeSet)</a>
+ * <li><a href="#remove(int, int)">remove(int, int)</a>
+ * <li><a href="#createPosition(int)">createPosition(int)</a>
+ * </ul>
+ *
+ * <p><b><font size=+1>Notification</font></b>
+ * <p>
+ * Mutations to the <code>Document</code> must be communicated to
+ * interested observers. The notification of change follows the event model
+ * guidelines that are specified for JavaBeans. In the JavaBeans
+ * event model, once an event notification is dispatched, all listeners
+ * must be notified before any further mutations occur to the source
+ * of the event. Further, order of delivery is not guaranteed.
+ * <p>
+ * Notification is provided as two separate events,
+ * <a href="../event/DocumentEvent.html">DocumentEvent</a>, and
+ * <a href="../event/UndoableEditEvent.html">UndoableEditEvent</a>.
+ * If a mutation is made to a <code>Document</code> through its api,
+ * a <code>DocumentEvent</code> will be sent to all of the registered
+ * <code>DocumentListeners</code>. If the <code>Document</code>
+ * implementation supports undo/redo capabilities, an
+ * <code>UndoableEditEvent</code> will be sent
+ * to all of the registered <code>UndoableEditListener</code>s.
+ * If an undoable edit is undone, a <code>DocumentEvent</code> should be
+ * fired from the Document to indicate it has changed again.
+ * In this case however, there should be no <code>UndoableEditEvent</code>
+ * generated since that edit is actually the source of the change
+ * rather than a mutation to the <code>Document</code> made through its
+ * api.
+ * <p align=center><img src="doc-files/Document-notification.gif"
+ * alt="The preceeding text describes this graphic.">
+ * <p>
+ * Referring to the above diagram, suppose that the component shown
+ * on the left mutates the document object represented by the blue
+ * rectangle. The document responds by dispatching a DocumentEvent to
+ * both component views and sends an UndoableEditEvent to the listening
+ * logic, which maintains a history buffer.
+ * <p>
+ * Now suppose that the component shown on the right mutates the same
+ * document. Again, the document dispatches a DocumentEvent to both
+ * component views and sends an UndoableEditEvent to the listening logic
+ * that is maintaining the history buffer.
+ * <p>
+ * If the history buffer is then rolled back (i.e. the last UndoableEdit
+ * undone), a DocumentEvent is sent to both views, causing both of them to
+ * reflect the undone mutation to the document (that is, the
+ * removal of the right component's mutation). If the history buffer again
+ * rolls back another change, another DocumentEvent is sent to both views,
+ * causing them to reflect the undone mutation to the document -- that is,
+ * the removal of the left component's mutation.
+ * <p>
+ * The methods related to observing mutations to the document are:
+ * <ul>
+ * <li><a href="#addDocumentListener(javax.swing.event.DocumentListener)">addDocumentListener(DocumentListener)</a>
+ * <li><a href="#removeDocumentListener(javax.swing.event.DocumentListener)">removeDocumentListener(DocumentListener)</a>
+ * <li><a href="#addUndoableEditListener(javax.swing.event.UndoableEditListener)">addUndoableEditListener(UndoableEditListener)</a>
+ * <li><a href="#removeUndoableEditListener(javax.swing.event.UndoableEditListener)">removeUndoableEditListener(UndoableEditListener)</a>
+ * </ul>
+ *
+ * <p><b><font size=+1>Properties</font></b>
+ * <p>
+ * Document implementations will generally have some set of properties
+ * associated with them at runtime. Two well known properties are the
+ * <a href="#StreamDescriptionProperty">StreamDescriptionProperty</a>,
+ * which can be used to describe where the <code>Document</code> came from,
+ * and the <a href="#TitleProperty">TitleProperty</a>, which can be used to
+ * name the <code>Document</code>. The methods related to the properties are:
+ * <ul>
+ * <li><a href="#getProperty(java.lang.Object)">getProperty(Object)</a>
+ * <li><a href="#putProperty(java.lang.Object, java.lang.Object)">putProperty(Object, Object)</a>
+ * </ul>
+ *
+ * <p>For more information on the <code>Document</code> class, see
+ * <a href="http://java.sun.com/products/jfc/tsc">The Swing Connection</a>
+ * and most particularly the article,
+ * <a href="http://java.sun.com/products/jfc/tsc/articles/text/element_interface">
+ * The Element Interface</a>.
+ *
+ * @author Timothy Prinzing
+ *
+ * @see javax.swing.event.DocumentEvent
+ * @see javax.swing.event.DocumentListener
+ * @see javax.swing.event.UndoableEditEvent
+ * @see javax.swing.event.UndoableEditListener
+ * @see Element
+ * @see Position
+ * @see AttributeSet
+ */
+public interface Document {
+
+ /**
+ * Returns number of characters of content currently
+ * in the document.
+ *
+ * @return number of characters >= 0
+ */
+ public int getLength();
+
+ /**
+ * Registers the given observer to begin receiving notifications
+ * when changes are made to the document.
+ *
+ * @param listener the observer to register
+ * @see Document#removeDocumentListener
+ */
+ public void addDocumentListener(DocumentListener listener);
+
+ /**
+ * Unregisters the given observer from the notification list
+ * so it will no longer receive change updates.
+ *
+ * @param listener the observer to register
+ * @see Document#addDocumentListener
+ */
+ public void removeDocumentListener(DocumentListener listener);
+
+ /**
+ * Registers the given observer to begin receiving notifications
+ * when undoable edits are made to the document.
+ *
+ * @param listener the observer to register
+ * @see javax.swing.event.UndoableEditEvent
+ */
+ public void addUndoableEditListener(UndoableEditListener listener);
+
+ /**
+ * Unregisters the given observer from the notification list
+ * so it will no longer receive updates.
+ *
+ * @param listener the observer to register
+ * @see javax.swing.event.UndoableEditEvent
+ */
+ public void removeUndoableEditListener(UndoableEditListener listener);
+
+ /**
+ * Gets the properties associated with the document.
+ *
+ * @param key a non-<code>null</code> property key
+ * @return the properties
+ * @see #putProperty(Object, Object)
+ */
+ public Object getProperty(Object key);
+
+ /**
+ * Associates a property with the document. Two standard
+ * property keys provided are: <a href="#StreamDescriptionProperty">
+ * <code>StreamDescriptionProperty</code></a> and
+ * <a href="#TitleProperty"><code>TitleProperty</code></a>.
+ * Other properties, such as author, may also be defined.
+ *
+ * @param key the non-<code>null</code> property key
+ * @param value the property value
+ * @see #getProperty(Object)
+ */
+ public void putProperty(Object key, Object value);
+
+ /**
+ * Removes a portion of the content of the document.
+ * This will cause a DocumentEvent of type
+ * DocumentEvent.EventType.REMOVE to be sent to the
+ * registered DocumentListeners, unless an exception
+ * is thrown. The notification will be sent to the
+ * listeners by calling the removeUpdate method on the
+ * DocumentListeners.
+ * <p>
+ * To ensure reasonable behavior in the face
+ * of concurrency, the event is dispatched after the
+ * mutation has occurred. This means that by the time a
+ * notification of removal is dispatched, the document
+ * has already been updated and any marks created by
+ * <code>createPosition</code> have already changed.
+ * For a removal, the end of the removal range is collapsed
+ * down to the start of the range, and any marks in the removal
+ * range are collapsed down to the start of the range.
+ * <p align=center><img src="doc-files/Document-remove.gif"
+ * alt="Diagram shows removal of 'quick' from 'The quick brown fox.'">
+ * <p>
+ * If the Document structure changed as result of the removal,
+ * the details of what Elements were inserted and removed in
+ * response to the change will also be contained in the generated
+ * DocumentEvent. It is up to the implementation of a Document
+ * to decide how the structure should change in response to a
+ * remove.
+ * <p>
+ * If the Document supports undo/redo, an UndoableEditEvent will
+ * also be generated.
+ *
+ * @param offs the offset from the beginning >= 0
+ * @param len the number of characters to remove >= 0
+ * @exception BadLocationException some portion of the removal range
+ * was not a valid part of the document. The location in the exception
+ * is the first bad position encountered.
+ * @see javax.swing.event.DocumentEvent
+ * @see javax.swing.event.DocumentListener
+ * @see javax.swing.event.UndoableEditEvent
+ * @see javax.swing.event.UndoableEditListener
+ */
+ public void remove(int offs, int len) throws BadLocationException;
+
+ /**
+ * Inserts a string of content. This will cause a DocumentEvent
+ * of type DocumentEvent.EventType.INSERT to be sent to the
+ * registered DocumentListers, unless an exception is thrown.
+ * The DocumentEvent will be delivered by calling the
+ * insertUpdate method on the DocumentListener.
+ * The offset and length of the generated DocumentEvent
+ * will indicate what change was actually made to the Document.
+ * <p align=center><img src="doc-files/Document-insert.gif"
+ * alt="Diagram shows insertion of 'quick' in 'The quick brown fox'">
+ * <p>
+ * If the Document structure changed as result of the insertion,
+ * the details of what Elements were inserted and removed in
+ * response to the change will also be contained in the generated
+ * DocumentEvent. It is up to the implementation of a Document
+ * to decide how the structure should change in response to an
+ * insertion.
+ * <p>
+ * If the Document supports undo/redo, an UndoableEditEvent will
+ * also be generated.
+ *
+ * @param offset the offset into the document to insert the content >= 0.
+ * All positions that track change at or after the given location
+ * will move.
+ * @param str the string to insert
+ * @param a the attributes to associate with the inserted
+ * content. This may be null if there are no attributes.
+ * @exception BadLocationException the given insert position is not a valid
+ * position within the document
+ * @see javax.swing.event.DocumentEvent
+ * @see javax.swing.event.DocumentListener
+ * @see javax.swing.event.UndoableEditEvent
+ * @see javax.swing.event.UndoableEditListener
+ */
+ public void insertString(int offset, String str, AttributeSet a) throws BadLocationException;
+
+ /**
+ * Fetches the text contained within the given portion
+ * of the document.
+ *
+ * @param offset the offset into the document representing the desired
+ * start of the text >= 0
+ * @param length the length of the desired string >= 0
+ * @return the text, in a String of length >= 0
+ * @exception BadLocationException some portion of the given range
+ * was not a valid part of the document. The location in the exception
+ * is the first bad position encountered.
+ */
+ public String getText(int offset, int length) throws BadLocationException;
+
+ /**
+ * Fetches the text contained within the given portion
+ * of the document.
+ * <p>
+ * If the partialReturn property on the txt parameter is false, the
+ * data returned in the Segment will be the entire length requested and
+ * may or may not be a copy depending upon how the data was stored.
+ * If the partialReturn property is true, only the amount of text that
+ * can be returned without creating a copy is returned. Using partial
+ * returns will give better performance for situations where large
+ * parts of the document are being scanned. The following is an example
+ * of using the partial return to access the entire document:
+ * <p>
+ * <pre><code>
+ *
+ * &nbsp; int nleft = doc.getDocumentLength();
+ * &nbsp; Segment text = new Segment();
+ * &nbsp; int offs = 0;
+ * &nbsp; text.setPartialReturn(true);
+ * &nbsp; while (nleft > 0) {
+ * &nbsp; doc.getText(offs, nleft, text);
+ * &nbsp; // do someting with text
+ * &nbsp; nleft -= text.count;
+ * &nbsp; offs += text.count;
+ * &nbsp; }
+ *
+ * </code></pre>
+ *
+ * @param offset the offset into the document representing the desired
+ * start of the text >= 0
+ * @param length the length of the desired string >= 0
+ * @param txt the Segment object to return the text in
+ *
+ * @exception BadLocationException Some portion of the given range
+ * was not a valid part of the document. The location in the exception
+ * is the first bad position encountered.
+ */
+ public void getText(int offset, int length, Segment txt) throws BadLocationException;
+
+ /**
+ * Returns a position that represents the start of the document. The
+ * position returned can be counted on to track change and stay
+ * located at the beginning of the document.
+ *
+ * @return the position
+ */
+ public Position getStartPosition();
+
+ /**
+ * Returns a position that represents the end of the document. The
+ * position returned can be counted on to track change and stay
+ * located at the end of the document.
+ *
+ * @return the position
+ */
+ public Position getEndPosition();
+
+ /**
+ * This method allows an application to mark a place in
+ * a sequence of character content. This mark can then be
+ * used to tracks change as insertions and removals are made
+ * in the content. The policy is that insertions always
+ * occur prior to the current position (the most common case)
+ * unless the insertion location is zero, in which case the
+ * insertion is forced to a position that follows the
+ * original position.
+ *
+ * @param offs the offset from the start of the document >= 0
+ * @return the position
+ * @exception BadLocationException if the given position does not
+ * represent a valid location in the associated document
+ */
+ public Position createPosition(int offs) throws BadLocationException;
+
+ /**
+ * Returns all of the root elements that are defined.
+ * <p>
+ * Typically there will be only one document structure, but the interface
+ * supports building an arbitrary number of structural projections over the
+ * text data. The document can have multiple root elements to support
+ * multiple document structures. Some examples might be:
+ * </p>
+ * <ul>
+ * <li>Text direction.
+ * <li>Lexical token streams.
+ * <li>Parse trees.
+ * <li>Conversions to formats other than the native format.
+ * <li>Modification specifications.
+ * <li>Annotations.
+ * </ul>
+ *
+ * @return the root element
+ */
+ public Element[] getRootElements();
+
+ /**
+ * Returns the root element that views should be based upon,
+ * unless some other mechanism for assigning views to element
+ * structures is provided.
+ *
+ * @return the root element
+ */
+ public Element getDefaultRootElement();
+
+ /**
+ * Allows the model to be safely rendered in the presence
+ * of concurrency, if the model supports being updated asynchronously.
+ * The given runnable will be executed in a way that allows it
+ * to safely read the model with no changes while the runnable
+ * is being executed. The runnable itself may <em>not</em>
+ * make any mutations.
+ *
+ * @param r a <code>Runnable</code> used to render the model
+ */
+ public void render(Runnable r);
+
+ /**
+ * The property name for the description of the stream
+ * used to initialize the document. This should be used
+ * if the document was initialized from a stream and
+ * anything is known about the stream.
+ */
+ public static final String StreamDescriptionProperty = "stream";
+
+ /**
+ * The property name for the title of the document, if
+ * there is one.
+ */
+ public static final String TitleProperty = "title";
+
+
+}
diff --git a/src/share/classes/javax/swing/text/DocumentFilter.java b/src/share/classes/javax/swing/text/DocumentFilter.java
new file mode 100644
index 000000000..bd6a37bc8
--- /dev/null
+++ b/src/share/classes/javax/swing/text/DocumentFilter.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2000-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;
+
+/**
+ * <code>DocumentFilter</code>, as the name implies, is a filter for the
+ * <code>Document</code> mutation methods. When a <code>Document</code>
+ * containing a <code>DocumentFilter</code> is modified (either through
+ * <code>insert</code> or <code>remove</code>), it forwards the appropriate
+ * method invocation to the <code>DocumentFilter</code>. The
+ * default implementation allows the modification to
+ * occur. Subclasses can filter the modifications by conditionally invoking
+ * methods on the superclass, or invoking the necessary methods on
+ * the passed in <code>FilterBypass</code>. Subclasses should NOT call back
+ * into the Document for the modification
+ * instead call into the superclass or the <code>FilterBypass</code>.
+ * <p>
+ * When <code>remove</code> or <code>insertString</code> is invoked
+ * on the <code>DocumentFilter</code>, the <code>DocumentFilter</code>
+ * may callback into the
+ * <code>FilterBypass</code> multiple times, or for different regions, but
+ * it should not callback into the <code>FilterBypass</code> after returning
+ * from the <code>remove</code> or <code>insertString</code> method.
+ * <p>
+ * By default, text related document mutation methods such as
+ * <code>insertString</code>, <code>replace</code> and <code>remove</code>
+ * in <code>AbstractDocument</code> use <code>DocumentFilter</code> when
+ * available, and <code>Element</code> related mutation methods such as
+ * <code>create</code>, <code>insert</code> and <code>removeElement</code> in
+ * <code>DefaultStyledDocument</code> do not use <code>DocumentFilter</code>.
+ * If a method doesn't follow these defaults, this must be explicitly stated
+ * in the method documentation.
+ *
+ * @see javax.swing.text.Document
+ * @see javax.swing.text.AbstractDocument
+ * @see javax.swing.text.DefaultStyledDocument
+ *
+ * @since 1.4
+ */
+public class DocumentFilter {
+ /**
+ * Invoked prior to removal of the specified region in the
+ * specified Document. Subclasses that want to conditionally allow
+ * removal should override this and only call supers implementation as
+ * necessary, or call directly into the <code>FilterBypass</code> as
+ * necessary.
+ *
+ * @param fb FilterBypass that can be used to mutate Document
+ * @param offset the offset from the beginning >= 0
+ * @param length the number of characters to remove >= 0
+ * @exception BadLocationException some portion of the removal range
+ * was not a valid part of the document. The location in the exception
+ * is the first bad position encountered.
+ */
+ public void remove(FilterBypass fb, int offset, int length) throws
+ BadLocationException {
+ fb.remove(offset, length);
+ }
+
+ /**
+ * Invoked prior to insertion of text into the
+ * specified Document. Subclasses that want to conditionally allow
+ * insertion should override this and only call supers implementation as
+ * necessary, or call directly into the FilterBypass.
+ *
+ * @param fb FilterBypass that can be used to mutate Document
+ * @param offset the offset into the document to insert the content >= 0.
+ * All positions that track change at or after the given location
+ * will move.
+ * @param string the string to insert
+ * @param attr the attributes to associate with the inserted
+ * content. This may be null if there are no attributes.
+ * @exception BadLocationException the given insert position is not a
+ * valid position within the document
+ */
+ public void insertString(FilterBypass fb, int offset, String string,
+ AttributeSet attr) throws BadLocationException {
+ fb.insertString(offset, string, attr);
+ }
+
+ /**
+ * Invoked prior to replacing a region of text in the
+ * specified Document. Subclasses that want to conditionally allow
+ * replace should override this and only call supers implementation as
+ * necessary, or call directly into the FilterBypass.
+ *
+ * @param fb FilterBypass that can be used to mutate Document
+ * @param offset Location in Document
+ * @param length Length of text to delete
+ * @param text Text to insert, null indicates no text to insert
+ * @param attrs AttributeSet indicating attributes of inserted text,
+ * null is legal.
+ * @exception BadLocationException the given insert position is not a
+ * valid position within the document
+ */
+ public void replace(FilterBypass fb, int offset, int length, String text,
+ AttributeSet attrs) throws BadLocationException {
+ fb.replace(offset, length, text, attrs);
+ }
+
+
+ /**
+ * Used as a way to circumvent calling back into the Document to
+ * change it. Document implementations that wish to support
+ * a DocumentFilter must provide an implementation that will
+ * not callback into the DocumentFilter when the following methods
+ * are invoked from the DocumentFilter.
+ * @since 1.4
+ */
+ public static abstract class FilterBypass {
+ /**
+ * Returns the Document the mutation is occuring on.
+ *
+ * @return Document that remove/insertString will operate on
+ */
+ public abstract Document getDocument();
+
+ /**
+ * Removes the specified region of text, bypassing the
+ * DocumentFilter.
+ *
+ * @param offset the offset from the beginning >= 0
+ * @param length the number of characters to remove >= 0
+ * @exception BadLocationException some portion of the removal range
+ * was not a valid part of the document. The location in the
+ * exception is the first bad position encountered.
+ */
+ public abstract void remove(int offset, int length) throws
+ BadLocationException;
+
+ /**
+ * Inserts the specified text, bypassing the
+ * DocumentFilter.
+ * @param offset the offset into the document to insert the
+ * content >= 0. All positions that track change at or after the
+ * given location will move.
+ * @param string the string to insert
+ * @param attr the attributes to associate with the inserted
+ * content. This may be null if there are no attributes.
+ * @exception BadLocationException the given insert position is not a
+ * valid position within the document
+ */
+ public abstract void insertString(int offset, String string,
+ AttributeSet attr) throws
+ BadLocationException;
+
+ /**
+ * Deletes the region of text from <code>offset</code> to
+ * <code>offset + length</code>, and replaces it with
+ * <code>text</code>.
+ *
+ * @param offset Location in Document
+ * @param length Length of text to delete
+ * @param string Text to insert, null indicates no text to insert
+ * @param attrs AttributeSet indicating attributes of inserted text,
+ * null is legal.
+ * @exception BadLocationException the given insert is not a
+ * valid position within the document
+ */
+ public abstract void replace(int offset, int length, String string,
+ AttributeSet attrs) throws
+ BadLocationException;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/EditorKit.java b/src/share/classes/javax/swing/text/EditorKit.java
new file mode 100644
index 000000000..86bfe1b33
--- /dev/null
+++ b/src/share/classes/javax/swing/text/EditorKit.java
@@ -0,0 +1,208 @@
+/*
+ * 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;
+
+import java.io.*;
+import javax.swing.Action;
+import javax.swing.JEditorPane;
+
+/**
+ * Establishes the set of things needed by a text component
+ * to be a reasonably functioning editor for some <em>type</em>
+ * of text content. The EditorKit acts as a factory for some
+ * kind of policy. For example, an implementation
+ * of html and rtf can be provided that is replaceable
+ * with other implementations.
+ * <p>
+ * A kit can safely store editing state as an instance
+ * of the kit will be dedicated to a text component.
+ * New kits will normally be created by cloning a
+ * prototype kit. The kit will have it's
+ * <code>setComponent</code> method called to establish
+ * it's relationship with a JTextComponent.
+ *
+ * @author Timothy Prinzing
+ */
+public abstract class EditorKit implements Cloneable, Serializable {
+
+ /**
+ * Construct an EditorKit.
+ */
+ public EditorKit() {
+ }
+
+ /**
+ * Creates a copy of the editor kit. This is implemented
+ * to use Object.clone</em>. If the kit cannot be cloned,
+ * null is returned.
+ *
+ * @return the copy
+ */
+ public Object clone() {
+ Object o;
+ try {
+ o = super.clone();
+ } catch (CloneNotSupportedException cnse) {
+ o = null;
+ }
+ return o;
+ }
+
+ /**
+ * Called when the kit is being installed into the
+ * a JEditorPane.
+ *
+ * @param c the JEditorPane
+ */
+ public void install(JEditorPane 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) {
+ }
+
+ /**
+ * Gets the MIME type of the data that this
+ * kit represents support for.
+ *
+ * @return the type
+ */
+ public abstract String getContentType();
+
+ /**
+ * Fetches a factory that is suitable for producing
+ * views of any models that are produced by this
+ * kit.
+ *
+ * @return the factory
+ */
+ public abstract ViewFactory getViewFactory();
+
+ /**
+ * Fetches the set of commands that can be used
+ * on a text component that is using a model and
+ * view produced by this kit.
+ *
+ * @return the set of actions
+ */
+ public abstract Action[] getActions();
+
+ /**
+ * Fetches a caret that can navigate through views
+ * produced by the associated ViewFactory.
+ *
+ * @return the caret
+ */
+ public abstract Caret createCaret();
+
+ /**
+ * Creates an uninitialized text storage model
+ * that is appropriate for this type of editor.
+ *
+ * @return the model
+ */
+ public abstract Document createDefaultDocument();
+
+ /**
+ * Inserts content from the given stream which is expected
+ * to be in a format appropriate for this kind of content
+ * handler.
+ *
+ * @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 >= 0.
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document.
+ */
+ public abstract void read(InputStream in, Document doc, int pos)
+ throws IOException, BadLocationException;
+
+ /**
+ * Writes 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 from >= 0.
+ * @param len The amount to write out >= 0.
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document.
+ */
+ public abstract void write(OutputStream out, Document doc, int pos, int len)
+ throws IOException, BadLocationException;
+
+ /**
+ * Inserts content from the given stream which is expected
+ * to be in a format appropriate for this kind of content
+ * handler.
+ * <p>
+ * Since actual text editing is unicode based, this would
+ * generally be the preferred way to read in the data.
+ * Some types of content are stored in an 8-bit form however,
+ * and will favor the InputStream.
+ *
+ * @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 >= 0.
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document.
+ */
+ public abstract void read(Reader in, Document doc, int pos)
+ throws IOException, BadLocationException;
+
+ /**
+ * Writes content from a document to the given stream
+ * in a format appropriate for this kind of content handler.
+ * <p>
+ * Since actual text editing is unicode based, this would
+ * generally be the preferred way to write the data.
+ * Some types of content are stored in an 8-bit form however,
+ * and will favor the OutputStream.
+ *
+ * @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 >= 0.
+ * @param len The amount to write out >= 0.
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document.
+ */
+ public abstract void write(Writer out, Document doc, int pos, int len)
+ throws IOException, BadLocationException;
+
+}
diff --git a/src/share/classes/javax/swing/text/Element.java b/src/share/classes/javax/swing/text/Element.java
new file mode 100644
index 000000000..8a71fa802
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Element.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 1997-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;
+
+/**
+ * Interface to describe a structural piece of a document. It
+ * is intended to capture the spirit of an SGML element.
+ *
+ * @author Timothy Prinzing
+ */
+public interface Element {
+
+ /**
+ * Fetches the document associated with this element.
+ *
+ * @return the document
+ */
+ public Document getDocument();
+
+ /**
+ * Fetches the parent element. If the element is a root level
+ * element returns <code>null</code>.
+ *
+ * @return the parent element
+ */
+ public Element getParentElement();
+
+ /**
+ * Fetches the name of the element. If the element is used to
+ * represent some type of structure, this would be the type
+ * name.
+ *
+ * @return the element name
+ */
+ public String getName();
+
+ /**
+ * Fetches the collection of attributes this element contains.
+ *
+ * @return the attributes for the element
+ */
+ public AttributeSet getAttributes();
+
+ /**
+ * Fetches the offset from the beginning of the document
+ * that this element begins at. If this element has
+ * children, this will be the offset of the first child.
+ * As a document position, there is an implied forward bias.
+ *
+ * @return the starting offset >= 0 and < getEndOffset();
+ * @see Document
+ * @see AbstractDocument
+ */
+ public int getStartOffset();
+
+ /**
+ * Fetches the offset from the beginning of the document
+ * that this element ends at. If this element has
+ * children, this will be the end offset of the last child.
+ * As a document position, there is an implied backward bias.
+ * <p>
+ * All the default <code>Document</code> implementations
+ * descend from <code>AbstractDocument</code>.
+ * <code>AbstractDocument</code> models an implied break at the end of
+ * the document. As a result of this, it is possible for this to
+ * return a value greater than the length of the document.
+ *
+ * @return the ending offset > getStartOffset() and
+ * <= getDocument().getLength() + 1
+ * @see Document
+ * @see AbstractDocument
+ */
+ public int getEndOffset();
+
+ /**
+ * Gets the child element index closest to the given offset.
+ * The offset is specified relative to the beginning of the
+ * document. Returns <code>-1</code> if the
+ * <code>Element</code> is a leaf, otherwise returns
+ * the index of the <code>Element</code> that best represents
+ * the given location. Returns <code>0</code> if the location
+ * is less than the start offset. Returns
+ * <code>getElementCount() - 1</code> if the location is
+ * greater than or equal to the end offset.
+ *
+ * @param offset the specified offset >= 0
+ * @return the element index >= 0
+ */
+ public int getElementIndex(int offset);
+
+ /**
+ * Gets the number of child elements contained by this element.
+ * If this element is a leaf, a count of zero is returned.
+ *
+ * @return the number of child elements >= 0
+ */
+ public int getElementCount();
+
+ /**
+ * Fetches the child element at the given index.
+ *
+ * @param index the specified index >= 0
+ * @return the child element
+ */
+ public Element getElement(int index);
+
+ /**
+ * Is this element a leaf element? An element that
+ * <i>may</i> have children, even if it currently
+ * has no children, would return <code>false</code>.
+ *
+ * @return true if a leaf element else false
+ */
+ public boolean isLeaf();
+
+
+}
diff --git a/src/share/classes/javax/swing/text/ElementIterator.java b/src/share/classes/javax/swing/text/ElementIterator.java
new file mode 100644
index 000000000..689ab0eeb
--- /dev/null
+++ b/src/share/classes/javax/swing/text/ElementIterator.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright 1998-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;
+
+import java.util.Stack;
+import java.util.Enumeration;
+
+/**
+ * <p>
+ * ElementIterator, as the name suggests, iteratates over the Element
+ * tree. The constructor can be invoked with either Document or an Element
+ * as an argument. If the constructor is invoked with a Document as an
+ * argument then the root of the iteration is the return value of
+ * document.getDefaultRootElement().
+ *
+ * The iteration happens in a depth-first manner. In terms of how
+ * boundary conditions are handled:
+ * a) if next() is called before first() or current(), the
+ * root will be returned.
+ * b) next() returns null to indicate the end of the list.
+ * c) previous() returns null when the current element is the root
+ * or next() has returned null.
+ *
+ * The ElementIterator does no locking of the Element tree. This means
+ * that it does not track any changes. It is the responsibility of the
+ * user of this class, to ensure that no changes happen during element
+ * iteration.
+ *
+ * Simple usage example:
+ *
+ * public void iterate() {
+ * ElementIterator it = new ElementIterator(root);
+ * Element elem;
+ * while (true) {
+ * if ((elem = next()) != null) {
+ * // process element
+ * System.out.println("elem: " + elem.getName());
+ * } else {
+ * break;
+ * }
+ * }
+ * }
+ *
+ * @author Sunita Mani
+ *
+ */
+
+public class ElementIterator implements Cloneable {
+
+
+ private Element root;
+ private Stack elementStack = null;
+
+ /**
+ * The StackItem class stores the element
+ * as well as a child index. If the
+ * index is -1, then the element represented
+ * on the stack is the element itself.
+ * Otherwise, the index functions as as index
+ * into the vector of children of the element.
+ * In this case, the item on the stack
+ * represents the "index"th child of the element
+ *
+ */
+ private class StackItem implements Cloneable {
+ Element item;
+ int childIndex;
+
+ private StackItem(Element elem) {
+ /**
+ * -1 index implies a self reference,
+ * as opposed to an index into its
+ * list of children.
+ */
+ this.item = elem;
+ this.childIndex = -1;
+ }
+
+ private void incrementIndex() {
+ childIndex++;
+ }
+
+ private Element getElement() {
+ return item;
+ }
+
+ private int getIndex() {
+ return childIndex;
+ }
+
+ protected Object clone() throws java.lang.CloneNotSupportedException {
+ return super.clone();
+ }
+ }
+
+ /**
+ * Creates a new ElementIterator. The
+ * root element is taken to get the
+ * default root element of the document.
+ *
+ * @param document a Document.
+ */
+ public ElementIterator(Document document) {
+ root = document.getDefaultRootElement();
+ }
+
+
+ /**
+ * Creates a new ElementIterator.
+ *
+ * @param root the root Element.
+ */
+ public ElementIterator(Element root) {
+ this.root = root;
+ }
+
+
+ /**
+ * Clones the ElementIterator.
+ *
+ * @return a cloned ElementIterator Object.
+ */
+ public synchronized Object clone() {
+
+ try {
+ ElementIterator it = new ElementIterator(root);
+ if (elementStack != null) {
+ it.elementStack = new Stack();
+ for (int i = 0; i < elementStack.size(); i++) {
+ StackItem item = (StackItem)elementStack.elementAt(i);
+ StackItem clonee = (StackItem)item.clone();
+ it.elementStack.push(clonee);
+ }
+ }
+ return it;
+ } catch (CloneNotSupportedException e) {
+ throw new InternalError();
+ }
+ }
+
+
+ /**
+ * Fetches the first element.
+ *
+ * @return an Element.
+ */
+ public Element first() {
+ // just in case...
+ if (root == null) {
+ return null;
+ }
+
+ elementStack = new Stack();
+ if (root.getElementCount() != 0) {
+ elementStack.push(new StackItem(root));
+ }
+ return root;
+ }
+
+ /**
+ * Fetches the current depth of element tree.
+ *
+ * @return the depth.
+ */
+ public int depth() {
+ if (elementStack == null) {
+ return 0;
+ }
+ return elementStack.size();
+ }
+
+
+ /**
+ * Fetches the current Element.
+ *
+ * @return element on top of the stack or
+ * <code>null</code> if the root element is <code>null</code>
+ */
+ public Element current() {
+
+ if (elementStack == null) {
+ return first();
+ }
+
+ /*
+ get a handle to the element on top of the stack.
+ */
+ if (! elementStack.empty()) {
+ StackItem item = (StackItem)elementStack.peek();
+ Element elem = item.getElement();
+ int index = item.getIndex();
+ // self reference
+ if (index == -1) {
+ return elem;
+ }
+ // return the child at location "index".
+ return elem.getElement(index);
+ }
+ return null;
+ }
+
+
+ /**
+ * Fetches the next Element. The strategy
+ * used to locate the next element is
+ * a depth-first search.
+ *
+ * @return the next element or <code>null</code>
+ * at the end of the list.
+ */
+ public Element next() {
+
+ /* if current() has not been invoked
+ and next is invoked, the very first
+ element will be returned. */
+ if (elementStack == null) {
+ return first();
+ }
+
+ // no more elements
+ if (elementStack.isEmpty()) {
+ return null;
+ }
+
+ // get a handle to the element on top of the stack
+
+ StackItem item = (StackItem)elementStack.peek();
+ Element elem = item.getElement();
+ int index = item.getIndex();
+
+ if (index+1 < elem.getElementCount()) {
+ Element child = elem.getElement(index+1);
+ if (child.isLeaf()) {
+ /* In this case we merely want to increment
+ the child index of the item on top of the
+ stack.*/
+ item.incrementIndex();
+ } else {
+ /* In this case we need to push the child(branch)
+ on the stack so that we can iterate over its
+ children. */
+ elementStack.push(new StackItem(child));
+ }
+ return child;
+ } else {
+ /* No more children for the item on top of the
+ stack therefore pop the stack. */
+ elementStack.pop();
+ if (!elementStack.isEmpty()) {
+ /* Increment the child index for the item that
+ is now on top of the stack. */
+ StackItem top = (StackItem)elementStack.peek();
+ top.incrementIndex();
+ /* We now want to return its next child, therefore
+ call next() recursively. */
+ return next();
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Fetches the previous Element. If howver the current
+ * element is the last element, or the current element
+ * is null, then null is returned.
+ *
+ * @return previous <code>Element</code> if available
+ *
+ */
+ public Element previous() {
+
+ int stackSize;
+ if (elementStack == null || (stackSize = elementStack.size()) == 0) {
+ return null;
+ }
+
+ // get a handle to the element on top of the stack
+ //
+ StackItem item = (StackItem)elementStack.peek();
+ Element elem = item.getElement();
+ int index = item.getIndex();
+
+ if (index > 0) {
+ /* return child at previous index. */
+ return getDeepestLeaf(elem.getElement(--index));
+ } else if (index == 0) {
+ /* this implies that current is the element's
+ first child, therefore previous is the
+ element itself. */
+ return elem;
+ } else if (index == -1) {
+ if (stackSize == 1) {
+ // current is the root, nothing before it.
+ return null;
+ }
+ /* We need to return either the item
+ below the top item or one of the
+ former's children. */
+ Object top = elementStack.pop();
+ item = (StackItem)elementStack.peek();
+
+ // restore the top item.
+ elementStack.push(top);
+ elem = item.getElement();
+ index = item.getIndex();
+ return ((index == -1) ? elem : getDeepestLeaf(elem.getElement
+ (index)));
+ }
+ // should never get here.
+ return null;
+ }
+
+ /**
+ * Returns the last child of <code>parent</code> that is a leaf. If the
+ * last child is a not a leaf, this method is called with the last child.
+ */
+ private Element getDeepestLeaf(Element parent) {
+ if (parent.isLeaf()) {
+ return parent;
+ }
+ int childCount = parent.getElementCount();
+ if (childCount == 0) {
+ return parent;
+ }
+ return getDeepestLeaf(parent.getElement(childCount - 1));
+ }
+
+ /*
+ Iterates through the element tree and prints
+ out each element and its attributes.
+ */
+ private void dumpTree() {
+
+ Element elem;
+ while (true) {
+ if ((elem = next()) != null) {
+ System.out.println("elem: " + elem.getName());
+ AttributeSet attr = elem.getAttributes();
+ String s = "";
+ Enumeration names = attr.getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object key = names.nextElement();
+ Object value = attr.getAttribute(key);
+ if (value instanceof AttributeSet) {
+ // don't go recursive
+ s = s + key + "=**AttributeSet** ";
+ } else {
+ s = s + key + "=" + value + " ";
+ }
+ }
+ System.out.println("attributes: " + s);
+ } else {
+ break;
+ }
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/FieldView.java b/src/share/classes/javax/swing/text/FieldView.java
new file mode 100644
index 000000000..6f9db3944
--- /dev/null
+++ b/src/share/classes/javax/swing/text/FieldView.java
@@ -0,0 +1,311 @@
+/*
+ * 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;
+
+import java.awt.*;
+import javax.swing.*;
+import javax.swing.event.*;
+
+/**
+ * Extends the multi-line plain text view to be suitable
+ * for a single-line editor view. If the view is
+ * allocated extra space, the field must adjust for it.
+ * If the hosting component is a JTextField, this view
+ * will manage the ranges of the associated BoundedRangeModel
+ * and will adjust the horizontal allocation to match the
+ * current visibility settings of the JTextField.
+ *
+ * @author Timothy Prinzing
+ * @see View
+ */
+public class FieldView extends PlainView {
+
+ /**
+ * Constructs a new FieldView wrapped on an element.
+ *
+ * @param elem the element
+ */
+ public FieldView(Element elem) {
+ super(elem);
+ }
+
+ /**
+ * Fetches the font metrics associated with the component hosting
+ * this view.
+ *
+ * @return the metrics
+ */
+ protected FontMetrics getFontMetrics() {
+ Component c = getContainer();
+ return c.getFontMetrics(c.getFont());
+ }
+
+ /**
+ * Adjusts the allocation given to the view
+ * to be a suitable allocation for a text field.
+ * If the view has been allocated more than the
+ * preferred span vertically, the allocation is
+ * changed to be centered vertically. Horizontally
+ * the view is adjusted according to the horizontal
+ * alignment property set on the associated JTextField
+ * (if that is the type of the hosting component).
+ *
+ * @param a the allocation given to the view, which may need
+ * to be adjusted.
+ * @return the allocation that the superclass should use.
+ */
+ protected Shape adjustAllocation(Shape a) {
+ if (a != null) {
+ Rectangle bounds = a.getBounds();
+ int vspan = (int) getPreferredSpan(Y_AXIS);
+ int hspan = (int) getPreferredSpan(X_AXIS);
+ if (bounds.height != vspan) {
+ int slop = bounds.height - vspan;
+ bounds.y += slop / 2;
+ bounds.height -= slop;
+ }
+
+ // horizontal adjustments
+ Component c = getContainer();
+ if (c instanceof JTextField) {
+ JTextField field = (JTextField) c;
+ BoundedRangeModel vis = field.getHorizontalVisibility();
+ int max = Math.max(hspan, bounds.width);
+ int value = vis.getValue();
+ int extent = Math.min(max, bounds.width - 1);
+ if ((value + extent) > max) {
+ value = max - extent;
+ }
+ vis.setRangeProperties(value, extent, vis.getMinimum(),
+ max, false);
+ if (hspan < bounds.width) {
+ // horizontally align the interior
+ int slop = bounds.width - 1 - hspan;
+
+ int align = ((JTextField)c).getHorizontalAlignment();
+ if(Utilities.isLeftToRight(c)) {
+ if(align==LEADING) {
+ align = LEFT;
+ }
+ else if(align==TRAILING) {
+ align = RIGHT;
+ }
+ }
+ else {
+ if(align==LEADING) {
+ align = RIGHT;
+ }
+ else if(align==TRAILING) {
+ align = LEFT;
+ }
+ }
+
+ switch (align) {
+ case SwingConstants.CENTER:
+ bounds.x += slop / 2;
+ bounds.width -= slop;
+ break;
+ case SwingConstants.RIGHT:
+ bounds.x += slop;
+ bounds.width -= slop;
+ break;
+ }
+ } else {
+ // adjust the allocation to match the bounded range.
+ bounds.width = hspan;
+ bounds.x -= vis.getValue();
+ }
+ }
+ return bounds;
+ }
+ return null;
+ }
+
+ /**
+ * Update the visibility model with the associated JTextField
+ * (if there is one) to reflect the current visibility as a
+ * result of changes to the document model. The bounded
+ * range properties are updated. If the view hasn't yet been
+ * shown the extent will be zero and we just set it to be full
+ * until determined otherwise.
+ */
+ void updateVisibilityModel() {
+ Component c = getContainer();
+ if (c instanceof JTextField) {
+ JTextField field = (JTextField) c;
+ BoundedRangeModel vis = field.getHorizontalVisibility();
+ int hspan = (int) getPreferredSpan(X_AXIS);
+ int extent = vis.getExtent();
+ int maximum = Math.max(hspan, extent);
+ extent = (extent == 0) ? maximum : extent;
+ int value = maximum - extent;
+ int oldValue = vis.getValue();
+ if ((oldValue + extent) > maximum) {
+ oldValue = maximum - extent;
+ }
+ value = Math.max(0, Math.min(value, oldValue));
+ vis.setRangeProperties(value, extent, 0, maximum, false);
+ }
+ }
+
+ // --- View methods -------------------------------------------
+
+ /**
+ * Renders using the given rendering surface and area on that surface.
+ * The view may need to do layout and create child views to enable
+ * itself to render into the given allocation.
+ *
+ * @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) {
+ Rectangle r = (Rectangle) a;
+ g.clipRect(r.x, r.y, r.width, r.height);
+ super.paint(g, a);
+ }
+
+ /**
+ * Adjusts <code>a</code> based on the visible region and returns it.
+ */
+ Shape adjustPaintRegion(Shape a) {
+ return adjustAllocation(a);
+ }
+
+ /**
+ * 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.
+ */
+ public float getPreferredSpan(int axis) {
+ switch (axis) {
+ case View.X_AXIS:
+ Segment buff = SegmentCache.getSharedSegment();
+ Document doc = getDocument();
+ int width;
+ try {
+ FontMetrics fm = getFontMetrics();
+ doc.getText(0, doc.getLength(), buff);
+ width = Utilities.getTabbedTextWidth(buff, fm, 0, this, 0);
+ if (buff.count > 0) {
+ Component c = getContainer();
+ firstLineOffset = sun.swing.SwingUtilities2.
+ getLeftSideBearing((c instanceof JComponent) ?
+ (JComponent)c : null, fm,
+ buff.array[buff.offset]);
+ firstLineOffset = Math.max(0, -firstLineOffset);
+ }
+ else {
+ firstLineOffset = 0;
+ }
+ } catch (BadLocationException bl) {
+ width = 0;
+ }
+ SegmentCache.releaseSharedSegment(buff);
+ return width + firstLineOffset;
+ default:
+ return super.getPreferredSpan(axis);
+ }
+ }
+
+ /**
+ * Determines the resizability of the view along the
+ * given axis. A value of 0 or less is not resizable.
+ *
+ * @param axis View.X_AXIS or View.Y_AXIS
+ * @return the weight -> 1 for View.X_AXIS, else 0
+ */
+ public int getResizeWeight(int axis) {
+ if (axis == View.X_AXIS) {
+ return 1;
+ }
+ return 0;
+ }
+
+ /**
+ * 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 >= 0
+ * @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 {
+ return super.modelToView(pos, adjustAllocation(a), b);
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param fx the X coordinate >= 0.0f
+ * @param fy the Y coordinate >= 0.0f
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents the
+ * given point in the view
+ * @see View#viewToModel
+ */
+ public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
+ return super.viewToModel(fx, fy, adjustAllocation(a), bias);
+ }
+
+ /**
+ * Gives notification that something was inserted into the document
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ super.insertUpdate(changes, adjustAllocation(a), f);
+ updateVisibilityModel();
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ super.removeUpdate(changes, adjustAllocation(a), f);
+ updateVisibilityModel();
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/FlowView.java b/src/share/classes/javax/swing/text/FlowView.java
new file mode 100644
index 000000000..39f8a5d60
--- /dev/null
+++ b/src/share/classes/javax/swing/text/FlowView.java
@@ -0,0 +1,863 @@
+/*
+ * Copyright 1999-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;
+
+import java.awt.*;
+import java.util.Vector;
+import javax.swing.event.*;
+import javax.swing.SizeRequirements;
+
+/**
+ * A View that tries to flow it's children into some
+ * partially constrained space. This can be used to
+ * build things like paragraphs, pages, etc. The
+ * flow is made up of the following pieces of functionality.
+ * <ul>
+ * <li>A logical set of child views, which as used as a
+ * layout pool from which a physical view is formed.
+ * <li>A strategy for translating the logical view to
+ * a physical (flowed) view.
+ * <li>Constraints for the strategy to work against.
+ * <li>A physical structure, that represents the flow.
+ * The children of this view are where the pieces of
+ * of the logical views are placed to create the flow.
+ * </ul>
+ *
+ * @author Timothy Prinzing
+ * @see View
+ * @since 1.3
+ */
+public abstract class FlowView extends BoxView {
+
+ /**
+ * Constructs a FlowView for the given element.
+ *
+ * @param elem the element that this view is responsible for
+ * @param axis may be either View.X_AXIS or View.Y_AXIS
+ */
+ public FlowView(Element elem, int axis) {
+ super(elem, axis);
+ layoutSpan = Integer.MAX_VALUE;
+ strategy = new FlowStrategy();
+ }
+
+ /**
+ * Fetches the axis along which views should be
+ * flowed. By default, this will be the axis
+ * orthogonal to the axis along which the flow
+ * rows are tiled (the axis of the default flow
+ * rows themselves). This is typically used
+ * by the <code>FlowStrategy</code>.
+ */
+ public int getFlowAxis() {
+ if (getAxis() == Y_AXIS) {
+ return X_AXIS;
+ }
+ return Y_AXIS;
+ }
+
+ /**
+ * Fetch the constraining span to flow against for
+ * the given child index. This is called by the
+ * FlowStrategy while it is updating the flow.
+ * A flow can be shaped by providing different values
+ * for the row constraints. By default, the entire
+ * span inside of the insets along the flow axis
+ * is returned.
+ *
+ * @param index the index of the row being updated.
+ * This should be a value >= 0 and < getViewCount().
+ * @see #getFlowStart
+ */
+ public int getFlowSpan(int index) {
+ return layoutSpan;
+ }
+
+ /**
+ * Fetch the location along the flow axis that the
+ * flow span will start at. This is called by the
+ * FlowStrategy while it is updating the flow.
+ * A flow can be shaped by providing different values
+ * for the row constraints.
+
+ * @param index the index of the row being updated.
+ * This should be a value >= 0 and < getViewCount().
+ * @see #getFlowSpan
+ */
+ public int getFlowStart(int index) {
+ return 0;
+ }
+
+ /**
+ * Create a View that should be used to hold a
+ * a rows worth of children in a flow. This is
+ * called by the FlowStrategy when new children
+ * are added or removed (i.e. rows are added or
+ * removed) in the process of updating the flow.
+ */
+ protected abstract View createRow();
+
+ // ---- BoxView methods -------------------------------------
+
+ /**
+ * Loads all of the children to initialize the view.
+ * This is called by the <code>setParent</code> method.
+ * This is reimplemented to not load any children directly
+ * (as they are created in the process of formatting).
+ * If the layoutPool variable is null, an instance of
+ * LogicalView is created to represent the logical view
+ * that is used in the process of formatting.
+ *
+ * @param f the view factory
+ */
+ protected void loadChildren(ViewFactory f) {
+ if (layoutPool == null) {
+ layoutPool = new LogicalView(getElement());
+ }
+ layoutPool.setParent(this);
+
+ // This synthetic insertUpdate call gives the strategy a chance
+ // to initialize.
+ strategy.insertUpdate(this, null, null);
+ }
+
+ /**
+ * Fetches the child view index representing the given position in
+ * the model.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ */
+ protected int getViewIndexAtPosition(int pos) {
+ if (pos >= getStartOffset() && (pos < getEndOffset())) {
+ for (int counter = 0; counter < getViewCount(); counter++) {
+ View v = getView(counter);
+ if(pos >= v.getStartOffset() &&
+ pos < v.getEndOffset()) {
+ return counter;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Lays out the children. If the span along the flow
+ * axis has changed, layout is marked as invalid which
+ * which will cause the superclass behavior to recalculate
+ * the layout along the box axis. The FlowStrategy.layout
+ * method will be called to rebuild the flow rows as
+ * appropriate. If the height of this view changes
+ * (determined by the perferred size along the box axis),
+ * a preferenceChanged is called. Following all of that,
+ * the normal box layout of the superclass is performed.
+ *
+ * @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 This
+ * is the height inside of the inset area.
+ */
+ protected void layout(int width, int height) {
+ final int faxis = getFlowAxis();
+ int newSpan;
+ if (faxis == X_AXIS) {
+ newSpan = (int)width;
+ } else {
+ newSpan = (int)height;
+ }
+ if (layoutSpan != newSpan) {
+ layoutChanged(faxis);
+ layoutChanged(getAxis());
+ layoutSpan = newSpan;
+ }
+
+ // repair the flow if necessary
+ if (! isLayoutValid(faxis)) {
+ final int heightAxis = getAxis();
+ int oldFlowHeight = (int)((heightAxis == X_AXIS)? getWidth() : getHeight());
+ strategy.layout(this);
+ int newFlowHeight = (int) getPreferredSpan(heightAxis);
+ if (oldFlowHeight != newFlowHeight) {
+ View p = getParent();
+ if (p != null) {
+ p.preferenceChanged(this, (heightAxis == X_AXIS), (heightAxis == Y_AXIS));
+ }
+
+ // PENDING(shannonh)
+ // Temporary fix for 4250847
+ // Can be removed when TraversalContext is added
+ Component host = getContainer();
+ if (host != null) {
+ //nb idk 12/12/2001 host should not be equal to null. We need to add assertion here
+ host.repaint();
+ }
+ }
+ }
+
+ super.layout(width, height);
+ }
+
+ /**
+ * Calculate equirements along the minor axis. This
+ * is implemented to forward the request to the logical
+ * view by calling getMinimumSpan, getPreferredSpan, and
+ * getMaximumSpan on it.
+ */
+ protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) {
+ if (r == null) {
+ r = new SizeRequirements();
+ }
+ float pref = layoutPool.getPreferredSpan(axis);
+ float min = layoutPool.getMinimumSpan(axis);
+ // Don't include insets, Box.getXXXSpan will include them.
+ r.minimum = (int)min;
+ r.preferred = Math.max(r.minimum, (int) pref);
+ r.maximum = Integer.MAX_VALUE;
+ r.alignment = 0.5f;
+ return r;
+ }
+
+ // ---- View methods ----------------------------------------------------
+
+ /**
+ * Gives notification that something was inserted into the document
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ layoutPool.insertUpdate(changes, a, f);
+ strategy.insertUpdate(this, changes, getInsideAllocation(a));
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ layoutPool.removeUpdate(changes, a, f);
+ strategy.removeUpdate(this, changes, getInsideAllocation(a));
+ }
+
+ /**
+ * Gives notification from the document that attributes were changed
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ layoutPool.changedUpdate(changes, a, f);
+ strategy.changedUpdate(this, changes, getInsideAllocation(a));
+ }
+
+ /** {@inheritDoc} */
+ public void setParent(View parent) {
+ super.setParent(parent);
+ if (parent == null
+ && layoutPool != null ) {
+ layoutPool.setParent(null);
+ }
+ }
+
+ // --- variables -----------------------------------------------
+
+ /**
+ * Default constraint against which the flow is
+ * created against.
+ */
+ protected int layoutSpan;
+
+ /**
+ * These are the views that represent the child elements
+ * of the element this view represents (The logical view
+ * to translate to a physical view). These are not
+ * directly children of this view. These are either
+ * placed into the rows directly or used for the purpose
+ * of breaking into smaller chunks, to form the physical
+ * view.
+ */
+ protected View layoutPool;
+
+ /**
+ * The behavior for keeping the flow updated. By
+ * default this is a singleton shared by all instances
+ * of FlowView (FlowStrategy is stateless). Subclasses
+ * can create an alternative strategy, which might keep
+ * state.
+ */
+ protected FlowStrategy strategy;
+
+ /**
+ * Strategy for maintaining the physical form
+ * of the flow. The default implementation is
+ * completely stateless, and recalculates the
+ * entire flow if the layout is invalid on the
+ * given FlowView. Alternative strategies can
+ * be implemented by subclassing, and might
+ * perform incrementatal repair to the layout
+ * or alternative breaking behavior.
+ * @since 1.3
+ */
+ public static class FlowStrategy {
+ int damageStart = Integer.MAX_VALUE;
+ Vector<View> viewBuffer;
+
+ void addDamage(FlowView fv, int offset) {
+ if (offset >= fv.getStartOffset() && offset < fv.getEndOffset()) {
+ damageStart = Math.min(damageStart, offset);
+ }
+ }
+
+ void unsetDamage() {
+ damageStart = Integer.MAX_VALUE;
+ }
+
+ /**
+ * Gives notification that something was inserted into the document
+ * in a location that the given flow view is responsible for. The
+ * strategy should update the appropriate changed region (which
+ * depends upon the strategy used for repair).
+ *
+ * @param e the change information from the associated document
+ * @param alloc the current allocation of the view inside of the insets.
+ * This value will be null if the view has not yet been displayed.
+ * @see View#insertUpdate
+ */
+ public void insertUpdate(FlowView fv, DocumentEvent e, Rectangle alloc) {
+ // FlowView.loadChildren() makes a synthetic call into this,
+ // passing null as e
+ if (e != null) {
+ addDamage(fv, e.getOffset());
+ }
+
+ if (alloc != null) {
+ Component host = fv.getContainer();
+ if (host != null) {
+ host.repaint(alloc.x, alloc.y, alloc.width, alloc.height);
+ }
+ } else {
+ fv.preferenceChanged(null, true, true);
+ }
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that the given flow view is responsible for.
+ *
+ * @param e the change information from the associated document
+ * @param alloc the current allocation of the view inside of the insets.
+ * @see View#removeUpdate
+ */
+ public void removeUpdate(FlowView fv, DocumentEvent e, Rectangle alloc) {
+ addDamage(fv, e.getOffset());
+ if (alloc != null) {
+ Component host = fv.getContainer();
+ if (host != null) {
+ host.repaint(alloc.x, alloc.y, alloc.width, alloc.height);
+ }
+ } else {
+ fv.preferenceChanged(null, true, true);
+ }
+ }
+
+ /**
+ * Gives notification from the document that attributes were changed
+ * in a location that this view is responsible for.
+ *
+ * @param fv the <code>FlowView</code> containing the changes
+ * @param e the <code>DocumentEvent</code> describing the changes
+ * done to the Document
+ * @param alloc Bounds of the View
+ * @see View#changedUpdate
+ */
+ public void changedUpdate(FlowView fv, DocumentEvent e, Rectangle alloc) {
+ addDamage(fv, e.getOffset());
+ if (alloc != null) {
+ Component host = fv.getContainer();
+ if (host != null) {
+ host.repaint(alloc.x, alloc.y, alloc.width, alloc.height);
+ }
+ } else {
+ fv.preferenceChanged(null, true, true);
+ }
+ }
+
+ /**
+ * This method gives flow strategies access to the logical
+ * view of the FlowView.
+ */
+ protected View getLogicalView(FlowView fv) {
+ return fv.layoutPool;
+ }
+
+ /**
+ * Update the flow on the given FlowView. By default, this causes
+ * all of the rows (child views) to be rebuilt to match the given
+ * constraints for each row. This is called by a FlowView.layout
+ * to update the child views in the flow.
+ *
+ * @param fv the view to reflow
+ */
+ public void layout(FlowView fv) {
+ View pool = getLogicalView(fv);
+ int rowIndex, p0;
+ int p1 = fv.getEndOffset();
+
+ if (fv.majorAllocValid) {
+ if (damageStart == Integer.MAX_VALUE) {
+ return;
+ }
+ // In some cases there's no view at position damageStart, so
+ // step back and search again. See 6452106 for details.
+ while ((rowIndex = fv.getViewIndexAtPosition(damageStart)) < 0) {
+ damageStart--;
+ }
+ if (rowIndex > 0) {
+ rowIndex--;
+ }
+ p0 = fv.getView(rowIndex).getStartOffset();
+ } else {
+ rowIndex = 0;
+ p0 = fv.getStartOffset();
+ }
+ reparentViews(pool, p0);
+
+ viewBuffer = new Vector<View>(10, 10);
+ int rowCount = fv.getViewCount();
+ while (p0 < p1) {
+ View row;
+ if (rowIndex >= rowCount) {
+ row = fv.createRow();
+ fv.append(row);
+ } else {
+ row = fv.getView(rowIndex);
+ }
+ p0 = layoutRow(fv, rowIndex, p0);
+ rowIndex++;
+ }
+ viewBuffer = null;
+
+ if (rowIndex < rowCount) {
+ fv.replace(rowIndex, rowCount - rowIndex, null);
+ }
+ unsetDamage();
+ }
+
+ /**
+ * Creates a row of views that will fit within the
+ * layout span of the row. This is called by the layout method.
+ * This is implemented to fill the row by repeatedly calling
+ * the createView method until the available span has been
+ * exhausted, a forced break was encountered, or the createView
+ * method returned null. If the remaining span was exhaused,
+ * the adjustRow method will be called to perform adjustments
+ * to the row to try and make it fit into the given span.
+ *
+ * @param rowIndex the index of the row to fill in with views. The
+ * row is assumed to be empty on entry.
+ * @param pos The current position in the children of
+ * this views element from which to start.
+ * @return the position to start the next row
+ */
+ protected int layoutRow(FlowView fv, int rowIndex, int pos) {
+ View row = fv.getView(rowIndex);
+ float x = fv.getFlowStart(rowIndex);
+ float spanLeft = fv.getFlowSpan(rowIndex);
+ int end = fv.getEndOffset();
+ TabExpander te = (fv instanceof TabExpander) ? (TabExpander)fv : null;
+ final int flowAxis = fv.getFlowAxis();
+
+ int breakWeight = BadBreakWeight;
+ float breakX = 0f;
+ float breakSpan = 0f;
+ int breakIndex = -1;
+ int n = 0;
+
+ viewBuffer.clear();
+ while (pos < end && spanLeft >= 0) {
+ View v = createView(fv, pos, (int)spanLeft, rowIndex);
+ if (v == null) {
+ break;
+ }
+
+ int bw = v.getBreakWeight(flowAxis, x, spanLeft);
+ if (bw >= ForcedBreakWeight) {
+ View w = v.breakView(flowAxis, pos, x, spanLeft);
+ if (w != null) {
+ viewBuffer.add(w);
+ } else if (n == 0) {
+ // if the view does not break, and it is the only view
+ // in a row, use the whole view
+ viewBuffer.add(v);
+ }
+ break;
+ } else if (bw >= breakWeight && bw > BadBreakWeight) {
+ breakWeight = bw;
+ breakX = x;
+ breakSpan = spanLeft;
+ breakIndex = n;
+ }
+
+ float chunkSpan;
+ if (flowAxis == X_AXIS && v instanceof TabableView) {
+ chunkSpan = ((TabableView)v).getTabbedSpan(x, te);
+ } else {
+ chunkSpan = v.getPreferredSpan(flowAxis);
+ }
+
+ if (chunkSpan > spanLeft && breakIndex >= 0) {
+ // row is too long, and we may break
+ if (breakIndex < n) {
+ v = viewBuffer.get(breakIndex);
+ }
+ for (int i = n - 1; i >= breakIndex; i--) {
+ viewBuffer.remove(i);
+ }
+ v = v.breakView(flowAxis, v.getStartOffset(), breakX, breakSpan);
+ }
+
+ spanLeft -= chunkSpan;
+ x += chunkSpan;
+ viewBuffer.add(v);
+ pos = v.getEndOffset();
+ n++;
+ }
+
+ View[] views = new View[viewBuffer.size()];
+ viewBuffer.toArray(views);
+ row.replace(0, row.getViewCount(), views);
+ return (views.length > 0 ? row.getEndOffset() : pos);
+ }
+
+ /**
+ * Adjusts the given row if possible to fit within the
+ * layout span. By default this will try to find the
+ * highest break weight possible nearest the end of
+ * the row. If a forced break is encountered, the
+ * break will be positioned there.
+ *
+ * @param rowIndex the row to adjust to the current layout
+ * span.
+ * @param desiredSpan the current layout span >= 0
+ * @param x the location r starts at.
+ */
+ protected void adjustRow(FlowView fv, int rowIndex, int desiredSpan, int x) {
+ final int flowAxis = fv.getFlowAxis();
+ View r = fv.getView(rowIndex);
+ int n = r.getViewCount();
+ int span = 0;
+ int bestWeight = BadBreakWeight;
+ int bestSpan = 0;
+ int bestIndex = -1;
+ View v;
+ for (int i = 0; i < n; i++) {
+ v = r.getView(i);
+ int spanLeft = desiredSpan - span;
+
+ int w = v.getBreakWeight(flowAxis, x + span, spanLeft);
+ if ((w >= bestWeight) && (w > BadBreakWeight)) {
+ bestWeight = w;
+ bestIndex = i;
+ bestSpan = span;
+ if (w >= ForcedBreakWeight) {
+ // it's a forced break, so there is
+ // no point in searching further.
+ break;
+ }
+ }
+ span += v.getPreferredSpan(flowAxis);
+ }
+ if (bestIndex < 0) {
+ // there is nothing that can be broken, leave
+ // it in it's current state.
+ return;
+ }
+
+ // Break the best candidate view, and patch up the row.
+ int spanLeft = desiredSpan - bestSpan;
+ v = r.getView(bestIndex);
+ v = v.breakView(flowAxis, v.getStartOffset(), x + bestSpan, spanLeft);
+ View[] va = new View[1];
+ va[0] = v;
+ View lv = getLogicalView(fv);
+ int p0 = r.getView(bestIndex).getStartOffset();
+ int p1 = r.getEndOffset();
+ for (int i = 0; i < lv.getViewCount(); i++) {
+ View tmpView = lv.getView(i);
+ if (tmpView.getEndOffset() > p1) {
+ break;
+ }
+ if (tmpView.getStartOffset() >= p0) {
+ tmpView.setParent(lv);
+ }
+ }
+ r.replace(bestIndex, n - bestIndex, va);
+ }
+
+ void reparentViews(View pool, int startPos) {
+ int n = pool.getViewIndex(startPos, Position.Bias.Forward);
+ if (n >= 0) {
+ for (int i = n; i < pool.getViewCount(); i++) {
+ pool.getView(i).setParent(pool);
+ }
+ }
+ }
+
+ /**
+ * Creates a view that can be used to represent the current piece
+ * of the flow. This can be either an entire view from the
+ * logical view, or a fragment of the logical view.
+ *
+ * @param fv the view holding the flow
+ * @param startOffset the start location for the view being created
+ * @param spanLeft the about of span left to fill in the row
+ * @param rowIndex the row the view will be placed into
+ */
+ protected View createView(FlowView fv, int startOffset, int spanLeft, int rowIndex) {
+ // Get the child view that contains the given starting position
+ View lv = getLogicalView(fv);
+ int childIndex = lv.getViewIndex(startOffset, Position.Bias.Forward);
+ View v = lv.getView(childIndex);
+ if (startOffset==v.getStartOffset()) {
+ // return the entire view
+ return v;
+ }
+
+ // return a fragment.
+ v = v.createFragment(startOffset, v.getEndOffset());
+ return v;
+ }
+ }
+
+ /**
+ * This class can be used to represent a logical view for
+ * a flow. It keeps the children updated to reflect the state
+ * of the model, gives the logical child views access to the
+ * view hierarchy, and calculates a preferred span. It doesn't
+ * do any rendering, layout, or model/view translation.
+ */
+ static class LogicalView extends CompositeView {
+
+ LogicalView(Element elem) {
+ super(elem);
+ }
+
+ protected int getViewIndexAtPosition(int pos) {
+ Element elem = getElement();
+ if (elem.isLeaf()) {
+ return 0;
+ }
+ return super.getViewIndexAtPosition(pos);
+ }
+
+ protected void loadChildren(ViewFactory f) {
+ Element elem = getElement();
+ if (elem.isLeaf()) {
+ View v = new LabelView(elem);
+ append(v);
+ } else {
+ super.loadChildren(f);
+ }
+ }
+
+ /**
+ * Fetches the attributes to use when rendering. This view
+ * isn't directly responsible for an element so it returns
+ * the outer classes attributes.
+ */
+ public AttributeSet getAttributes() {
+ View p = getParent();
+ return (p != null) ? p.getAttributes() : null;
+ }
+
+ /**
+ * 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.
+ * 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 View#getPreferredSpan
+ */
+ public float getPreferredSpan(int axis) {
+ float maxpref = 0;
+ float pref = 0;
+ int n = getViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ pref += v.getPreferredSpan(axis);
+ if (v.getBreakWeight(axis, 0, Integer.MAX_VALUE) >= ForcedBreakWeight) {
+ maxpref = Math.max(maxpref, pref);
+ pref = 0;
+ }
+ }
+ maxpref = Math.max(maxpref, pref);
+ return maxpref;
+ }
+
+ /**
+ * Determines the minimum span for this view along an
+ * axis. The is implemented to find the minimum unbreakable
+ * span.
+ *
+ * @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 View#getPreferredSpan
+ */
+ public float getMinimumSpan(int axis) {
+ float maxmin = 0;
+ float min = 0;
+ boolean nowrap = false;
+ int n = getViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ if (v.getBreakWeight(axis, 0, Integer.MAX_VALUE) == BadBreakWeight) {
+ min += v.getPreferredSpan(axis);
+ nowrap = true;
+ } else if (nowrap) {
+ maxmin = Math.max(min, maxmin);
+ nowrap = false;
+ min = 0;
+ }
+ if (v instanceof ComponentView) {
+ maxmin = Math.max(maxmin, v.getMinimumSpan(axis));
+ }
+ }
+ maxmin = Math.max(maxmin, min);
+ return maxmin;
+ }
+
+ /**
+ * Forward the DocumentEvent to the given child view. This
+ * is implemented to reparent the child to the logical view
+ * (the children may have been parented by a row in the flow
+ * if they fit without breaking) and then execute the superclass
+ * behavior.
+ *
+ * @param v the child view to forward the event to.
+ * @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 #forwardUpdate
+ * @since 1.3
+ */
+ protected void forwardUpdateToView(View v, DocumentEvent e,
+ Shape a, ViewFactory f) {
+ View parent = v.getParent();
+ v.setParent(this);
+ super.forwardUpdateToView(v, e, a, f);
+ v.setParent(parent);
+ }
+
+ // The following methods don't do anything useful, they
+ // simply keep the class from being abstract.
+
+ /**
+ * Renders using the given rendering surface and area on that
+ * surface. This is implemented to do nothing, the logical
+ * view is never visible.
+ *
+ * @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) {
+ }
+
+ /**
+ * Tests whether a point lies before the rectangle range.
+ * Implemented to return false, as hit detection is not
+ * performed on the logical view.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param alloc the rectangle
+ * @return true if the point is before the specified range
+ */
+ protected boolean isBefore(int x, int y, Rectangle alloc) {
+ return false;
+ }
+
+ /**
+ * Tests whether a point lies after the rectangle range.
+ * Implemented to return false, as hit detection is not
+ * performed on the logical view.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param alloc the rectangle
+ * @return true if the point is after the specified range
+ */
+ protected boolean isAfter(int x, int y, Rectangle alloc) {
+ return false;
+ }
+
+ /**
+ * Fetches the child view at the given point.
+ * Implemented to return null, as hit detection is not
+ * performed on the logical view.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param alloc the parent's allocation on entry, which should
+ * be changed to the child's allocation on exit
+ * @return the child view
+ */
+ protected View getViewAtPoint(int x, int y, Rectangle alloc) {
+ return null;
+ }
+
+ /**
+ * Returns the allocation for a given child.
+ * Implemented to do nothing, as the logical view doesn't
+ * perform layout on the children.
+ *
+ * @param index the index of the child, >= 0 && < getViewCount()
+ * @param a the allocation to the interior of the box on entry,
+ * and the allocation of the child view at the index on exit.
+ */
+ protected void childAllocation(int index, Rectangle a) {
+ }
+ }
+
+
+}
diff --git a/src/share/classes/javax/swing/text/GapContent.java b/src/share/classes/javax/swing/text/GapContent.java
new file mode 100644
index 000000000..23db31c74
--- /dev/null
+++ b/src/share/classes/javax/swing/text/GapContent.java
@@ -0,0 +1,954 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.CannotRedoException;
+import javax.swing.undo.CannotUndoException;
+import javax.swing.undo.UndoableEdit;
+import javax.swing.SwingUtilities;
+import java.lang.ref.WeakReference;
+import java.lang.ref.ReferenceQueue;
+
+/**
+ * An implementation of the AbstractDocument.Content interface
+ * implemented using a gapped buffer similar to that used by emacs.
+ * The underlying storage is a array of unicode characters with
+ * a gap somewhere. The gap is moved to the location of changes
+ * to take advantage of common behavior where most changes are
+ * in the same location. Changes that occur at a gap boundary are
+ * generally cheap and moving the gap is generally cheaper than
+ * moving the array contents directly to accomodate the change.
+ * <p>
+ * The positions tracking change are also generally cheap to
+ * maintain. The Position implementations (marks) store the array
+ * index and can easily calculate the sequential position from
+ * the current gap location. Changes only require update to the
+ * the marks between the old and new gap boundaries when the gap
+ * is moved, so generally updating the marks is pretty cheap.
+ * The marks are stored sorted so they can be located quickly
+ * with a binary search. This increases the cost of adding a
+ * mark, and decreases the cost of keeping the mark updated.
+ *
+ * @author Timothy Prinzing
+ */
+public class GapContent extends GapVector implements AbstractDocument.Content, Serializable {
+
+ /**
+ * Creates a new GapContent object. Initial size defaults to 10.
+ */
+ public GapContent() {
+ this(10);
+ }
+
+ /**
+ * Creates a new GapContent object, with the initial
+ * size specified. The initial size will not be allowed
+ * to go below 2, to give room for the implied break and
+ * the gap.
+ *
+ * @param initialLength the initial size
+ */
+ public GapContent(int initialLength) {
+ super(Math.max(initialLength,2));
+ char[] implied = new char[1];
+ implied[0] = '\n';
+ replace(0, 0, implied, implied.length);
+
+ marks = new MarkVector();
+ search = new MarkData(0);
+ queue = new ReferenceQueue();
+ }
+
+ /**
+ * Allocate an array to store items of the type
+ * appropriate (which is determined by the subclass).
+ */
+ protected Object allocateArray(int len) {
+ return new char[len];
+ }
+
+ /**
+ * Get the length of the allocated array.
+ */
+ protected int getArrayLength() {
+ char[] carray = (char[]) getArray();
+ return carray.length;
+ }
+
+ // --- AbstractDocument.Content methods -------------------------
+
+ /**
+ * Returns the length of the content.
+ *
+ * @return the length >= 1
+ * @see AbstractDocument.Content#length
+ */
+ public int length() {
+ int len = getArrayLength() - (getGapEnd() - getGapStart());
+ return len;
+ }
+
+ /**
+ * Inserts a string into the content.
+ *
+ * @param where the starting position >= 0, < length()
+ * @param str the non-null string to insert
+ * @return an UndoableEdit object for undoing
+ * @exception BadLocationException if the specified position is invalid
+ * @see AbstractDocument.Content#insertString
+ */
+ public UndoableEdit insertString(int where, String str) throws BadLocationException {
+ if (where > length() || where < 0) {
+ throw new BadLocationException("Invalid insert", length());
+ }
+ char[] chars = str.toCharArray();
+ replace(where, 0, chars, chars.length);
+ return new InsertUndo(where, str.length());
+ }
+
+ /**
+ * Removes part of the content.
+ *
+ * @param where the starting position >= 0, where + nitems < length()
+ * @param nitems the number of characters to remove >= 0
+ * @return an UndoableEdit object for undoing
+ * @exception BadLocationException if the specified position is invalid
+ * @see AbstractDocument.Content#remove
+ */
+ public UndoableEdit remove(int where, int nitems) throws BadLocationException {
+ if (where + nitems >= length()) {
+ throw new BadLocationException("Invalid remove", length() + 1);
+ }
+ String removedString = getString(where, nitems);
+ UndoableEdit edit = new RemoveUndo(where, removedString);
+ replace(where, nitems, empty, 0);
+ return edit;
+
+ }
+
+ /**
+ * Retrieves a portion of the content.
+ *
+ * @param where the starting position >= 0
+ * @param len the length to retrieve >= 0
+ * @return a string representing the content
+ * @exception BadLocationException if the specified position is invalid
+ * @see AbstractDocument.Content#getString
+ */
+ public String getString(int where, int len) throws BadLocationException {
+ Segment s = new Segment();
+ getChars(where, len, s);
+ return new String(s.array, s.offset, s.count);
+ }
+
+ /**
+ * Retrieves a portion of the content. If the desired content spans
+ * the gap, we copy the content. If the desired content does not
+ * span the gap, the actual store is returned to avoid the copy since
+ * it is contiguous.
+ *
+ * @param where the starting position >= 0, where + len <= length()
+ * @param len the number of characters to retrieve >= 0
+ * @param chars the Segment object to return the characters in
+ * @exception BadLocationException if the specified position is invalid
+ * @see AbstractDocument.Content#getChars
+ */
+ public void getChars(int where, int len, Segment chars) throws BadLocationException {
+ int end = where + len;
+ if (where < 0 || end < 0) {
+ throw new BadLocationException("Invalid location", -1);
+ }
+ if (end > length() || where > length()) {
+ throw new BadLocationException("Invalid location", length() + 1);
+ }
+ int g0 = getGapStart();
+ int g1 = getGapEnd();
+ char[] array = (char[]) getArray();
+ if ((where + len) <= g0) {
+ // below gap
+ chars.array = array;
+ chars.offset = where;
+ } else if (where >= g0) {
+ // above gap
+ chars.array = array;
+ chars.offset = g1 + where - g0;
+ } else {
+ // spans the gap
+ int before = g0 - where;
+ if (chars.isPartialReturn()) {
+ // partial return allowed, return amount before the gap
+ chars.array = array;
+ chars.offset = where;
+ chars.count = before;
+ return;
+ }
+ // partial return not allowed, must copy
+ chars.array = new char[len];
+ chars.offset = 0;
+ System.arraycopy(array, where, chars.array, 0, before);
+ System.arraycopy(array, g1, chars.array, before, len - before);
+ }
+ chars.count = len;
+ }
+
+ /**
+ * Creates a position within the content that will
+ * track change as the content is mutated.
+ *
+ * @param offset the offset to track >= 0
+ * @return the position
+ * @exception BadLocationException if the specified position is invalid
+ */
+ public Position createPosition(int offset) throws BadLocationException {
+ while ( queue.poll() != null ) {
+ unusedMarks++;
+ }
+ if (unusedMarks > Math.max(5, (marks.size() / 10))) {
+ removeUnusedMarks();
+ }
+ int g0 = getGapStart();
+ int g1 = getGapEnd();
+ int index = (offset < g0) ? offset : offset + (g1 - g0);
+ search.index = index;
+ int sortIndex = findSortIndex(search);
+ MarkData m;
+ StickyPosition position;
+ if (sortIndex < marks.size()
+ && (m = marks.elementAt(sortIndex)).index == index
+ && (position = m.getPosition()) != null) {
+ //position references the correct StickyPostition
+ } else {
+ position = new StickyPosition();
+ m = new MarkData(index,position,queue);
+ position.setMark(m);
+ marks.insertElementAt(m, sortIndex);
+ }
+
+ return position;
+ }
+
+ /**
+ * Holds the data for a mark... separately from
+ * the real mark so that the real mark (Position
+ * that the caller of createPosition holds) can be
+ * collected if there are no more references to
+ * it. The update table holds only a reference
+ * to this data.
+ */
+ final class MarkData extends WeakReference {
+
+ MarkData(int index) {
+ super(null);
+ this.index = index;
+ }
+ MarkData(int index, StickyPosition position, ReferenceQueue queue) {
+ super(position, queue);
+ this.index = index;
+ }
+
+ /**
+ * Fetch the location in the contiguous sequence
+ * being modeled. The index in the gap array
+ * is held by the mark, so it is adjusted according
+ * to it's relationship to the gap.
+ */
+ public final int getOffset() {
+ int g0 = getGapStart();
+ int g1 = getGapEnd();
+ int offs = (index < g0) ? index : index - (g1 - g0);
+ return Math.max(offs, 0);
+ }
+
+ StickyPosition getPosition() {
+ return (StickyPosition)get();
+ }
+ int index;
+ }
+
+ final class StickyPosition implements Position {
+
+ StickyPosition() {
+ }
+
+ void setMark(MarkData mark) {
+ this.mark = mark;
+ }
+
+ public final int getOffset() {
+ return mark.getOffset();
+ }
+
+ public String toString() {
+ return Integer.toString(getOffset());
+ }
+
+ MarkData mark;
+ }
+
+ // --- variables --------------------------------------
+
+ private static final char[] empty = new char[0];
+ private transient MarkVector marks;
+
+ /**
+ * Record used for searching for the place to
+ * start updating mark indexs when the gap
+ * boundaries are moved.
+ */
+ private transient MarkData search;
+
+ /**
+ * The number of unused mark entries
+ */
+ private transient int unusedMarks = 0;
+
+ private transient ReferenceQueue queue;
+
+ final static int GROWTH_SIZE = 1024 * 512;
+
+ // --- gap management -------------------------------
+
+ /**
+ * Make the gap bigger, moving any necessary data and updating
+ * the appropriate marks
+ */
+ protected void shiftEnd(int newSize) {
+ int oldGapEnd = getGapEnd();
+
+ super.shiftEnd(newSize);
+
+ // Adjust marks.
+ int dg = getGapEnd() - oldGapEnd;
+ int adjustIndex = findMarkAdjustIndex(oldGapEnd);
+ int n = marks.size();
+ for (int i = adjustIndex; i < n; i++) {
+ MarkData mark = marks.elementAt(i);
+ mark.index += dg;
+ }
+ }
+
+ /**
+ * Overridden to make growth policy less agressive for large
+ * text amount.
+ */
+ int getNewArraySize(int reqSize) {
+ if (reqSize < GROWTH_SIZE) {
+ return super.getNewArraySize(reqSize);
+ } else {
+ return reqSize + GROWTH_SIZE;
+ }
+ }
+
+ /**
+ * Move the start of the gap to a new location,
+ * without changing the size of the gap. This
+ * moves the data in the array and updates the
+ * marks accordingly.
+ */
+ protected void shiftGap(int newGapStart) {
+ int oldGapStart = getGapStart();
+ int dg = newGapStart - oldGapStart;
+ int oldGapEnd = getGapEnd();
+ int newGapEnd = oldGapEnd + dg;
+ int gapSize = oldGapEnd - oldGapStart;
+
+ // shift gap in the character array
+ super.shiftGap(newGapStart);
+
+ // update the marks
+ if (dg > 0) {
+ // Move gap up, move data and marks down.
+ int adjustIndex = findMarkAdjustIndex(oldGapStart);
+ int n = marks.size();
+ for (int i = adjustIndex; i < n; i++) {
+ MarkData mark = marks.elementAt(i);
+ if (mark.index >= newGapEnd) {
+ break;
+ }
+ mark.index -= gapSize;
+ }
+ } else if (dg < 0) {
+ // Move gap down, move data and marks up.
+ int adjustIndex = findMarkAdjustIndex(newGapStart);
+ int n = marks.size();
+ for (int i = adjustIndex; i < n; i++) {
+ MarkData mark = marks.elementAt(i);
+ if (mark.index >= oldGapEnd) {
+ break;
+ }
+ mark.index += gapSize;
+ }
+ }
+ resetMarksAtZero();
+ }
+
+ /**
+ * Resets all the marks that have an offset of 0 to have an index of
+ * zero as well.
+ */
+ protected void resetMarksAtZero() {
+ if (marks != null && getGapStart() == 0) {
+ int g1 = getGapEnd();
+ for (int counter = 0, maxCounter = marks.size();
+ counter < maxCounter; counter++) {
+ MarkData mark = marks.elementAt(counter);
+ if (mark.index <= g1) {
+ mark.index = 0;
+ }
+ else {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Adjust the gap end downward. This doesn't move
+ * any data, but it does update any marks affected
+ * by the boundary change. All marks from the old
+ * gap start down to the new gap start are squeezed
+ * to the end of the gap (their location has been
+ * removed).
+ */
+ protected void shiftGapStartDown(int newGapStart) {
+ // Push aside all marks from oldGapStart down to newGapStart.
+ int adjustIndex = findMarkAdjustIndex(newGapStart);
+ int n = marks.size();
+ int g0 = getGapStart();
+ int g1 = getGapEnd();
+ for (int i = adjustIndex; i < n; i++) {
+ MarkData mark = marks.elementAt(i);
+ if (mark.index > g0) {
+ // no more marks to adjust
+ break;
+ }
+ mark.index = g1;
+ }
+
+ // shift the gap in the character array
+ super.shiftGapStartDown(newGapStart);
+
+ resetMarksAtZero();
+ }
+
+ /**
+ * Adjust the gap end upward. This doesn't move
+ * any data, but it does update any marks affected
+ * by the boundary change. All marks from the old
+ * gap end up to the new gap end are squeezed
+ * to the end of the gap (their location has been
+ * removed).
+ */
+ protected void shiftGapEndUp(int newGapEnd) {
+ int adjustIndex = findMarkAdjustIndex(getGapEnd());
+ int n = marks.size();
+ for (int i = adjustIndex; i < n; i++) {
+ MarkData mark = marks.elementAt(i);
+ if (mark.index >= newGapEnd) {
+ break;
+ }
+ mark.index = newGapEnd;
+ }
+
+ // shift the gap in the character array
+ super.shiftGapEndUp(newGapEnd);
+
+ resetMarksAtZero();
+ }
+
+ /**
+ * Compares two marks.
+ *
+ * @param o1 the first object
+ * @param o2 the second object
+ * @return < 0 if o1 < o2, 0 if the same, > 0 if o1 > o2
+ */
+ final int compare(MarkData o1, MarkData o2) {
+ if (o1.index < o2.index) {
+ return -1;
+ } else if (o1.index > o2.index) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Finds the index to start mark adjustments given
+ * some search index.
+ */
+ final int findMarkAdjustIndex(int searchIndex) {
+ search.index = Math.max(searchIndex, 1);
+ int index = findSortIndex(search);
+
+ // return the first in the series
+ // (ie. there may be duplicates).
+ for (int i = index - 1; i >= 0; i--) {
+ MarkData d = marks.elementAt(i);
+ if (d.index != search.index) {
+ break;
+ }
+ index -= 1;
+ }
+ return index;
+ }
+
+ /**
+ * Finds the index of where to insert a new mark.
+ *
+ * @param o the mark to insert
+ * @return the index
+ */
+ final int findSortIndex(MarkData o) {
+ int lower = 0;
+ int upper = marks.size() - 1;
+ int mid = 0;
+
+ if (upper == -1) {
+ return 0;
+ }
+
+ int cmp = 0;
+ MarkData last = marks.elementAt(upper);
+ cmp = compare(o, last);
+ if (cmp > 0)
+ return upper + 1;
+
+ while (lower <= upper) {
+ mid = lower + ((upper - lower) / 2);
+ MarkData entry = marks.elementAt(mid);
+ cmp = compare(o, entry);
+
+ if (cmp == 0) {
+ // found a match
+ return mid;
+ } else if (cmp < 0) {
+ upper = mid - 1;
+ } else {
+ lower = mid + 1;
+ }
+ }
+
+ // didn't find it, but we indicate the index of where it would belong.
+ return (cmp < 0) ? mid : mid + 1;
+ }
+
+ /**
+ * Remove all unused marks out of the sorted collection
+ * of marks.
+ */
+ final void removeUnusedMarks() {
+ int n = marks.size();
+ MarkVector cleaned = new MarkVector(n);
+ for (int i = 0; i < n; i++) {
+ MarkData mark = marks.elementAt(i);
+ if (mark.get() != null) {
+ cleaned.addElement(mark);
+ }
+ }
+ marks = cleaned;
+ unusedMarks = 0;
+ }
+
+
+ static class MarkVector extends GapVector {
+
+ MarkVector() {
+ super();
+ }
+
+ MarkVector(int size) {
+ super(size);
+ }
+
+ /**
+ * Allocate an array to store items of the type
+ * appropriate (which is determined by the subclass).
+ */
+ protected Object allocateArray(int len) {
+ return new MarkData[len];
+ }
+
+ /**
+ * Get the length of the allocated array
+ */
+ protected int getArrayLength() {
+ MarkData[] marks = (MarkData[]) getArray();
+ return marks.length;
+ }
+
+ /**
+ * Returns the number of marks currently held
+ */
+ public int size() {
+ int len = getArrayLength() - (getGapEnd() - getGapStart());
+ return len;
+ }
+
+ /**
+ * Inserts a mark into the vector
+ */
+ public void insertElementAt(MarkData m, int index) {
+ oneMark[0] = m;
+ replace(index, 0, oneMark, 1);
+ }
+
+ /**
+ * Add a mark to the end
+ */
+ public void addElement(MarkData m) {
+ insertElementAt(m, size());
+ }
+
+ /**
+ * Fetches the mark at the given index
+ */
+ public MarkData elementAt(int index) {
+ int g0 = getGapStart();
+ int g1 = getGapEnd();
+ MarkData[] array = (MarkData[]) getArray();
+ if (index < g0) {
+ // below gap
+ return array[index];
+ } else {
+ // above gap
+ index += g1 - g0;
+ return array[index];
+ }
+ }
+
+ /**
+ * Replaces the elements in the specified range with the passed
+ * in objects. This will NOT adjust the gap. The passed in indices
+ * do not account for the gap, they are the same as would be used
+ * int <code>elementAt</code>.
+ */
+ protected void replaceRange(int start, int end, Object[] marks) {
+ int g0 = getGapStart();
+ int g1 = getGapEnd();
+ int index = start;
+ int newIndex = 0;
+ Object[] array = (Object[]) getArray();
+ if (start >= g0) {
+ // Completely passed gap
+ index += (g1 - g0);
+ end += (g1 - g0);
+ }
+ else if (end >= g0) {
+ // straddles gap
+ end += (g1 - g0);
+ while (index < g0) {
+ array[index++] = marks[newIndex++];
+ }
+ index = g1;
+ }
+ else {
+ // below gap
+ while (index < end) {
+ array[index++] = marks[newIndex++];
+ }
+ }
+ while (index < end) {
+ array[index++] = marks[newIndex++];
+ }
+ }
+
+ MarkData[] oneMark = new MarkData[1];
+
+ }
+
+ // --- serialization -------------------------------------
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException {
+ s.defaultReadObject();
+ marks = new MarkVector();
+ search = new MarkData(0);
+ queue = new ReferenceQueue();
+ }
+
+
+ // --- undo support --------------------------------------
+
+ /**
+ * Returns a Vector containing instances of UndoPosRef for the
+ * Positions in the range
+ * <code>offset</code> to <code>offset</code> + <code>length</code>.
+ * If <code>v</code> is not null the matching Positions are placed in
+ * there. The vector with the resulting Positions are returned.
+ *
+ * @param v the Vector to use, with a new one created on null
+ * @param offset the starting offset >= 0
+ * @param length the length >= 0
+ * @return the set of instances
+ */
+ protected Vector getPositionsInRange(Vector v, int offset, int length) {
+ int endOffset = offset + length;
+ int startIndex;
+ int endIndex;
+ int g0 = getGapStart();
+ int g1 = getGapEnd();
+
+ // Find the index of the marks.
+ if (offset < g0) {
+ if (offset == 0) {
+ // findMarkAdjustIndex start at 1!
+ startIndex = 0;
+ }
+ else {
+ startIndex = findMarkAdjustIndex(offset);
+ }
+ if (endOffset >= g0) {
+ endIndex = findMarkAdjustIndex(endOffset + (g1 - g0) + 1);
+ }
+ else {
+ endIndex = findMarkAdjustIndex(endOffset + 1);
+ }
+ }
+ else {
+ startIndex = findMarkAdjustIndex(offset + (g1 - g0));
+ endIndex = findMarkAdjustIndex(endOffset + (g1 - g0) + 1);
+ }
+
+ Vector placeIn = (v == null) ? new Vector(Math.max(1, endIndex -
+ startIndex)) : v;
+
+ for (int counter = startIndex; counter < endIndex; counter++) {
+ placeIn.addElement(new UndoPosRef(marks.elementAt(counter)));
+ }
+ return placeIn;
+ }
+
+ /**
+ * Resets the location for all the UndoPosRef instances
+ * in <code>positions</code>.
+ * <p>
+ * This is meant for internal usage, and is generally not of interest
+ * to subclasses.
+ *
+ * @param positions the UndoPosRef instances to reset
+ */
+ protected void updateUndoPositions(Vector positions, int offset,
+ int length) {
+ // Find the indexs of the end points.
+ int endOffset = offset + length;
+ int g1 = getGapEnd();
+ int startIndex;
+ int endIndex = findMarkAdjustIndex(g1 + 1);
+
+ if (offset != 0) {
+ startIndex = findMarkAdjustIndex(g1);
+ }
+ else {
+ startIndex = 0;
+ }
+
+ // Reset the location of the refenences.
+ for(int counter = positions.size() - 1; counter >= 0; counter--) {
+ UndoPosRef ref = (UndoPosRef)positions.elementAt(counter);
+ ref.resetLocation(endOffset, g1);
+ }
+ // We have to resort the marks in the range startIndex to endIndex.
+ // We can take advantage of the fact that it will be in
+ // increasing order, accept there will be a bunch of MarkData's with
+ // the index g1 (or 0 if offset == 0) interspersed throughout.
+ if (startIndex < endIndex) {
+ Object[] sorted = new Object[endIndex - startIndex];
+ int addIndex = 0;
+ int counter;
+ if (offset == 0) {
+ // If the offset is 0, the positions won't have incremented,
+ // have to do the reverse thing.
+ // Find the elements in startIndex whose index is 0
+ for (counter = startIndex; counter < endIndex; counter++) {
+ MarkData mark = marks.elementAt(counter);
+ if (mark.index == 0) {
+ sorted[addIndex++] = mark;
+ }
+ }
+ for (counter = startIndex; counter < endIndex; counter++) {
+ MarkData mark = marks.elementAt(counter);
+ if (mark.index != 0) {
+ sorted[addIndex++] = mark;
+ }
+ }
+ }
+ else {
+ for (counter = startIndex; counter < endIndex; counter++) {
+ MarkData mark = marks.elementAt(counter);
+ if (mark.index != g1) {
+ sorted[addIndex++] = mark;
+ }
+ }
+ for (counter = startIndex; counter < endIndex; counter++) {
+ MarkData mark = marks.elementAt(counter);
+ if (mark.index == g1) {
+ sorted[addIndex++] = mark;
+ }
+ }
+ }
+ // And replace
+ marks.replaceRange(startIndex, endIndex, sorted);
+ }
+ }
+
+ /**
+ * Used to hold a reference to a Mark that is being reset as the
+ * result of removing from the content.
+ */
+ final class UndoPosRef {
+ UndoPosRef(MarkData rec) {
+ this.rec = rec;
+ this.undoLocation = rec.getOffset();
+ }
+
+ /**
+ * Resets the location of the Position to the offset when the
+ * receiver was instantiated.
+ *
+ * @param endOffset end location of inserted string.
+ * @param g1 resulting end of gap.
+ */
+ protected void resetLocation(int endOffset, int g1) {
+ if (undoLocation != endOffset) {
+ this.rec.index = undoLocation;
+ }
+ else {
+ this.rec.index = g1;
+ }
+ }
+
+ /** Previous Offset of rec. */
+ protected int undoLocation;
+ /** Mark to reset offset. */
+ protected MarkData rec;
+ } // End of GapContent.UndoPosRef
+
+
+ /**
+ * UnoableEdit created for inserts.
+ */
+ class InsertUndo extends AbstractUndoableEdit {
+ protected InsertUndo(int offset, int length) {
+ super();
+ this.offset = offset;
+ this.length = length;
+ }
+
+ public void undo() throws CannotUndoException {
+ super.undo();
+ try {
+ // Get the Positions in the range being removed.
+ posRefs = getPositionsInRange(null, offset, length);
+ string = getString(offset, length);
+ remove(offset, length);
+ } catch (BadLocationException bl) {
+ throw new CannotUndoException();
+ }
+ }
+
+ public void redo() throws CannotRedoException {
+ super.redo();
+ try {
+ insertString(offset, string);
+ string = null;
+ // Update the Positions that were in the range removed.
+ if(posRefs != null) {
+ updateUndoPositions(posRefs, offset, length);
+ posRefs = null;
+ }
+ } catch (BadLocationException bl) {
+ throw new CannotRedoException();
+ }
+ }
+
+ /** Where string was inserted. */
+ protected int offset;
+ /** Length of string inserted. */
+ protected int length;
+ /** The string that was inserted. This will only be valid after an
+ * undo. */
+ protected String string;
+ /** An array of instances of UndoPosRef for the Positions in the
+ * range that was removed, valid after undo. */
+ protected Vector posRefs;
+ } // GapContent.InsertUndo
+
+
+ /**
+ * UndoableEdit created for removes.
+ */
+ class RemoveUndo extends AbstractUndoableEdit {
+ protected RemoveUndo(int offset, String string) {
+ super();
+ this.offset = offset;
+ this.string = string;
+ this.length = string.length();
+ posRefs = getPositionsInRange(null, offset, length);
+ }
+
+ public void undo() throws CannotUndoException {
+ super.undo();
+ try {
+ insertString(offset, string);
+ // Update the Positions that were in the range removed.
+ if(posRefs != null) {
+ updateUndoPositions(posRefs, offset, length);
+ posRefs = null;
+ }
+ string = null;
+ } catch (BadLocationException bl) {
+ throw new CannotUndoException();
+ }
+ }
+
+ public void redo() throws CannotRedoException {
+ super.redo();
+ try {
+ string = getString(offset, length);
+ // Get the Positions in the range being removed.
+ posRefs = getPositionsInRange(null, offset, length);
+ remove(offset, length);
+ } catch (BadLocationException bl) {
+ throw new CannotRedoException();
+ }
+ }
+
+ /** Where the string was removed from. */
+ protected int offset;
+ /** Length of string removed. */
+ protected int length;
+ /** The string that was removed. This is valid when redo is valid. */
+ protected String string;
+ /** An array of instances of UndoPosRef for the Positions in the
+ * range that was removed, valid before undo. */
+ protected Vector posRefs;
+ } // GapContent.RemoveUndo
+}
diff --git a/src/share/classes/javax/swing/text/GapVector.java b/src/share/classes/javax/swing/text/GapVector.java
new file mode 100644
index 000000000..7901da991
--- /dev/null
+++ b/src/share/classes/javax/swing/text/GapVector.java
@@ -0,0 +1,299 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import java.io.Serializable;
+import javax.swing.undo.UndoableEdit;
+
+/**
+ * An implementation of a gapped buffer similar to that used by
+ * emacs. The underlying storage is a java array of some type,
+ * which is known only by the subclass of this class. The array
+ * has a gap somewhere. The gap is moved to the location of changes
+ * to take advantage of common behavior where most changes occur
+ * in the same location. Changes that occur at a gap boundary are
+ * generally cheap and moving the gap is generally cheaper than
+ * moving the array contents directly to accomodate the change.
+ *
+ * @author Timothy Prinzing
+ * @see GapContent
+ */
+abstract class GapVector implements Serializable {
+
+
+ /**
+ * Creates a new GapVector object. Initial size defaults to 10.
+ */
+ public GapVector() {
+ this(10);
+ }
+
+ /**
+ * Creates a new GapVector object, with the initial
+ * size specified.
+ *
+ * @param initialLength the initial size
+ */
+ public GapVector(int initialLength) {
+ array = allocateArray(initialLength);
+ g0 = 0;
+ g1 = initialLength;
+ }
+
+ /**
+ * Allocate an array to store items of the type
+ * appropriate (which is determined by the subclass).
+ */
+ protected abstract Object allocateArray(int len);
+
+ /**
+ * Get the length of the allocated array
+ */
+ protected abstract int getArrayLength();
+
+ /**
+ * Access to the array. The actual type
+ * of the array is known only by the subclass.
+ */
+ protected final Object getArray() {
+ return array;
+ }
+
+ /**
+ * Access to the start of the gap.
+ */
+ protected final int getGapStart() {
+ return g0;
+ }
+
+ /**
+ * Access to the end of the gap.
+ */
+ protected final int getGapEnd() {
+ return g1;
+ }
+
+ // ---- variables -----------------------------------
+
+ /**
+ * The array of items. The type is determined by the subclass.
+ */
+ private Object array;
+
+ /**
+ * start of gap in the array
+ */
+ private int g0;
+
+ /**
+ * end of gap in the array
+ */
+ private int g1;
+
+
+ // --- gap management -------------------------------
+
+ /**
+ * Replace the given logical position in the storage with
+ * the given new items. This will move the gap to the area
+ * being changed if the gap is not currently located at the
+ * change location.
+ *
+ * @param position the location to make the replacement. This
+ * is not the location in the underlying storage array, but
+ * the location in the contiguous space being modeled.
+ * @param rmSize the number of items to remove
+ * @param addItems the new items to place in storage.
+ */
+ protected void replace(int position, int rmSize, Object addItems, int addSize) {
+ int addOffset = 0;
+ if (addSize == 0) {
+ close(position, rmSize);
+ return;
+ } else if (rmSize > addSize) {
+ /* Shrink the end. */
+ close(position+addSize, rmSize-addSize);
+ } else {
+ /* Grow the end, do two chunks. */
+ int endSize = addSize - rmSize;
+ int end = open(position + rmSize, endSize);
+ System.arraycopy(addItems, rmSize, array, end, endSize);
+ addSize = rmSize;
+ }
+ System.arraycopy(addItems, addOffset, array, position, addSize);
+ }
+
+ /**
+ * Delete nItems at position. Squeezes any marks
+ * within the deleted area to position. This moves
+ * the gap to the best place by minimizing it's
+ * overall movement. The gap must intersect the
+ * target block.
+ */
+ void close(int position, int nItems) {
+ if (nItems == 0) return;
+
+ int end = position + nItems;
+ int new_gs = (g1 - g0) + nItems;
+ if (end <= g0) {
+ // Move gap to end of block.
+ if (g0 != end) {
+ shiftGap(end);
+ }
+ // Adjust g0.
+ shiftGapStartDown(g0 - nItems);
+ } else if (position >= g0) {
+ // Move gap to beginning of block.
+ if (g0 != position) {
+ shiftGap(position);
+ }
+ // Adjust g1.
+ shiftGapEndUp(g0 + new_gs);
+ } else {
+ // The gap is properly inside the target block.
+ // No data movement necessary, simply move both gap pointers.
+ shiftGapStartDown(position);
+ shiftGapEndUp(g0 + new_gs);
+ }
+ }
+
+ /**
+ * Make space for the given number of items at the given
+ * location.
+ *
+ * @return the location that the caller should fill in
+ */
+ int open(int position, int nItems) {
+ int gapSize = g1 - g0;
+ if (nItems == 0) {
+ if (position > g0)
+ position += gapSize;
+ return position;
+ }
+
+ // Expand the array if the gap is too small.
+ shiftGap(position);
+ if (nItems >= gapSize) {
+ // Pre-shift the gap, to reduce total movement.
+ shiftEnd(getArrayLength() - gapSize + nItems);
+ gapSize = g1 - g0;
+ }
+
+ g0 = g0 + nItems;
+ return position;
+ }
+
+ /**
+ * resize the underlying storage array to the
+ * given new size
+ */
+ void resize(int nsize) {
+ Object narray = allocateArray(nsize);
+ System.arraycopy(array, 0, narray, 0, Math.min(nsize, getArrayLength()));
+ array = narray;
+ }
+
+ /**
+ * Make the gap bigger, moving any necessary data and updating
+ * the appropriate marks
+ */
+ protected void shiftEnd(int newSize) {
+ int oldSize = getArrayLength();
+ int oldGapEnd = g1;
+ int upperSize = oldSize - oldGapEnd;
+ int arrayLength = getNewArraySize(newSize);
+ int newGapEnd = arrayLength - upperSize;
+ resize(arrayLength);
+ g1 = newGapEnd;
+
+ if (upperSize != 0) {
+ // Copy array items to new end of array.
+ System.arraycopy(array, oldGapEnd, array, newGapEnd, upperSize);
+ }
+ }
+
+ /**
+ * Calculates a new size of the storage array depending on required
+ * capacity.
+ * @param reqSize the size which is necessary for new content
+ * @return the new size of the storage array
+ */
+ int getNewArraySize(int reqSize) {
+ return (reqSize + 1) * 2;
+ }
+
+ /**
+ * Move the start of the gap to a new location,
+ * without changing the size of the gap. This
+ * moves the data in the array and updates the
+ * marks accordingly.
+ */
+ protected void shiftGap(int newGapStart) {
+ if (newGapStart == g0) {
+ return;
+ }
+ int oldGapStart = g0;
+ int dg = newGapStart - oldGapStart;
+ int oldGapEnd = g1;
+ int newGapEnd = oldGapEnd + dg;
+ int gapSize = oldGapEnd - oldGapStart;
+
+ g0 = newGapStart;
+ g1 = newGapEnd;
+ if (dg > 0) {
+ // Move gap up, move data down.
+ System.arraycopy(array, oldGapEnd, array, oldGapStart, dg);
+ } else if (dg < 0) {
+ // Move gap down, move data up.
+ System.arraycopy(array, newGapStart, array, newGapEnd, -dg);
+ }
+ }
+
+ /**
+ * Adjust the gap end downward. This doesn't move
+ * any data, but it does update any marks affected
+ * by the boundary change. All marks from the old
+ * gap start down to the new gap start are squeezed
+ * to the end of the gap (their location has been
+ * removed).
+ */
+ protected void shiftGapStartDown(int newGapStart) {
+ g0 = newGapStart;
+ }
+
+ /**
+ * Adjust the gap end upward. This doesn't move
+ * any data, but it does update any marks affected
+ * by the boundary change. All marks from the old
+ * gap end up to the new gap end are squeezed
+ * to the end of the gap (their location has been
+ * removed).
+ */
+ protected void shiftGapEndUp(int newGapEnd) {
+ g1 = newGapEnd;
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/GlyphPainter1.java b/src/share/classes/javax/swing/text/GlyphPainter1.java
new file mode 100644
index 000000000..3d50d2ece
--- /dev/null
+++ b/src/share/classes/javax/swing/text/GlyphPainter1.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 1999-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;
+
+import java.awt.*;
+
+/**
+ * A class to perform rendering of the glyphs.
+ * This can be implemented to be stateless, or
+ * to hold some information as a cache to
+ * facilitate faster rendering and model/view
+ * translation. At a minimum, the GlyphPainter
+ * allows a View implementation to perform its
+ * duties independent of a particular version
+ * of JVM and selection of capabilities (i.e.
+ * shaping for i18n, etc).
+ * <p>
+ * This implementation is intended for operation
+ * under the JDK1.1 API of the Java Platform.
+ * Since the JDK is backward compatible with
+ * JDK1.1 API, this class will also function on
+ * Java 2. The JDK introduces improved
+ * API for rendering text however, so the GlyphPainter2
+ * is recommended for the DK.
+ *
+ * @author Timothy Prinzing
+ * @see GlyphView
+ */
+class GlyphPainter1 extends GlyphView.GlyphPainter {
+
+ /**
+ * Determine the span the glyphs given a start location
+ * (for tab expansion).
+ */
+ public float getSpan(GlyphView v, int p0, int p1,
+ TabExpander e, float x) {
+ sync(v);
+ Segment text = v.getText(p0, p1);
+ int[] justificationData = getJustificationData(v);
+ int width = Utilities.getTabbedTextWidth(v, text, metrics, (int) x, e, p0,
+ justificationData);
+ SegmentCache.releaseSharedSegment(text);
+ return width;
+ }
+
+ public float getHeight(GlyphView v) {
+ sync(v);
+ return metrics.getHeight();
+ }
+
+ /**
+ * Fetches the ascent above the baseline for the glyphs
+ * corresponding to the given range in the model.
+ */
+ public float getAscent(GlyphView v) {
+ sync(v);
+ return metrics.getAscent();
+ }
+
+ /**
+ * Fetches the descent below the baseline for the glyphs
+ * corresponding to the given range in the model.
+ */
+ public float getDescent(GlyphView v) {
+ sync(v);
+ return metrics.getDescent();
+ }
+
+ /**
+ * Paints the glyphs representing the given range.
+ */
+ public void paint(GlyphView v, Graphics g, Shape a, int p0, int p1) {
+ sync(v);
+ Segment text;
+ TabExpander expander = v.getTabExpander();
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
+
+ // determine the x coordinate to render the glyphs
+ int x = alloc.x;
+ int p = v.getStartOffset();
+ int[] justificationData = getJustificationData(v);
+ if (p != p0) {
+ text = v.getText(p, p0);
+ int width = Utilities.getTabbedTextWidth(v, text, metrics, x, expander, p,
+ justificationData);
+ x += width;
+ SegmentCache.releaseSharedSegment(text);
+ }
+
+ // determine the y coordinate to render the glyphs
+ int y = alloc.y + metrics.getHeight() - metrics.getDescent();
+
+ // render the glyphs
+ text = v.getText(p0, p1);
+ g.setFont(metrics.getFont());
+
+ Utilities.drawTabbedText(v, text, x, y, g, expander,p0,
+ justificationData);
+ SegmentCache.releaseSharedSegment(text);
+ }
+
+ public Shape modelToView(GlyphView v, int pos, Position.Bias bias,
+ Shape a) throws BadLocationException {
+
+ sync(v);
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
+ int p0 = v.getStartOffset();
+ int p1 = v.getEndOffset();
+ TabExpander expander = v.getTabExpander();
+ Segment text;
+
+ if(pos == p1) {
+ // The caller of this is left to right and borders a right to
+ // left view, return our end location.
+ return new Rectangle(alloc.x + alloc.width, alloc.y, 0,
+ metrics.getHeight());
+ }
+ if ((pos >= p0) && (pos <= p1)) {
+ // determine range to the left of the position
+ text = v.getText(p0, pos);
+ int[] justificationData = getJustificationData(v);
+ int width = Utilities.getTabbedTextWidth(v, text, metrics, alloc.x, expander, p0,
+ justificationData);
+ SegmentCache.releaseSharedSegment(text);
+ return new Rectangle(alloc.x + width, alloc.y, 0, metrics.getHeight());
+ }
+ throw new BadLocationException("modelToView - can't convert", p1);
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param v the view containing the view coordinates
+ * @param x the X coordinate
+ * @param y the Y coordinate
+ * @param a the allocated region to render into
+ * @param biasReturn always returns <code>Position.Bias.Forward</code>
+ * as the zero-th element of this array
+ * @return the location within the model that best represents the
+ * given point in the view
+ * @see View#viewToModel
+ */
+ public int viewToModel(GlyphView v, float x, float y, Shape a,
+ Position.Bias[] biasReturn) {
+
+ sync(v);
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
+ int p0 = v.getStartOffset();
+ int p1 = v.getEndOffset();
+ TabExpander expander = v.getTabExpander();
+ Segment text = v.getText(p0, p1);
+ int[] justificationData = getJustificationData(v);
+ int offs = Utilities.getTabbedTextOffset(v, text, metrics,
+ alloc.x, (int) x, expander, p0,
+ justificationData);
+ SegmentCache.releaseSharedSegment(text);
+ int retValue = p0 + offs;
+ if(retValue == p1) {
+ // No need to return backward bias as GlyphPainter1 is used for
+ // ltr text only.
+ retValue--;
+ }
+ biasReturn[0] = Position.Bias.Forward;
+ return retValue;
+ }
+
+ /**
+ * Determines the best location (in the model) to break
+ * the given view.
+ * This method attempts to break on a whitespace
+ * location. If a whitespace location can't be found, the
+ * nearest character location is returned.
+ *
+ * @param v the view
+ * @param p0 the location in the model where the
+ * fragment should start its representation >= 0
+ * @param pos the graphic location along the axis that the
+ * broken view would occupy >= 0; this may be useful for
+ * things like tab calculations
+ * @param len specifies the distance into the view
+ * where a potential break is desired >= 0
+ * @return the model location desired for a break
+ * @see View#breakView
+ */
+ public int getBoundedPosition(GlyphView v, int p0, float x, float len) {
+ sync(v);
+ TabExpander expander = v.getTabExpander();
+ Segment s = v.getText(p0, v.getEndOffset());
+ int[] justificationData = getJustificationData(v);
+ int index = Utilities.getTabbedTextOffset(v, s, metrics, (int)x, (int)(x+len),
+ expander, p0, false,
+ justificationData);
+ SegmentCache.releaseSharedSegment(s);
+ int p1 = p0 + index;
+ return p1;
+ }
+
+ void sync(GlyphView v) {
+ Font f = v.getFont();
+ if ((metrics == null) || (! f.equals(metrics.getFont()))) {
+ // fetch a new FontMetrics
+ Container c = v.getContainer();
+ metrics = (c != null) ? c.getFontMetrics(f) :
+ Toolkit.getDefaultToolkit().getFontMetrics(f);
+ }
+ }
+
+
+
+ /**
+ * @return justificationData from the ParagraphRow this GlyphView
+ * is in or {@code null} if no justification is needed
+ */
+ private int[] getJustificationData(GlyphView v) {
+ View parent = v.getParent();
+ int [] ret = null;
+ if (parent instanceof ParagraphView.Row) {
+ ParagraphView.Row row = ((ParagraphView.Row) parent);
+ ret = row.justificationData;
+ }
+ return ret;
+ }
+
+ // --- variables ---------------------------------------------
+
+ FontMetrics metrics;
+}
diff --git a/src/share/classes/javax/swing/text/GlyphPainter2.java b/src/share/classes/javax/swing/text/GlyphPainter2.java
new file mode 100644
index 000000000..6572d6b6e
--- /dev/null
+++ b/src/share/classes/javax/swing/text/GlyphPainter2.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright 1999-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;
+
+import java.text.*;
+import java.awt.*;
+import java.awt.font.*;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * A class to perform rendering of the glyphs.
+ * This can be implemented to be stateless, or
+ * to hold some information as a cache to
+ * facilitate faster rendering and model/view
+ * translation. At a minimum, the GlyphPainter
+ * allows a View implementation to perform its
+ * duties independent of a particular version
+ * of JVM and selection of capabilities (i.e.
+ * shaping for i18n, etc).
+ * <p>
+ * This implementation is intended for operation
+ * under the JDK. It uses the
+ * java.awt.font.TextLayout class to do i18n capable
+ * rendering.
+ *
+ * @author Timothy Prinzing
+ * @see GlyphView
+ */
+class GlyphPainter2 extends GlyphView.GlyphPainter {
+
+ public GlyphPainter2(TextLayout layout) {
+ this.layout = layout;
+ }
+
+ /**
+ * Create a painter to use for the given GlyphView.
+ */
+ public GlyphView.GlyphPainter getPainter(GlyphView v, int p0, int p1) {
+ return null;
+ }
+
+ /**
+ * Determine the span the glyphs given a start location
+ * (for tab expansion). This implementation assumes it
+ * has no tabs (i.e. TextLayout doesn't deal with tab
+ * expansion).
+ */
+ public float getSpan(GlyphView v, int p0, int p1,
+ TabExpander e, float x) {
+
+ if ((p0 == v.getStartOffset()) && (p1 == v.getEndOffset())) {
+ return layout.getAdvance();
+ }
+ int p = v.getStartOffset();
+ int index0 = p0 - p;
+ int index1 = p1 - p;
+
+ TextHitInfo hit0 = TextHitInfo.afterOffset(index0);
+ TextHitInfo hit1 = TextHitInfo.beforeOffset(index1);
+ float[] locs = layout.getCaretInfo(hit0);
+ float x0 = locs[0];
+ locs = layout.getCaretInfo(hit1);
+ float x1 = locs[0];
+ return (x1 > x0) ? x1 - x0 : x0 - x1;
+ }
+
+ public float getHeight(GlyphView v) {
+ return layout.getAscent() + layout.getDescent() + layout.getLeading();
+ }
+
+ /**
+ * Fetch the ascent above the baseline for the glyphs
+ * corresponding to the given range in the model.
+ */
+ public float getAscent(GlyphView v) {
+ return layout.getAscent();
+ }
+
+ /**
+ * Fetch the descent below the baseline for the glyphs
+ * corresponding to the given range in the model.
+ */
+ public float getDescent(GlyphView v) {
+ return layout.getDescent();
+ }
+
+ /**
+ * Paint the glyphs for the given view. This is implemented
+ * to only render if the Graphics is of type Graphics2D which
+ * is required by TextLayout (and this should be the case if
+ * running on the JDK).
+ */
+ public void paint(GlyphView v, Graphics g, Shape a, int p0, int p1) {
+ if (g instanceof Graphics2D) {
+ Rectangle2D alloc = a.getBounds2D();
+ Graphics2D g2d = (Graphics2D)g;
+ float y = (float) alloc.getY() + layout.getAscent() + layout.getLeading();
+ float x = (float) alloc.getX();
+ if( p0 > v.getStartOffset() || p1 < v.getEndOffset() ) {
+ try {
+ //TextLayout can't render only part of it's range, so if a
+ //partial range is required, add a clip region.
+ Shape s = v.modelToView(p0, Position.Bias.Forward,
+ p1, Position.Bias.Backward, a);
+ Shape savedClip = g.getClip();
+ g2d.clip(s);
+ layout.draw(g2d, x, y);
+ g.setClip(savedClip);
+ } catch (BadLocationException e) {}
+ } else {
+ layout.draw(g2d, x, y);
+ }
+ }
+ }
+
+ public Shape modelToView(GlyphView v, int pos, Position.Bias bias,
+ Shape a) throws BadLocationException {
+ int offs = pos - v.getStartOffset();
+ Rectangle2D alloc = a.getBounds2D();
+ TextHitInfo hit = (bias == Position.Bias.Forward) ?
+ TextHitInfo.afterOffset(offs) : TextHitInfo.beforeOffset(offs);
+ float[] locs = layout.getCaretInfo(hit);
+
+ // vertical at the baseline, should use slope and check if glyphs
+ // are being rendered vertically.
+ alloc.setRect(alloc.getX() + locs[0], alloc.getY(), 1, alloc.getHeight());
+ return alloc;
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param v the view containing the view coordinates
+ * @param x the X coordinate
+ * @param y the Y coordinate
+ * @param a the allocated region to render into
+ * @param biasReturn either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code> is returned as the
+ * zero-th element of this array
+ * @return the location within the model that best represents the
+ * given point of view
+ * @see View#viewToModel
+ */
+ public int viewToModel(GlyphView v, float x, float y, Shape a,
+ Position.Bias[] biasReturn) {
+
+ Rectangle2D alloc = (a instanceof Rectangle2D) ? (Rectangle2D)a : a.getBounds2D();
+ //Move the y co-ord of the hit onto the baseline. This is because TextLayout supports
+ //italic carets and we do not.
+ TextHitInfo hit = layout.hitTestChar(x - (float)alloc.getX(), 0);
+ int pos = hit.getInsertionIndex();
+ biasReturn[0] = hit.isLeadingEdge() ? Position.Bias.Forward : Position.Bias.Backward;
+ return pos + v.getStartOffset();
+ }
+
+ /**
+ * Determines the model location that represents the
+ * maximum advance that fits within the given span.
+ * This could be used to break the given view. The result
+ * should be a location just shy of the given advance. This
+ * differs from viewToModel which returns the closest
+ * position which might be proud of the maximum advance.
+ *
+ * @param v the view to find the model location to break at.
+ * @param p0 the location in the model where the
+ * fragment should start it's representation >= 0.
+ * @param pos the graphic location along the axis that the
+ * broken view would occupy >= 0. This may be useful for
+ * things like tab calculations.
+ * @param len specifies the distance into the view
+ * where a potential break is desired >= 0.
+ * @return the maximum model location possible for a break.
+ * @see View#breakView
+ */
+ public int getBoundedPosition(GlyphView v, int p0, float x, float len) {
+ if( len < 0 )
+ throw new IllegalArgumentException("Length must be >= 0.");
+ // note: this only works because swing uses TextLayouts that are
+ // only pure rtl or pure ltr
+ TextHitInfo hit;
+ if (layout.isLeftToRight()) {
+ hit = layout.hitTestChar(len, 0);
+ } else {
+ hit = layout.hitTestChar(layout.getAdvance() - len, 0);
+ }
+ return v.getStartOffset() + hit.getCharIndex();
+ }
+
+ /**
+ * Provides a way to determine the next visually represented model
+ * location that one might place a caret. Some views may not be
+ * visible, they might not be in the same order found in the model, or
+ * they just might not allow access to some of the locations in the
+ * model.
+ *
+ * @param v the view to use
+ * @param pos the position to convert >= 0
+ * @param a the allocated region to render into
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard.
+ * This may be SwingConstants.WEST, SwingConstants.EAST,
+ * SwingConstants.NORTH, or SwingConstants.SOUTH.
+ * @return the location within the model that best represents the next
+ * location visual position.
+ * @exception BadLocationException
+ * @exception IllegalArgumentException for an invalid direction
+ */
+ public int getNextVisualPositionFrom(GlyphView v, int pos,
+ Position.Bias b, Shape a,
+ int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+
+ int startOffset = v.getStartOffset();
+ int endOffset = v.getEndOffset();
+ Segment text;
+ AbstractDocument doc;
+ boolean viewIsLeftToRight;
+ TextHitInfo currentHit, nextHit;
+
+ switch (direction) {
+ case View.NORTH:
+ break;
+ case View.SOUTH:
+ break;
+ case View.EAST:
+ doc = (AbstractDocument)v.getDocument();
+ viewIsLeftToRight = doc.isLeftToRight(startOffset, endOffset);
+
+ if(startOffset == doc.getLength()) {
+ if(pos == -1) {
+ biasRet[0] = Position.Bias.Forward;
+ return startOffset;
+ }
+ // End case for bidi text where newline is at beginning
+ // of line.
+ return -1;
+ }
+ if(pos == -1) {
+ // Entering view from the left.
+ if( viewIsLeftToRight ) {
+ biasRet[0] = Position.Bias.Forward;
+ return startOffset;
+ } else {
+ text = v.getText(endOffset - 1, endOffset);
+ char c = text.array[text.offset];
+ SegmentCache.releaseSharedSegment(text);
+ if(c == '\n') {
+ biasRet[0] = Position.Bias.Forward;
+ return endOffset-1;
+ }
+ biasRet[0] = Position.Bias.Backward;
+ return endOffset;
+ }
+ }
+ if( b==Position.Bias.Forward )
+ currentHit = TextHitInfo.afterOffset(pos-startOffset);
+ else
+ currentHit = TextHitInfo.beforeOffset(pos-startOffset);
+ nextHit = layout.getNextRightHit(currentHit);
+ if( nextHit == null ) {
+ return -1;
+ }
+ if( viewIsLeftToRight != layout.isLeftToRight() ) {
+ // If the layout's base direction is different from
+ // this view's run direction, we need to use the weak
+ // carrat.
+ nextHit = layout.getVisualOtherHit(nextHit);
+ }
+ pos = nextHit.getInsertionIndex() + startOffset;
+
+ if(pos == endOffset) {
+ // A move to the right from an internal position will
+ // only take us to the endOffset in a left to right run.
+ text = v.getText(endOffset - 1, endOffset);
+ char c = text.array[text.offset];
+ SegmentCache.releaseSharedSegment(text);
+ if(c == '\n') {
+ return -1;
+ }
+ biasRet[0] = Position.Bias.Backward;
+ }
+ else {
+ biasRet[0] = Position.Bias.Forward;
+ }
+ return pos;
+ case View.WEST:
+ doc = (AbstractDocument)v.getDocument();
+ viewIsLeftToRight = doc.isLeftToRight(startOffset, endOffset);
+
+ if(startOffset == doc.getLength()) {
+ if(pos == -1) {
+ biasRet[0] = Position.Bias.Forward;
+ return startOffset;
+ }
+ // End case for bidi text where newline is at beginning
+ // of line.
+ return -1;
+ }
+ if(pos == -1) {
+ // Entering view from the right
+ if( viewIsLeftToRight ) {
+ text = v.getText(endOffset - 1, endOffset);
+ char c = text.array[text.offset];
+ SegmentCache.releaseSharedSegment(text);
+ if ((c == '\n') || Character.isSpaceChar(c)) {
+ biasRet[0] = Position.Bias.Forward;
+ return endOffset - 1;
+ }
+ biasRet[0] = Position.Bias.Backward;
+ return endOffset;
+ } else {
+ biasRet[0] = Position.Bias.Forward;
+ return startOffset;
+ }
+ }
+ if( b==Position.Bias.Forward )
+ currentHit = TextHitInfo.afterOffset(pos-startOffset);
+ else
+ currentHit = TextHitInfo.beforeOffset(pos-startOffset);
+ nextHit = layout.getNextLeftHit(currentHit);
+ if( nextHit == null ) {
+ return -1;
+ }
+ if( viewIsLeftToRight != layout.isLeftToRight() ) {
+ // If the layout's base direction is different from
+ // this view's run direction, we need to use the weak
+ // carrat.
+ nextHit = layout.getVisualOtherHit(nextHit);
+ }
+ pos = nextHit.getInsertionIndex() + startOffset;
+
+ if(pos == endOffset) {
+ // A move to the left from an internal position will
+ // only take us to the endOffset in a right to left run.
+ text = v.getText(endOffset - 1, endOffset);
+ char c = text.array[text.offset];
+ SegmentCache.releaseSharedSegment(text);
+ if(c == '\n') {
+ return -1;
+ }
+ biasRet[0] = Position.Bias.Backward;
+ }
+ else {
+ biasRet[0] = Position.Bias.Forward;
+ }
+ return pos;
+ default:
+ throw new IllegalArgumentException("Bad direction: " + direction);
+ }
+ return pos;
+
+ }
+ // --- variables ---------------------------------------------
+
+ TextLayout layout;
+
+}
diff --git a/src/share/classes/javax/swing/text/GlyphView.java b/src/share/classes/javax/swing/text/GlyphView.java
new file mode 100644
index 000000000..3cb4efe59
--- /dev/null
+++ b/src/share/classes/javax/swing/text/GlyphView.java
@@ -0,0 +1,1334 @@
+/*
+ * Copyright 1999-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;
+
+import java.awt.*;
+import java.text.BreakIterator;
+import javax.swing.event.*;
+import java.util.BitSet;
+import java.util.Locale;
+
+import sun.swing.SwingUtilities2;
+
+/**
+ * A GlyphView is a styled chunk of text that represents a view
+ * mapped over an element in the text model. This view is generally
+ * responsible for displaying text glyphs using character level
+ * attributes in some way.
+ * An implementation of the GlyphPainter class is used to do the
+ * actual rendering and model/view translations. This separates
+ * rendering from layout and management of the association with
+ * the model.
+ * <p>
+ * The view supports breaking for the purpose of formatting.
+ * The fragments produced by breaking share the view that has
+ * primary responsibility for the element (i.e. they are nested
+ * classes and carry only a small amount of state of their own)
+ * so they can share its resources.
+ * <p>
+ * Since this view
+ * represents text that may have tabs embedded in it, it implements the
+ * <code>TabableView</code> interface. Tabs will only be
+ * expanded if this view is embedded in a container that does
+ * tab expansion. ParagraphView is an example of a container
+ * that does tab expansion.
+ * <p>
+ *
+ * @since 1.3
+ *
+ * @author Timothy Prinzing
+ */
+public class GlyphView extends View implements TabableView, Cloneable {
+
+ /**
+ * Constructs a new view wrapped on an element.
+ *
+ * @param elem the element
+ */
+ public GlyphView(Element elem) {
+ super(elem);
+ offset = 0;
+ length = 0;
+ Element parent = elem.getParentElement();
+ AttributeSet attr = elem.getAttributes();
+
+ // if there was an implied CR
+ impliedCR = (attr != null && attr.getAttribute(IMPLIED_CR) != null &&
+ // if this is non-empty paragraph
+ parent != null && parent.getElementCount() > 1);
+ skipWidth = elem.getName().equals("br");
+ }
+
+ /**
+ * Creates a shallow copy. This is used by the
+ * createFragment and breakView methods.
+ *
+ * @return the copy
+ */
+ protected final Object clone() {
+ Object o;
+ try {
+ o = super.clone();
+ } catch (CloneNotSupportedException cnse) {
+ o = null;
+ }
+ return o;
+ }
+
+ /**
+ * Fetch the currently installed glyph painter.
+ * If a painter has not yet been installed, and
+ * a default was not yet needed, null is returned.
+ */
+ public GlyphPainter getGlyphPainter() {
+ return painter;
+ }
+
+ /**
+ * Sets the painter to use for rendering glyphs.
+ */
+ public void setGlyphPainter(GlyphPainter p) {
+ painter = p;
+ }
+
+ /**
+ * Fetch a reference to the text that occupies
+ * the given range. This is normally used by
+ * the GlyphPainter to determine what characters
+ * it should render glyphs for.
+ *
+ * @param p0 the starting document offset >= 0
+ * @param p1 the ending document offset >= p0
+ * @return the <code>Segment</code> containing the text
+ */
+ public Segment getText(int p0, int p1) {
+ // When done with the returned Segment it should be released by
+ // invoking:
+ // SegmentCache.releaseSharedSegment(segment);
+ Segment text = SegmentCache.getSharedSegment();
+ try {
+ Document doc = getDocument();
+ doc.getText(p0, p1 - p0, text);
+ } catch (BadLocationException bl) {
+ throw new StateInvariantError("GlyphView: Stale view: " + bl);
+ }
+ return text;
+ }
+
+ /**
+ * Fetch the background color to use to render the
+ * glyphs. If there is no background color, null should
+ * be returned. This is implemented to call
+ * <code>StyledDocument.getBackground</code> if the associated
+ * document is a styled document, otherwise it returns null.
+ */
+ public Color getBackground() {
+ Document doc = getDocument();
+ if (doc instanceof StyledDocument) {
+ AttributeSet attr = getAttributes();
+ if (attr.isDefined(StyleConstants.Background)) {
+ return ((StyledDocument)doc).getBackground(attr);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Fetch the foreground color to use to render the
+ * glyphs. If there is no foreground color, null should
+ * be returned. This is implemented to call
+ * <code>StyledDocument.getBackground</code> if the associated
+ * document is a StyledDocument. If the associated document
+ * is not a StyledDocument, the associated components foreground
+ * color is used. If there is no associated component, null
+ * is returned.
+ */
+ public Color getForeground() {
+ Document doc = getDocument();
+ if (doc instanceof StyledDocument) {
+ AttributeSet attr = getAttributes();
+ return ((StyledDocument)doc).getForeground(attr);
+ }
+ Component c = getContainer();
+ if (c != null) {
+ return c.getForeground();
+ }
+ return null;
+ }
+
+ /**
+ * Fetch the font that the glyphs should be based
+ * upon. This is implemented to call
+ * <code>StyledDocument.getFont</code> if the associated
+ * document is a StyledDocument. If the associated document
+ * is not a StyledDocument, the associated components font
+ * is used. If there is no associated component, null
+ * is returned.
+ */
+ public Font getFont() {
+ Document doc = getDocument();
+ if (doc instanceof StyledDocument) {
+ AttributeSet attr = getAttributes();
+ return ((StyledDocument)doc).getFont(attr);
+ }
+ Component c = getContainer();
+ if (c != null) {
+ return c.getFont();
+ }
+ return null;
+ }
+
+ /**
+ * Determine if the glyphs should be underlined. If true,
+ * an underline should be drawn through the baseline.
+ */
+ public boolean isUnderline() {
+ AttributeSet attr = getAttributes();
+ return StyleConstants.isUnderline(attr);
+ }
+
+ /**
+ * Determine if the glyphs should have a strikethrough
+ * line. If true, a line should be drawn through the center
+ * of the glyphs.
+ */
+ public boolean isStrikeThrough() {
+ AttributeSet attr = getAttributes();
+ return StyleConstants.isStrikeThrough(attr);
+ }
+
+ /**
+ * Determine if the glyphs should be rendered as superscript.
+ */
+ public boolean isSubscript() {
+ AttributeSet attr = getAttributes();
+ return StyleConstants.isSubscript(attr);
+ }
+
+ /**
+ * Determine if the glyphs should be rendered as subscript.
+ */
+ public boolean isSuperscript() {
+ AttributeSet attr = getAttributes();
+ return StyleConstants.isSuperscript(attr);
+ }
+
+ /**
+ * Fetch the TabExpander to use if tabs are present in this view.
+ */
+ public TabExpander getTabExpander() {
+ return expander;
+ }
+
+ /**
+ * Check to see that a glyph painter exists. If a painter
+ * doesn't exist, a default glyph painter will be installed.
+ */
+ protected void checkPainter() {
+ if (painter == null) {
+ if (defaultPainter == null) {
+ // the classname should probably come from a property file.
+ String classname = "javax.swing.text.GlyphPainter1";
+ try {
+ Class c;
+ ClassLoader loader = getClass().getClassLoader();
+ if (loader != null) {
+ c = loader.loadClass(classname);
+ } else {
+ c = Class.forName(classname);
+ }
+ Object o = c.newInstance();
+ if (o instanceof GlyphPainter) {
+ defaultPainter = (GlyphPainter) o;
+ }
+ } catch (Throwable e) {
+ throw new StateInvariantError("GlyphView: Can't load glyph painter: "
+ + classname);
+ }
+ }
+ setGlyphPainter(defaultPainter.getPainter(this, getStartOffset(),
+ getEndOffset()));
+ }
+ }
+
+ // --- TabableView methods --------------------------------------
+
+ /**
+ * Determines the desired span when using the given
+ * tab expansion implementation.
+ *
+ * @param x the position the view would be located
+ * at for the purpose of tab expansion >= 0.
+ * @param e how to expand the tabs when encountered.
+ * @return the desired span >= 0
+ * @see TabableView#getTabbedSpan
+ */
+ public float getTabbedSpan(float x, TabExpander e) {
+ checkPainter();
+
+ TabExpander old = expander;
+ expander = e;
+
+ if (expander != old) {
+ // setting expander can change horizontal span of the view,
+ // so we have to call preferenceChanged()
+ preferenceChanged(null, true, false);
+ }
+
+ this.x = (int) x;
+ int p0 = getStartOffset();
+ int p1 = getEndOffset();
+ float width = painter.getSpan(this, p0, p1, expander, x);
+ return width;
+ }
+
+ /**
+ * Determines the span along the same axis as tab
+ * expansion for a portion of the view. This is
+ * intended for use by the TabExpander for cases
+ * where the tab expansion involves aligning the
+ * portion of text that doesn't have whitespace
+ * relative to the tab stop. There is therefore
+ * an assumption that the range given does not
+ * contain tabs.
+ * <p>
+ * This method can be called while servicing the
+ * getTabbedSpan or getPreferredSize. It has to
+ * arrange for its own text buffer to make the
+ * measurements.
+ *
+ * @param p0 the starting document offset >= 0
+ * @param p1 the ending document offset >= p0
+ * @return the span >= 0
+ */
+ public float getPartialSpan(int p0, int p1) {
+ checkPainter();
+ float width = painter.getSpan(this, p0, p1, expander, x);
+ return width;
+ }
+
+ // --- View methods ---------------------------------------------
+
+ /**
+ * Fetches the portion of the model that this view is responsible for.
+ *
+ * @return the starting offset into the model
+ * @see View#getStartOffset
+ */
+ public int getStartOffset() {
+ Element e = getElement();
+ return (length > 0) ? e.getStartOffset() + offset : e.getStartOffset();
+ }
+
+ /**
+ * Fetches the portion of the model that this view is responsible for.
+ *
+ * @return the ending offset into the model
+ * @see View#getEndOffset
+ */
+ public int getEndOffset() {
+ Element e = getElement();
+ return (length > 0) ? e.getStartOffset() + offset + length : e.getEndOffset();
+ }
+
+ /**
+ * Lazily initializes the selections field
+ */
+ private void initSelections(int p0, int p1) {
+ int viewPosCount = p1 - p0 + 1;
+ if (selections == null || viewPosCount > selections.length) {
+ selections = new byte[viewPosCount];
+ return;
+ }
+ for (int i = 0; i < viewPosCount; selections[i++] = 0);
+ }
+
+ /**
+ * Renders a portion of a text style run.
+ *
+ * @param g the rendering surface to use
+ * @param a the allocated region to render into
+ */
+ public void paint(Graphics g, Shape a) {
+ checkPainter();
+
+ boolean paintedText = false;
+ Component c = getContainer();
+ int p0 = getStartOffset();
+ int p1 = getEndOffset();
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
+ Color bg = getBackground();
+ Color fg = getForeground();
+
+ if (c instanceof JTextComponent) {
+ JTextComponent tc = (JTextComponent) c;
+ if (!tc.isEnabled()) {
+ fg = tc.getDisabledTextColor();
+ }
+ }
+ if (bg != null) {
+ g.setColor(bg);
+ g.fillRect(alloc.x, alloc.y, alloc.width, alloc.height);
+ }
+ if (c instanceof JTextComponent) {
+ JTextComponent tc = (JTextComponent) c;
+ Highlighter h = tc.getHighlighter();
+ if (h instanceof LayeredHighlighter) {
+ ((LayeredHighlighter)h).paintLayeredHighlights
+ (g, p0, p1, a, tc, this);
+ }
+ }
+
+ if (Utilities.isComposedTextElement(getElement())) {
+ Utilities.paintComposedText(g, a.getBounds(), this);
+ paintedText = true;
+ } else if(c instanceof JTextComponent) {
+ JTextComponent tc = (JTextComponent) c;
+ Color selFG = tc.getSelectedTextColor();
+
+ if (// there's a highlighter (bug 4532590), and
+ (tc.getHighlighter() != null) &&
+ // selected text color is different from regular foreground
+ (selFG != null) && !selFG.equals(fg)) {
+
+ Highlighter.Highlight[] h = tc.getHighlighter().getHighlights();
+ if(h.length != 0) {
+ boolean initialized = false;
+ int viewSelectionCount = 0;
+ for (int i = 0; i < h.length; i++) {
+ Highlighter.Highlight highlight = h[i];
+ int hStart = highlight.getStartOffset();
+ int hEnd = highlight.getEndOffset();
+ if (hStart > p1 || hEnd < p0) {
+ // the selection is out of this view
+ continue;
+ }
+ if (!SwingUtilities2.useSelectedTextColor(highlight, tc)) {
+ continue;
+ }
+ if (hStart <= p0 && hEnd >= p1){
+ // the whole view is selected
+ paintTextUsingColor(g, a, selFG, p0, p1);
+ paintedText = true;
+ break;
+ }
+ // the array is lazily created only when the view
+ // is partially selected
+ if (!initialized) {
+ initSelections(p0, p1);
+ initialized = true;
+ }
+ hStart = Math.max(p0, hStart);
+ hEnd = Math.min(p1, hEnd);
+ paintTextUsingColor(g, a, selFG, hStart, hEnd);
+ // the array represents view positions [0, p1-p0+1]
+ // later will iterate this array and sum its
+ // elements. Positions with sum == 0 are not selected.
+ selections[hStart-p0]++;
+ selections[hEnd-p0]--;
+
+ viewSelectionCount++;
+ }
+
+ if (!paintedText && viewSelectionCount > 0) {
+ // the view is partially selected
+ int curPos = -1;
+ int startPos = 0;
+ int viewLen = p1 - p0;
+ while (curPos++ < viewLen) {
+ // searching for the next selection start
+ while(curPos < viewLen &&
+ selections[curPos] == 0) curPos++;
+ if (startPos != curPos) {
+ // paint unselected text
+ paintTextUsingColor(g, a, fg,
+ p0 + startPos, p0 + curPos);
+ }
+ int checkSum = 0;
+ // searching for next start position of unselected text
+ while (curPos < viewLen &&
+ (checkSum += selections[curPos]) != 0) curPos++;
+ startPos = curPos;
+ }
+ paintedText = true;
+ }
+ }
+ }
+ }
+ if(!paintedText)
+ paintTextUsingColor(g, a, fg, p0, p1);
+ }
+
+ /**
+ * Paints the specified region of text in the specified color.
+ */
+ final void paintTextUsingColor(Graphics g, Shape a, Color c, int p0, int p1) {
+ // render the glyphs
+ g.setColor(c);
+ painter.paint(this, g, a, p0, p1);
+
+ // render underline or strikethrough if set.
+ boolean underline = isUnderline();
+ boolean strike = isStrikeThrough();
+ if (underline || strike) {
+ // calculate x coordinates
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
+ View parent = getParent();
+ if ((parent != null) && (parent.getEndOffset() == p1)) {
+ // strip whitespace on end
+ Segment s = getText(p0, p1);
+ while (Character.isWhitespace(s.last())) {
+ p1 -= 1;
+ s.count -= 1;
+ }
+ SegmentCache.releaseSharedSegment(s);
+ }
+ int x0 = alloc.x;
+ int p = getStartOffset();
+ if (p != p0) {
+ x0 += (int) painter.getSpan(this, p, p0, getTabExpander(), x0);
+ }
+ int x1 = x0 + (int) painter.getSpan(this, p0, p1, getTabExpander(), x0);
+
+ // calculate y coordinate
+ int y = alloc.y + alloc.height - (int) painter.getDescent(this);
+ if (underline) {
+ int yTmp = y + 1;
+ g.drawLine(x0, yTmp, x1, yTmp);
+ }
+ if (strike) {
+ // move y coordinate above baseline
+ int yTmp = y - (int) (painter.getAscent(this) * 0.3f);
+ g.drawLine(x0, yTmp, x1, yTmp);
+ }
+
+ }
+ }
+
+ /**
+ * Determines the minimum span for this view along an axis.
+ *
+ * <p>This implementation returns the longest non-breakable area within
+ * the view as a minimum span for {@code View.X_AXIS}.</p>
+ *
+ * @param axis may be either {@code View.X_AXIS} or {@code View.Y_AXIS}
+ * @return the minimum span the view can be rendered into
+ * @throws IllegalArgumentException if the {@code axis} parameter is invalid
+ * @see javax.swing.text.View#getMinimumSpan
+ */
+ @Override
+ public float getMinimumSpan(int axis) {
+ switch (axis) {
+ case View.X_AXIS:
+ if (minimumSpan < 0) {
+ minimumSpan = 0;
+ int p0 = getStartOffset();
+ int p1 = getEndOffset();
+ while (p1 > p0) {
+ int breakSpot = getBreakSpot(p0, p1);
+ if (breakSpot == BreakIterator.DONE) {
+ // the rest of the view is non-breakable
+ breakSpot = p0;
+ }
+ minimumSpan = Math.max(minimumSpan,
+ getPartialSpan(breakSpot, p1));
+ // Note: getBreakSpot returns the *last* breakspot
+ p1 = breakSpot - 1;
+ }
+ }
+ return minimumSpan;
+ case View.Y_AXIS:
+ return super.getMinimumSpan(axis);
+ default:
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public float getPreferredSpan(int axis) {
+ if (impliedCR) {
+ return 0;
+ }
+ checkPainter();
+ int p0 = getStartOffset();
+ int p1 = getEndOffset();
+ switch (axis) {
+ case View.X_AXIS:
+ if (skipWidth) {
+ return 0;
+ }
+ return painter.getSpan(this, p0, p1, expander, this.x);
+ case View.Y_AXIS:
+ float h = painter.getHeight(this);
+ if (isSuperscript()) {
+ h += h/3;
+ }
+ return h;
+ default:
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ }
+
+ /**
+ * Determines the desired alignment for this view along an
+ * axis. For the label, the alignment is along the font
+ * baseline for the y axis, and the superclasses alignment
+ * along the x axis.
+ *
+ * @param axis may be either View.X_AXIS or View.Y_AXIS
+ * @return the desired alignment. This should be a value
+ * between 0.0 and 1.0 inclusive, 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) {
+ checkPainter();
+ if (axis == View.Y_AXIS) {
+ boolean sup = isSuperscript();
+ boolean sub = isSubscript();
+ float h = painter.getHeight(this);
+ float d = painter.getDescent(this);
+ float a = painter.getAscent(this);
+ float align;
+ if (sup) {
+ align = 1.0f;
+ } else if (sub) {
+ align = (h > 0) ? (h - (d + (a / 2))) / h : 0;
+ } else {
+ align = (h > 0) ? (h - d) / h : 0;
+ }
+ return align;
+ }
+ 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 >= 0
+ * @param a the allocated region to render into
+ * @param b either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * @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 {
+ checkPainter();
+ return painter.modelToView(this, pos, b, a);
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param a the allocated region to render into
+ * @param biasReturn either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code> is returned as the
+ * zero-th element of this array
+ * @return the location within the model that best represents the
+ * given point of view >= 0
+ * @see View#viewToModel
+ */
+ public int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn) {
+ checkPainter();
+ return painter.viewToModel(this, x, y, a, biasReturn);
+ }
+
+ /**
+ * Determines how attractive a break opportunity in
+ * this view is. This can be used for determining which
+ * view is the most attractive to call <code>breakView</code>
+ * on in the process of formatting. The
+ * higher the weight, the more attractive the break. A
+ * value equal to or lower than <code>View.BadBreakWeight</code>
+ * should not be considered for a break. A value greater
+ * than or equal to <code>View.ForcedBreakWeight</code> should
+ * be broken.
+ * <p>
+ * This is implemented to forward to the superclass for
+ * the Y_AXIS. Along the X_AXIS the following values
+ * may be returned.
+ * <dl>
+ * <dt><b>View.ExcellentBreakWeight</b>
+ * <dd>if there is whitespace proceeding the desired break
+ * location.
+ * <dt><b>View.BadBreakWeight</b>
+ * <dd>if the desired break location results in a break
+ * location of the starting offset.
+ * <dt><b>View.GoodBreakWeight</b>
+ * <dd>if the other conditions don't occur.
+ * </dl>
+ * This will normally result in the behavior of breaking
+ * on a whitespace location if one can be found, otherwise
+ * breaking between characters.
+ *
+ * @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 <em>pos</em>
+ * where a potential break is desired >= 0.
+ * @return the weight, which should be a value between
+ * View.ForcedBreakWeight and View.BadBreakWeight.
+ * @see LabelView
+ * @see ParagraphView
+ * @see View#BadBreakWeight
+ * @see View#GoodBreakWeight
+ * @see View#ExcellentBreakWeight
+ * @see View#ForcedBreakWeight
+ */
+ public int getBreakWeight(int axis, float pos, float len) {
+ if (axis == View.X_AXIS) {
+ checkPainter();
+ int p0 = getStartOffset();
+ int p1 = painter.getBoundedPosition(this, p0, pos, len);
+ return ((p1 > p0) && (getBreakSpot(p0, p1) != BreakIterator.DONE)) ?
+ View.ExcellentBreakWeight : View.BadBreakWeight;
+ }
+ return super.getBreakWeight(axis, pos, len);
+ }
+
+ /**
+ * Breaks this view on the given axis at the given length.
+ * This is implemented to attempt to break on a whitespace
+ * location, and returns a fragment with the whitespace at
+ * the end. If a whitespace location can't be found, the
+ * nearest character is used.
+ *
+ * @param axis may be either View.X_AXIS or View.Y_AXIS
+ * @param p0 the location in the model where the
+ * fragment should start it's representation >= 0.
+ * @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, if the view can be broken. If the view
+ * doesn't support breaking behavior, the view itself is
+ * returned.
+ * @see View#breakView
+ */
+ public View breakView(int axis, int p0, float pos, float len) {
+ if (axis == View.X_AXIS) {
+ checkPainter();
+ int p1 = painter.getBoundedPosition(this, p0, pos, len);
+ int breakSpot = getBreakSpot(p0, p1);
+
+ if (breakSpot != -1) {
+ p1 = breakSpot;
+ }
+ // else, no break in the region, return a fragment of the
+ // bounded region.
+ if (p0 == getStartOffset() && p1 == getEndOffset()) {
+ return this;
+ }
+ GlyphView v = (GlyphView) createFragment(p0, p1);
+ v.x = (int) pos;
+ return v;
+ }
+ return this;
+ }
+
+ /**
+ * Returns a location to break at in the passed in region, or
+ * BreakIterator.DONE if there isn't a good location to break at
+ * in the specified region.
+ */
+ private int getBreakSpot(int p0, int p1) {
+ if (breakSpots == null) {
+ // Re-calculate breakpoints for the whole view
+ int start = getStartOffset();
+ int end = getEndOffset();
+ int[] bs = new int[end + 1 - start];
+ int ix = 0;
+
+ // Breaker should work on the parent element because there may be
+ // a valid breakpoint at the end edge of the view (space, etc.)
+ Element parent = getElement().getParentElement();
+ int pstart = (parent == null ? start : parent.getStartOffset());
+ int pend = (parent == null ? end : parent.getEndOffset());
+
+ Segment s = getText(pstart, pend);
+ s.first();
+ BreakIterator breaker = getBreaker();
+ breaker.setText(s);
+
+ // Backward search should start from end+1 unless there's NO end+1
+ int startFrom = end + (pend > end ? 1 : 0);
+ for (;;) {
+ startFrom = breaker.preceding(s.offset + (startFrom - pstart))
+ + (pstart - s.offset);
+ if (startFrom > start) {
+ // The break spot is within the view
+ bs[ix++] = startFrom;
+ } else {
+ break;
+ }
+ }
+
+ SegmentCache.releaseSharedSegment(s);
+ breakSpots = new int[ix];
+ System.arraycopy(bs, 0, breakSpots, 0, ix);
+ }
+
+ int breakSpot = BreakIterator.DONE;
+ for (int i = 0; i < breakSpots.length; i++) {
+ int bsp = breakSpots[i];
+ if (bsp <= p1) {
+ if (bsp > p0) {
+ breakSpot = bsp;
+ }
+ break;
+ }
+ }
+ return breakSpot;
+ }
+
+ /**
+ * Return break iterator appropriate for the current document.
+ *
+ * For non-i18n documents a fast whitespace-based break iterator is used.
+ */
+ private BreakIterator getBreaker() {
+ Document doc = getDocument();
+ if ((doc != null) && Boolean.TRUE.equals(
+ doc.getProperty(AbstractDocument.MultiByteProperty))) {
+ Container c = getContainer();
+ Locale locale = (c == null ? Locale.getDefault() : c.getLocale());
+ return BreakIterator.getLineInstance(locale);
+ } else {
+ return new WhitespaceBasedBreakIterator();
+ }
+ }
+
+ /**
+ * Creates a view that represents a portion of the element.
+ * This is potentially useful during formatting operations
+ * for taking measurements of fragments of the view. If
+ * the view doesn't support fragmenting (the default), it
+ * should return itself.
+ * <p>
+ * This view does support fragmenting. It is implemented
+ * to return a nested class that shares state in this view
+ * representing only a portion of the view.
+ *
+ * @param p0 the starting offset >= 0. This should be a value
+ * greater or equal to the element starting offset and
+ * less than the element ending offset.
+ * @param p1 the ending offset > p0. This should be a value
+ * less than or equal to the elements end offset and
+ * greater than the elements starting offset.
+ * @return the view fragment, or itself if the view doesn't
+ * support breaking into fragments
+ * @see LabelView
+ */
+ public View createFragment(int p0, int p1) {
+ checkPainter();
+ Element elem = getElement();
+ GlyphView v = (GlyphView) clone();
+ v.offset = p0 - elem.getStartOffset();
+ v.length = p1 - p0;
+ v.painter = painter.getPainter(v, p0, p1);
+ v.justificationInfo = null;
+ return v;
+ }
+
+ /**
+ * Provides a way to determine the next visually represented model
+ * location that one might place a caret. Some views may not be
+ * visible, they might not be in the same order found in the model, or
+ * they just might not allow access to some of the locations in the
+ * model.
+ *
+ * @param pos the position to convert >= 0
+ * @param a the allocated region to render into
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard.
+ * This may be SwingConstants.WEST, SwingConstants.EAST,
+ * SwingConstants.NORTH, or SwingConstants.SOUTH.
+ * @return the location within the model that best represents the next
+ * location visual position.
+ * @exception BadLocationException
+ * @exception IllegalArgumentException for an invalid direction
+ */
+ public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a,
+ int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+
+ return painter.getNextVisualPositionFrom(this, pos, b, a, direction, biasRet);
+ }
+
+ /**
+ * Gives notification that something was inserted into
+ * the document in a location that this view is responsible for.
+ * This is implemented to call preferenceChanged along the
+ * axis the glyphs are rendered.
+ *
+ * @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) {
+ justificationInfo = null;
+ breakSpots = null;
+ minimumSpan = -1;
+ syncCR();
+ preferenceChanged(null, true, false);
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that this view is responsible for.
+ * This is implemented to call preferenceChanged along the
+ * axis the glyphs are rendered.
+ *
+ * @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) {
+ justificationInfo = null;
+ breakSpots = null;
+ minimumSpan = -1;
+ syncCR();
+ preferenceChanged(null, true, false);
+ }
+
+ /**
+ * Gives notification from the document that attributes were changed
+ * in a location that this view is responsible for.
+ * This is implemented to call preferenceChanged along both the
+ * horizontal and vertical axis.
+ *
+ * @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) {
+ minimumSpan = -1;
+ syncCR();
+ preferenceChanged(null, true, true);
+ }
+
+ // checks if the paragraph is empty and updates impliedCR flag
+ // accordingly
+ private void syncCR() {
+ if (impliedCR) {
+ Element parent = getElement().getParentElement();
+ impliedCR = (parent != null && parent.getElementCount() > 1);
+ }
+ }
+
+ /**
+ * Class to hold data needed to justify this GlyphView in a PargraphView.Row
+ */
+ static class JustificationInfo {
+ //justifiable content start
+ final int start;
+ //justifiable content end
+ final int end;
+ final int leadingSpaces;
+ final int contentSpaces;
+ final int trailingSpaces;
+ final boolean hasTab;
+ final BitSet spaceMap;
+ JustificationInfo(int start, int end,
+ int leadingSpaces,
+ int contentSpaces,
+ int trailingSpaces,
+ boolean hasTab,
+ BitSet spaceMap) {
+ this.start = start;
+ this.end = end;
+ this.leadingSpaces = leadingSpaces;
+ this.contentSpaces = contentSpaces;
+ this.trailingSpaces = trailingSpaces;
+ this.hasTab = hasTab;
+ this.spaceMap = spaceMap;
+ }
+ }
+
+
+
+ JustificationInfo getJustificationInfo(int rowStartOffset) {
+ if (justificationInfo != null) {
+ return justificationInfo;
+ }
+ //states for the parsing
+ final int TRAILING = 0;
+ final int CONTENT = 1;
+ final int SPACES = 2;
+ int startOffset = getStartOffset();
+ int endOffset = getEndOffset();
+ Segment segment = getText(startOffset, endOffset);
+ int txtOffset = segment.offset;
+ int txtEnd = segment.offset + segment.count - 1;
+ int startContentPosition = txtEnd + 1;
+ int endContentPosition = txtOffset - 1;
+ int lastTabPosition = txtOffset - 1;
+ int trailingSpaces = 0;
+ int contentSpaces = 0;
+ int leadingSpaces = 0;
+ boolean hasTab = false;
+ BitSet spaceMap = new BitSet(endOffset - startOffset + 1);
+
+ //we parse conent to the right of the rightmost TAB only.
+ //we are looking for the trailing and leading spaces.
+ //position after the leading spaces (startContentPosition)
+ //position before the trailing spaces (endContentPosition)
+ for (int i = txtEnd, state = TRAILING; i >= txtOffset; i--) {
+ if (' ' == segment.array[i]) {
+ spaceMap.set(i - txtOffset);
+ if (state == TRAILING) {
+ trailingSpaces++;
+ } else if (state == CONTENT) {
+ state = SPACES;
+ leadingSpaces = 1;
+ } else if (state == SPACES) {
+ leadingSpaces++;
+ }
+ } else if ('\t' == segment.array[i]) {
+ hasTab = true;
+ break;
+ } else {
+ if (state == TRAILING) {
+ if ('\n' != segment.array[i]
+ && '\r' != segment.array[i]) {
+ state = CONTENT;
+ endContentPosition = i;
+ }
+ } else if (state == CONTENT) {
+ //do nothing
+ } else if (state == SPACES) {
+ contentSpaces += leadingSpaces;
+ leadingSpaces = 0;
+ }
+ startContentPosition = i;
+ }
+ }
+
+ SegmentCache.releaseSharedSegment(segment);
+
+ int startJustifiableContent = -1;
+ if (startContentPosition < txtEnd) {
+ startJustifiableContent =
+ startContentPosition - txtOffset;
+ }
+ int endJustifiableContent = -1;
+ if (endContentPosition > txtOffset) {
+ endJustifiableContent =
+ endContentPosition - txtOffset;
+ }
+ justificationInfo =
+ new JustificationInfo(startJustifiableContent,
+ endJustifiableContent,
+ leadingSpaces,
+ contentSpaces,
+ trailingSpaces,
+ hasTab,
+ spaceMap);
+ return justificationInfo;
+ }
+
+ // --- variables ------------------------------------------------
+
+ /**
+ * Used by paint() to store highlighted view positions
+ */
+ private byte[] selections = null;
+
+ int offset;
+ int length;
+ // if it is an implied newline character
+ boolean impliedCR;
+ private static final String IMPLIED_CR = "CR";
+ boolean skipWidth;
+
+ /**
+ * how to expand tabs
+ */
+ TabExpander expander;
+
+ /** Cached minimum x-span value */
+ private float minimumSpan = -1;
+
+ /** Cached breakpoints within the view */
+ private int[] breakSpots = null;
+
+ /**
+ * location for determining tab expansion against.
+ */
+ int x;
+
+ /**
+ * Glyph rendering functionality.
+ */
+ GlyphPainter painter;
+
+ /**
+ * The prototype painter used by default.
+ */
+ static GlyphPainter defaultPainter;
+
+ private JustificationInfo justificationInfo = null;
+
+ /**
+ * A class to perform rendering of the glyphs.
+ * This can be implemented to be stateless, or
+ * to hold some information as a cache to
+ * facilitate faster rendering and model/view
+ * translation. At a minimum, the GlyphPainter
+ * allows a View implementation to perform its
+ * duties independant of a particular version
+ * of JVM and selection of capabilities (i.e.
+ * shaping for i18n, etc).
+ *
+ * @since 1.3
+ */
+ public static abstract class GlyphPainter {
+
+ /**
+ * Determine the span the glyphs given a start location
+ * (for tab expansion).
+ */
+ public abstract float getSpan(GlyphView v, int p0, int p1, TabExpander e, float x);
+
+ public abstract float getHeight(GlyphView v);
+
+ public abstract float getAscent(GlyphView v);
+
+ public abstract float getDescent(GlyphView v);
+
+ /**
+ * Paint the glyphs representing the given range.
+ */
+ public abstract void paint(GlyphView v, Graphics g, Shape a, int p0, int p1);
+
+ /**
+ * Provides a mapping from the document model coordinate space
+ * to the coordinate space of the view mapped to it.
+ * This is shared by the broken views.
+ *
+ * @param v the <code>GlyphView</code> containing the
+ * destination coordinate space
+ * @param pos the position to convert
+ * @param bias either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * @param a Bounds of the View
+ * @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 abstract Shape modelToView(GlyphView v,
+ int pos, Position.Bias bias,
+ Shape a) throws BadLocationException;
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param v the <code>GlyphView</code> to provide a mapping for
+ * @param x the X coordinate
+ * @param y the Y coordinate
+ * @param a the allocated region to render into
+ * @param biasReturn either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * is returned as the zero-th element of this array
+ * @return the location within the model that best represents the
+ * given point of view
+ * @see View#viewToModel
+ */
+ public abstract int viewToModel(GlyphView v,
+ float x, float y, Shape a,
+ Position.Bias[] biasReturn);
+
+ /**
+ * Determines the model location that represents the
+ * maximum advance that fits within the given span.
+ * This could be used to break the given view. The result
+ * should be a location just shy of the given advance. This
+ * differs from viewToModel which returns the closest
+ * position which might be proud of the maximum advance.
+ *
+ * @param v the view to find the model location to break at.
+ * @param p0 the location in the model where the
+ * fragment should start it's representation >= 0.
+ * @param x the graphic location along the axis that the
+ * broken view would occupy >= 0. This may be useful for
+ * things like tab calculations.
+ * @param len specifies the distance into the view
+ * where a potential break is desired >= 0.
+ * @return the maximum model location possible for a break.
+ * @see View#breakView
+ */
+ public abstract int getBoundedPosition(GlyphView v, int p0, float x, float len);
+
+ /**
+ * Create a painter to use for the given GlyphView. If
+ * the painter carries state it can create another painter
+ * to represent a new GlyphView that is being created. If
+ * the painter doesn't hold any significant state, it can
+ * return itself. The default behavior is to return itself.
+ * @param v the <code>GlyphView</code> to provide a painter for
+ * @param p0 the starting document offset >= 0
+ * @param p1 the ending document offset >= p0
+ */
+ public GlyphPainter getPainter(GlyphView v, int p0, int p1) {
+ return this;
+ }
+
+ /**
+ * Provides a way to determine the next visually represented model
+ * location that one might place a caret. Some views may not be
+ * visible, they might not be in the same order found in the model, or
+ * they just might not allow access to some of the locations in the
+ * model.
+ *
+ * @param v the view to use
+ * @param pos the position to convert >= 0
+ * @param b either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * @param a the allocated region to render into
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard.
+ * This may be SwingConstants.WEST, SwingConstants.EAST,
+ * SwingConstants.NORTH, or SwingConstants.SOUTH.
+ * @param biasRet either <code>Position.Bias.Forward</code>
+ * or <code>Position.Bias.Backward</code>
+ * is returned as the zero-th element of this array
+ * @return the location within the model that best represents the next
+ * location visual position.
+ * @exception BadLocationException
+ * @exception IllegalArgumentException for an invalid direction
+ */
+ public int getNextVisualPositionFrom(GlyphView v, int pos, Position.Bias b, Shape a,
+ int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+
+ int startOffset = v.getStartOffset();
+ int endOffset = v.getEndOffset();
+ Segment text;
+
+ switch (direction) {
+ case View.NORTH:
+ case View.SOUTH:
+ if (pos != -1) {
+ // Presumably pos is between startOffset and endOffset,
+ // since GlyphView is only one line, we won't contain
+ // the position to the nort/south, therefore return -1.
+ return -1;
+ }
+ Container container = v.getContainer();
+
+ if (container instanceof JTextComponent) {
+ Caret c = ((JTextComponent)container).getCaret();
+ Point magicPoint;
+ magicPoint = (c != null) ? c.getMagicCaretPosition() :null;
+
+ if (magicPoint == null) {
+ biasRet[0] = Position.Bias.Forward;
+ return startOffset;
+ }
+ int value = v.viewToModel(magicPoint.x, 0f, a, biasRet);
+ return value;
+ }
+ break;
+ case View.EAST:
+ if(startOffset == v.getDocument().getLength()) {
+ if(pos == -1) {
+ biasRet[0] = Position.Bias.Forward;
+ return startOffset;
+ }
+ // End case for bidi text where newline is at beginning
+ // of line.
+ return -1;
+ }
+ if(pos == -1) {
+ biasRet[0] = Position.Bias.Forward;
+ return startOffset;
+ }
+ if(pos == endOffset) {
+ return -1;
+ }
+ if(++pos == endOffset) {
+ // Assumed not used in bidi text, GlyphPainter2 will
+ // override as necessary, therefore return -1.
+ return -1;
+ }
+ else {
+ biasRet[0] = Position.Bias.Forward;
+ }
+ return pos;
+ case View.WEST:
+ if(startOffset == v.getDocument().getLength()) {
+ if(pos == -1) {
+ biasRet[0] = Position.Bias.Forward;
+ return startOffset;
+ }
+ // End case for bidi text where newline is at beginning
+ // of line.
+ return -1;
+ }
+ if(pos == -1) {
+ // Assumed not used in bidi text, GlyphPainter2 will
+ // override as necessary, therefore return -1.
+ biasRet[0] = Position.Bias.Forward;
+ return endOffset - 1;
+ }
+ if(pos == startOffset) {
+ return -1;
+ }
+ biasRet[0] = Position.Bias.Forward;
+ return (pos - 1);
+ default:
+ throw new IllegalArgumentException("Bad direction: " + direction);
+ }
+ return pos;
+
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/Highlighter.java b/src/share/classes/javax/swing/text/Highlighter.java
new file mode 100644
index 000000000..bb26e83b4
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Highlighter.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 1997-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;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Shape;
+
+/**
+ * An interface for an object that allows one to mark up the background
+ * with colored areas.
+ *
+ * @author Timothy Prinzing
+ */
+public interface Highlighter {
+
+ /**
+ * Called when the UI is being installed into the
+ * interface of a JTextComponent. This can be used
+ * to gain access to the model that is being navigated
+ * by the implementation of this interface.
+ *
+ * @param c the JTextComponent editor
+ */
+ public void install(JTextComponent c);
+
+ /**
+ * Called when the UI is being removed from the
+ * interface of a JTextComponent. This is used to
+ * unregister any listeners that were attached.
+ *
+ * @param c the JTextComponent editor
+ */
+ public void deinstall(JTextComponent c);
+
+ /**
+ * Renders the highlights.
+ *
+ * @param g the graphics context.
+ */
+ public void paint(Graphics g);
+
+ /**
+ * Adds a highlight to the view. Returns a tag that can be used
+ * to refer to the highlight.
+ *
+ * @param p0 the beginning of the range >= 0
+ * @param p1 the end of the range >= p0
+ * @param p the painter to use for the actual highlighting
+ * @return an object that refers to the highlight
+ * @exception BadLocationException for an invalid range specification
+ */
+ public Object addHighlight(int p0, int p1, HighlightPainter p) throws BadLocationException;
+
+ /**
+ * Removes a highlight from the view.
+ *
+ * @param tag which highlight to remove
+ */
+ public void removeHighlight(Object tag);
+
+ /**
+ * Removes all highlights this highlighter is responsible for.
+ */
+ public void removeAllHighlights();
+
+ /**
+ * Changes the given highlight to span a different portion of
+ * the document. This may be more efficient than a remove/add
+ * when a selection is expanding/shrinking (such as a sweep
+ * with a mouse) by damaging only what changed.
+ *
+ * @param tag which highlight to change
+ * @param p0 the beginning of the range >= 0
+ * @param p1 the end of the range >= p0
+ * @exception BadLocationException for an invalid range specification
+ */
+ public void changeHighlight(Object tag, int p0, int p1) throws BadLocationException;
+
+ /**
+ * Fetches the current list of highlights.
+ *
+ * @return the highlight list
+ */
+ public Highlight[] getHighlights();
+
+ /**
+ * Highlight renderer.
+ */
+ public interface HighlightPainter {
+
+ /**
+ * Renders the highlight.
+ *
+ * @param g the graphics context
+ * @param p0 the starting offset in the model >= 0
+ * @param p1 the ending offset in the model >= p0
+ * @param bounds the bounding box for the highlight
+ * @param c the editor
+ */
+ public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c);
+
+ }
+
+ public interface Highlight {
+
+ /**
+ * Gets the starting model offset for the highlight.
+ *
+ * @return the starting offset >= 0
+ */
+ public int getStartOffset();
+
+ /**
+ * Gets the ending model offset for the highlight.
+ *
+ * @return the ending offset >= 0
+ */
+ public int getEndOffset();
+
+ /**
+ * Gets the painter for the highlighter.
+ *
+ * @return the painter
+ */
+ public HighlightPainter getPainter();
+
+ }
+
+};
diff --git a/src/share/classes/javax/swing/text/IconView.java b/src/share/classes/javax/swing/text/IconView.java
new file mode 100644
index 000000000..23b31fda5
--- /dev/null
+++ b/src/share/classes/javax/swing/text/IconView.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 1997-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;
+
+import java.awt.*;
+import javax.swing.Icon;
+import javax.swing.event.*;
+
+/**
+ * Icon decorator that implements the view interface. The
+ * entire element is used to represent the icon. This acts
+ * as a gateway from the display-only View implementations to
+ * interactive lightweight icons (that is, it allows icons
+ * to be embedded into the View hierarchy. The parent of the icon
+ * is the container that is handed out by the associated view
+ * factory.
+ *
+ * @author Timothy Prinzing
+ */
+public class IconView extends View {
+
+ /**
+ * Creates a new icon view that represents an element.
+ *
+ * @param elem the element to create a view for
+ */
+ public IconView(Element elem) {
+ super(elem);
+ AttributeSet attr = elem.getAttributes();
+ c = StyleConstants.getIcon(attr);
+ }
+
+ // --- View methods ---------------------------------------------
+
+ /**
+ * Paints the icon.
+ * The real paint behavior occurs naturally from the association
+ * that the icon has with its parent container (the same
+ * container hosting this view), so this simply allows us to
+ * position the icon properly relative to the view. Since
+ * the coordinate system for the view is simply the parent
+ * containers, positioning the child icon is easy.
+ *
+ * @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) {
+ Rectangle alloc = a.getBounds();
+ c.paintIcon(getContainer(), g, alloc.x, alloc.y);
+ }
+
+ /**
+ * 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
+ * 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 getPreferredSpan(int axis) {
+ switch (axis) {
+ case View.X_AXIS:
+ return c.getIconWidth();
+ case View.Y_AXIS:
+ return c.getIconHeight();
+ 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 View.X_AXIS or View.Y_AXIS
+ * @return the desired alignment >= 0.0f && <= 1.0f. 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 1;
+ 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 >= 0
+ * @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;
+ }
+ throw new BadLocationException(pos + " not in range " + p0 + "," + p1, pos);
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents the
+ * given point of view >= 0
+ * @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();
+ }
+
+ // --- member variables ------------------------------------------------
+
+ private Icon c;
+}
diff --git a/src/share/classes/javax/swing/text/InternationalFormatter.java b/src/share/classes/javax/swing/text/InternationalFormatter.java
new file mode 100644
index 000000000..d4f87907d
--- /dev/null
+++ b/src/share/classes/javax/swing/text/InternationalFormatter.java
@@ -0,0 +1,1103 @@
+/*
+ * 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;
+
+import java.awt.event.ActionEvent;
+import java.io.*;
+import java.text.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.text.*;
+
+/**
+ * <code>InternationalFormatter</code> extends <code>DefaultFormatter</code>,
+ * using an instance of <code>java.text.Format</code> to handle the
+ * conversion to a String, and the conversion from a String.
+ * <p>
+ * If <code>getAllowsInvalid()</code> is false, this will ask the
+ * <code>Format</code> to format the current text on every edit.
+ * <p>
+ * You can specify a minimum and maximum value by way of the
+ * <code>setMinimum</code> and <code>setMaximum</code> methods. In order
+ * for this to work the values returned from <code>stringToValue</code> must be
+ * comparable to the min/max values by way of the <code>Comparable</code>
+ * interface.
+ * <p>
+ * Be careful how you configure the <code>Format</code> and the
+ * <code>InternationalFormatter</code>, as it is possible to create a
+ * situation where certain values can not be input. Consider the date
+ * format 'M/d/yy', an <code>InternationalFormatter</code> that is always
+ * valid (<code>setAllowsInvalid(false)</code>), is in overwrite mode
+ * (<code>setOverwriteMode(true)</code>) and the date 7/1/99. In this
+ * case the user will not be able to enter a two digit month or day of
+ * month. To avoid this, the format should be 'MM/dd/yy'.
+ * <p>
+ * If <code>InternationalFormatter</code> is configured to only allow valid
+ * values (<code>setAllowsInvalid(false)</code>), every valid edit will result
+ * in the text of the <code>JFormattedTextField</code> being completely reset
+ * from the <code>Format</code>.
+ * The cursor position will also be adjusted as literal characters are
+ * added/removed from the resulting String.
+ * <p>
+ * <code>InternationalFormatter</code>'s behavior of
+ * <code>stringToValue</code> is slightly different than that of
+ * <code>DefaultTextFormatter</code>, it does the following:
+ * <ol>
+ * <li><code>parseObject</code> is invoked on the <code>Format</code>
+ * specified by <code>setFormat</code>
+ * <li>If a Class has been set for the values (<code>setValueClass</code>),
+ * supers implementation is invoked to convert the value returned
+ * from <code>parseObject</code> to the appropriate class.
+ * <li>If a <code>ParseException</code> has not been thrown, and the value
+ * is outside the min/max a <code>ParseException</code> is thrown.
+ * <li>The value is returned.
+ * </ol>
+ * <code>InternationalFormatter</code> implements <code>stringToValue</code>
+ * in this manner so that you can specify an alternate Class than
+ * <code>Format</code> may return.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @see java.text.Format
+ * @see java.lang.Comparable
+ *
+ * @since 1.4
+ */
+public class InternationalFormatter extends DefaultFormatter {
+ /**
+ * Used by <code>getFields</code>.
+ */
+ private static final Format.Field[] EMPTY_FIELD_ARRAY =new Format.Field[0];
+
+ /**
+ * Object used to handle the conversion.
+ */
+ private Format format;
+ /**
+ * Can be used to impose a maximum value.
+ */
+ private Comparable max;
+ /**
+ * Can be used to impose a minimum value.
+ */
+ private Comparable min;
+
+ /**
+ * <code>InternationalFormatter</code>'s behavior is dicatated by a
+ * <code>AttributedCharacterIterator</code> that is obtained from
+ * the <code>Format</code>. On every edit, assuming
+ * allows invalid is false, the <code>Format</code> instance is invoked
+ * with <code>formatToCharacterIterator</code>. A <code>BitSet</code> is
+ * also kept upto date with the non-literal characters, that is
+ * for every index in the <code>AttributedCharacterIterator</code> an
+ * entry in the bit set is updated based on the return value from
+ * <code>isLiteral(Map)</code>. <code>isLiteral(int)</code> then uses
+ * this cached information.
+ * <p>
+ * If allowsInvalid is false, every edit results in resetting the complete
+ * text of the JTextComponent.
+ * <p>
+ * InternationalFormatterFilter can also provide two actions suitable for
+ * incrementing and decrementing. To enable this a subclass must
+ * override <code>getSupportsIncrement</code> to return true, and
+ * override <code>adjustValue</code> to handle the changing of the
+ * value. If you want to support changing the value outside of
+ * the valid FieldPositions, you will need to override
+ * <code>canIncrement</code>.
+ */
+ /**
+ * A bit is set for every index identified in the
+ * AttributedCharacterIterator that is not considered decoration.
+ * This should only be used if validMask is true.
+ */
+ private transient BitSet literalMask;
+ /**
+ * Used to iterate over characters.
+ */
+ private transient AttributedCharacterIterator iterator;
+ /**
+ * True if the Format was able to convert the value to a String and
+ * back.
+ */
+ private transient boolean validMask;
+ /**
+ * Current value being displayed.
+ */
+ private transient String string;
+ /**
+ * If true, DocumentFilter methods are unconditionally allowed,
+ * and no checking is done on their values. This is used when
+ * incrementing/decrementing via the actions.
+ */
+ private transient boolean ignoreDocumentMutate;
+
+
+ /**
+ * Creates an <code>InternationalFormatter</code> with no
+ * <code>Format</code> specified.
+ */
+ public InternationalFormatter() {
+ setOverwriteMode(false);
+ }
+
+ /**
+ * Creates an <code>InternationalFormatter</code> with the specified
+ * <code>Format</code> instance.
+ *
+ * @param format Format instance used for converting from/to Strings
+ */
+ public InternationalFormatter(Format format) {
+ this();
+ setFormat(format);
+ }
+
+ /**
+ * Sets the format that dictates the legal values that can be edited
+ * and displayed.
+ *
+ * @param format <code>Format</code> instance used for converting
+ * from/to Strings
+ */
+ public void setFormat(Format format) {
+ this.format = format;
+ }
+
+ /**
+ * Returns the format that dictates the legal values that can be edited
+ * and displayed.
+ *
+ * @return Format instance used for converting from/to Strings
+ */
+ public Format getFormat() {
+ return format;
+ }
+
+ /**
+ * Sets the minimum permissible value. If the <code>valueClass</code> has
+ * not been specified, and <code>minimum</code> is non null, the
+ * <code>valueClass</code> will be set to that of the class of
+ * <code>minimum</code>.
+ *
+ * @param minimum Minimum legal value that can be input
+ * @see #setValueClass
+ */
+ public void setMinimum(Comparable minimum) {
+ if (getValueClass() == null && minimum != null) {
+ setValueClass(minimum.getClass());
+ }
+ min = minimum;
+ }
+
+ /**
+ * Returns the minimum permissible value.
+ *
+ * @return Minimum legal value that can be input
+ */
+ public Comparable getMinimum() {
+ return min;
+ }
+
+ /**
+ * Sets the maximum permissible value. If the <code>valueClass</code> has
+ * not been specified, and <code>max</code> is non null, the
+ * <code>valueClass</code> will be set to that of the class of
+ * <code>max</code>.
+ *
+ * @param max Maximum legal value that can be input
+ * @see #setValueClass
+ */
+ public void setMaximum(Comparable max) {
+ if (getValueClass() == null && max != null) {
+ setValueClass(max.getClass());
+ }
+ this.max = max;
+ }
+
+ /**
+ * Returns the maximum permissible value.
+ *
+ * @return Maximum legal value that can be input
+ */
+ public Comparable getMaximum() {
+ return max;
+ }
+
+ /**
+ * Installs the <code>DefaultFormatter</code> onto a particular
+ * <code>JFormattedTextField</code>.
+ * This will invoke <code>valueToString</code> to convert the
+ * current value from the <code>JFormattedTextField</code> to
+ * a String. This will then install the <code>Action</code>s from
+ * <code>getActions</code>, the <code>DocumentFilter</code>
+ * returned from <code>getDocumentFilter</code> and the
+ * <code>NavigationFilter</code> returned from
+ * <code>getNavigationFilter</code> onto the
+ * <code>JFormattedTextField</code>.
+ * <p>
+ * Subclasses will typically only need to override this if they
+ * wish to install additional listeners on the
+ * <code>JFormattedTextField</code>.
+ * <p>
+ * If there is a <code>ParseException</code> in converting the
+ * current value to a String, this will set the text to an empty
+ * String, and mark the <code>JFormattedTextField</code> as being
+ * in an invalid state.
+ * <p>
+ * While this is a public method, this is typically only useful
+ * for subclassers of <code>JFormattedTextField</code>.
+ * <code>JFormattedTextField</code> will invoke this method at
+ * the appropriate times when the value changes, or its internal
+ * state changes.
+ *
+ * @param ftf JFormattedTextField to format for, may be null indicating
+ * uninstall from current JFormattedTextField.
+ */
+ public void install(JFormattedTextField ftf) {
+ super.install(ftf);
+ updateMaskIfNecessary();
+ // invoked again as the mask should now be valid.
+ positionCursorAtInitialLocation();
+ }
+
+ /**
+ * Returns a String representation of the Object <code>value</code>.
+ * This invokes <code>format</code> on the current <code>Format</code>.
+ *
+ * @throws ParseException if there is an error in the conversion
+ * @param value Value to convert
+ * @return String representation of value
+ */
+ public String valueToString(Object value) throws ParseException {
+ if (value == null) {
+ return "";
+ }
+ Format f = getFormat();
+
+ if (f == null) {
+ return value.toString();
+ }
+ return f.format(value);
+ }
+
+ /**
+ * Returns the <code>Object</code> representation of the
+ * <code>String</code> <code>text</code>.
+ *
+ * @param text <code>String</code> to convert
+ * @return <code>Object</code> representation of text
+ * @throws ParseException if there is an error in the conversion
+ */
+ public Object stringToValue(String text) throws ParseException {
+ Object value = stringToValue(text, getFormat());
+
+ // Convert to the value class if the Value returned from the
+ // Format does not match.
+ if (value != null && getValueClass() != null &&
+ !getValueClass().isInstance(value)) {
+ value = super.stringToValue(value.toString());
+ }
+ try {
+ if (!isValidValue(value, true)) {
+ throw new ParseException("Value not within min/max range", 0);
+ }
+ } catch (ClassCastException cce) {
+ throw new ParseException("Class cast exception comparing values: "
+ + cce, 0);
+ }
+ return value;
+ }
+
+ /**
+ * Returns the <code>Format.Field</code> constants associated with
+ * the text at <code>offset</code>. If <code>offset</code> is not
+ * a valid location into the current text, this will return an
+ * empty array.
+ *
+ * @param offset offset into text to be examined
+ * @return Format.Field constants associated with the text at the
+ * given position.
+ */
+ public Format.Field[] getFields(int offset) {
+ if (getAllowsInvalid()) {
+ // This will work if the currently edited value is valid.
+ updateMask();
+ }
+
+ Map attrs = getAttributes(offset);
+
+ if (attrs != null && attrs.size() > 0) {
+ ArrayList al = new ArrayList();
+
+ al.addAll(attrs.keySet());
+ return (Format.Field[])al.toArray(EMPTY_FIELD_ARRAY);
+ }
+ return EMPTY_FIELD_ARRAY;
+ }
+
+ /**
+ * Creates a copy of the DefaultFormatter.
+ *
+ * @return copy of the DefaultFormatter
+ */
+ public Object clone() throws CloneNotSupportedException {
+ InternationalFormatter formatter = (InternationalFormatter)super.
+ clone();
+
+ formatter.literalMask = null;
+ formatter.iterator = null;
+ formatter.validMask = false;
+ formatter.string = null;
+ return formatter;
+ }
+
+ /**
+ * If <code>getSupportsIncrement</code> returns true, this returns
+ * two Actions suitable for incrementing/decrementing the value.
+ */
+ protected Action[] getActions() {
+ if (getSupportsIncrement()) {
+ return new Action[] { new IncrementAction("increment", 1),
+ new IncrementAction("decrement", -1) };
+ }
+ return null;
+ }
+
+ /**
+ * Invokes <code>parseObject</code> on <code>f</code>, returning
+ * its value.
+ */
+ Object stringToValue(String text, Format f) throws ParseException {
+ if (f == null) {
+ return text;
+ }
+ return f.parseObject(text);
+ }
+
+ /**
+ * Returns true if <code>value</code> is between the min/max.
+ *
+ * @param wantsCCE If false, and a ClassCastException is thrown in
+ * comparing the values, the exception is consumed and
+ * false is returned.
+ */
+ boolean isValidValue(Object value, boolean wantsCCE) {
+ Comparable min = getMinimum();
+
+ try {
+ if (min != null && min.compareTo(value) > 0) {
+ return false;
+ }
+ } catch (ClassCastException cce) {
+ if (wantsCCE) {
+ throw cce;
+ }
+ return false;
+ }
+
+ Comparable max = getMaximum();
+ try {
+ if (max != null && max.compareTo(value) < 0) {
+ return false;
+ }
+ } catch (ClassCastException cce) {
+ if (wantsCCE) {
+ throw cce;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns a Set of the attribute identifiers at <code>index</code>.
+ */
+ Map getAttributes(int index) {
+ if (isValidMask()) {
+ AttributedCharacterIterator iterator = getIterator();
+
+ if (index >= 0 && index <= iterator.getEndIndex()) {
+ iterator.setIndex(index);
+ return iterator.getAttributes();
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Returns the start of the first run that contains the attribute
+ * <code>id</code>. This will return <code>-1</code> if the attribute
+ * can not be found.
+ */
+ int getAttributeStart(AttributedCharacterIterator.Attribute id) {
+ if (isValidMask()) {
+ AttributedCharacterIterator iterator = getIterator();
+
+ iterator.first();
+ while (iterator.current() != CharacterIterator.DONE) {
+ if (iterator.getAttribute(id) != null) {
+ return iterator.getIndex();
+ }
+ iterator.next();
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the <code>AttributedCharacterIterator</code> used to
+ * format the last value.
+ */
+ AttributedCharacterIterator getIterator() {
+ return iterator;
+ }
+
+ /**
+ * Updates the AttributedCharacterIterator and bitset, if necessary.
+ */
+ void updateMaskIfNecessary() {
+ if (!getAllowsInvalid() && (getFormat() != null)) {
+ if (!isValidMask()) {
+ updateMask();
+ }
+ else {
+ String newString = getFormattedTextField().getText();
+
+ if (!newString.equals(string)) {
+ updateMask();
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the AttributedCharacterIterator by invoking
+ * <code>formatToCharacterIterator</code> on the <code>Format</code>.
+ * If this is successful,
+ * <code>updateMask(AttributedCharacterIterator)</code>
+ * is then invoked to update the internal bitmask.
+ */
+ void updateMask() {
+ if (getFormat() != null) {
+ Document doc = getFormattedTextField().getDocument();
+
+ validMask = false;
+ if (doc != null) {
+ try {
+ string = doc.getText(0, doc.getLength());
+ } catch (BadLocationException ble) {
+ string = null;
+ }
+ if (string != null) {
+ try {
+ Object value = stringToValue(string);
+ AttributedCharacterIterator iterator = getFormat().
+ formatToCharacterIterator(value);
+
+ updateMask(iterator);
+ }
+ catch (ParseException pe) {}
+ catch (IllegalArgumentException iae) {}
+ catch (NullPointerException npe) {}
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the number of literal characters before <code>index</code>.
+ */
+ int getLiteralCountTo(int index) {
+ int lCount = 0;
+
+ for (int counter = 0; counter < index; counter++) {
+ if (isLiteral(counter)) {
+ lCount++;
+ }
+ }
+ return lCount;
+ }
+
+ /**
+ * Returns true if the character at index is a literal, that is
+ * not editable.
+ */
+ boolean isLiteral(int index) {
+ if (isValidMask() && index < string.length()) {
+ return literalMask.get(index);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the literal character at index.
+ */
+ char getLiteral(int index) {
+ if (isValidMask() && string != null && index < string.length()) {
+ return string.charAt(index);
+ }
+ return (char)0;
+ }
+
+ /**
+ * Returns true if the character at offset is navigatable too. This
+ * is implemented in terms of <code>isLiteral</code>, subclasses
+ * may wish to provide different behavior.
+ */
+ boolean isNavigatable(int offset) {
+ return !isLiteral(offset);
+ }
+
+ /**
+ * Overriden to update the mask after invoking supers implementation.
+ */
+ void updateValue(Object value) {
+ super.updateValue(value);
+ updateMaskIfNecessary();
+ }
+
+ /**
+ * Overriden to unconditionally allow the replace if
+ * ignoreDocumentMutate is true.
+ */
+ void replace(DocumentFilter.FilterBypass fb, int offset,
+ int length, String text,
+ AttributeSet attrs) throws BadLocationException {
+ if (ignoreDocumentMutate) {
+ fb.replace(offset, length, text, attrs);
+ return;
+ }
+ super.replace(fb, offset, length, text, attrs);
+ }
+
+ /**
+ * Returns the index of the next non-literal character starting at
+ * index. If index is not a literal, it will be returned.
+ *
+ * @param direction Amount to increment looking for non-literal
+ */
+ private int getNextNonliteralIndex(int index, int direction) {
+ int max = getFormattedTextField().getDocument().getLength();
+
+ while (index >= 0 && index < max) {
+ if (isLiteral(index)) {
+ index += direction;
+ }
+ else {
+ return index;
+ }
+ }
+ return (direction == -1) ? 0 : max;
+ }
+
+ /**
+ * Overriden in an attempt to honor the literals.
+ * <p>
+ * If we do
+ * not allow invalid values and are in overwrite mode, this does the
+ * following for each character in the replacement range:
+ * <ol>
+ * <li>If the character is a literal, add it to the string to replace
+ * with. If there is text to insert and it doesn't match the
+ * literal, then insert the literal in the the middle of the insert
+ * text. This allows you to either paste in literals or not and
+ * get the same behavior.
+ * <li>If there is no text to insert, replace it with ' '.
+ * </ol>
+ * If not in overwrite mode, and there is text to insert it is
+ * inserted at the next non literal index going forward. If there
+ * is only text to remove, it is removed from the next non literal
+ * index going backward.
+ */
+ boolean canReplace(ReplaceHolder rh) {
+ if (!getAllowsInvalid()) {
+ String text = rh.text;
+ int tl = (text != null) ? text.length() : 0;
+
+ if (tl == 0 && rh.length == 1 && getFormattedTextField().
+ getSelectionStart() != rh.offset) {
+ // Backspace, adjust to actually delete next non-literal.
+ rh.offset = getNextNonliteralIndex(rh.offset, -1);
+ }
+ if (getOverwriteMode()) {
+ StringBuffer replace = null;
+
+ for (int counter = 0, textIndex = 0,
+ max = Math.max(tl, rh.length); counter < max;
+ counter++) {
+ if (isLiteral(rh.offset + counter)) {
+ if (replace != null) {
+ replace.append(getLiteral(rh.offset +
+ counter));
+ }
+ if (textIndex < tl && text.charAt(textIndex) ==
+ getLiteral(rh.offset + counter)) {
+ textIndex++;
+ }
+ else if (textIndex == 0) {
+ rh.offset++;
+ rh.length--;
+ counter--;
+ max--;
+ }
+ else if (replace == null) {
+ replace = new StringBuffer(max);
+ replace.append(text.substring(0, textIndex));
+ replace.append(getLiteral(rh.offset +
+ counter));
+ }
+ }
+ else if (textIndex < tl) {
+ if (replace != null) {
+ replace.append(text.charAt(textIndex));
+ }
+ textIndex++;
+ }
+ else {
+ // Nothing to replace it with, assume ' '
+ if (replace == null) {
+ replace = new StringBuffer(max);
+ if (textIndex > 0) {
+ replace.append(text.substring(0, textIndex));
+ }
+ }
+ if (replace != null) {
+ replace.append(' ');
+ }
+ }
+ }
+ if (replace != null) {
+ rh.text = replace.toString();
+ }
+ }
+ else if (tl > 0) {
+ // insert (or insert and remove)
+ rh.offset = getNextNonliteralIndex(rh.offset, 1);
+ }
+ else {
+ // remove only
+ rh.offset = getNextNonliteralIndex(rh.offset, -1);
+ }
+ ((ExtendedReplaceHolder)rh).endOffset = rh.offset;
+ ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ?
+ rh.text.length() : 0;
+ }
+ else {
+ ((ExtendedReplaceHolder)rh).endOffset = rh.offset;
+ ((ExtendedReplaceHolder)rh).endTextLength = (rh.text != null) ?
+ rh.text.length() : 0;
+ }
+ boolean can = super.canReplace(rh);
+ if (can && !getAllowsInvalid()) {
+ ((ExtendedReplaceHolder)rh).resetFromValue(this);
+ }
+ return can;
+ }
+
+ /**
+ * When in !allowsInvalid mode the text is reset on every edit, thus
+ * supers implementation will position the cursor at the wrong position.
+ * As such, this invokes supers implementation and then invokes
+ * <code>repositionCursor</code> to correctly reset the cursor.
+ */
+ boolean replace(ReplaceHolder rh) throws BadLocationException {
+ int start = -1;
+ int direction = 1;
+ int literalCount = -1;
+
+ if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
+ (getFormattedTextField().getSelectionStart() != rh.offset ||
+ rh.length > 1)) {
+ direction = -1;
+ }
+ if (!getAllowsInvalid()) {
+ if ((rh.text == null || rh.text.length() == 0) && rh.length > 0) {
+ // remove
+ start = getFormattedTextField().getSelectionStart();
+ }
+ else {
+ start = rh.offset;
+ }
+ literalCount = getLiteralCountTo(start);
+ }
+ if (super.replace(rh)) {
+ if (start != -1) {
+ int end = ((ExtendedReplaceHolder)rh).endOffset;
+
+ end += ((ExtendedReplaceHolder)rh).endTextLength;
+ repositionCursor(literalCount, end, direction);
+ }
+ else {
+ start = ((ExtendedReplaceHolder)rh).endOffset;
+ if (direction == 1) {
+ start += ((ExtendedReplaceHolder)rh).endTextLength;
+ }
+ repositionCursor(start, direction);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Repositions the cursor. <code>startLiteralCount</code> gives
+ * the number of literals to the start of the deleted range, end
+ * gives the ending location to adjust from, direction gives
+ * the direction relative to <code>end</code> to position the
+ * cursor from.
+ */
+ private void repositionCursor(int startLiteralCount, int end,
+ int direction) {
+ int endLiteralCount = getLiteralCountTo(end);
+
+ if (endLiteralCount != end) {
+ end -= startLiteralCount;
+ for (int counter = 0; counter < end; counter++) {
+ if (isLiteral(counter)) {
+ end++;
+ }
+ }
+ }
+ repositionCursor(end, 1 /*direction*/);
+ }
+
+ /**
+ * Returns the character from the mask that has been buffered
+ * at <code>index</code>.
+ */
+ char getBufferedChar(int index) {
+ if (isValidMask()) {
+ if (string != null && index < string.length()) {
+ return string.charAt(index);
+ }
+ }
+ return (char)0;
+ }
+
+ /**
+ * Returns true if the current mask is valid.
+ */
+ boolean isValidMask() {
+ return validMask;
+ }
+
+ /**
+ * Returns true if <code>attributes</code> is null or empty.
+ */
+ boolean isLiteral(Map attributes) {
+ return ((attributes == null) || attributes.size() == 0);
+ }
+
+ /**
+ * Updates the interal bitset from <code>iterator</code>. This will
+ * set <code>validMask</code> to true if <code>iterator</code> is
+ * non-null.
+ */
+ private void updateMask(AttributedCharacterIterator iterator) {
+ if (iterator != null) {
+ validMask = true;
+ this.iterator = iterator;
+
+ // Update the literal mask
+ if (literalMask == null) {
+ literalMask = new BitSet();
+ }
+ else {
+ for (int counter = literalMask.length() - 1; counter >= 0;
+ counter--) {
+ literalMask.clear(counter);
+ }
+ }
+
+ iterator.first();
+ while (iterator.current() != CharacterIterator.DONE) {
+ Map attributes = iterator.getAttributes();
+ boolean set = isLiteral(attributes);
+ int start = iterator.getIndex();
+ int end = iterator.getRunLimit();
+
+ while (start < end) {
+ if (set) {
+ literalMask.set(start);
+ }
+ else {
+ literalMask.clear(start);
+ }
+ start++;
+ }
+ iterator.setIndex(start);
+ }
+ }
+ }
+
+ /**
+ * Returns true if <code>field</code> is non-null.
+ * Subclasses that wish to allow incrementing to happen outside of
+ * the known fields will need to override this.
+ */
+ boolean canIncrement(Object field, int cursorPosition) {
+ return (field != null);
+ }
+
+ /**
+ * Selects the fields identified by <code>attributes</code>.
+ */
+ void selectField(Object f, int count) {
+ AttributedCharacterIterator iterator = getIterator();
+
+ if (iterator != null &&
+ (f instanceof AttributedCharacterIterator.Attribute)) {
+ AttributedCharacterIterator.Attribute field =
+ (AttributedCharacterIterator.Attribute)f;
+
+ iterator.first();
+ while (iterator.current() != CharacterIterator.DONE) {
+ while (iterator.getAttribute(field) == null &&
+ iterator.next() != CharacterIterator.DONE);
+ if (iterator.current() != CharacterIterator.DONE) {
+ int limit = iterator.getRunLimit(field);
+
+ if (--count <= 0) {
+ getFormattedTextField().select(iterator.getIndex(),
+ limit);
+ break;
+ }
+ iterator.setIndex(limit);
+ iterator.next();
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the field that will be adjusted by adjustValue.
+ */
+ Object getAdjustField(int start, Map attributes) {
+ return null;
+ }
+
+ /**
+ * Returns the number of occurences of <code>f</code> before
+ * the location <code>start</code> in the current
+ * <code>AttributedCharacterIterator</code>.
+ */
+ private int getFieldTypeCountTo(Object f, int start) {
+ AttributedCharacterIterator iterator = getIterator();
+ int count = 0;
+
+ if (iterator != null &&
+ (f instanceof AttributedCharacterIterator.Attribute)) {
+ AttributedCharacterIterator.Attribute field =
+ (AttributedCharacterIterator.Attribute)f;
+ int index = 0;
+
+ iterator.first();
+ while (iterator.getIndex() < start) {
+ while (iterator.getAttribute(field) == null &&
+ iterator.next() != CharacterIterator.DONE);
+ if (iterator.current() != CharacterIterator.DONE) {
+ iterator.setIndex(iterator.getRunLimit(field));
+ iterator.next();
+ count++;
+ }
+ else {
+ break;
+ }
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Subclasses supporting incrementing must override this to handle
+ * the actual incrementing. <code>value</code> is the current value,
+ * <code>attributes</code> gives the field the cursor is in (may be
+ * null depending upon <code>canIncrement</code>) and
+ * <code>direction</code> is the amount to increment by.
+ */
+ Object adjustValue(Object value, Map attributes, Object field,
+ int direction) throws
+ BadLocationException, ParseException {
+ return null;
+ }
+
+ /**
+ * Returns false, indicating InternationalFormatter does not allow
+ * incrementing of the value. Subclasses that wish to support
+ * incrementing/decrementing the value should override this and
+ * return true. Subclasses should also override
+ * <code>adjustValue</code>.
+ */
+ boolean getSupportsIncrement() {
+ return false;
+ }
+
+ /**
+ * Resets the value of the JFormattedTextField to be
+ * <code>value</code>.
+ */
+ void resetValue(Object value) throws BadLocationException, ParseException {
+ Document doc = getFormattedTextField().getDocument();
+ String string = valueToString(value);
+
+ try {
+ ignoreDocumentMutate = true;
+ doc.remove(0, doc.getLength());
+ doc.insertString(0, string, null);
+ } finally {
+ ignoreDocumentMutate = false;
+ }
+ updateValue(value);
+ }
+
+ /**
+ * Subclassed to update the internal representation of the mask after
+ * the default read operation has completed.
+ */
+ private void readObject(ObjectInputStream s)
+ throws IOException, ClassNotFoundException {
+ s.defaultReadObject();
+ updateMaskIfNecessary();
+ }
+
+
+ /**
+ * Overriden to return an instance of <code>ExtendedReplaceHolder</code>.
+ */
+ ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset,
+ int length, String text,
+ AttributeSet attrs) {
+ if (replaceHolder == null) {
+ replaceHolder = new ExtendedReplaceHolder();
+ }
+ return super.getReplaceHolder(fb, offset, length, text, attrs);
+ }
+
+
+ /**
+ * As InternationalFormatter replaces the complete text on every edit,
+ * ExtendedReplaceHolder keeps track of the offset and length passed
+ * into canReplace.
+ */
+ static class ExtendedReplaceHolder extends ReplaceHolder {
+ /** Offset of the insert/remove. This may differ from offset in
+ * that if !allowsInvalid the text is replaced on every edit. */
+ int endOffset;
+ /** Length of the text. This may differ from text.length in
+ * that if !allowsInvalid the text is replaced on every edit. */
+ int endTextLength;
+
+ /**
+ * Resets the region to delete to be the complete document and
+ * the text from invoking valueToString on the current value.
+ */
+ void resetFromValue(InternationalFormatter formatter) {
+ // Need to reset the complete string as Format's result can
+ // be completely different.
+ offset = 0;
+ try {
+ text = formatter.valueToString(value);
+ } catch (ParseException pe) {
+ // Should never happen, otherwise canReplace would have
+ // returned value.
+ text = "";
+ }
+ length = fb.getDocument().getLength();
+ }
+ }
+
+
+ /**
+ * IncrementAction is used to increment the value by a certain amount.
+ * It calls into <code>adjustValue</code> to handle the actual
+ * incrementing of the value.
+ */
+ private class IncrementAction extends AbstractAction {
+ private int direction;
+
+ IncrementAction(String name, int direction) {
+ super(name);
+ this.direction = direction;
+ }
+
+ public void actionPerformed(ActionEvent ae) {
+
+ if (getFormattedTextField().isEditable()) {
+ if (getAllowsInvalid()) {
+ // This will work if the currently edited value is valid.
+ updateMask();
+ }
+
+ boolean validEdit = false;
+
+ if (isValidMask()) {
+ int start = getFormattedTextField().getSelectionStart();
+
+ if (start != -1) {
+ AttributedCharacterIterator iterator = getIterator();
+
+ iterator.setIndex(start);
+
+ Map attributes = iterator.getAttributes();
+ Object field = getAdjustField(start, attributes);
+
+ if (canIncrement(field, start)) {
+ try {
+ Object value = stringToValue(
+ getFormattedTextField().getText());
+ int fieldTypeCount = getFieldTypeCountTo(
+ field, start);
+
+ value = adjustValue(value, attributes,
+ field, direction);
+ if (value != null && isValidValue(value, false)) {
+ resetValue(value);
+ updateMask();
+
+ if (isValidMask()) {
+ selectField(field, fieldTypeCount);
+ }
+ validEdit = true;
+ }
+ }
+ catch (ParseException pe) { }
+ catch (BadLocationException ble) { }
+ }
+ }
+ }
+ if (!validEdit) {
+ invalidEdit();
+ }
+ }
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/JTextComponent.java b/src/share/classes/javax/swing/text/JTextComponent.java
new file mode 100644
index 000000000..9ba898956
--- /dev/null
+++ b/src/share/classes/javax/swing/text/JTextComponent.java
@@ -0,0 +1,5042 @@
+/*
+ * 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;
+
+import java.lang.reflect.Method;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Enumeration;
+import java.util.Vector;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import java.util.concurrent.*;
+
+import java.io.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.print.*;
+import java.awt.datatransfer.*;
+import java.awt.im.InputContext;
+import java.awt.im.InputMethodRequests;
+import java.awt.font.TextHitInfo;
+import java.awt.font.TextAttribute;
+
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+
+import javax.print.PrintService;
+import javax.print.attribute.PrintRequestAttributeSet;
+
+import java.text.*;
+import java.text.AttributedCharacterIterator.Attribute;
+
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.plaf.*;
+
+import javax.accessibility.*;
+
+import javax.print.attribute.*;
+
+import sun.awt.AppContext;
+
+
+import sun.swing.PrintingStatus;
+import sun.swing.SwingUtilities2;
+import sun.swing.text.TextComponentPrintable;
+
+/**
+ * <code>JTextComponent</code> is the base class for swing text
+ * components. It tries to be compatible with the
+ * <code>java.awt.TextComponent</code> class
+ * where it can reasonably do so. Also provided are other services
+ * for additional flexibility (beyond the pluggable UI and bean
+ * support).
+ * You can find information on how to use the functionality
+ * this class provides in
+ * <a href="http://java.sun.com/docs/books/tutorial/uiswing/components/generaltext.html">General Rules for Using Text Components</a>,
+ * a section in <em>The Java Tutorial.</em>
+ *
+ * <p>
+ * <dl>
+ * <dt><b><font size=+1>Caret Changes</font></b>
+ * <dd>
+ * The caret is a pluggable object in swing text components.
+ * Notification of changes to the caret position and the selection
+ * are sent to implementations of the <code>CaretListener</code>
+ * interface that have been registered with the text component.
+ * The UI will install a default caret unless a customized caret
+ * has been set. <br>
+ * By default the caret tracks all the document changes
+ * performed on the Event Dispatching Thread and updates it's position
+ * accordingly if an insertion occurs before or at the caret position
+ * or a removal occurs before the caret position. <code>DefaultCaret</code>
+ * tries to make itself visible which may lead to scrolling
+ * of a text component within <code>JScrollPane</code>. The default caret
+ * behavior can be changed by the {@link DefaultCaret#setUpdatePolicy} method.
+ * <br>
+ * <b>Note</b>: Non-editable text components also have a caret though
+ * it may not be painted.
+ *
+ * <p>
+ * <dt><b><font size=+1>Commands</font></b>
+ * <dd>
+ * Text components provide a number of commands that can be used
+ * to manipulate the component. This is essentially the way that
+ * the component expresses its capabilities. These are expressed
+ * in terms of the swing <code>Action</code> interface,
+ * using the <code>TextAction</code> implementation.
+ * The set of commands supported by the text component can be
+ * found with the {@link #getActions} method. These actions
+ * can be bound to key events, fired from buttons, etc.
+ *
+ * <p>
+ * <dt><b><font size=+1>Text Input</font></b>
+ * <dd>
+ * The text components support flexible and internationalized text input, using
+ * keymaps and the input method framework, while maintaining compatibility with
+ * the AWT listener model.
+ * <p>
+ * A {@link javax.swing.text.Keymap} lets an application bind key
+ * strokes to actions.
+ * In order to allow keymaps to be shared across multiple text components, they
+ * can use actions that extend <code>TextAction</code>.
+ * <code>TextAction</code> can determine which <code>JTextComponent</code>
+ * most recently has or had focus and therefore is the subject of
+ * the action (In the case that the <code>ActionEvent</code>
+ * sent to the action doesn't contain the target text component as its source).
+ * <p>
+ * The <a href="../../../../technotes/guides/imf/spec.html">input method framework</a>
+ * lets text components interact with input methods, separate software
+ * components that preprocess events to let users enter thousands of
+ * different characters using keyboards with far fewer keys.
+ * <code>JTextComponent</code> is an <em>active client</em> of
+ * the framework, so it implements the preferred user interface for interacting
+ * with input methods. As a consequence, some key events do not reach the text
+ * component because they are handled by an input method, and some text input
+ * reaches the text component as committed text within an {@link
+ * java.awt.event.InputMethodEvent} instead of as a key event.
+ * The complete text input is the combination of the characters in
+ * <code>keyTyped</code> key events and committed text in input method events.
+ * <p>
+ * The AWT listener model lets applications attach event listeners to
+ * components in order to bind events to actions. Swing encourages the
+ * use of keymaps instead of listeners, but maintains compatibility
+ * with listeners by giving the listeners a chance to steal an event
+ * by consuming it.
+ * <p>
+ * Keyboard event and input method events are handled in the following stages,
+ * with each stage capable of consuming the event:
+ *
+ * <table border=1 summary="Stages of keyboard and input method event handling">
+ * <tr>
+ * <th id="stage"><p align="left">Stage</p></th>
+ * <th id="ke"><p align="left">KeyEvent</p></th>
+ * <th id="ime"><p align="left">InputMethodEvent</p></th></tr>
+ * <tr><td headers="stage">1. </td>
+ * <td headers="ke">input methods </td>
+ * <td headers="ime">(generated here)</td></tr>
+ * <tr><td headers="stage">2. </td>
+ * <td headers="ke">focus manager </td>
+ * <td headers="ime"></td>
+ * </tr>
+ * <tr>
+ * <td headers="stage">3. </td>
+ * <td headers="ke">registered key listeners</td>
+ * <td headers="ime">registered input method listeners</tr>
+ * <tr>
+ * <td headers="stage">4. </td>
+ * <td headers="ke"></td>
+ * <td headers="ime">input method handling in JTextComponent</tr>
+ * <tr>
+ * <td headers="stage">5. </td><td headers="ke ime" colspan=2>keymap handling using the current keymap</td></tr>
+ * <tr><td headers="stage">6. </td><td headers="ke">keyboard handling in JComponent (e.g. accelerators, component navigation, etc.)</td>
+ * <td headers="ime"></td></tr>
+ * </table>
+ *
+ * <p>
+ * To maintain compatibility with applications that listen to key
+ * events but are not aware of input method events, the input
+ * method handling in stage 4 provides a compatibility mode for
+ * components that do not process input method events. For these
+ * components, the committed text is converted to keyTyped key events
+ * and processed in the key event pipeline starting at stage 3
+ * instead of in the input method event pipeline.
+ * <p>
+ * By default the component will create a keymap (named <b>DEFAULT_KEYMAP</b>)
+ * that is shared by all JTextComponent instances as the default keymap.
+ * Typically a look-and-feel implementation will install a different keymap
+ * that resolves to the default keymap for those bindings not found in the
+ * different keymap. The minimal bindings include:
+ * <ul>
+ * <li>inserting content into the editor for the
+ * printable keys.
+ * <li>removing content with the backspace and del
+ * keys.
+ * <li>caret movement forward and backward
+ * </ul>
+ *
+ * <p>
+ * <dt><b><font size=+1>Model/View Split</font></b>
+ * <dd>
+ * The text components have a model-view split. A text component pulls
+ * together the objects used to represent the model, view, and controller.
+ * The text document model may be shared by other views which act as observers
+ * of the model (e.g. a document may be shared by multiple components).
+ *
+ * <p align=center><img src="doc-files/editor.gif" alt="Diagram showing interaction between Controller, Document, events, and ViewFactory"
+ * HEIGHT=358 WIDTH=587></p>
+ *
+ * <p>
+ * The model is defined by the {@link Document} interface.
+ * This is intended to provide a flexible text storage mechanism
+ * that tracks change during edits and can be extended to more sophisticated
+ * models. The model interfaces are meant to capture the capabilities of
+ * expression given by SGML, a system used to express a wide variety of
+ * content.
+ * Each modification to the document causes notification of the
+ * details of the change to be sent to all observers in the form of a
+ * {@link DocumentEvent} which allows the views to stay up to date with the model.
+ * This event is sent to observers that have implemented the
+ * {@link DocumentListener}
+ * interface and registered interest with the model being observed.
+ *
+ * <p>
+ * <dt><b><font size=+1>Location Information</font></b>
+ * <dd>
+ * The capability of determining the location of text in
+ * the view is provided. There are two methods, {@link #modelToView}
+ * and {@link #viewToModel} for determining this information.
+ *
+ * <p>
+ * <dt><b><font size=+1>Undo/Redo support</font></b>
+ * <dd>
+ * Support for an edit history mechanism is provided to allow
+ * undo/redo operations. The text component does not itself
+ * provide the history buffer by default, but does provide
+ * the <code>UndoableEdit</code> records that can be used in conjunction
+ * with a history buffer to provide the undo/redo support.
+ * The support is provided by the Document model, which allows
+ * one to attach UndoableEditListener implementations.
+ *
+ * <p>
+ * <dt><b><font size=+1>Thread Safety</font></b>
+ * <dd>
+ * The swing text components provide some support of thread
+ * safe operations. Because of the high level of configurability
+ * of the text components, it is possible to circumvent the
+ * protection provided. The protection primarily comes from
+ * the model, so the documentation of <code>AbstractDocument</code>
+ * describes the assumptions of the protection provided.
+ * The methods that are safe to call asynchronously are marked
+ * with comments.
+ *
+ * <p>
+ * <dt><b><font size=+1>Newlines</font></b>
+ * <dd>
+ * For a discussion on how newlines are handled, see
+ * <a href="DefaultEditorKit.html">DefaultEditorKit</a>.
+ *
+ * <p>
+ * <dt><b><font size=+1>Printing support</font></b>
+ * <dd>
+ * Several {@link #print print} methods are provided for basic
+ * document printing. If more advanced printing is needed, use the
+ * {@link #getPrintable} method.
+ * </dl>
+ *
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @beaninfo
+ * attribute: isContainer false
+ *
+ * @author Timothy Prinzing
+ * @author Igor Kushnirskiy (printing support)
+ * @see Document
+ * @see DocumentEvent
+ * @see DocumentListener
+ * @see Caret
+ * @see CaretEvent
+ * @see CaretListener
+ * @see TextUI
+ * @see View
+ * @see ViewFactory
+ */
+public abstract class JTextComponent extends JComponent implements Scrollable, Accessible
+{
+ /**
+ * Creates a new <code>JTextComponent</code>.
+ * Listeners for caret events are established, and the pluggable
+ * UI installed. The component is marked as editable. No layout manager
+ * is used, because layout is managed by the view subsystem of text.
+ * The document model is set to <code>null</code>.
+ */
+ public JTextComponent() {
+ super();
+ // enable InputMethodEvent for on-the-spot pre-editing
+ enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.INPUT_METHOD_EVENT_MASK);
+ caretEvent = new MutableCaretEvent(this);
+ addMouseListener(caretEvent);
+ addFocusListener(caretEvent);
+ setEditable(true);
+ setDragEnabled(false);
+ setLayout(null); // layout is managed by View hierarchy
+ updateUI();
+ }
+
+ /**
+ * Fetches the user-interface factory for this text-oriented editor.
+ *
+ * @return the factory
+ */
+ public TextUI getUI() { return (TextUI)ui; }
+
+ /**
+ * Sets the user-interface factory for this text-oriented editor.
+ *
+ * @param ui the factory
+ */
+ public void setUI(TextUI ui) {
+ super.setUI(ui);
+ }
+
+ /**
+ * Reloads the pluggable UI. The key used to fetch the
+ * new interface is <code>getUIClassID()</code>. The type of
+ * the UI is <code>TextUI</code>. <code>invalidate</code>
+ * is called after setting the UI.
+ */
+ public void updateUI() {
+ setUI((TextUI)UIManager.getUI(this));
+ invalidate();
+ }
+
+ /**
+ * Adds a caret listener for notification of any changes
+ * to the caret.
+ *
+ * @param listener the listener to be added
+ * @see javax.swing.event.CaretEvent
+ */
+ public void addCaretListener(CaretListener listener) {
+ listenerList.add(CaretListener.class, listener);
+ }
+
+ /**
+ * Removes a caret listener.
+ *
+ * @param listener the listener to be removed
+ * @see javax.swing.event.CaretEvent
+ */
+ public void removeCaretListener(CaretListener listener) {
+ listenerList.remove(CaretListener.class, listener);
+ }
+
+ /**
+ * Returns an array of all the caret listeners
+ * registered on this text component.
+ *
+ * @return all of this component's <code>CaretListener</code>s
+ * or an empty
+ * array if no caret listeners are currently registered
+ *
+ * @see #addCaretListener
+ * @see #removeCaretListener
+ *
+ * @since 1.4
+ */
+ public CaretListener[] getCaretListeners() {
+ return (CaretListener[])listenerList.getListeners(CaretListener.class);
+ }
+
+ /**
+ * 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. The listener list is processed in a
+ * last-to-first manner.
+ *
+ * @param e the event
+ * @see EventListenerList
+ */
+ protected void fireCaretUpdate(CaretEvent e) {
+ // Guaranteed to return a non-null array
+ Object[] listeners = listenerList.getListenerList();
+ // Process the listeners last to first, notifying
+ // those that are interested in this event
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==CaretListener.class) {
+ ((CaretListener)listeners[i+1]).caretUpdate(e);
+ }
+ }
+ }
+
+ /**
+ * Associates the editor with a text document.
+ * The currently registered factory is used to build a view for
+ * the document, which gets displayed by the editor after revalidation.
+ * A PropertyChange event ("document") is propagated to each listener.
+ *
+ * @param doc the document to display/edit
+ * @see #getDocument
+ * @beaninfo
+ * description: the text document model
+ * bound: true
+ * expert: true
+ */
+ public void setDocument(Document doc) {
+ Document old = model;
+
+ /*
+ * aquire a read lock on the old model to prevent notification of
+ * mutations while we disconnecting the old model.
+ */
+ try {
+ if (old instanceof AbstractDocument) {
+ ((AbstractDocument)old).readLock();
+ }
+ if (accessibleContext != null) {
+ model.removeDocumentListener(
+ ((AccessibleJTextComponent)accessibleContext));
+ }
+ if (inputMethodRequestsHandler != null) {
+ model.removeDocumentListener((DocumentListener)inputMethodRequestsHandler);
+ }
+ model = doc;
+
+ // Set the document's run direction property to match the
+ // component's ComponentOrientation property.
+ Boolean runDir = getComponentOrientation().isLeftToRight()
+ ? TextAttribute.RUN_DIRECTION_LTR
+ : TextAttribute.RUN_DIRECTION_RTL;
+ if (runDir != doc.getProperty(TextAttribute.RUN_DIRECTION)) {
+ doc.putProperty(TextAttribute.RUN_DIRECTION, runDir );
+ }
+ firePropertyChange("document", old, doc);
+ } finally {
+ if (old instanceof AbstractDocument) {
+ ((AbstractDocument)old).readUnlock();
+ }
+ }
+
+ revalidate();
+ repaint();
+ if (accessibleContext != null) {
+ model.addDocumentListener(
+ ((AccessibleJTextComponent)accessibleContext));
+ }
+ if (inputMethodRequestsHandler != null) {
+ model.addDocumentListener((DocumentListener)inputMethodRequestsHandler);
+ }
+ }
+
+ /**
+ * Fetches the model associated with the editor. This is
+ * primarily for the UI to get at the minimal amount of
+ * state required to be a text editor. Subclasses will
+ * return the actual type of the model which will typically
+ * be something that extends Document.
+ *
+ * @return the model
+ */
+ public Document getDocument() {
+ return model;
+ }
+
+ // Override of Component.setComponentOrientation
+ public void setComponentOrientation( ComponentOrientation o ) {
+ // Set the document's run direction property to match the
+ // ComponentOrientation property.
+ Document doc = getDocument();
+ if( doc != null ) {
+ Boolean runDir = o.isLeftToRight()
+ ? TextAttribute.RUN_DIRECTION_LTR
+ : TextAttribute.RUN_DIRECTION_RTL;
+ doc.putProperty( TextAttribute.RUN_DIRECTION, runDir );
+ }
+ super.setComponentOrientation( o );
+ }
+
+ /**
+ * Fetches the command list for the editor. This is
+ * the list of commands supported by the plugged-in UI
+ * augmented by the collection of commands that the
+ * editor itself supports. These are useful for binding
+ * to events, such as in a keymap.
+ *
+ * @return the command list
+ */
+ public Action[] getActions() {
+ return getUI().getEditorKit(this).getActions();
+ }
+
+ /**
+ * Sets margin space between the text component's border
+ * and its text. The text component's default <code>Border</code>
+ * object will use this value to create the proper margin.
+ * However, if a non-default border is set on the text component,
+ * it is that <code>Border</code> object's responsibility to create the
+ * appropriate margin space (else this property will effectively
+ * be ignored). This causes a redraw of the component.
+ * A PropertyChange event ("margin") is sent to all listeners.
+ *
+ * @param m the space between the border and the text
+ * @beaninfo
+ * description: desired space between the border and text area
+ * bound: true
+ */
+ public void setMargin(Insets m) {
+ Insets old = margin;
+ margin = m;
+ firePropertyChange("margin", old, m);
+ invalidate();
+ }
+
+ /**
+ * Returns the margin between the text component's border and
+ * its text.
+ *
+ * @return the margin
+ */
+ public Insets getMargin() {
+ return margin;
+ }
+
+ /**
+ * Sets the <code>NavigationFilter</code>. <code>NavigationFilter</code>
+ * is used by <code>DefaultCaret</code> and the default cursor movement
+ * actions as a way to restrict the cursor movement.
+ *
+ * @since 1.4
+ */
+ public void setNavigationFilter(NavigationFilter filter) {
+ navigationFilter = filter;
+ }
+
+ /**
+ * Returns the <code>NavigationFilter</code>. <code>NavigationFilter</code>
+ * is used by <code>DefaultCaret</code> and the default cursor movement
+ * actions as a way to restrict the cursor movement. A null return value
+ * implies the cursor movement and selection should not be restricted.
+ *
+ * @since 1.4
+ * @return the NavigationFilter
+ */
+ public NavigationFilter getNavigationFilter() {
+ return navigationFilter;
+ }
+
+ /**
+ * Fetches the caret that allows text-oriented navigation over
+ * the view.
+ *
+ * @return the caret
+ */
+ public Caret getCaret() {
+ return caret;
+ }
+
+ /**
+ * Sets the caret to be used. By default this will be set
+ * by the UI that gets installed. This can be changed to
+ * a custom caret if desired. Setting the caret results in a
+ * PropertyChange event ("caret") being fired.
+ *
+ * @param c the caret
+ * @see #getCaret
+ * @beaninfo
+ * description: the caret used to select/navigate
+ * bound: true
+ * expert: true
+ */
+ public void setCaret(Caret c) {
+ if (caret != null) {
+ caret.removeChangeListener(caretEvent);
+ caret.deinstall(this);
+ }
+ Caret old = caret;
+ caret = c;
+ if (caret != null) {
+ caret.install(this);
+ caret.addChangeListener(caretEvent);
+ }
+ firePropertyChange("caret", old, caret);
+ }
+
+ /**
+ * Fetches the object responsible for making highlights.
+ *
+ * @return the highlighter
+ */
+ public Highlighter getHighlighter() {
+ return highlighter;
+ }
+
+ /**
+ * Sets the highlighter to be used. By default this will be set
+ * by the UI that gets installed. This can be changed to
+ * a custom highlighter if desired. The highlighter can be set to
+ * <code>null</code> to disable it.
+ * A PropertyChange event ("highlighter") is fired
+ * when a new highlighter is installed.
+ *
+ * @param h the highlighter
+ * @see #getHighlighter
+ * @beaninfo
+ * description: object responsible for background highlights
+ * bound: true
+ * expert: true
+ */
+ public void setHighlighter(Highlighter h) {
+ if (highlighter != null) {
+ highlighter.deinstall(this);
+ }
+ Highlighter old = highlighter;
+ highlighter = h;
+ if (highlighter != null) {
+ highlighter.install(this);
+ }
+ firePropertyChange("highlighter", old, h);
+ }
+
+ /**
+ * Sets the keymap to use for binding events to
+ * actions. Setting to <code>null</code> effectively disables
+ * keyboard input.
+ * A PropertyChange event ("keymap") is fired when a new keymap
+ * is installed.
+ *
+ * @param map the keymap
+ * @see #getKeymap
+ * @beaninfo
+ * description: set of key event to action bindings to use
+ * bound: true
+ */
+ public void setKeymap(Keymap map) {
+ Keymap old = keymap;
+ keymap = map;
+ firePropertyChange("keymap", old, keymap);
+ updateInputMap(old, map);
+ }
+
+ /**
+ * Turns on or off automatic drag handling. In order to enable automatic
+ * drag handling, this property should be set to {@code true}, and the
+ * component's {@code TransferHandler} needs to be {@code non-null}.
+ * The default value of the {@code dragEnabled} property is {@code false}.
+ * <p>
+ * The job of honoring this property, and recognizing a user drag gesture,
+ * lies with the look and feel implementation, and in particular, the component's
+ * {@code TextUI}. When automatic drag handling is enabled, most look and
+ * feels (including those that subclass {@code BasicLookAndFeel}) begin a
+ * drag and drop operation whenever the user presses the mouse button over
+ * a selection and then moves the mouse a few pixels. Setting this property to
+ * {@code true} can therefore have a subtle effect on how selections behave.
+ * <p>
+ * If a look and feel is used that ignores this property, you can still
+ * begin a drag and drop operation by calling {@code exportAsDrag} on the
+ * component's {@code TransferHandler}.
+ *
+ * @param b whether or not to enable automatic drag handling
+ * @exception HeadlessException if
+ * <code>b</code> is <code>true</code> and
+ * <code>GraphicsEnvironment.isHeadless()</code>
+ * returns <code>true</code>
+ * @see java.awt.GraphicsEnvironment#isHeadless
+ * @see #getDragEnabled
+ * @see #setTransferHandler
+ * @see TransferHandler
+ * @since 1.4
+ *
+ * @beaninfo
+ * description: determines whether automatic drag handling is enabled
+ * bound: false
+ */
+ public void setDragEnabled(boolean b) {
+ if (b && GraphicsEnvironment.isHeadless()) {
+ throw new HeadlessException();
+ }
+ dragEnabled = b;
+ }
+
+ /**
+ * Returns whether or not automatic drag handling is enabled.
+ *
+ * @return the value of the {@code dragEnabled} property
+ * @see #setDragEnabled
+ * @since 1.4
+ */
+ public boolean getDragEnabled() {
+ return dragEnabled;
+ }
+
+ /**
+ * Sets the drop mode for this component. For backward compatibility,
+ * the default for this property is <code>DropMode.USE_SELECTION</code>.
+ * Usage of <code>DropMode.INSERT</code> is recommended, however,
+ * for an improved user experience. It offers similar behavior of dropping
+ * between text locations, but does so without affecting the actual text
+ * selection and caret location.
+ * <p>
+ * <code>JTextComponents</code> support the following drop modes:
+ * <ul>
+ * <li><code>DropMode.USE_SELECTION</code></li>
+ * <li><code>DropMode.INSERT</code></li>
+ * </ul>
+ * <p>
+ * The drop mode is only meaningful if this component has a
+ * <code>TransferHandler</code> that accepts drops.
+ *
+ * @param dropMode the drop mode to use
+ * @throws IllegalArgumentException if the drop mode is unsupported
+ * or <code>null</code>
+ * @see #getDropMode
+ * @see #getDropLocation
+ * @see #setTransferHandler
+ * @see javax.swing.TransferHandler
+ * @since 1.6
+ */
+ public final void setDropMode(DropMode dropMode) {
+ if (dropMode != null) {
+ switch (dropMode) {
+ case USE_SELECTION:
+ case INSERT:
+ this.dropMode = dropMode;
+ return;
+ }
+ }
+
+ throw new IllegalArgumentException(dropMode + ": Unsupported drop mode for text");
+ }
+
+ /**
+ * Returns the drop mode for this component.
+ *
+ * @return the drop mode for this component
+ * @see #setDropMode
+ * @since 1.6
+ */
+ public final DropMode getDropMode() {
+ return dropMode;
+ }
+
+
+ /**
+ * Calculates a drop location in this component, representing where a
+ * drop at the given point should insert data.
+ * <p>
+ * Note: This method is meant to override
+ * <code>JComponent.dropLocationForPoint()</code>, which is package-private
+ * in javax.swing. <code>TransferHandler</code> will detect text components
+ * and call this method instead via reflection. It's name should therefore
+ * not be changed.
+ *
+ * @param p the point to calculate a drop location for
+ * @return the drop location, or <code>null</code>
+ */
+ DropLocation dropLocationForPoint(Point p) {
+ Position.Bias[] bias = new Position.Bias[1];
+ int index = getUI().viewToModel(this, p, bias);
+
+ // viewToModel currently returns null for some HTML content
+ // when the point is within the component's top inset
+ if (bias[0] == null) {
+ bias[0] = Position.Bias.Forward;
+ }
+
+ return new DropLocation(p, index, bias[0]);
+ }
+
+ /**
+ * Called to set or clear the drop location during a DnD operation.
+ * In some cases, the component may need to use it's internal selection
+ * temporarily to indicate the drop location. To help facilitate this,
+ * this method returns and accepts as a parameter a state object.
+ * This state object can be used to store, and later restore, the selection
+ * state. Whatever this method returns will be passed back to it in
+ * future calls, as the state parameter. If it wants the DnD system to
+ * continue storing the same state, it must pass it back every time.
+ * Here's how this is used:
+ * <p>
+ * Let's say that on the first call to this method the component decides
+ * to save some state (because it is about to use the selection to show
+ * a drop index). It can return a state object to the caller encapsulating
+ * any saved selection state. On a second call, let's say the drop location
+ * is being changed to something else. The component doesn't need to
+ * restore anything yet, so it simply passes back the same state object
+ * to have the DnD system continue storing it. Finally, let's say this
+ * method is messaged with <code>null</code>. This means DnD
+ * is finished with this component for now, meaning it should restore
+ * state. At this point, it can use the state parameter to restore
+ * said state, and of course return <code>null</code> since there's
+ * no longer anything to store.
+ * <p>
+ * Note: This method is meant to override
+ * <code>JComponent.setDropLocation()</code>, which is package-private
+ * in javax.swing. <code>TransferHandler</code> will detect text components
+ * and call this method instead via reflection. It's name should therefore
+ * not be changed.
+ *
+ * @param location the drop location (as calculated by
+ * <code>dropLocationForPoint</code>) or <code>null</code>
+ * if there's no longer a valid drop location
+ * @param state the state object saved earlier for this component,
+ * or <code>null</code>
+ * @param forDrop whether or not the method is being called because an
+ * actual drop occurred
+ * @return any saved state for this component, or <code>null</code> if none
+ */
+ Object setDropLocation(TransferHandler.DropLocation location,
+ Object state,
+ boolean forDrop) {
+
+ Object retVal = null;
+ DropLocation textLocation = (DropLocation)location;
+
+ if (dropMode == DropMode.USE_SELECTION) {
+ if (textLocation == null) {
+ if (state != null) {
+ /*
+ * This object represents the state saved earlier.
+ * If the caret is a DefaultCaret it will be
+ * an Object array containing, in order:
+ * - the saved caret mark (Integer)
+ * - the saved caret dot (Integer)
+ * - the saved caret visibility (Boolean)
+ * - the saved mark bias (Position.Bias)
+ * - the saved dot bias (Position.Bias)
+ * If the caret is not a DefaultCaret it will
+ * be similar, but will not contain the dot
+ * or mark bias.
+ */
+ Object[] vals = (Object[])state;
+
+ if (!forDrop) {
+ if (caret instanceof DefaultCaret) {
+ ((DefaultCaret)caret).setDot(((Integer)vals[0]).intValue(),
+ (Position.Bias)vals[3]);
+ ((DefaultCaret)caret).moveDot(((Integer)vals[1]).intValue(),
+ (Position.Bias)vals[4]);
+ } else {
+ caret.setDot(((Integer)vals[0]).intValue());
+ caret.moveDot(((Integer)vals[1]).intValue());
+ }
+ }
+
+ caret.setVisible(((Boolean)vals[2]).booleanValue());
+ }
+ } else {
+ if (dropLocation == null) {
+ boolean visible;
+
+ if (caret instanceof DefaultCaret) {
+ DefaultCaret dc = (DefaultCaret)caret;
+ visible = dc.isActive();
+ retVal = new Object[] {Integer.valueOf(dc.getMark()),
+ Integer.valueOf(dc.getDot()),
+ Boolean.valueOf(visible),
+ dc.getMarkBias(),
+ dc.getDotBias()};
+ } else {
+ visible = caret.isVisible();
+ retVal = new Object[] {Integer.valueOf(caret.getMark()),
+ Integer.valueOf(caret.getDot()),
+ Boolean.valueOf(visible)};
+ }
+
+ caret.setVisible(true);
+ } else {
+ retVal = state;
+ }
+
+ if (caret instanceof DefaultCaret) {
+ ((DefaultCaret)caret).setDot(textLocation.getIndex(), textLocation.getBias());
+ } else {
+ caret.setDot(textLocation.getIndex());
+ }
+ }
+ } else {
+ if (textLocation == null) {
+ if (state != null) {
+ caret.setVisible(((Boolean)state).booleanValue());
+ }
+ } else {
+ if (dropLocation == null) {
+ boolean visible = caret instanceof DefaultCaret
+ ? ((DefaultCaret)caret).isActive()
+ : caret.isVisible();
+ retVal = Boolean.valueOf(visible);
+ caret.setVisible(false);
+ } else {
+ retVal = state;
+ }
+ }
+ }
+
+ DropLocation old = dropLocation;
+ dropLocation = textLocation;
+ firePropertyChange("dropLocation", old, dropLocation);
+
+ return retVal;
+ }
+
+ /**
+ * Returns the location that this component should visually indicate
+ * as the drop location during a DnD operation over the component,
+ * or {@code null} if no location is to currently be shown.
+ * <p>
+ * This method is not meant for querying the drop location
+ * from a {@code TransferHandler}, as the drop location is only
+ * set after the {@code TransferHandler}'s <code>canImport</code>
+ * has returned and has allowed for the location to be shown.
+ * <p>
+ * When this property changes, a property change event with
+ * name "dropLocation" is fired by the component.
+ *
+ * @return the drop location
+ * @see #setDropMode
+ * @see TransferHandler#canImport(TransferHandler.TransferSupport)
+ * @since 1.6
+ */
+ public final DropLocation getDropLocation() {
+ return dropLocation;
+ }
+
+
+ /**
+ * Updates the <code>InputMap</code>s in response to a
+ * <code>Keymap</code> change.
+ * @param oldKm the old <code>Keymap</code>
+ * @param newKm the new <code>Keymap</code>
+ */
+ void updateInputMap(Keymap oldKm, Keymap newKm) {
+ // Locate the current KeymapWrapper.
+ InputMap km = getInputMap(JComponent.WHEN_FOCUSED);
+ InputMap last = km;
+ while (km != null && !(km instanceof KeymapWrapper)) {
+ last = km;
+ km = km.getParent();
+ }
+ if (km != null) {
+ // Found it, tweak the InputMap that points to it, as well
+ // as anything it points to.
+ if (newKm == null) {
+ if (last != km) {
+ last.setParent(km.getParent());
+ }
+ else {
+ last.setParent(null);
+ }
+ }
+ else {
+ InputMap newKM = new KeymapWrapper(newKm);
+ last.setParent(newKM);
+ if (last != km) {
+ newKM.setParent(km.getParent());
+ }
+ }
+ }
+ else if (newKm != null) {
+ km = getInputMap(JComponent.WHEN_FOCUSED);
+ if (km != null) {
+ // Couldn't find it.
+ // Set the parent of WHEN_FOCUSED InputMap to be the new one.
+ InputMap newKM = new KeymapWrapper(newKm);
+ newKM.setParent(km.getParent());
+ km.setParent(newKM);
+ }
+ }
+
+ // Do the same thing with the ActionMap
+ ActionMap am = getActionMap();
+ ActionMap lastAM = am;
+ while (am != null && !(am instanceof KeymapActionMap)) {
+ lastAM = am;
+ am = am.getParent();
+ }
+ if (am != null) {
+ // Found it, tweak the Actionap that points to it, as well
+ // as anything it points to.
+ if (newKm == null) {
+ if (lastAM != am) {
+ lastAM.setParent(am.getParent());
+ }
+ else {
+ lastAM.setParent(null);
+ }
+ }
+ else {
+ ActionMap newAM = new KeymapActionMap(newKm);
+ lastAM.setParent(newAM);
+ if (lastAM != am) {
+ newAM.setParent(am.getParent());
+ }
+ }
+ }
+ else if (newKm != null) {
+ am = getActionMap();
+ if (am != null) {
+ // Couldn't find it.
+ // Set the parent of ActionMap to be the new one.
+ ActionMap newAM = new KeymapActionMap(newKm);
+ newAM.setParent(am.getParent());
+ am.setParent(newAM);
+ }
+ }
+ }
+
+ /**
+ * Fetches the keymap currently active in this text
+ * component.
+ *
+ * @return the keymap
+ */
+ public Keymap getKeymap() {
+ return keymap;
+ }
+
+ /**
+ * Adds a new keymap into the keymap hierarchy. Keymap bindings
+ * resolve from bottom up so an attribute specified in a child
+ * will override an attribute specified in the parent.
+ *
+ * @param nm the name of the keymap (must be unique within the
+ * collection of named keymaps in the document); the name may
+ * be <code>null</code> if the keymap is unnamed,
+ * but the caller is responsible for managing the reference
+ * returned as an unnamed keymap can't
+ * be fetched by name
+ * @param parent the parent keymap; this may be <code>null</code> if
+ * unspecified bindings need not be resolved in some other keymap
+ * @return the keymap
+ */
+ public static Keymap addKeymap(String nm, Keymap parent) {
+ Keymap map = new DefaultKeymap(nm, parent);
+ if (nm != null) {
+ // add a named keymap, a class of bindings
+ getKeymapTable().put(nm, map);
+ }
+ return map;
+ }
+
+ /**
+ * Removes a named keymap previously added to the document. Keymaps
+ * with <code>null</code> names may not be removed in this way.
+ *
+ * @param nm the name of the keymap to remove
+ * @return the keymap that was removed
+ */
+ public static Keymap removeKeymap(String nm) {
+ return getKeymapTable().remove(nm);
+ }
+
+ /**
+ * Fetches a named keymap previously added to the document.
+ * This does not work with <code>null</code>-named keymaps.
+ *
+ * @param nm the name of the keymap
+ * @return the keymap
+ */
+ public static Keymap getKeymap(String nm) {
+ return getKeymapTable().get(nm);
+ }
+
+ private static HashMap<String,Keymap> getKeymapTable() {
+ synchronized (KEYMAP_TABLE) {
+ AppContext appContext = AppContext.getAppContext();
+ HashMap<String,Keymap> keymapTable =
+ (HashMap<String,Keymap>)appContext.get(KEYMAP_TABLE);
+ if (keymapTable == null) {
+ keymapTable = new HashMap<String,Keymap>(17);
+ appContext.put(KEYMAP_TABLE, keymapTable);
+ //initialize default keymap
+ Keymap binding = addKeymap(DEFAULT_KEYMAP, null);
+ binding.setDefaultAction(new
+ DefaultEditorKit.DefaultKeyTypedAction());
+ }
+ return keymapTable;
+ }
+ }
+
+ /**
+ * Binding record for creating key bindings.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class KeyBinding {
+
+ /**
+ * The key.
+ */
+ public KeyStroke key;
+
+ /**
+ * The name of the action for the key.
+ */
+ public String actionName;
+
+ /**
+ * Creates a new key binding.
+ *
+ * @param key the key
+ * @param actionName the name of the action for the key
+ */
+ public KeyBinding(KeyStroke key, String actionName) {
+ this.key = key;
+ this.actionName = actionName;
+ }
+ }
+
+ /**
+ * <p>
+ * Loads a keymap with a bunch of
+ * bindings. This can be used to take a static table of
+ * definitions and load them into some keymap. The following
+ * example illustrates an example of binding some keys to
+ * the cut, copy, and paste actions associated with a
+ * JTextComponent. A code fragment to accomplish
+ * this might look as follows:
+ * <pre><code>
+ *
+ * static final JTextComponent.KeyBinding[] defaultBindings = {
+ * new JTextComponent.KeyBinding(
+ * KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK),
+ * DefaultEditorKit.copyAction),
+ * new JTextComponent.KeyBinding(
+ * KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK),
+ * DefaultEditorKit.pasteAction),
+ * new JTextComponent.KeyBinding(
+ * KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_MASK),
+ * DefaultEditorKit.cutAction),
+ * };
+ *
+ * JTextComponent c = new JTextPane();
+ * Keymap k = c.getKeymap();
+ * JTextComponent.loadKeymap(k, defaultBindings, c.getActions());
+ *
+ * </code></pre>
+ * The sets of bindings and actions may be empty but must be
+ * non-<code>null</code>.
+ *
+ * @param map the keymap
+ * @param bindings the bindings
+ * @param actions the set of actions
+ */
+ public static void loadKeymap(Keymap map, KeyBinding[] bindings, Action[] actions) {
+ Hashtable h = new Hashtable();
+ for (int i = 0; i < actions.length; i++) {
+ Action a = actions[i];
+ String value = (String)a.getValue(Action.NAME);
+ h.put((value!=null ? value:""), a);
+ }
+ for (int i = 0; i < bindings.length; i++) {
+ Action a = (Action) h.get(bindings[i].actionName);
+ if (a != null) {
+ map.addActionForKeyStroke(bindings[i].key, a);
+ }
+ }
+ }
+
+ /**
+ * Returns true if <code>klass</code> is NOT a JTextComponent and it or
+ * one of its superclasses (stoping at JTextComponent) overrides
+ * <code>processInputMethodEvent</code>. It is assumed this will be
+ * invoked from within a <code>doPrivileged</code>, and it is also
+ * assumed <code>klass</code> extends <code>JTextComponent</code>.
+ */
+ private static Boolean isProcessInputMethodEventOverridden(Class klass) {
+ if (klass == JTextComponent.class) {
+ return Boolean.FALSE;
+ }
+ Boolean retValue = (Boolean)overrideMap.get(klass.getName());
+
+ if (retValue != null) {
+ return retValue;
+ }
+ Boolean sOverriden = isProcessInputMethodEventOverridden(
+ klass.getSuperclass());
+
+ if (sOverriden.booleanValue()) {
+ // If our superclass has overriden it, then by definition klass
+ // overrides it.
+ overrideMap.put(klass.getName(), sOverriden);
+ return sOverriden;
+ }
+ // klass's superclass didn't override it, check for an override in
+ // klass.
+ try {
+ Class[] classes = new Class[1];
+ classes[0] = InputMethodEvent.class;
+
+ Method m = klass.getDeclaredMethod("processInputMethodEvent",
+ classes);
+ retValue = Boolean.TRUE;
+ } catch (NoSuchMethodException nsme) {
+ retValue = Boolean.FALSE;
+ }
+ overrideMap.put(klass.getName(), retValue);
+ return retValue;
+ }
+
+ /**
+ * Fetches the current color used to render the
+ * caret.
+ *
+ * @return the color
+ */
+ public Color getCaretColor() {
+ return caretColor;
+ }
+
+ /**
+ * Sets the current color used to render the caret.
+ * Setting to <code>null</code> effectively restores the default color.
+ * Setting the color results in a PropertyChange event ("caretColor")
+ * being fired.
+ *
+ * @param c the color
+ * @see #getCaretColor
+ * @beaninfo
+ * description: the color used to render the caret
+ * bound: true
+ * preferred: true
+ */
+ public void setCaretColor(Color c) {
+ Color old = caretColor;
+ caretColor = c;
+ firePropertyChange("caretColor", old, caretColor);
+ }
+
+ /**
+ * Fetches the current color used to render the
+ * selection.
+ *
+ * @return the color
+ */
+ public Color getSelectionColor() {
+ return selectionColor;
+ }
+
+ /**
+ * Sets the current color used to render the selection.
+ * Setting the color to <code>null</code> is the same as setting
+ * <code>Color.white</code>. Setting the color results in a
+ * PropertyChange event ("selectionColor").
+ *
+ * @param c the color
+ * @see #getSelectionColor
+ * @beaninfo
+ * description: color used to render selection background
+ * bound: true
+ * preferred: true
+ */
+ public void setSelectionColor(Color c) {
+ Color old = selectionColor;
+ selectionColor = c;
+ firePropertyChange("selectionColor", old, selectionColor);
+ }
+
+ /**
+ * Fetches the current color used to render the
+ * selected text.
+ *
+ * @return the color
+ */
+ public Color getSelectedTextColor() {
+ return selectedTextColor;
+ }
+
+ /**
+ * Sets the current color used to render the selected text.
+ * Setting the color to <code>null</code> is the same as
+ * <code>Color.black</code>. Setting the color results in a
+ * PropertyChange event ("selectedTextColor") being fired.
+ *
+ * @param c the color
+ * @see #getSelectedTextColor
+ * @beaninfo
+ * description: color used to render selected text
+ * bound: true
+ * preferred: true
+ */
+ public void setSelectedTextColor(Color c) {
+ Color old = selectedTextColor;
+ selectedTextColor = c;
+ firePropertyChange("selectedTextColor", old, selectedTextColor);
+ }
+
+ /**
+ * Fetches the current color used to render the
+ * disabled text.
+ *
+ * @return the color
+ */
+ public Color getDisabledTextColor() {
+ return disabledTextColor;
+ }
+
+ /**
+ * Sets the current color used to render the
+ * disabled text. Setting the color fires off a
+ * PropertyChange event ("disabledTextColor").
+ *
+ * @param c the color
+ * @see #getDisabledTextColor
+ * @beaninfo
+ * description: color used to render disabled text
+ * bound: true
+ * preferred: true
+ */
+ public void setDisabledTextColor(Color c) {
+ Color old = disabledTextColor;
+ disabledTextColor = c;
+ firePropertyChange("disabledTextColor", old, disabledTextColor);
+ }
+
+ /**
+ * Replaces the currently selected content with new content
+ * represented by the given string. If there is no selection
+ * this amounts to an insert of the given text. If there
+ * is no replacement text this amounts to a removal of the
+ * current selection.
+ * <p>
+ * This is the method that is used by the default implementation
+ * of the action for inserting content that gets bound to the
+ * keymap actions.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param content the content to replace the selection with
+ */
+ public void replaceSelection(String content) {
+ Document doc = getDocument();
+ if (doc != null) {
+ try {
+ boolean composedTextSaved = saveComposedText(caret.getDot());
+ int p0 = Math.min(caret.getDot(), caret.getMark());
+ int p1 = Math.max(caret.getDot(), caret.getMark());
+ if (doc instanceof AbstractDocument) {
+ ((AbstractDocument)doc).replace(p0, p1 - p0, content,null);
+ }
+ else {
+ if (p0 != p1) {
+ doc.remove(p0, p1 - p0);
+ }
+ if (content != null && content.length() > 0) {
+ doc.insertString(p0, content, null);
+ }
+ }
+ if (composedTextSaved) {
+ restoreComposedText();
+ }
+ } catch (BadLocationException e) {
+ UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this);
+ }
+ }
+ }
+
+ /**
+ * Fetches a portion of the text represented by the
+ * component. Returns an empty string if length is 0.
+ *
+ * @param offs the offset >= 0
+ * @param len the length >= 0
+ * @return the text
+ * @exception BadLocationException if the offset or length are invalid
+ */
+ public String getText(int offs, int len) throws BadLocationException {
+ return getDocument().getText(offs, len);
+ }
+
+ /**
+ * Converts the given location in the model to a place in
+ * the view coordinate system.
+ * The component must have a positive size for
+ * this translation to be computed (i.e. layout cannot
+ * be computed until the component has been sized). The
+ * component does not have to be visible or painted.
+ *
+ * @param pos the position >= 0
+ * @return the coordinates as a rectangle, with (r.x, r.y) as the location
+ * in the coordinate system, or null if the component does
+ * not yet have a positive size.
+ * @exception BadLocationException if the given position does not
+ * represent a valid location in the associated document
+ * @see TextUI#modelToView
+ */
+ public Rectangle modelToView(int pos) throws BadLocationException {
+ return getUI().modelToView(this, pos);
+ }
+
+ /**
+ * Converts the given place in the view coordinate system
+ * to the nearest representative location in the model.
+ * The component must have a positive size for
+ * this translation to be computed (i.e. layout cannot
+ * be computed until the component has been sized). The
+ * component does not have to be visible or painted.
+ *
+ * @param pt the location in the view to translate
+ * @return the offset >= 0 from the start of the document,
+ * or -1 if the component does not yet have a positive
+ * size.
+ * @see TextUI#viewToModel
+ */
+ public int viewToModel(Point pt) {
+ return getUI().viewToModel(this, pt);
+ }
+
+ /**
+ * Transfers the currently selected range in the associated
+ * text model to the system clipboard, removing the contents
+ * from the model. The current selection is reset. Does nothing
+ * for <code>null</code> selections.
+ *
+ * @see java.awt.Toolkit#getSystemClipboard
+ * @see java.awt.datatransfer.Clipboard
+ */
+ public void cut() {
+ if (isEditable() && isEnabled()) {
+ invokeAction("cut", TransferHandler.getCutAction());
+ }
+ }
+
+ /**
+ * Transfers the currently selected range in the associated
+ * text model to the system clipboard, leaving the contents
+ * in the text model. The current selection remains intact.
+ * Does nothing for <code>null</code> selections.
+ *
+ * @see java.awt.Toolkit#getSystemClipboard
+ * @see java.awt.datatransfer.Clipboard
+ */
+ public void copy() {
+ invokeAction("copy", TransferHandler.getCopyAction());
+ }
+
+ /**
+ * Transfers the contents of the system clipboard into the
+ * associated text model. If there is a selection in the
+ * associated view, it is replaced with the contents of the
+ * clipboard. If there is no selection, the clipboard contents
+ * are inserted in front of the current insert position in
+ * the associated view. If the clipboard is empty, does nothing.
+ *
+ * @see #replaceSelection
+ * @see java.awt.Toolkit#getSystemClipboard
+ * @see java.awt.datatransfer.Clipboard
+ */
+ public void paste() {
+ if (isEditable() && isEnabled()) {
+ invokeAction("paste", TransferHandler.getPasteAction());
+ }
+ }
+
+ /**
+ * This is a conveniance method that is only useful for
+ * <code>cut</code>, <code>copy</code> and <code>paste</code>. If
+ * an <code>Action</code> with the name <code>name</code> does not
+ * exist in the <code>ActionMap</code>, this will attemp to install a
+ * <code>TransferHandler</code> and then use <code>altAction</code>.
+ */
+ private void invokeAction(String name, Action altAction) {
+ ActionMap map = getActionMap();
+ Action action = null;
+
+ if (map != null) {
+ action = map.get(name);
+ }
+ if (action == null) {
+ installDefaultTransferHandlerIfNecessary();
+ action = altAction;
+ }
+ action.actionPerformed(new ActionEvent(this,
+ ActionEvent.ACTION_PERFORMED, (String)action.
+ getValue(Action.NAME),
+ EventQueue.getMostRecentEventTime(),
+ getCurrentEventModifiers()));
+ }
+
+ /**
+ * If the current <code>TransferHandler</code> is null, this will
+ * install a new one.
+ */
+ private void installDefaultTransferHandlerIfNecessary() {
+ if (getTransferHandler() == null) {
+ if (defaultTransferHandler == null) {
+ defaultTransferHandler = new DefaultTransferHandler();
+ }
+ setTransferHandler(defaultTransferHandler);
+ }
+ }
+
+ /**
+ * Moves the caret to a new position, leaving behind a mark
+ * defined by the last time <code>setCaretPosition</code> was
+ * called. This forms a selection.
+ * If the document is <code>null</code>, does nothing. The position
+ * must be between 0 and the length of the component's text or else
+ * an exception is thrown.
+ *
+ * @param pos the position
+ * @exception IllegalArgumentException if the value supplied
+ * for <code>position</code> is less than zero or greater
+ * than the component's text length
+ * @see #setCaretPosition
+ */
+ public void moveCaretPosition(int pos) {
+ Document doc = getDocument();
+ if (doc != null) {
+ if (pos > doc.getLength() || pos < 0) {
+ throw new IllegalArgumentException("bad position: " + pos);
+ }
+ caret.moveDot(pos);
+ }
+ }
+
+ /**
+ * The bound property name for the focus accelerator.
+ */
+ public static final String FOCUS_ACCELERATOR_KEY = "focusAcceleratorKey";
+
+ /**
+ * Sets the key accelerator that will cause the receiving text
+ * component to get the focus. The accelerator will be the
+ * key combination of the <em>alt</em> key and the character
+ * given (converted to upper case). By default, there is no focus
+ * accelerator key. Any previous key accelerator setting will be
+ * superseded. A '\0' key setting will be registered, and has the
+ * effect of turning off the focus accelerator. When the new key
+ * is set, a PropertyChange event (FOCUS_ACCELERATOR_KEY) will be fired.
+ *
+ * @param aKey the key
+ * @see #getFocusAccelerator
+ * @beaninfo
+ * description: accelerator character used to grab focus
+ * bound: true
+ */
+ public void setFocusAccelerator(char aKey) {
+ aKey = Character.toUpperCase(aKey);
+ char old = focusAccelerator;
+ focusAccelerator = aKey;
+ // Fix for 4341002: value of FOCUS_ACCELERATOR_KEY is wrong.
+ // So we fire both FOCUS_ACCELERATOR_KEY, for compatibility,
+ // and the correct event here.
+ firePropertyChange(FOCUS_ACCELERATOR_KEY, old, focusAccelerator);
+ firePropertyChange("focusAccelerator", old, focusAccelerator);
+ }
+
+ /**
+ * Returns the key accelerator that will cause the receiving
+ * text component to get the focus. Return '\0' if no focus
+ * accelerator has been set.
+ *
+ * @return the key
+ */
+ public char getFocusAccelerator() {
+ return focusAccelerator;
+ }
+
+ /**
+ * Initializes from a stream. This creates a
+ * model of the type appropriate for the component
+ * and initializes the model from the stream.
+ * By default this will load the model as plain
+ * text. Previous contents of the model are discarded.
+ *
+ * @param in the stream to read from
+ * @param desc an object describing the stream; this
+ * might be a string, a File, a URL, etc. Some kinds
+ * of documents (such as html for example) might be
+ * able to make use of this information; if non-<code>null</code>,
+ * it is added as a property of the document
+ * @exception IOException as thrown by the stream being
+ * used to initialize
+ * @see EditorKit#createDefaultDocument
+ * @see #setDocument
+ * @see PlainDocument
+ */
+ public void read(Reader in, Object desc) throws IOException {
+ EditorKit kit = getUI().getEditorKit(this);
+ Document doc = kit.createDefaultDocument();
+ if (desc != null) {
+ doc.putProperty(Document.StreamDescriptionProperty, desc);
+ }
+ try {
+ kit.read(in, doc, 0);
+ setDocument(doc);
+ } catch (BadLocationException e) {
+ throw new IOException(e.getMessage());
+ }
+ }
+
+ /**
+ * Stores the contents of the model into the given
+ * stream. By default this will store the model as plain
+ * text.
+ *
+ * @param out the output stream
+ * @exception IOException on any I/O error
+ */
+ public void write(Writer out) throws IOException {
+ Document doc = getDocument();
+ try {
+ getUI().getEditorKit(this).write(out, doc, 0, doc.getLength());
+ } catch (BadLocationException e) {
+ throw new IOException(e.getMessage());
+ }
+ }
+
+ public void removeNotify() {
+ super.removeNotify();
+ if (getFocusedComponent() == this) {
+ AppContext.getAppContext().remove(FOCUSED_COMPONENT);
+ }
+ }
+
+ // --- java.awt.TextComponent methods ------------------------
+
+ /**
+ * Sets the position of the text insertion caret for the
+ * <code>TextComponent</code>. Note that the caret tracks change,
+ * so this may move if the underlying text of the component is changed.
+ * If the document is <code>null</code>, does nothing. The position
+ * must be between 0 and the length of the component's text or else
+ * an exception is thrown.
+ *
+ * @param position the position
+ * @exception IllegalArgumentException if the value supplied
+ * for <code>position</code> is less than zero or greater
+ * than the component's text length
+ * @beaninfo
+ * description: the caret position
+ */
+ public void setCaretPosition(int position) {
+ Document doc = getDocument();
+ if (doc != null) {
+ if (position > doc.getLength() || position < 0) {
+ throw new IllegalArgumentException("bad position: " + position);
+ }
+ caret.setDot(position);
+ }
+ }
+
+ /**
+ * Returns the position of the text insertion caret for the
+ * text component.
+ *
+ * @return the position of the text insertion caret for the
+ * text component >= 0
+ */
+ public int getCaretPosition() {
+ return caret.getDot();
+ }
+
+ /**
+ * Sets the text of this <code>TextComponent</code>
+ * to the specified text. If the text is <code>null</code>
+ * or empty, has the effect of simply deleting the old text.
+ * When text has been inserted, the resulting caret location
+ * is determined by the implementation of the caret class.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * Note that text is not a bound property, so no <code>PropertyChangeEvent
+ * </code> is fired when it changes. To listen for changes to the text,
+ * use <code>DocumentListener</code>.
+ *
+ * @param t the new text to be set
+ * @see #getText
+ * @see DefaultCaret
+ * @beaninfo
+ * description: the text of this component
+ */
+ public void setText(String t) {
+ try {
+ Document doc = getDocument();
+ if (doc instanceof AbstractDocument) {
+ ((AbstractDocument)doc).replace(0, doc.getLength(), t,null);
+ }
+ else {
+ doc.remove(0, doc.getLength());
+ doc.insertString(0, t, null);
+ }
+ } catch (BadLocationException e) {
+ UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this);
+ }
+ }
+
+ /**
+ * Returns the text contained in this <code>TextComponent</code>.
+ * If the underlying document is <code>null</code>,
+ * will give a <code>NullPointerException</code>.
+ *
+ * Note that text is not a bound property, so no <code>PropertyChangeEvent
+ * </code> is fired when it changes. To listen for changes to the text,
+ * use <code>DocumentListener</code>.
+ *
+ * @return the text
+ * @exception NullPointerException if the document is <code>null</code>
+ * @see #setText
+ */
+ public String getText() {
+ Document doc = getDocument();
+ String txt;
+ try {
+ txt = doc.getText(0, doc.getLength());
+ } catch (BadLocationException e) {
+ txt = null;
+ }
+ return txt;
+ }
+
+ /**
+ * Returns the selected text contained in this
+ * <code>TextComponent</code>. If the selection is
+ * <code>null</code> or the document empty, returns <code>null</code>.
+ *
+ * @return the text
+ * @exception IllegalArgumentException if the selection doesn't
+ * have a valid mapping into the document for some reason
+ * @see #setText
+ */
+ public String getSelectedText() {
+ String txt = null;
+ int p0 = Math.min(caret.getDot(), caret.getMark());
+ int p1 = Math.max(caret.getDot(), caret.getMark());
+ if (p0 != p1) {
+ try {
+ Document doc = getDocument();
+ txt = doc.getText(p0, p1 - p0);
+ } catch (BadLocationException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+ return txt;
+ }
+
+ /**
+ * Returns the boolean indicating whether this
+ * <code>TextComponent</code> is editable or not.
+ *
+ * @return the boolean value
+ * @see #setEditable
+ */
+ public boolean isEditable() {
+ return editable;
+ }
+
+ /**
+ * Sets the specified boolean to indicate whether or not this
+ * <code>TextComponent</code> should be editable.
+ * A PropertyChange event ("editable") is fired when the
+ * state is changed.
+ *
+ * @param b the boolean to be set
+ * @see #isEditable
+ * @beaninfo
+ * description: specifies if the text can be edited
+ * bound: true
+ */
+ public void setEditable(boolean b) {
+ if (b != editable) {
+ boolean oldVal = editable;
+ editable = b;
+ enableInputMethods(editable);
+ firePropertyChange("editable", Boolean.valueOf(oldVal), Boolean.valueOf(editable));
+ repaint();
+ }
+ }
+
+ /**
+ * Returns the selected text's start position. Return 0 for an
+ * empty document, or the value of dot if no selection.
+ *
+ * @return the start position >= 0
+ */
+ public int getSelectionStart() {
+ int start = Math.min(caret.getDot(), caret.getMark());
+ return start;
+ }
+
+ /**
+ * Sets the selection start to the specified position. The new
+ * starting point is constrained to be before or at the current
+ * selection end.
+ * <p>
+ * This is available for backward compatibility to code
+ * that called this method on <code>java.awt.TextComponent</code>.
+ * This is implemented to forward to the <code>Caret</code>
+ * implementation which is where the actual selection is maintained.
+ *
+ * @param selectionStart the start position of the text >= 0
+ * @beaninfo
+ * description: starting location of the selection.
+ */
+ public void setSelectionStart(int selectionStart) {
+ /* Route through select method to enforce consistent policy
+ * between selectionStart and selectionEnd.
+ */
+ select(selectionStart, getSelectionEnd());
+ }
+
+ /**
+ * Returns the selected text's end position. Return 0 if the document
+ * is empty, or the value of dot if there is no selection.
+ *
+ * @return the end position >= 0
+ */
+ public int getSelectionEnd() {
+ int end = Math.max(caret.getDot(), caret.getMark());
+ return end;
+ }
+
+ /**
+ * Sets the selection end to the specified position. The new
+ * end point is constrained to be at or after the current
+ * selection start.
+ * <p>
+ * This is available for backward compatibility to code
+ * that called this method on <code>java.awt.TextComponent</code>.
+ * This is implemented to forward to the <code>Caret</code>
+ * implementation which is where the actual selection is maintained.
+ *
+ * @param selectionEnd the end position of the text >= 0
+ * @beaninfo
+ * description: ending location of the selection.
+ */
+ public void setSelectionEnd(int selectionEnd) {
+ /* Route through select method to enforce consistent policy
+ * between selectionStart and selectionEnd.
+ */
+ select(getSelectionStart(), selectionEnd);
+ }
+
+ /**
+ * Selects the text between the specified start and end positions.
+ * <p>
+ * This method sets the start and end positions of the
+ * selected text, enforcing the restriction that the start position
+ * must be greater than or equal to zero. The end position must be
+ * greater than or equal to the start position, and less than or
+ * equal to the length of the text component's text.
+ * <p>
+ * If the caller supplies values that are inconsistent or out of
+ * bounds, the method enforces these constraints silently, and
+ * without failure. Specifically, if the start position or end
+ * position is greater than the length of the text, it is reset to
+ * equal the text length. If the start position is less than zero,
+ * it is reset to zero, and if the end position is less than the
+ * start position, it is reset to the start position.
+ * <p>
+ * This call is provided for backward compatibility.
+ * It is routed to a call to <code>setCaretPosition</code>
+ * followed by a call to <code>moveCaretPosition</code>.
+ * The preferred way to manage selection is by calling
+ * those methods directly.
+ *
+ * @param selectionStart the start position of the text
+ * @param selectionEnd the end position of the text
+ * @see #setCaretPosition
+ * @see #moveCaretPosition
+ */
+ public void select(int selectionStart, int selectionEnd) {
+ // argument adjustment done by java.awt.TextComponent
+ int docLength = getDocument().getLength();
+
+ if (selectionStart < 0) {
+ selectionStart = 0;
+ }
+ if (selectionStart > docLength) {
+ selectionStart = docLength;
+ }
+ if (selectionEnd > docLength) {
+ selectionEnd = docLength;
+ }
+ if (selectionEnd < selectionStart) {
+ selectionEnd = selectionStart;
+ }
+
+ setCaretPosition(selectionStart);
+ moveCaretPosition(selectionEnd);
+ }
+
+ /**
+ * Selects all the text in the <code>TextComponent</code>.
+ * Does nothing on a <code>null</code> or empty document.
+ */
+ public void selectAll() {
+ Document doc = getDocument();
+ if (doc != null) {
+ setCaretPosition(0);
+ moveCaretPosition(doc.getLength());
+ }
+ }
+
+ // --- Tooltip Methods ---------------------------------------------
+
+ /**
+ * Returns the string to be used as the tooltip for <code>event</code>.
+ * This will return one of:
+ * <ol>
+ * <li>If <code>setToolTipText</code> has been invoked with a
+ * non-<code>null</code>
+ * value, it will be returned, otherwise
+ * <li>The value from invoking <code>getToolTipText</code> on
+ * the UI will be returned.
+ * </ol>
+ * By default <code>JTextComponent</code> does not register
+ * itself with the <code>ToolTipManager</code>.
+ * This means that tooltips will NOT be shown from the
+ * <code>TextUI</code> unless <code>registerComponent</code> has
+ * been invoked on the <code>ToolTipManager</code>.
+ *
+ * @param event the event in question
+ * @return the string to be used as the tooltip for <code>event</code>
+ * @see javax.swing.JComponent#setToolTipText
+ * @see javax.swing.plaf.TextUI#getToolTipText
+ * @see javax.swing.ToolTipManager#registerComponent
+ */
+ public String getToolTipText(MouseEvent event) {
+ String retValue = super.getToolTipText(event);
+
+ if (retValue == null) {
+ TextUI ui = getUI();
+ if (ui != null) {
+ retValue = ui.getToolTipText(this, new Point(event.getX(),
+ event.getY()));
+ }
+ }
+ return retValue;
+ }
+
+ // --- Scrollable methods ---------------------------------------------
+
+ /**
+ * Returns the preferred size of the viewport for a view component.
+ * This is implemented to do the default behavior of returning
+ * the preferred size of the component.
+ *
+ * @return the <code>preferredSize</code> of a <code>JViewport</code>
+ * whose view is this <code>Scrollable</code>
+ */
+ public Dimension getPreferredScrollableViewportSize() {
+ return getPreferredSize();
+ }
+
+
+ /**
+ * Components that display logical rows or columns should compute
+ * the scroll increment that will completely expose one new row
+ * or column, depending on the value of orientation. Ideally,
+ * components should handle a partially exposed row or column by
+ * returning the distance required to completely expose the item.
+ * <p>
+ * The default implementation of this is to simply return 10% of
+ * the visible area. Subclasses are likely to be able to provide
+ * a much more reasonable value.
+ *
+ * @param visibleRect the view area visible within the viewport
+ * @param orientation either <code>SwingConstants.VERTICAL</code> or
+ * <code>SwingConstants.HORIZONTAL</code>
+ * @param direction less than zero to scroll up/left, greater than
+ * zero for down/right
+ * @return the "unit" increment for scrolling in the specified direction
+ * @exception IllegalArgumentException for an invalid orientation
+ * @see JScrollBar#setUnitIncrement
+ */
+ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
+ switch(orientation) {
+ case SwingConstants.VERTICAL:
+ return visibleRect.height / 10;
+ case SwingConstants.HORIZONTAL:
+ return visibleRect.width / 10;
+ default:
+ throw new IllegalArgumentException("Invalid orientation: " + orientation);
+ }
+ }
+
+
+ /**
+ * Components that display logical rows or columns should compute
+ * the scroll increment that will completely expose one block
+ * of rows or columns, depending on the value of orientation.
+ * <p>
+ * The default implementation of this is to simply return the visible
+ * area. Subclasses will likely be able to provide a much more
+ * reasonable value.
+ *
+ * @param visibleRect the view area visible within the viewport
+ * @param orientation either <code>SwingConstants.VERTICAL</code> or
+ * <code>SwingConstants.HORIZONTAL</code>
+ * @param direction less than zero to scroll up/left, greater than zero
+ * for down/right
+ * @return the "block" increment for scrolling in the specified direction
+ * @exception IllegalArgumentException for an invalid orientation
+ * @see JScrollBar#setBlockIncrement
+ */
+ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
+ switch(orientation) {
+ case SwingConstants.VERTICAL:
+ return visibleRect.height;
+ case SwingConstants.HORIZONTAL:
+ return visibleRect.width;
+ default:
+ throw new IllegalArgumentException("Invalid orientation: " + orientation);
+ }
+ }
+
+
+ /**
+ * Returns true if a viewport should always force the width of this
+ * <code>Scrollable</code> to match the width of the viewport.
+ * For example a normal text view that supported line wrapping
+ * would return true here, since it would be undesirable for
+ * wrapped lines to disappear beyond the right
+ * edge of the viewport. Note that returning true for a
+ * <code>Scrollable</code> whose ancestor is a <code>JScrollPane</code>
+ * effectively disables horizontal scrolling.
+ * <p>
+ * Scrolling containers, like <code>JViewport</code>,
+ * will use this method each time they are validated.
+ *
+ * @return true if a viewport should force the <code>Scrollable</code>s
+ * width to match its own
+ */
+ public boolean getScrollableTracksViewportWidth() {
+ if (getParent() instanceof JViewport) {
+ return (((JViewport)getParent()).getWidth() > getPreferredSize().width);
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if a viewport should always force the height of this
+ * <code>Scrollable</code> to match the height of the viewport.
+ * For example a columnar text view that flowed text in left to
+ * right columns could effectively disable vertical scrolling by
+ * returning true here.
+ * <p>
+ * Scrolling containers, like <code>JViewport</code>,
+ * will use this method each time they are validated.
+ *
+ * @return true if a viewport should force the Scrollables height
+ * to match its own
+ */
+ public boolean getScrollableTracksViewportHeight() {
+ if (getParent() instanceof JViewport) {
+ return (((JViewport)getParent()).getHeight() > getPreferredSize().height);
+ }
+ return false;
+ }
+
+
+//////////////////
+// Printing Support
+//////////////////
+
+ /**
+ * A convenience print method that displays a print dialog, and then
+ * prints this {@code JTextComponent} in <i>interactive</i> mode with no
+ * header or footer text. Note: this method
+ * blocks until printing is done.
+ * <p>
+ * Note: In <i>headless</i> mode, no dialogs will be shown.
+ *
+ * <p> This method calls the full featured
+ * {@link #print(MessageFormat, MessageFormat, boolean, PrintService, PrintRequestAttributeSet, boolean)
+ * print} method to perform printing.
+ * @return {@code true}, unless printing is canceled by the user
+ * @throws PrinterException if an error in the print system causes the job
+ * to be aborted
+ * @throws SecurityException if this thread is not allowed to
+ * initiate a print job request
+ *
+ * @see #print(MessageFormat, MessageFormat, boolean, PrintService, PrintRequestAttributeSet, boolean)
+ *
+ * @since 1.6
+ */
+
+ public boolean print() throws PrinterException {
+ return print(null, null, true, null, null, true);
+ }
+
+ /**
+ * A convenience print method that displays a print dialog, and then
+ * prints this {@code JTextComponent} in <i>interactive</i> mode with
+ * the specified header and footer text. Note: this method
+ * blocks until printing is done.
+ * <p>
+ * Note: In <i>headless</i> mode, no dialogs will be shown.
+ *
+ * <p> This method calls the full featured
+ * {@link #print(MessageFormat, MessageFormat, boolean, PrintService, PrintRequestAttributeSet, boolean)
+ * print} method to perform printing.
+ * @param headerFormat the text, in {@code MessageFormat}, to be
+ * used as the header, or {@code null} for no header
+ * @param footerFormat the text, in {@code MessageFormat}, to be
+ * used as the footer, or {@code null} for no footer
+ * @return {@code true}, unless printing is canceled by the user
+ * @throws PrinterException if an error in the print system causes the job
+ * to be aborted
+ * @throws SecurityException if this thread is not allowed to
+ * initiate a print job request
+ *
+ * @see #print(MessageFormat, MessageFormat, boolean, PrintService, PrintRequestAttributeSet, boolean)
+ * @see java.text.MessageFormat
+ * @since 1.6
+ */
+ public boolean print(final MessageFormat headerFormat,
+ final MessageFormat footerFormat) throws PrinterException {
+ return print(headerFormat, footerFormat, true, null, null, true);
+ }
+
+ /**
+ * Prints the content of this {@code JTextComponent}. Note: this method
+ * blocks until printing is done.
+ *
+ * <p>
+ * Page header and footer text can be added to the output by providing
+ * {@code MessageFormat} arguments. The printing code requests
+ * {@code Strings} from the formats, providing a single item which may be
+ * included in the formatted string: an {@code Integer} representing the
+ * current page number.
+ *
+ * <p>
+ * {@code showPrintDialog boolean} parameter allows you to specify whether
+ * a print dialog is displayed to the user. When it is, the user
+ * may use the dialog to change printing attributes or even cancel the
+ * print.
+ *
+ * <p>
+ * {@code service} allows you to provide the initial
+ * {@code PrintService} for the print dialog, or to specify
+ * {@code PrintService} to print to when the dialog is not shown.
+ *
+ * <p>
+ * {@code attributes} can be used to provide the
+ * initial values for the print dialog, or to supply any needed
+ * attributes when the dialog is not shown. {@code attributes} can
+ * be used to control how the job will print, for example
+ * <i>duplex</i> or <i>single-sided</i>.
+ *
+ * <p>
+ * {@code interactive boolean} parameter allows you to specify
+ * whether to perform printing in <i>interactive</i>
+ * mode. If {@code true}, a progress dialog, with an abort option,
+ * is displayed for the duration of printing. This dialog is
+ * <i>modal</i> when {@code print} is invoked on the <i>Event Dispatch
+ * Thread</i> and <i>non-modal</i> otherwise. <b>Warning</b>:
+ * calling this method on the <i>Event Dispatch Thread</i> with {@code
+ * interactive false} blocks <i>all</i> events, including repaints, from
+ * being processed until printing is complete. It is only
+ * recommended when printing from an application with no
+ * visible GUI.
+ *
+ * <p>
+ * Note: In <i>headless</i> mode, {@code showPrintDialog} and
+ * {@code interactive} parameters are ignored and no dialogs are
+ * shown.
+ *
+ * <p>
+ * This method ensures the {@code document} is not mutated during printing.
+ * To indicate it visually, {@code setEnabled(false)} is set for the
+ * duration of printing.
+ *
+ * <p>
+ * This method uses {@link #getPrintable} to render document content.
+ *
+ * <p>
+ * This method is thread-safe, although most Swing methods are not. Please
+ * see <A
+ * HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">
+ * How to Use Threads</A> for more information.
+ *
+ * <p>
+ * <b>Sample Usage</b>. This code snippet shows a cross-platform print
+ * dialog and then prints the {@code JTextComponent} in <i>interactive</i> mode
+ * unless the user cancels the dialog:
+ *
+ * <pre>
+ * textComponent.print(new MessageFormat(&quot;My text component header&quot;),
+ * new MessageFormat(&quot;Footer. Page - {0}&quot;), true, null, null, true);
+ * </pre>
+ * <p>
+ * Executing this code off the <i>Event Dispatch Thread</i>
+ * performs printing on the <i>background</i>.
+ * The following pattern might be used for <i>background</i>
+ * printing:
+ * <pre>
+ * FutureTask&lt;Boolean&gt; future =
+ * new FutureTask&lt;Boolean&gt;(
+ * new Callable&lt;Boolean&gt;() {
+ * public Boolean call() {
+ * return textComponent.print(.....);
+ * }
+ * });
+ * executor.execute(future);
+ * </pre>
+ *
+ * @param headerFormat the text, in {@code MessageFormat}, to be
+ * used as the header, or {@code null} for no header
+ * @param footerFormat the text, in {@code MessageFormat}, to be
+ * used as the footer, or {@code null} for no footer
+ * @param showPrintDialog {@code true} to display a print dialog,
+ * {@code false} otherwise
+ * @param service initial {@code PrintService}, or {@code null} for the
+ * default
+ * @param attributes the job attributes to be applied to the print job, or
+ * {@code null} for none
+ * @param interactive whether to print in an interactive mode
+ * @return {@code true}, unless printing is canceled by the user
+ * @throws PrinterException if an error in the print system causes the job
+ * to be aborted
+ * @throws SecurityException if this thread is not allowed to
+ * initiate a print job request
+ *
+ * @see #getPrintable
+ * @see java.text.MessageFormat
+ * @see java.awt.GraphicsEnvironment#isHeadless
+ * @see java.util.concurrent.FutureTask
+ *
+ * @since 1.6
+ */
+ public boolean print(final MessageFormat headerFormat,
+ final MessageFormat footerFormat,
+ final boolean showPrintDialog,
+ final PrintService service,
+ final PrintRequestAttributeSet attributes,
+ final boolean interactive)
+ throws PrinterException {
+
+ final PrinterJob job = PrinterJob.getPrinterJob();
+ final Printable printable;
+ final PrintingStatus printingStatus;
+ final boolean isHeadless = GraphicsEnvironment.isHeadless();
+ final boolean isEventDispatchThread =
+ SwingUtilities.isEventDispatchThread();
+ final Printable textPrintable = getPrintable(headerFormat, footerFormat);
+ if (interactive && ! isHeadless) {
+ printingStatus =
+ PrintingStatus.createPrintingStatus(this, job);
+ printable =
+ printingStatus.createNotificationPrintable(textPrintable);
+ } else {
+ printingStatus = null;
+ printable = textPrintable;
+ }
+
+ if (service != null) {
+ job.setPrintService(service);
+ }
+
+ job.setPrintable(printable);
+
+ final PrintRequestAttributeSet attr = (attributes == null)
+ ? new HashPrintRequestAttributeSet()
+ : attributes;
+
+ if (showPrintDialog && ! isHeadless && ! job.printDialog(attr)) {
+ return false;
+ }
+
+ /*
+ * there are three cases for printing:
+ * 1. print non interactively (! interactive || isHeadless)
+ * 2. print interactively off EDT
+ * 3. print interactively on EDT
+ *
+ * 1 and 2 prints on the current thread (3 prints on another thread)
+ * 2 and 3 deal with PrintingStatusDialog
+ */
+ final Callable<Object> doPrint =
+ new Callable<Object>() {
+ public Object call() throws Exception {
+ try {
+ job.print(attr);
+ } finally {
+ if (printingStatus != null) {
+ printingStatus.dispose();
+ }
+ }
+ return null;
+ }
+ };
+
+ final FutureTask<Object> futurePrinting =
+ new FutureTask<Object>(doPrint);
+
+ final Runnable runnablePrinting =
+ new Runnable() {
+ public void run() {
+ //disable component
+ boolean wasEnabled = false;
+ if (isEventDispatchThread) {
+ if (isEnabled()) {
+ wasEnabled = true;
+ setEnabled(false);
+ }
+ } else {
+ try {
+ wasEnabled = SwingUtilities2.submit(
+ new Callable<Boolean>() {
+ public Boolean call() throws Exception {
+ boolean rv = isEnabled();
+ if (rv) {
+ setEnabled(false);
+ }
+ return rv;
+ }
+ }).get();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Error) {
+ throw (Error) cause;
+ }
+ if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ }
+ throw new AssertionError(cause);
+ }
+ }
+
+ getDocument().render(futurePrinting);
+
+ //enable component
+ if (wasEnabled) {
+ if (isEventDispatchThread) {
+ setEnabled(true);
+ } else {
+ try {
+ SwingUtilities2.submit(
+ new Runnable() {
+ public void run() {
+ setEnabled(true);
+ }
+ }, null).get();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Error) {
+ throw (Error) cause;
+ }
+ if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ }
+ throw new AssertionError(cause);
+ }
+ }
+ }
+ }
+ };
+
+ if (! interactive || isHeadless) {
+ runnablePrinting.run();
+ } else {
+ if (isEventDispatchThread) {
+ (new Thread(runnablePrinting)).start();
+ printingStatus.showModal(true);
+ } else {
+ printingStatus.showModal(false);
+ runnablePrinting.run();
+ }
+ }
+
+ //the printing is done successfully or otherwise.
+ //dialog is hidden if needed.
+ try {
+ futurePrinting.get();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof PrinterAbortException) {
+ if (printingStatus != null
+ && printingStatus.isAborted()) {
+ return false;
+ } else {
+ throw (PrinterAbortException) cause;
+ }
+ } else if (cause instanceof PrinterException) {
+ throw (PrinterException) cause;
+ } else if (cause instanceof RuntimeException) {
+ throw (RuntimeException) cause;
+ } else if (cause instanceof Error) {
+ throw (Error) cause;
+ } else {
+ throw new AssertionError(cause);
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Returns a {@code Printable} to use for printing the content of this
+ * {@code JTextComponent}. The returned {@code Printable} prints
+ * the document as it looks on the screen except being reformatted
+ * to fit the paper.
+ * The returned {@code Printable} can be wrapped inside another
+ * {@code Printable} in order to create complex reports and
+ * documents.
+ *
+ *
+ * <p>
+ * The returned {@code Printable} shares the {@code document} with this
+ * {@code JTextComponent}. It is the responsibility of the developer to
+ * ensure that the {@code document} is not mutated while this {@code Printable}
+ * is used. Printing behavior is undefined when the {@code document} is
+ * mutated during printing.
+ *
+ * <p>
+ * Page header and footer text can be added to the output by providing
+ * {@code MessageFormat} arguments. The printing code requests
+ * {@code Strings} from the formats, providing a single item which may be
+ * included in the formatted string: an {@code Integer} representing the
+ * current page number.
+ *
+ * <p>
+ * The returned {@code Printable} when printed, formats the
+ * document content appropriately for the page size. For correct
+ * line wrapping the {@code imageable width} of all pages must be the
+ * same. See {@link java.awt.print.PageFormat#getImageableWidth}.
+ *
+ * <p>
+ * This method is thread-safe, although most Swing methods are not. Please
+ * see <A
+ * HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">
+ * How to Use Threads</A> for more information.
+ *
+ * <p>
+ * The returned {@code Printable} can be printed on any thread.
+ *
+ * <p>
+ * This implementation returned {@code Printable} performs all painting on
+ * the <i>Event Dispatch Thread</i>, regardless of what thread it is
+ * used on.
+ *
+ * @param headerFormat the text, in {@code MessageFormat}, to be
+ * used as the header, or {@code null} for no header
+ * @param footerFormat the text, in {@code MessageFormat}, to be
+ * used as the footer, or {@code null} for no footer
+ * @return a {@code Printable} for use in printing content of this
+ * {@code JTextComponent}
+ *
+ *
+ * @see java.awt.print.Printable
+ * @see java.awt.print.PageFormat
+ * @see javax.swing.text.Document#render(java.lang.Runnable)
+ *
+ * @since 1.6
+ */
+ public Printable getPrintable(final MessageFormat headerFormat,
+ final MessageFormat footerFormat) {
+ return TextComponentPrintable.getPrintable(
+ this, headerFormat, footerFormat);
+ }
+
+
+/////////////////
+// Accessibility support
+////////////////
+
+
+ /**
+ * Gets the <code>AccessibleContext</code> associated with this
+ * <code>JTextComponent</code>. For text components,
+ * the <code>AccessibleContext</code> takes the form of an
+ * <code>AccessibleJTextComponent</code>.
+ * A new <code>AccessibleJTextComponent</code> instance
+ * is created if necessary.
+ *
+ * @return an <code>AccessibleJTextComponent</code> that serves as the
+ * <code>AccessibleContext</code> of this
+ * <code>JTextComponent</code>
+ */
+ public AccessibleContext getAccessibleContext() {
+ if (accessibleContext == null) {
+ accessibleContext = new AccessibleJTextComponent();
+ }
+ return accessibleContext;
+ }
+
+ /**
+ * This class implements accessibility support for the
+ * <code>JTextComponent</code> class. It provides an implementation of
+ * the Java Accessibility API appropriate to menu user-interface elements.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public class AccessibleJTextComponent extends AccessibleJComponent
+ implements AccessibleText, CaretListener, DocumentListener,
+ AccessibleAction, AccessibleEditableText,
+ AccessibleExtendedText {
+
+ int caretPos;
+ Point oldLocationOnScreen;
+
+ /**
+ * Constructs an AccessibleJTextComponent. Adds a listener to track
+ * caret change.
+ */
+ public AccessibleJTextComponent() {
+ Document doc = JTextComponent.this.getDocument();
+ if (doc != null) {
+ doc.addDocumentListener(this);
+ }
+ JTextComponent.this.addCaretListener(this);
+ caretPos = getCaretPosition();
+
+ try {
+ oldLocationOnScreen = getLocationOnScreen();
+ } catch (IllegalComponentStateException iae) {
+ }
+
+ // Fire a ACCESSIBLE_VISIBLE_DATA_PROPERTY PropertyChangeEvent
+ // when the text component moves (e.g., when scrolling).
+ // Using an anonymous class since making AccessibleJTextComponent
+ // implement ComponentListener would be an API change.
+ JTextComponent.this.addComponentListener(new ComponentAdapter() {
+
+ public void componentMoved(ComponentEvent e) {
+ try {
+ Point newLocationOnScreen = getLocationOnScreen();
+ firePropertyChange(ACCESSIBLE_VISIBLE_DATA_PROPERTY,
+ oldLocationOnScreen,
+ newLocationOnScreen);
+
+ oldLocationOnScreen = newLocationOnScreen;
+ } catch (IllegalComponentStateException iae) {
+ }
+ }
+ });
+ }
+
+ /**
+ * Handles caret updates (fire appropriate property change event,
+ * which are AccessibleContext.ACCESSIBLE_CARET_PROPERTY and
+ * AccessibleContext.ACCESSIBLE_SELECTION_PROPERTY).
+ * This keeps track of the dot position internally. When the caret
+ * moves, the internal position is updated after firing the event.
+ *
+ * @param e the CaretEvent
+ */
+ public void caretUpdate(CaretEvent e) {
+ int dot = e.getDot();
+ int mark = e.getMark();
+ if (caretPos != dot) {
+ // the caret moved
+ firePropertyChange(ACCESSIBLE_CARET_PROPERTY,
+ new Integer(caretPos), new Integer(dot));
+ caretPos = dot;
+
+ try {
+ oldLocationOnScreen = getLocationOnScreen();
+ } catch (IllegalComponentStateException iae) {
+ }
+ }
+ if (mark != dot) {
+ // there is a selection
+ firePropertyChange(ACCESSIBLE_SELECTION_PROPERTY, null,
+ getSelectedText());
+ }
+ }
+
+ // DocumentListener methods
+
+ /**
+ * Handles document insert (fire appropriate property change event
+ * which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY).
+ * This tracks the changed offset via the event.
+ *
+ * @param e the DocumentEvent
+ */
+ public void insertUpdate(DocumentEvent e) {
+ final Integer pos = new Integer (e.getOffset());
+ if (SwingUtilities.isEventDispatchThread()) {
+ firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos);
+ } else {
+ Runnable doFire = new Runnable() {
+ public void run() {
+ firePropertyChange(ACCESSIBLE_TEXT_PROPERTY,
+ null, pos);
+ }
+ };
+ SwingUtilities.invokeLater(doFire);
+ }
+ }
+
+ /**
+ * Handles document remove (fire appropriate property change event,
+ * which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY).
+ * This tracks the changed offset via the event.
+ *
+ * @param e the DocumentEvent
+ */
+ public void removeUpdate(DocumentEvent e) {
+ final Integer pos = new Integer (e.getOffset());
+ if (SwingUtilities.isEventDispatchThread()) {
+ firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos);
+ } else {
+ Runnable doFire = new Runnable() {
+ public void run() {
+ firePropertyChange(ACCESSIBLE_TEXT_PROPERTY,
+ null, pos);
+ }
+ };
+ SwingUtilities.invokeLater(doFire);
+ }
+ }
+
+ /**
+ * Handles document remove (fire appropriate property change event,
+ * which is AccessibleContext.ACCESSIBLE_TEXT_PROPERTY).
+ * This tracks the changed offset via the event.
+ *
+ * @param e the DocumentEvent
+ */
+ public void changedUpdate(DocumentEvent e) {
+ final Integer pos = new Integer (e.getOffset());
+ if (SwingUtilities.isEventDispatchThread()) {
+ firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, pos);
+ } else {
+ Runnable doFire = new Runnable() {
+ public void run() {
+ firePropertyChange(ACCESSIBLE_TEXT_PROPERTY,
+ null, pos);
+ }
+ };
+ SwingUtilities.invokeLater(doFire);
+ }
+ }
+
+ /**
+ * Gets the state set of the JTextComponent.
+ * The AccessibleStateSet of an object is composed of a set of
+ * unique AccessibleState's. A change in the AccessibleStateSet
+ * of an object will cause a PropertyChangeEvent to be fired
+ * for the AccessibleContext.ACCESSIBLE_STATE_PROPERTY property.
+ *
+ * @return an instance of AccessibleStateSet containing the
+ * current state set of the object
+ * @see AccessibleStateSet
+ * @see AccessibleState
+ * @see #addPropertyChangeListener
+ */
+ public AccessibleStateSet getAccessibleStateSet() {
+ AccessibleStateSet states = super.getAccessibleStateSet();
+ if (JTextComponent.this.isEditable()) {
+ states.add(AccessibleState.EDITABLE);
+ }
+ return states;
+ }
+
+
+ /**
+ * Gets the role of this object.
+ *
+ * @return an instance of AccessibleRole describing the role of the
+ * object (AccessibleRole.TEXT)
+ * @see AccessibleRole
+ */
+ public AccessibleRole getAccessibleRole() {
+ return AccessibleRole.TEXT;
+ }
+
+ /**
+ * Get the AccessibleText associated with this object. In the
+ * implementation of the Java Accessibility API for this class,
+ * return this object, which is responsible for implementing the
+ * AccessibleText interface on behalf of itself.
+ *
+ * @return this object
+ */
+ public AccessibleText getAccessibleText() {
+ return this;
+ }
+
+
+ // --- interface AccessibleText methods ------------------------
+
+ /**
+ * Many of these methods are just convenience methods; they
+ * just call the equivalent on the parent
+ */
+
+ /**
+ * 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.
+ */
+ public int getIndexAtPoint(Point p) {
+ if (p == null) {
+ return -1;
+ }
+ return JTextComponent.this.viewToModel(p);
+ }
+
+ /**
+ * Gets the editor's drawing rectangle. Stolen
+ * from the unfortunately named
+ * BasicTextUI.getVisibleEditorRect()
+ *
+ * @return the bounding box for the root view
+ */
+ Rectangle getRootEditorRect() {
+ Rectangle alloc = JTextComponent.this.getBounds();
+ if ((alloc.width > 0) && (alloc.height > 0)) {
+ alloc.x = alloc.y = 0;
+ Insets insets = JTextComponent.this.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;
+ }
+
+ /**
+ * Determines 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 a null rectangle
+ * is returned.
+ *
+ * The screen coordinates returned are "unscrolled coordinates"
+ * if the JTextComponent is contained in a JScrollPane in which
+ * case the resulting rectangle should be composed with the parent
+ * coordinates. A good algorithm to use is:
+ * <nf>
+ * Accessible a:
+ * AccessibleText at = a.getAccessibleText();
+ * AccessibleComponent ac = a.getAccessibleComponent();
+ * Rectangle r = at.getCharacterBounds();
+ * Point p = ac.getLocation();
+ * r.x += p.x;
+ * r.y += p.y;
+ * </nf>
+ *
+ * Note: the JTextComponent must have a valid size (e.g. have
+ * been added to a parent container whose ancestor container
+ * is a valid top-level window) for this method to be able
+ * to return a meaningful (non-null) value.
+ *
+ * @param i the index into the String >= 0
+ * @return the screen coordinates of the character's bounding box
+ */
+ public Rectangle getCharacterBounds(int i) {
+ if (i < 0 || i > model.getLength()-1) {
+ return null;
+ }
+ TextUI ui = getUI();
+ if (ui == null) {
+ return null;
+ }
+ Rectangle rect = null;
+ Rectangle alloc = getRootEditorRect();
+ if (alloc == null) {
+ return null;
+ }
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readLock();
+ }
+ try {
+ View rootView = ui.getRootView(JTextComponent.this);
+ if (rootView != null) {
+ rootView.setSize(alloc.width, alloc.height);
+
+ Shape bounds = rootView.modelToView(i,
+ Position.Bias.Forward, i+1,
+ Position.Bias.Backward, alloc);
+
+ rect = (bounds instanceof Rectangle) ?
+ (Rectangle)bounds : bounds.getBounds();
+
+ }
+ } catch (BadLocationException e) {
+ } finally {
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readUnlock();
+ }
+ }
+ return rect;
+ }
+
+ /**
+ * Returns the number of characters (valid indices)
+ *
+ * @return the number of characters >= 0
+ */
+ public int getCharCount() {
+ return model.getLength();
+ }
+
+ /**
+ * Returns the zero-based offset of the caret.
+ *
+ * Note: The character 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() {
+ return JTextComponent.this.getCaretPosition();
+ }
+
+ /**
+ * Returns 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) {
+ Element e = null;
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readLock();
+ }
+ try {
+ for (e = model.getDefaultRootElement(); ! e.isLeaf(); ) {
+ int index = e.getElementIndex(i);
+ e = e.getElement(index);
+ }
+ } finally {
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readUnlock();
+ }
+ }
+ return e.getAttributes();
+ }
+
+
+ /**
+ * 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 0 if the text is empty, or the caret position
+ * if no selection.
+ *
+ * @return the index into the text of the start of the selection >= 0
+ */
+ public int getSelectionStart() {
+ return JTextComponent.this.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 0 if the text is empty, or the caret position
+ * if no selection.
+ *
+ * @return the index into teh text of the end of the selection >= 0
+ */
+ public int getSelectionEnd() {
+ return JTextComponent.this.getSelectionEnd();
+ }
+
+ /**
+ * Returns the portion of the text that is selected.
+ *
+ * @return the text, null if no selection
+ */
+ public String getSelectedText() {
+ return JTextComponent.this.getSelectedText();
+ }
+
+ /**
+ * IndexedSegment extends Segment adding the offset into the
+ * the model the <code>Segment</code> was asked for.
+ */
+ private class IndexedSegment extends Segment {
+ /**
+ * Offset into the model that the position represents.
+ */
+ public int modelOffset;
+ }
+
+
+ // TIGER - 4170173
+ /**
+ * Returns the String at a given index. Whitespace
+ * between words is treated as a word.
+ *
+ * @param part the CHARACTER, WORD, or SENTENCE to retrieve
+ * @param index an index within the text
+ * @return the letter, word, or sentence.
+ *
+ */
+ public String getAtIndex(int part, int index) {
+ return getAtIndex(part, index, 0);
+ }
+
+
+ /**
+ * Returns the String after a given index. Whitespace
+ * between words is treated as a word.
+ *
+ * @param part the CHARACTER, WORD, or SENTENCE to retrieve
+ * @param index an index within the text
+ * @return the letter, word, or sentence.
+ */
+ public String getAfterIndex(int part, int index) {
+ return getAtIndex(part, index, 1);
+ }
+
+
+ /**
+ * Returns the String before a given index. Whitespace
+ * between words is treated a word.
+ *
+ * @param part the CHARACTER, WORD, or SENTENCE to retrieve
+ * @param index an index within the text
+ * @return the letter, word, or sentence.
+ */
+ public String getBeforeIndex(int part, int index) {
+ return getAtIndex(part, index, -1);
+ }
+
+
+ /**
+ * Gets the word, sentence, or character at <code>index</code>.
+ * If <code>direction</code> 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 <code>Segment</code> containing the paragraph text
+ * at <code>index</code>, or null if <code>index</code> 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 <code>index</code> representing either
+ * the paragraph or sentence as identified by <code>part</code>, 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;
+ }
+
+ // begin AccessibleEditableText methods -----
+
+ /**
+ * Returns the AccessibleEditableText interface for
+ * this text component.
+ *
+ * @return the AccessibleEditableText interface
+ * @since 1.4
+ */
+ public AccessibleEditableText getAccessibleEditableText() {
+ return this;
+ }
+
+ /**
+ * Sets the text contents to the specified string.
+ *
+ * @param s the string to set the text contents
+ * @since 1.4
+ */
+ public void setTextContents(String s) {
+ JTextComponent.this.setText(s);
+ }
+
+ /**
+ * Inserts the specified string at the given index
+ *
+ * @param index the index in the text where the string will
+ * be inserted
+ * @param s the string to insert in the text
+ * @since 1.4
+ */
+ public void insertTextAtIndex(int index, String s) {
+ Document doc = JTextComponent.this.getDocument();
+ if (doc != null) {
+ try {
+ if (s != null && s.length() > 0) {
+ boolean composedTextSaved = saveComposedText(index);
+ doc.insertString(index, s, null);
+ if (composedTextSaved) {
+ restoreComposedText();
+ }
+ }
+ } catch (BadLocationException e) {
+ UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this);
+ }
+ }
+ }
+
+ /**
+ * Returns the text string between two indices.
+ *
+ * @param startIndex the starting index in the text
+ * @param endIndex the ending index in the text
+ * @return the text string between the indices
+ * @since 1.4
+ */
+ public String getTextRange(int startIndex, int endIndex) {
+ String txt = null;
+ int p0 = Math.min(startIndex, endIndex);
+ int p1 = Math.max(startIndex, endIndex);
+ if (p0 != p1) {
+ try {
+ Document doc = JTextComponent.this.getDocument();
+ txt = doc.getText(p0, p1 - p0);
+ } catch (BadLocationException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+ return txt;
+ }
+
+ /**
+ * Deletes the text between two indices
+ *
+ * @param startIndex the starting index in the text
+ * @param endIndex the ending index in the text
+ * @since 1.4
+ */
+ public void delete(int startIndex, int endIndex) {
+ if (isEditable() && isEnabled()) {
+ try {
+ int p0 = Math.min(startIndex, endIndex);
+ int p1 = Math.max(startIndex, endIndex);
+ if (p0 != p1) {
+ Document doc = getDocument();
+ doc.remove(p0, p1 - p0);
+ }
+ } catch (BadLocationException e) {
+ }
+ } else {
+ UIManager.getLookAndFeel().provideErrorFeedback(JTextComponent.this);
+ }
+ }
+
+ /**
+ * Cuts the text between two indices into the system clipboard.
+ *
+ * @param startIndex the starting index in the text
+ * @param endIndex the ending index in the text
+ * @since 1.4
+ */
+ public void cut(int startIndex, int endIndex) {
+ selectText(startIndex, endIndex);
+ JTextComponent.this.cut();
+ }
+
+ /**
+ * Pastes the text from the system clipboard into the text
+ * starting at the specified index.
+ *
+ * @param startIndex the starting index in the text
+ * @since 1.4
+ */
+ public void paste(int startIndex) {
+ setCaretPosition(startIndex);
+ JTextComponent.this.paste();
+ }
+
+ /**
+ * Replaces the text between two indices with the specified
+ * string.
+ *
+ * @param startIndex the starting index in the text
+ * @param endIndex the ending index in the text
+ * @param s the string to replace the text between two indices
+ * @since 1.4
+ */
+ public void replaceText(int startIndex, int endIndex, String s) {
+ selectText(startIndex, endIndex);
+ JTextComponent.this.replaceSelection(s);
+ }
+
+ /**
+ * Selects the text between two indices.
+ *
+ * @param startIndex the starting index in the text
+ * @param endIndex the ending index in the text
+ * @since 1.4
+ */
+ public void selectText(int startIndex, int endIndex) {
+ JTextComponent.this.select(startIndex, endIndex);
+ }
+
+ /**
+ * Sets attributes for the text between two indices.
+ *
+ * @param startIndex the starting index in the text
+ * @param endIndex the ending index in the text
+ * @param as the attribute set
+ * @see AttributeSet
+ * @since 1.4
+ */
+ public void setAttributes(int startIndex, int endIndex,
+ AttributeSet as) {
+
+ // Fixes bug 4487492
+ Document doc = JTextComponent.this.getDocument();
+ if (doc != null && doc instanceof StyledDocument) {
+ StyledDocument sDoc = (StyledDocument)doc;
+ int offset = startIndex;
+ int length = endIndex - startIndex;
+ sDoc.setCharacterAttributes(offset, length, as, true);
+ }
+ }
+
+ // ----- end AccessibleEditableText methods
+
+
+ // ----- begin AccessibleExtendedText methods
+
+// Probably should replace the helper method getAtIndex() to return
+// instead an AccessibleTextSequence also for LINE & ATTRIBUTE_RUN
+// and then make the AccessibleText methods get[At|After|Before]Point
+// call this new method instead and return only the string portion
+
+ /**
+ * Returns the AccessibleTextSequence at a given <code>index</code>.
+ * If <code>direction</code> is non-null this will find the
+ * next/previous word/sentence/character.
+ *
+ * @param part the <code>CHARACTER</code>, <code>WORD</code>,
+ * <code>SENTENCE</code>, <code>LINE</code> or
+ * <code>ATTRIBUTE_RUN</code> to retrieve
+ * @param index an index within the text
+ * @param direction is either -1, 0, or 1
+ * @return an <code>AccessibleTextSequence</code> specifying the text
+ * if <code>part</code> and <code>index</code> are valid. Otherwise,
+ * <code>null</code> is returned.
+ *
+ * @see javax.accessibility.AccessibleText#CHARACTER
+ * @see javax.accessibility.AccessibleText#WORD
+ * @see javax.accessibility.AccessibleText#SENTENCE
+ * @see javax.accessibility.AccessibleExtendedText#LINE
+ * @see javax.accessibility.AccessibleExtendedText#ATTRIBUTE_RUN
+ *
+ * @since 1.6
+ */
+ private AccessibleTextSequence getSequenceAtIndex(int part,
+ int index, int direction) {
+ if (index < 0 || index >= model.getLength()) {
+ return null;
+ }
+ if (direction < -1 || direction > 1) {
+ return null; // direction must be 1, 0, or -1
+ }
+
+ switch (part) {
+ case AccessibleText.CHARACTER:
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readLock();
+ }
+ AccessibleTextSequence charSequence = null;
+ try {
+ if (index + direction < model.getLength() &&
+ index + direction >= 0) {
+ charSequence =
+ new AccessibleTextSequence(index + direction,
+ index + direction + 1,
+ model.getText(index + direction, 1));
+ }
+
+ } catch (BadLocationException e) {
+ // we are intentionally silent; our contract says we return
+ // null if there is any failure in this method
+ } finally {
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readUnlock();
+ }
+ }
+ return charSequence;
+
+ case AccessibleText.WORD:
+ case AccessibleText.SENTENCE:
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readLock();
+ }
+ AccessibleTextSequence rangeSequence = null;
+ try {
+ IndexedSegment seg = getSegmentAt(part, index);
+ if (seg != null) {
+ if (direction != 0) {
+ int next;
+
+ if (direction < 0) {
+ next = seg.modelOffset - 1;
+ }
+ else {
+ next = seg.modelOffset + seg.count;
+ }
+ if (next >= 0 && next <= model.getLength()) {
+ seg = getSegmentAt(part, next);
+ }
+ else {
+ seg = null;
+ }
+ }
+ if (seg != null &&
+ (seg.offset + seg.count) <= model.getLength()) {
+ rangeSequence =
+ new AccessibleTextSequence (seg.offset,
+ seg.offset + seg.count,
+ new String(seg.array, seg.offset, seg.count));
+ } // else we leave rangeSequence set to null
+ }
+ } catch(BadLocationException e) {
+ // we are intentionally silent; our contract says we return
+ // null if there is any failure in this method
+ } finally {
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readUnlock();
+ }
+ }
+ return rangeSequence;
+
+ case AccessibleExtendedText.LINE:
+ AccessibleTextSequence lineSequence = null;
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readLock();
+ }
+ try {
+ int startIndex =
+ Utilities.getRowStart(JTextComponent.this, index);
+ int endIndex =
+ Utilities.getRowEnd(JTextComponent.this, index);
+ if (startIndex >= 0 && endIndex >= startIndex) {
+ if (direction == 0) {
+ lineSequence =
+ new AccessibleTextSequence(startIndex, endIndex,
+ model.getText(startIndex,
+ endIndex - startIndex + 1));
+ } else if (direction == -1 && startIndex > 0) {
+ endIndex =
+ Utilities.getRowEnd(JTextComponent.this,
+ startIndex - 1);
+ startIndex =
+ Utilities.getRowStart(JTextComponent.this,
+ startIndex - 1);
+ if (startIndex >= 0 && endIndex >= startIndex) {
+ lineSequence =
+ new AccessibleTextSequence(startIndex,
+ endIndex,
+ model.getText(startIndex,
+ endIndex - startIndex + 1));
+ }
+ } else if (direction == 1 &&
+ endIndex < model.getLength()) {
+ startIndex =
+ Utilities.getRowStart(JTextComponent.this,
+ endIndex + 1);
+ endIndex =
+ Utilities.getRowEnd(JTextComponent.this,
+ endIndex + 1);
+ if (startIndex >= 0 && endIndex >= startIndex) {
+ lineSequence =
+ new AccessibleTextSequence(startIndex,
+ endIndex, model.getText(startIndex,
+ endIndex - startIndex + 1));
+ }
+ }
+ // already validated 'direction' above...
+ }
+ } catch(BadLocationException e) {
+ // we are intentionally silent; our contract says we return
+ // null if there is any failure in this method
+ } finally {
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readUnlock();
+ }
+ }
+ return lineSequence;
+
+ case AccessibleExtendedText.ATTRIBUTE_RUN:
+ // assumptions: (1) that all characters in a single element
+ // share the same attribute set; (2) that adjacent elements
+ // *may* share the same attribute set
+
+ int attributeRunStartIndex, attributeRunEndIndex;
+ String runText = null;
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readLock();
+ }
+
+ try {
+ attributeRunStartIndex = attributeRunEndIndex =
+ Integer.MIN_VALUE;
+ int tempIndex = index;
+ switch (direction) {
+ case -1:
+ // going backwards, so find left edge of this run -
+ // that'll be the end of the previous run
+ // (off-by-one counting)
+ attributeRunEndIndex = getRunEdge(index, direction);
+ // now set ourselves up to find the left edge of the
+ // prev. run
+ tempIndex = attributeRunEndIndex - 1;
+ break;
+ case 1:
+ // going forward, so find right edge of this run -
+ // that'll be the start of the next run
+ // (off-by-one counting)
+ attributeRunStartIndex = getRunEdge(index, direction);
+ // now set ourselves up to find the right edge of the
+ // next run
+ tempIndex = attributeRunStartIndex;
+ break;
+ case 0:
+ // interested in the current run, so nothing special to
+ // set up in advance...
+ break;
+ default:
+ // only those three values of direction allowed...
+ throw new AssertionError(direction);
+ }
+
+ // set the unset edge; if neither set then we're getting
+ // both edges of the current run around our 'index'
+ attributeRunStartIndex =
+ (attributeRunStartIndex != Integer.MIN_VALUE) ?
+ attributeRunStartIndex : getRunEdge(tempIndex, -1);
+ attributeRunEndIndex =
+ (attributeRunEndIndex != Integer.MIN_VALUE) ?
+ attributeRunEndIndex : getRunEdge(tempIndex, 1);
+
+ runText = model.getText(attributeRunStartIndex,
+ attributeRunEndIndex -
+ attributeRunStartIndex);
+ } catch (BadLocationException e) {
+ // we are intentionally silent; our contract says we return
+ // null if there is any failure in this method
+ return null;
+ } finally {
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readUnlock();
+ }
+ }
+ return new AccessibleTextSequence(attributeRunStartIndex,
+ attributeRunEndIndex,
+ runText);
+
+ default:
+ break;
+ }
+ return null;
+ }
+
+
+ /**
+ * Starting at text position <code>index</code>, and going in
+ * <code>direction</code>, return the edge of run that shares the
+ * same <code>AttributeSet</code> and parent element as those at
+ * <code>index</code>.
+ *
+ * Note: we assume the document is already locked...
+ */
+ private int getRunEdge(int index, int direction) throws
+ BadLocationException {
+ if (index < 0 || index >= model.getLength()) {
+ throw new BadLocationException("Location out of bounds", index);
+ }
+ // locate the Element at index
+ Element indexElement = null;
+ // locate the Element at our index/offset
+ int elementIndex = -1; // test for initialization
+ for (indexElement = model.getDefaultRootElement();
+ ! indexElement.isLeaf(); ) {
+ elementIndex = indexElement.getElementIndex(index);
+ indexElement = indexElement.getElement(elementIndex);
+ }
+ if (elementIndex == -1) {
+ throw new AssertionError(index);
+ }
+ // cache the AttributeSet and parentElement atindex
+ AttributeSet indexAS = indexElement.getAttributes();
+ Element parent = indexElement.getParentElement();
+
+ // find the first Element before/after ours w/the same AttributeSet
+ // if we are already at edge of the first element in our parent
+ // then return that edge
+ Element edgeElement = indexElement;
+ switch (direction) {
+ case -1:
+ case 1:
+ int edgeElementIndex = elementIndex;
+ int elementCount = parent.getElementCount();
+ while ((edgeElementIndex + direction) > 0 &&
+ ((edgeElementIndex + direction) < elementCount) &&
+ parent.getElement(edgeElementIndex
+ + direction).getAttributes().isEqual(indexAS)) {
+ edgeElementIndex += direction;
+ }
+ edgeElement = parent.getElement(edgeElementIndex);
+ break;
+ default:
+ throw new AssertionError(direction);
+ }
+ switch (direction) {
+ case -1:
+ return edgeElement.getStartOffset();
+ case 1:
+ return edgeElement.getEndOffset();
+ default:
+ // we already caught this case earlier; this is to satisfy
+ // the compiler...
+ return Integer.MIN_VALUE;
+ }
+ }
+
+ // getTextRange() not needed; defined in AccessibleEditableText
+
+ /**
+ * Returns the <code>AccessibleTextSequence</code> at a given
+ * <code>index</code>.
+ *
+ * @param part the <code>CHARACTER</code>, <code>WORD</code>,
+ * <code>SENTENCE</code>, <code>LINE</code> or
+ * <code>ATTRIBUTE_RUN</code> to retrieve
+ * @param index an index within the text
+ * @return an <code>AccessibleTextSequence</code> specifying the text if
+ * <code>part</code> and <code>index</code> are valid. Otherwise,
+ * <code>null</code> is returned
+ *
+ * @see javax.accessibility.AccessibleText#CHARACTER
+ * @see javax.accessibility.AccessibleText#WORD
+ * @see javax.accessibility.AccessibleText#SENTENCE
+ * @see javax.accessibility.AccessibleExtendedText#LINE
+ * @see javax.accessibility.AccessibleExtendedText#ATTRIBUTE_RUN
+ *
+ * @since 1.6
+ */
+ public AccessibleTextSequence getTextSequenceAt(int part, int index) {
+ return getSequenceAtIndex(part, index, 0);
+ }
+
+ /**
+ * Returns the <code>AccessibleTextSequence</code> after a given
+ * <code>index</code>.
+ *
+ * @param part the <code>CHARACTER</code>, <code>WORD</code>,
+ * <code>SENTENCE</code>, <code>LINE</code> or
+ * <code>ATTRIBUTE_RUN</code> to retrieve
+ * @param index an index within the text
+ * @return an <code>AccessibleTextSequence</code> specifying the text
+ * if <code>part</code> and <code>index</code> are valid. Otherwise,
+ * <code>null</code> is returned
+ *
+ * @see javax.accessibility.AccessibleText#CHARACTER
+ * @see javax.accessibility.AccessibleText#WORD
+ * @see javax.accessibility.AccessibleText#SENTENCE
+ * @see javax.accessibility.AccessibleExtendedText#LINE
+ * @see javax.accessibility.AccessibleExtendedText#ATTRIBUTE_RUN
+ *
+ * @since 1.6
+ */
+ public AccessibleTextSequence getTextSequenceAfter(int part, int index) {
+ return getSequenceAtIndex(part, index, 1);
+ }
+
+ /**
+ * Returns the <code>AccessibleTextSequence</code> before a given
+ * <code>index</code>.
+ *
+ * @param part the <code>CHARACTER</code>, <code>WORD</code>,
+ * <code>SENTENCE</code>, <code>LINE</code> or
+ * <code>ATTRIBUTE_RUN</code> to retrieve
+ * @param index an index within the text
+ * @return an <code>AccessibleTextSequence</code> specifying the text
+ * if <code>part</code> and <code>index</code> are valid. Otherwise,
+ * <code>null</code> is returned
+ *
+ * @see javax.accessibility.AccessibleText#CHARACTER
+ * @see javax.accessibility.AccessibleText#WORD
+ * @see javax.accessibility.AccessibleText#SENTENCE
+ * @see javax.accessibility.AccessibleExtendedText#LINE
+ * @see javax.accessibility.AccessibleExtendedText#ATTRIBUTE_RUN
+ *
+ * @since 1.6
+ */
+ public AccessibleTextSequence getTextSequenceBefore(int part, int index) {
+ return getSequenceAtIndex(part, index, -1);
+ }
+
+ /**
+ * Returns the <code>Rectangle</code> enclosing the text between
+ * two indicies.
+ *
+ * @param startIndex the start index in the text
+ * @param endIndex the end index in the text
+ * @return the bounding rectangle of the text if the indices are valid.
+ * Otherwise, <code>null</code> is returned
+ *
+ * @since 1.6
+ */
+ public Rectangle getTextBounds(int startIndex, int endIndex) {
+ if (startIndex < 0 || startIndex > model.getLength()-1 ||
+ endIndex < 0 || endIndex > model.getLength()-1 ||
+ startIndex > endIndex) {
+ return null;
+ }
+ TextUI ui = getUI();
+ if (ui == null) {
+ return null;
+ }
+ Rectangle rect = null;
+ Rectangle alloc = getRootEditorRect();
+ if (alloc == null) {
+ return null;
+ }
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readLock();
+ }
+ try {
+ View rootView = ui.getRootView(JTextComponent.this);
+ if (rootView != null) {
+ Shape bounds = rootView.modelToView(startIndex,
+ Position.Bias.Forward, endIndex,
+ Position.Bias.Backward, alloc);
+
+ rect = (bounds instanceof Rectangle) ?
+ (Rectangle)bounds : bounds.getBounds();
+
+ }
+ } catch (BadLocationException e) {
+ } finally {
+ if (model instanceof AbstractDocument) {
+ ((AbstractDocument)model).readUnlock();
+ }
+ }
+ return rect;
+ }
+
+ // ----- end AccessibleExtendedText methods
+
+
+ // --- interface AccessibleAction methods ------------------------
+
+ public AccessibleAction getAccessibleAction() {
+ return this;
+ }
+
+ /**
+ * Returns the number of accessible actions available in this object
+ * If there are more than one, the first one is considered the
+ * "default" action of the object.
+ *
+ * @return the zero-based number of Actions in this object
+ * @since 1.4
+ */
+ public int getAccessibleActionCount() {
+ Action [] actions = JTextComponent.this.getActions();
+ return actions.length;
+ }
+
+ /**
+ * Returns a description of the specified action of the object.
+ *
+ * @param i zero-based index of the actions
+ * @return a String description of the action
+ * @see #getAccessibleActionCount
+ * @since 1.4
+ */
+ public String getAccessibleActionDescription(int i) {
+ Action [] actions = JTextComponent.this.getActions();
+ if (i < 0 || i >= actions.length) {
+ return null;
+ }
+ return (String)actions[i].getValue(Action.NAME);
+ }
+
+ /**
+ * Performs the specified Action on the object
+ *
+ * @param i zero-based index of actions
+ * @return true if the action was performed; otherwise false.
+ * @see #getAccessibleActionCount
+ * @since 1.4
+ */
+ public boolean doAccessibleAction(int i) {
+ Action [] actions = JTextComponent.this.getActions();
+ if (i < 0 || i >= actions.length) {
+ return false;
+ }
+ ActionEvent ae =
+ new ActionEvent(JTextComponent.this,
+ ActionEvent.ACTION_PERFORMED, null,
+ EventQueue.getMostRecentEventTime(),
+ getCurrentEventModifiers());
+ actions[i].actionPerformed(ae);
+ return true;
+ }
+
+ // ----- end AccessibleAction methods
+
+
+ }
+
+
+ // --- serialization ---------------------------------------------
+
+ private void readObject(ObjectInputStream s)
+ throws IOException, ClassNotFoundException
+ {
+ s.defaultReadObject();
+ caretEvent = new MutableCaretEvent(this);
+ addMouseListener(caretEvent);
+ addFocusListener(caretEvent);
+ }
+
+ // --- member variables ----------------------------------
+
+ /**
+ * The document model.
+ */
+ private Document model;
+
+ /**
+ * The caret used to display the insert position
+ * and navigate throughout the document.
+ *
+ * PENDING(prinz)
+ * This should be serializable, default installed
+ * by UI.
+ */
+ private transient Caret caret;
+
+ /**
+ * Object responsible for restricting the cursor navigation.
+ */
+ private NavigationFilter navigationFilter;
+
+ /**
+ * The object responsible for managing highlights.
+ *
+ * PENDING(prinz)
+ * This should be serializable, default installed
+ * by UI.
+ */
+ private transient Highlighter highlighter;
+
+ /**
+ * The current key bindings in effect.
+ *
+ * PENDING(prinz)
+ * This should be serializable, default installed
+ * by UI.
+ */
+ private transient Keymap keymap;
+
+ private transient MutableCaretEvent caretEvent;
+ private Color caretColor;
+ private Color selectionColor;
+ private Color selectedTextColor;
+ private Color disabledTextColor;
+ private boolean editable;
+ private Insets margin;
+ private char focusAccelerator;
+ private boolean dragEnabled;
+
+ /**
+ * The drop mode for this component.
+ */
+ private DropMode dropMode = DropMode.USE_SELECTION;
+
+ /**
+ * The drop location.
+ */
+ private transient DropLocation dropLocation;
+
+ /**
+ * Represents a drop location for <code>JTextComponent</code>s.
+ *
+ * @see #getDropLocation
+ * @since 1.6
+ */
+ public static final class DropLocation extends TransferHandler.DropLocation {
+ private final int index;
+ private final Position.Bias bias;
+
+ private DropLocation(Point p, int index, Position.Bias bias) {
+ super(p);
+ this.index = index;
+ this.bias = bias;
+ }
+
+ /**
+ * Returns the index where dropped data should be inserted into the
+ * associated component. This index represents a position between
+ * characters, as would be interpreted by a caret.
+ *
+ * @return the drop index
+ */
+ public int getIndex() {
+ return index;
+ }
+
+ /**
+ * Returns the bias for the drop index.
+ *
+ * @return the drop bias
+ */
+ public Position.Bias getBias() {
+ return bias;
+ }
+
+ /**
+ * Returns a string representation of this drop location.
+ * This method is intended to be used for debugging purposes,
+ * and the content and format of the returned string may vary
+ * between implementations.
+ *
+ * @return a string representation of this drop location
+ */
+ public String toString() {
+ return getClass().getName()
+ + "[dropPoint=" + getDropPoint() + ","
+ + "index=" + index + ","
+ + "bias=" + bias + "]";
+ }
+ }
+
+ /**
+ * TransferHandler used if one hasn't been supplied by the UI.
+ */
+ private static DefaultTransferHandler defaultTransferHandler;
+
+ /**
+ * Maps from class name to Boolean indicating if
+ * <code>processInputMethodEvent</code> has been overriden.
+ */
+ private static Map overrideMap;
+
+ /**
+ * Returns a string representation of this <code>JTextComponent</code>.
+ * This method is intended to be used only for debugging purposes, and the
+ * content and format of the returned string may vary between
+ * implementations. The returned string may be empty but may not
+ * be <code>null</code>.
+ * <P>
+ * Overriding <code>paramString</code> to provide information about the
+ * specific new aspects of the JFC components.
+ *
+ * @return a string representation of this <code>JTextComponent</code>
+ */
+ protected String paramString() {
+ String editableString = (editable ?
+ "true" : "false");
+ String caretColorString = (caretColor != null ?
+ caretColor.toString() : "");
+ String selectionColorString = (selectionColor != null ?
+ selectionColor.toString() : "");
+ String selectedTextColorString = (selectedTextColor != null ?
+ selectedTextColor.toString() : "");
+ String disabledTextColorString = (disabledTextColor != null ?
+ disabledTextColor.toString() : "");
+ String marginString = (margin != null ?
+ margin.toString() : "");
+
+ return super.paramString() +
+ ",caretColor=" + caretColorString +
+ ",disabledTextColor=" + disabledTextColorString +
+ ",editable=" + editableString +
+ ",margin=" + marginString +
+ ",selectedTextColor=" + selectedTextColorString +
+ ",selectionColor=" + selectionColorString;
+ }
+
+
+ /**
+ * A Simple TransferHandler that exports the data as a String, and
+ * imports the data from the String clipboard. This is only used
+ * if the UI hasn't supplied one, which would only happen if someone
+ * hasn't subclassed Basic.
+ */
+ static class DefaultTransferHandler extends TransferHandler implements
+ UIResource {
+ public void exportToClipboard(JComponent comp, Clipboard clipboard,
+ int action) throws IllegalStateException {
+ if (comp instanceof JTextComponent) {
+ JTextComponent text = (JTextComponent)comp;
+ int p0 = text.getSelectionStart();
+ int p1 = text.getSelectionEnd();
+ if (p0 != p1) {
+ try {
+ Document doc = text.getDocument();
+ String srcData = doc.getText(p0, p1 - p0);
+ StringSelection contents =new StringSelection(srcData);
+
+ // this may throw an IllegalStateException,
+ // but it will be caught and handled in the
+ // action that invoked this method
+ clipboard.setContents(contents, null);
+
+ if (action == TransferHandler.MOVE) {
+ doc.remove(p0, p1 - p0);
+ }
+ } catch (BadLocationException ble) {}
+ }
+ }
+ }
+ public boolean importData(JComponent comp, Transferable t) {
+ if (comp instanceof JTextComponent) {
+ DataFlavor flavor = getFlavor(t.getTransferDataFlavors());
+
+ if (flavor != null) {
+ InputContext ic = comp.getInputContext();
+ if (ic != null) {
+ ic.endComposition();
+ }
+ try {
+ String data = (String)t.getTransferData(flavor);
+
+ ((JTextComponent)comp).replaceSelection(data);
+ return true;
+ } catch (UnsupportedFlavorException ufe) {
+ } catch (IOException ioe) {
+ }
+ }
+ }
+ return false;
+ }
+ public boolean canImport(JComponent comp,
+ DataFlavor[] transferFlavors) {
+ JTextComponent c = (JTextComponent)comp;
+ if (!(c.isEditable() && c.isEnabled())) {
+ return false;
+ }
+ return (getFlavor(transferFlavors) != null);
+ }
+ public int getSourceActions(JComponent c) {
+ return NONE;
+ }
+ private DataFlavor getFlavor(DataFlavor[] flavors) {
+ if (flavors != null) {
+ for (int counter = 0; counter < flavors.length; counter++) {
+ if (flavors[counter].equals(DataFlavor.stringFlavor)) {
+ return flavors[counter];
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Returns the JTextComponent that most recently had focus. The returned
+ * value may currently have focus.
+ */
+ static final JTextComponent getFocusedComponent() {
+ return (JTextComponent)AppContext.getAppContext().
+ get(FOCUSED_COMPONENT);
+ }
+
+ private int getCurrentEventModifiers() {
+ int modifiers = 0;
+ AWTEvent currentEvent = EventQueue.getCurrentEvent();
+ if (currentEvent instanceof InputEvent) {
+ modifiers = ((InputEvent)currentEvent).getModifiers();
+ } else if (currentEvent instanceof ActionEvent) {
+ modifiers = ((ActionEvent)currentEvent).getModifiers();
+ }
+ return modifiers;
+ }
+
+ private static final Object KEYMAP_TABLE =
+ new StringBuilder("JTextComponent_KeymapTable");
+ private JTextComponent editor;
+ //
+ // member variables used for on-the-spot input method
+ // editing style support
+ //
+ private transient InputMethodRequests inputMethodRequestsHandler;
+ private SimpleAttributeSet composedTextAttribute;
+ private String composedTextContent;
+ private Position composedTextStart;
+ private Position composedTextEnd;
+ private Position latestCommittedTextStart;
+ private Position latestCommittedTextEnd;
+ private ComposedTextCaret composedTextCaret;
+ private transient Caret originalCaret;
+ /**
+ * Set to true after the check for the override of processInputMethodEvent
+ * has been checked.
+ */
+ private boolean checkedInputOverride;
+ private boolean needToSendKeyTypedEvent;
+
+ static class DefaultKeymap implements Keymap {
+
+ DefaultKeymap(String nm, Keymap parent) {
+ this.nm = nm;
+ this.parent = parent;
+ bindings = new Hashtable();
+ }
+
+ /**
+ * Fetch the default action to fire if a
+ * key is typed (ie a KEY_TYPED KeyEvent is received)
+ * and there is no binding for it. Typically this
+ * would be some action that inserts text so that
+ * the keymap doesn't require an action for each
+ * possible key.
+ */
+ public Action getDefaultAction() {
+ if (defaultAction != null) {
+ return defaultAction;
+ }
+ return (parent != null) ? parent.getDefaultAction() : null;
+ }
+
+ /**
+ * Set the default action to fire if a key is typed.
+ */
+ public void setDefaultAction(Action a) {
+ defaultAction = a;
+ }
+
+ public String getName() {
+ return nm;
+ }
+
+ public Action getAction(KeyStroke key) {
+ Action a = (Action) bindings.get(key);
+ if ((a == null) && (parent != null)) {
+ a = parent.getAction(key);
+ }
+ return a;
+ }
+
+ public KeyStroke[] getBoundKeyStrokes() {
+ KeyStroke[] keys = new KeyStroke[bindings.size()];
+ int i = 0;
+ for (Enumeration e = bindings.keys() ; e.hasMoreElements() ;) {
+ keys[i++] = (KeyStroke) e.nextElement();
+ }
+ return keys;
+ }
+
+ public Action[] getBoundActions() {
+ Action[] actions = new Action[bindings.size()];
+ int i = 0;
+ for (Enumeration e = bindings.elements() ; e.hasMoreElements() ;) {
+ actions[i++] = (Action) e.nextElement();
+ }
+ return actions;
+ }
+
+ public KeyStroke[] getKeyStrokesForAction(Action a) {
+ if (a == null) {
+ return null;
+ }
+ KeyStroke[] retValue = null;
+ // Determine local bindings first.
+ Vector keyStrokes = null;
+ for (Enumeration enum_ = bindings.keys();
+ enum_.hasMoreElements();) {
+ Object key = enum_.nextElement();
+ if (bindings.get(key) == a) {
+ if (keyStrokes == null) {
+ keyStrokes = new Vector();
+ }
+ keyStrokes.addElement(key);
+ }
+ }
+ // See if the parent has any.
+ if (parent != null) {
+ KeyStroke[] pStrokes = parent.getKeyStrokesForAction(a);
+ if (pStrokes != null) {
+ // Remove any bindings defined in the parent that
+ // are locally defined.
+ int rCount = 0;
+ for (int counter = pStrokes.length - 1; counter >= 0;
+ counter--) {
+ if (isLocallyDefined(pStrokes[counter])) {
+ pStrokes[counter] = null;
+ rCount++;
+ }
+ }
+ if (rCount > 0 && rCount < pStrokes.length) {
+ if (keyStrokes == null) {
+ keyStrokes = new Vector();
+ }
+ for (int counter = pStrokes.length - 1; counter >= 0;
+ counter--) {
+ if (pStrokes[counter] != null) {
+ keyStrokes.addElement(pStrokes[counter]);
+ }
+ }
+ }
+ else if (rCount == 0) {
+ if (keyStrokes == null) {
+ retValue = pStrokes;
+ }
+ else {
+ retValue = new KeyStroke[keyStrokes.size() +
+ pStrokes.length];
+ keyStrokes.copyInto(retValue);
+ System.arraycopy(pStrokes, 0, retValue,
+ keyStrokes.size(), pStrokes.length);
+ keyStrokes = null;
+ }
+ }
+ }
+ }
+ if (keyStrokes != null) {
+ retValue = new KeyStroke[keyStrokes.size()];
+ keyStrokes.copyInto(retValue);
+ }
+ return retValue;
+ }
+
+ public boolean isLocallyDefined(KeyStroke key) {
+ return bindings.containsKey(key);
+ }
+
+ public void addActionForKeyStroke(KeyStroke key, Action a) {
+ bindings.put(key, a);
+ }
+
+ public void removeKeyStrokeBinding(KeyStroke key) {
+ bindings.remove(key);
+ }
+
+ public void removeBindings() {
+ bindings.clear();
+ }
+
+ public Keymap getResolveParent() {
+ return parent;
+ }
+
+ public void setResolveParent(Keymap parent) {
+ this.parent = parent;
+ }
+
+ /**
+ * String representation of the keymap... potentially
+ * a very long string.
+ */
+ public String toString() {
+ return "Keymap[" + nm + "]" + bindings;
+ }
+
+ String nm;
+ Keymap parent;
+ Hashtable bindings;
+ Action defaultAction;
+ }
+
+
+ /**
+ * KeymapWrapper wraps a Keymap inside an InputMap. For KeymapWrapper
+ * to be useful it must be used with a KeymapActionMap.
+ * KeymapWrapper for the most part, is an InputMap with two parents.
+ * The first parent visited is ALWAYS the Keymap, with the second
+ * parent being the parent inherited from InputMap. If
+ * <code>keymap.getAction</code> returns null, implying the Keymap
+ * does not have a binding for the KeyStroke,
+ * the parent is then visited. If the Keymap has a binding, the
+ * Action is returned, if not and the KeyStroke represents a
+ * KeyTyped event and the Keymap has a defaultAction,
+ * <code>DefaultActionKey</code> is returned.
+ * <p>KeymapActionMap is then able to transate the object passed in
+ * to either message the Keymap, or message its default implementation.
+ */
+ static class KeymapWrapper extends InputMap {
+ static final Object DefaultActionKey = new Object();
+
+ private Keymap keymap;
+
+ KeymapWrapper(Keymap keymap) {
+ this.keymap = keymap;
+ }
+
+ public KeyStroke[] keys() {
+ KeyStroke[] sKeys = super.keys();
+ KeyStroke[] keymapKeys = keymap.getBoundKeyStrokes();
+ int sCount = (sKeys == null) ? 0 : sKeys.length;
+ int keymapCount = (keymapKeys == null) ? 0 : keymapKeys.length;
+ if (sCount == 0) {
+ return keymapKeys;
+ }
+ if (keymapCount == 0) {
+ return sKeys;
+ }
+ KeyStroke[] retValue = new KeyStroke[sCount + keymapCount];
+ // There may be some duplication here...
+ System.arraycopy(sKeys, 0, retValue, 0, sCount);
+ System.arraycopy(keymapKeys, 0, retValue, sCount, keymapCount);
+ return retValue;
+ }
+
+ public int size() {
+ // There may be some duplication here...
+ KeyStroke[] keymapStrokes = keymap.getBoundKeyStrokes();
+ int keymapCount = (keymapStrokes == null) ? 0:
+ keymapStrokes.length;
+ return super.size() + keymapCount;
+ }
+
+ public Object get(KeyStroke keyStroke) {
+ Object retValue = keymap.getAction(keyStroke);
+ if (retValue == null) {
+ retValue = super.get(keyStroke);
+ if (retValue == null &&
+ keyStroke.getKeyChar() != KeyEvent.CHAR_UNDEFINED &&
+ keymap.getDefaultAction() != null) {
+ // Implies this is a KeyTyped event, use the default
+ // action.
+ retValue = DefaultActionKey;
+ }
+ }
+ return retValue;
+ }
+ }
+
+
+ /**
+ * Wraps a Keymap inside an ActionMap. This is used with
+ * a KeymapWrapper. If <code>get</code> is passed in
+ * <code>KeymapWrapper.DefaultActionKey</code>, the default action is
+ * returned, otherwise if the key is an Action, it is returned.
+ */
+ static class KeymapActionMap extends ActionMap {
+ private Keymap keymap;
+
+ KeymapActionMap(Keymap keymap) {
+ this.keymap = keymap;
+ }
+
+ public Object[] keys() {
+ Object[] sKeys = super.keys();
+ Object[] keymapKeys = keymap.getBoundActions();
+ int sCount = (sKeys == null) ? 0 : sKeys.length;
+ int keymapCount = (keymapKeys == null) ? 0 : keymapKeys.length;
+ boolean hasDefault = (keymap.getDefaultAction() != null);
+ if (hasDefault) {
+ keymapCount++;
+ }
+ if (sCount == 0) {
+ if (hasDefault) {
+ Object[] retValue = new Object[keymapCount];
+ if (keymapCount > 1) {
+ System.arraycopy(keymapKeys, 0, retValue, 0,
+ keymapCount - 1);
+ }
+ retValue[keymapCount - 1] = KeymapWrapper.DefaultActionKey;
+ return retValue;
+ }
+ return keymapKeys;
+ }
+ if (keymapCount == 0) {
+ return sKeys;
+ }
+ Object[] retValue = new Object[sCount + keymapCount];
+ // There may be some duplication here...
+ System.arraycopy(sKeys, 0, retValue, 0, sCount);
+ if (hasDefault) {
+ if (keymapCount > 1) {
+ System.arraycopy(keymapKeys, 0, retValue, sCount,
+ keymapCount - 1);
+ }
+ retValue[sCount + keymapCount - 1] = KeymapWrapper.
+ DefaultActionKey;
+ }
+ else {
+ System.arraycopy(keymapKeys, 0, retValue, sCount, keymapCount);
+ }
+ return retValue;
+ }
+
+ public int size() {
+ // There may be some duplication here...
+ Object[] actions = keymap.getBoundActions();
+ int keymapCount = (actions == null) ? 0 : actions.length;
+ if (keymap.getDefaultAction() != null) {
+ keymapCount++;
+ }
+ return super.size() + keymapCount;
+ }
+
+ public Action get(Object key) {
+ Action retValue = super.get(key);
+ if (retValue == null) {
+ // Try the Keymap.
+ if (key == KeymapWrapper.DefaultActionKey) {
+ retValue = keymap.getDefaultAction();
+ }
+ else if (key instanceof Action) {
+ // This is a little iffy, technically an Action is
+ // a valid Key. We're assuming the Action came from
+ // the InputMap though.
+ retValue = (Action)key;
+ }
+ }
+ return retValue;
+ }
+ }
+
+ private static final Object FOCUSED_COMPONENT =
+ new StringBuilder("JTextComponent_FocusedComponent");
+
+ /**
+ * The default keymap that will be shared by all
+ * <code>JTextComponent</code> instances unless they
+ * have had a different keymap set.
+ */
+ public static final String DEFAULT_KEYMAP = "default";
+
+ /**
+ * Event to use when firing a notification of change to caret
+ * position. This is mutable so that the event can be reused
+ * since caret events can be fairly high in bandwidth.
+ */
+ static class MutableCaretEvent extends CaretEvent implements ChangeListener, FocusListener, MouseListener {
+
+ MutableCaretEvent(JTextComponent c) {
+ super(c);
+ }
+
+ final void fire() {
+ JTextComponent c = (JTextComponent) getSource();
+ if (c != null) {
+ Caret caret = c.getCaret();
+ dot = caret.getDot();
+ mark = caret.getMark();
+ c.fireCaretUpdate(this);
+ }
+ }
+
+ public final String toString() {
+ return "dot=" + dot + "," + "mark=" + mark;
+ }
+
+ // --- CaretEvent methods -----------------------
+
+ public final int getDot() {
+ return dot;
+ }
+
+ public final int getMark() {
+ return mark;
+ }
+
+ // --- ChangeListener methods -------------------
+
+ public final void stateChanged(ChangeEvent e) {
+ if (! dragActive) {
+ fire();
+ }
+ }
+
+ // --- FocusListener methods -----------------------------------
+ public void focusGained(FocusEvent fe) {
+ AppContext.getAppContext().put(FOCUSED_COMPONENT,
+ fe.getSource());
+ }
+
+ public void focusLost(FocusEvent fe) {
+ }
+
+ // --- MouseListener methods -----------------------------------
+
+ /**
+ * Requests focus on the associated
+ * text component, and try to set the cursor position.
+ *
+ * @param e the mouse event
+ * @see MouseListener#mousePressed
+ */
+ public final void mousePressed(MouseEvent e) {
+ dragActive = true;
+ }
+
+ /**
+ * Called when the mouse is released.
+ *
+ * @param e the mouse event
+ * @see MouseListener#mouseReleased
+ */
+ public final void mouseReleased(MouseEvent e) {
+ dragActive = false;
+ fire();
+ }
+
+ public final void mouseClicked(MouseEvent e) {
+ }
+
+ public final void mouseEntered(MouseEvent e) {
+ }
+
+ public final void mouseExited(MouseEvent e) {
+ }
+
+ private boolean dragActive;
+ private int dot;
+ private int mark;
+ }
+
+ //
+ // Process any input method events that the component itself
+ // recognizes. The default on-the-spot handling for input method
+ // composed(uncommitted) text is done here after all input
+ // method listeners get called for stealing the events.
+ //
+ protected void processInputMethodEvent(InputMethodEvent e) {
+ // let listeners handle the events
+ super.processInputMethodEvent(e);
+
+ if (!e.isConsumed()) {
+ if (! isEditable()) {
+ return;
+ } else {
+ switch (e.getID()) {
+ case InputMethodEvent.INPUT_METHOD_TEXT_CHANGED:
+ replaceInputMethodText(e);
+
+ // fall through
+
+ case InputMethodEvent.CARET_POSITION_CHANGED:
+ setInputMethodCaretPosition(e);
+ break;
+ }
+ }
+
+ e.consume();
+ }
+ }
+
+ //
+ // Overrides this method to become an active input method client.
+ //
+ public InputMethodRequests getInputMethodRequests() {
+ if (inputMethodRequestsHandler == null) {
+ inputMethodRequestsHandler =
+ (InputMethodRequests)new InputMethodRequestsHandler();
+ Document doc = getDocument();
+ if (doc != null) {
+ doc.addDocumentListener((DocumentListener)inputMethodRequestsHandler);
+ }
+ }
+
+ return inputMethodRequestsHandler;
+ }
+
+ //
+ // Overrides this method to watch the listener installed.
+ //
+ public void addInputMethodListener(InputMethodListener l) {
+ super.addInputMethodListener(l);
+ if (l != null) {
+ needToSendKeyTypedEvent = false;
+ checkedInputOverride = true;
+ }
+ }
+
+
+ //
+ // Default implementation of the InputMethodRequests interface.
+ //
+ class InputMethodRequestsHandler implements InputMethodRequests, DocumentListener {
+
+ // --- InputMethodRequests methods ---
+
+ public AttributedCharacterIterator cancelLatestCommittedText(
+ Attribute[] attributes) {
+ Document doc = getDocument();
+ if ((doc != null) && (latestCommittedTextStart != null)
+ && (!latestCommittedTextStart.equals(latestCommittedTextEnd))) {
+ try {
+ int startIndex = latestCommittedTextStart.getOffset();
+ int endIndex = latestCommittedTextEnd.getOffset();
+ String latestCommittedText =
+ doc.getText(startIndex, endIndex - startIndex);
+ doc.remove(startIndex, endIndex - startIndex);
+ return new AttributedString(latestCommittedText).getIterator();
+ } catch (BadLocationException ble) {}
+ }
+ return null;
+ }
+
+ public AttributedCharacterIterator getCommittedText(int beginIndex,
+ int endIndex, Attribute[] attributes) {
+ int composedStartIndex = 0;
+ int composedEndIndex = 0;
+ if (composedTextExists()) {
+ composedStartIndex = composedTextStart.getOffset();
+ composedEndIndex = composedTextEnd.getOffset();
+ }
+
+ String committed;
+ try {
+ if (beginIndex < composedStartIndex) {
+ if (endIndex <= composedStartIndex) {
+ committed = getText(beginIndex, endIndex - beginIndex);
+ } else {
+ int firstPartLength = composedStartIndex - beginIndex;
+ committed = getText(beginIndex, firstPartLength) +
+ getText(composedEndIndex, endIndex - beginIndex - firstPartLength);
+ }
+ } else {
+ committed = getText(beginIndex + (composedEndIndex - composedStartIndex),
+ endIndex - beginIndex);
+ }
+ } catch (BadLocationException ble) {
+ throw new IllegalArgumentException("Invalid range");
+ }
+ return new AttributedString(committed).getIterator();
+ }
+
+ public int getCommittedTextLength() {
+ Document doc = getDocument();
+ int length = 0;
+ if (doc != null) {
+ length = doc.getLength();
+ if (composedTextContent != null) {
+ if (composedTextEnd == null
+ || composedTextStart == null) {
+ /*
+ * fix for : 6355666
+ * this is the case when this method is invoked
+ * from DocumentListener. At this point
+ * composedTextEnd and composedTextStart are
+ * not defined yet.
+ */
+ length -= composedTextContent.length();
+ } else {
+ length -= composedTextEnd.getOffset() -
+ composedTextStart.getOffset();
+ }
+ }
+ }
+ return length;
+ }
+
+ public int getInsertPositionOffset() {
+ int composedStartIndex = 0;
+ int composedEndIndex = 0;
+ if (composedTextExists()) {
+ composedStartIndex = composedTextStart.getOffset();
+ composedEndIndex = composedTextEnd.getOffset();
+ }
+ int caretIndex = getCaretPosition();
+
+ if (caretIndex < composedStartIndex) {
+ return caretIndex;
+ } else if (caretIndex < composedEndIndex) {
+ return composedStartIndex;
+ } else {
+ return caretIndex - (composedEndIndex - composedStartIndex);
+ }
+ }
+
+ public TextHitInfo getLocationOffset(int x, int y) {
+ if (composedTextAttribute == null) {
+ return null;
+ } else {
+ Point p = getLocationOnScreen();
+ p.x = x - p.x;
+ p.y = y - p.y;
+ int pos = viewToModel(p);
+ if ((pos >= composedTextStart.getOffset()) &&
+ (pos <= composedTextEnd.getOffset())) {
+ return TextHitInfo.leading(pos - composedTextStart.getOffset());
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public Rectangle getTextLocation(TextHitInfo offset) {
+ Rectangle r;
+
+ try {
+ r = modelToView(getCaretPosition());
+ if (r != null) {
+ Point p = getLocationOnScreen();
+ r.translate(p.x, p.y);
+ }
+ } catch (BadLocationException ble) {
+ r = null;
+ }
+
+ if (r == null)
+ r = new Rectangle();
+
+ return r;
+ }
+
+ public AttributedCharacterIterator getSelectedText(
+ Attribute[] attributes) {
+ String selection = JTextComponent.this.getSelectedText();
+ if (selection != null) {
+ return new AttributedString(selection).getIterator();
+ } else {
+ return null;
+ }
+ }
+
+ // --- DocumentListener methods ---
+
+ public void changedUpdate(DocumentEvent e) {
+ latestCommittedTextStart = latestCommittedTextEnd = null;
+ }
+
+ public void insertUpdate(DocumentEvent e) {
+ latestCommittedTextStart = latestCommittedTextEnd = null;
+ }
+
+ public void removeUpdate(DocumentEvent e) {
+ latestCommittedTextStart = latestCommittedTextEnd = null;
+ }
+ }
+
+ //
+ // Replaces the current input method (composed) text according to
+ // the passed input method event. This method also inserts the
+ // committed text into the document.
+ //
+ private void replaceInputMethodText(InputMethodEvent e) {
+ int commitCount = e.getCommittedCharacterCount();
+ AttributedCharacterIterator text = e.getText();
+ int composedTextIndex;
+
+ // old composed text deletion
+ Document doc = getDocument();
+ if (composedTextExists()) {
+ try {
+ doc.remove(composedTextStart.getOffset(),
+ composedTextEnd.getOffset() -
+ composedTextStart.getOffset());
+ } catch (BadLocationException ble) {}
+ composedTextStart = composedTextEnd = null;
+ composedTextAttribute = null;
+ composedTextContent = null;
+ }
+
+ if (text != null) {
+ text.first();
+ int committedTextStartIndex = 0;
+ int committedTextEndIndex = 0;
+
+ // committed text insertion
+ if (commitCount > 0) {
+ // Remember latest committed text start index
+ committedTextStartIndex = caret.getDot();
+
+ // Need to generate KeyTyped events for the committed text for components
+ // that are not aware they are active input method clients.
+ if (shouldSynthensizeKeyEvents()) {
+ for (char c = text.current(); commitCount > 0;
+ c = text.next(), commitCount--) {
+ KeyEvent ke = new KeyEvent(this, KeyEvent.KEY_TYPED,
+ EventQueue.getMostRecentEventTime(),
+ 0, KeyEvent.VK_UNDEFINED, c);
+ processKeyEvent(ke);
+ }
+ } else {
+ StringBuffer strBuf = new StringBuffer();
+ for (char c = text.current(); commitCount > 0;
+ c = text.next(), commitCount--) {
+ strBuf.append(c);
+ }
+
+ // map it to an ActionEvent
+ mapCommittedTextToAction(new String(strBuf));
+ }
+
+ // Remember latest committed text end index
+ committedTextEndIndex = caret.getDot();
+ }
+
+ // new composed text insertion
+ composedTextIndex = text.getIndex();
+ if (composedTextIndex < text.getEndIndex()) {
+ createComposedTextAttribute(composedTextIndex, text);
+ try {
+ replaceSelection(null);
+ doc.insertString(caret.getDot(), composedTextContent,
+ composedTextAttribute);
+ composedTextStart = doc.createPosition(caret.getDot() -
+ composedTextContent.length());
+ composedTextEnd = doc.createPosition(caret.getDot());
+ } catch (BadLocationException ble) {
+ composedTextStart = composedTextEnd = null;
+ composedTextAttribute = null;
+ composedTextContent = null;
+ }
+ }
+
+ // Save the latest committed text information
+ if (committedTextStartIndex != committedTextEndIndex) {
+ try {
+ latestCommittedTextStart = doc.
+ createPosition(committedTextStartIndex);
+ latestCommittedTextEnd = doc.
+ createPosition(committedTextEndIndex);
+ } catch (BadLocationException ble) {
+ latestCommittedTextStart =
+ latestCommittedTextEnd = null;
+ }
+ } else {
+ latestCommittedTextStart =
+ latestCommittedTextEnd = null;
+ }
+ }
+ }
+
+ private void createComposedTextAttribute(int composedIndex,
+ AttributedCharacterIterator text) {
+ Document doc = getDocument();
+ StringBuffer strBuf = new StringBuffer();
+
+ // create attributed string with no attributes
+ for (char c = text.setIndex(composedIndex);
+ c != CharacterIterator.DONE; c = text.next()) {
+ strBuf.append(c);
+ }
+
+ composedTextContent = new String(strBuf);
+ composedTextAttribute = new SimpleAttributeSet();
+ composedTextAttribute.addAttribute(StyleConstants.ComposedTextAttribute,
+ new AttributedString(text, composedIndex, text.getEndIndex()));
+ }
+
+ private boolean saveComposedText(int pos) {
+ if (composedTextExists()) {
+ int start = composedTextStart.getOffset();
+ int len = composedTextEnd.getOffset() -
+ composedTextStart.getOffset();
+ if (pos >= start && pos <= start + len) {
+ try {
+ getDocument().remove(start, len);
+ return true;
+ } catch (BadLocationException ble) {}
+ }
+ }
+ return false;
+ }
+
+ private void restoreComposedText() {
+ Document doc = getDocument();
+ try {
+ doc.insertString(caret.getDot(),
+ composedTextContent,
+ composedTextAttribute);
+ composedTextStart = doc.createPosition(caret.getDot() -
+ composedTextContent.length());
+ composedTextEnd = doc.createPosition(caret.getDot());
+ } catch (BadLocationException ble) {}
+ }
+
+ //
+ // Map committed text to an ActionEvent. If the committed text length is 1,
+ // treat it as a KeyStroke, otherwise or there is no KeyStroke defined,
+ // treat it just as a default action.
+ //
+ private void mapCommittedTextToAction(String committedText) {
+ Keymap binding = getKeymap();
+ if (binding != null) {
+ Action a = null;
+ if (committedText.length() == 1) {
+ KeyStroke k = KeyStroke.getKeyStroke(committedText.charAt(0));
+ a = binding.getAction(k);
+ }
+
+ if (a == null) {
+ a = binding.getDefaultAction();
+ }
+
+ if (a != null) {
+ ActionEvent ae =
+ new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
+ committedText,
+ EventQueue.getMostRecentEventTime(),
+ getCurrentEventModifiers());
+ a.actionPerformed(ae);
+ }
+ }
+ }
+
+ //
+ // Sets the caret position according to the passed input method
+ // event. Also, sets/resets composed text caret appropriately.
+ //
+ private void setInputMethodCaretPosition(InputMethodEvent e) {
+ int dot;
+
+ if (composedTextExists()) {
+ dot = composedTextStart.getOffset();
+ if (!(caret instanceof ComposedTextCaret)) {
+ if (composedTextCaret == null) {
+ composedTextCaret = new ComposedTextCaret();
+ }
+ originalCaret = caret;
+ // Sets composed text caret
+ exchangeCaret(originalCaret, composedTextCaret);
+ }
+
+ TextHitInfo caretPos = e.getCaret();
+ if (caretPos != null) {
+ int index = caretPos.getInsertionIndex();
+ dot += index;
+ if (index == 0) {
+ // Scroll the component if needed so that the composed text
+ // becomes visible.
+ try {
+ Rectangle d = modelToView(dot);
+ Rectangle end = modelToView(composedTextEnd.getOffset());
+ Rectangle b = getBounds();
+ d.x += Math.min(end.x - d.x, b.width);
+ scrollRectToVisible(d);
+ } catch (BadLocationException ble) {}
+ }
+ }
+ caret.setDot(dot);
+ } else if (caret instanceof ComposedTextCaret) {
+ dot = caret.getDot();
+ // Restores original caret
+ exchangeCaret(caret, originalCaret);
+ caret.setDot(dot);
+ }
+ }
+
+ private void exchangeCaret(Caret oldCaret, Caret newCaret) {
+ int blinkRate = oldCaret.getBlinkRate();
+ setCaret(newCaret);
+ caret.setBlinkRate(blinkRate);
+ caret.setVisible(hasFocus());
+ }
+
+ /**
+ * Returns true if KeyEvents should be synthesized from an InputEvent.
+ */
+ private boolean shouldSynthensizeKeyEvents() {
+ if (!checkedInputOverride) {
+ checkedInputOverride = true;
+ needToSendKeyTypedEvent =
+ !isProcessInputMethodEventOverridden();
+ }
+ return needToSendKeyTypedEvent;
+ }
+
+ //
+ // Checks whether the client code overrides processInputMethodEvent. If it is overridden,
+ // need not to generate KeyTyped events for committed text. If it's not, behave as an
+ // passive input method client.
+ //
+ private boolean isProcessInputMethodEventOverridden() {
+ if (overrideMap == null) {
+ overrideMap = Collections.synchronizedMap(new HashMap());
+ }
+ Boolean retValue = (Boolean)overrideMap.get(getClass().getName());
+
+ if (retValue != null) {
+ return retValue.booleanValue();
+ }
+ Boolean ret = (Boolean)AccessController.doPrivileged(new
+ PrivilegedAction() {
+ public Object run() {
+ return isProcessInputMethodEventOverridden(
+ JTextComponent.this.getClass());
+ }
+ });
+
+ return ret.booleanValue();
+ }
+
+ //
+ // Checks whether a composed text in this text component
+ //
+ boolean composedTextExists() {
+ return (composedTextStart != null);
+ }
+
+ //
+ // Caret implementation for editing the composed text.
+ //
+ class ComposedTextCaret extends DefaultCaret implements Serializable {
+ Color bg;
+
+ //
+ // Get the background color of the component
+ //
+ public void install(JTextComponent c) {
+ super.install(c);
+
+ Document doc = c.getDocument();
+ if (doc instanceof StyledDocument) {
+ StyledDocument sDoc = (StyledDocument)doc;
+ Element elem = sDoc.getCharacterElement(c.composedTextStart.getOffset());
+ AttributeSet attr = elem.getAttributes();
+ bg = sDoc.getBackground(attr);
+ }
+
+ if (bg == null) {
+ bg = c.getBackground();
+ }
+ }
+
+ //
+ // Draw caret in XOR mode.
+ //
+ public void paint(Graphics g) {
+ if(isVisible()) {
+ try {
+ Rectangle r = component.modelToView(getDot());
+ g.setXORMode(bg);
+ g.drawLine(r.x, r.y, r.x, r.y + r.height - 1);
+ g.setPaintMode();
+ } catch (BadLocationException e) {
+ // can't render I guess
+ //System.err.println("Can't render cursor");
+ }
+ }
+ }
+
+ //
+ // If some area other than the composed text is clicked by mouse,
+ // issue endComposition() to force commit the composed text.
+ //
+ protected void positionCaret(MouseEvent me) {
+ JTextComponent host = component;
+ Point pt = new Point(me.getX(), me.getY());
+ int offset = host.viewToModel(pt);
+ int composedStartIndex = host.composedTextStart.getOffset();
+ if ((offset < composedStartIndex) ||
+ (offset > composedTextEnd.getOffset())) {
+ try {
+ // Issue endComposition
+ Position newPos = host.getDocument().createPosition(offset);
+ host.getInputContext().endComposition();
+
+ // Post a caret positioning runnable to assure that the positioning
+ // occurs *after* committing the composed text.
+ EventQueue.invokeLater(new DoSetCaretPosition(host, newPos));
+ } catch (BadLocationException ble) {
+ System.err.println(ble);
+ }
+ } else {
+ // Normal processing
+ super.positionCaret(me);
+ }
+ }
+ }
+
+ //
+ // Runnable class for invokeLater() to set caret position later.
+ //
+ private class DoSetCaretPosition implements Runnable {
+ JTextComponent host;
+ Position newPos;
+
+ DoSetCaretPosition(JTextComponent host, Position newPos) {
+ this.host = host;
+ this.newPos = newPos;
+ }
+
+ public void run() {
+ host.setCaretPosition(newPos.getOffset());
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/Keymap.java b/src/share/classes/javax/swing/text/Keymap.java
new file mode 100644
index 000000000..dea401c96
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Keymap.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 1997-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;
+
+import javax.swing.Action;
+import javax.swing.KeyStroke;
+
+/**
+ * A collection of bindings of KeyStrokes to actions. The
+ * bindings are basically name-value pairs that potentially
+ * resolve in a hierarchy.
+ *
+ * @author Timothy Prinzing
+ */
+public interface Keymap {
+
+ /**
+ * Fetches the name of the set of key-bindings.
+ *
+ * @return the name
+ */
+ public String getName();
+
+ /**
+ * Fetches the default action to fire if a
+ * key is typed (i.e. a KEY_TYPED KeyEvent is received)
+ * and there is no binding for it. Typically this
+ * would be some action that inserts text so that
+ * the keymap doesn't require an action for each
+ * possible key.
+ *
+ * @return the default action
+ */
+ public Action getDefaultAction();
+
+ /**
+ * Set the default action to fire if a key is typed.
+ *
+ * @param a the action
+ */
+ public void setDefaultAction(Action a);
+
+ /**
+ * Fetches the action appropriate for the given symbolic
+ * event sequence. This is used by JTextController to
+ * determine how to interpret key sequences. If the
+ * binding is not resolved locally, an attempt is made
+ * to resolve through the parent keymap, if one is set.
+ *
+ * @param key the key sequence
+ * @return the action associated with the key
+ * sequence if one is defined, otherwise <code>null</code>
+ */
+ public Action getAction(KeyStroke key);
+
+ /**
+ * Fetches all of the keystrokes in this map that
+ * are bound to some action.
+ *
+ * @return the list of keystrokes
+ */
+ public KeyStroke[] getBoundKeyStrokes();
+
+ /**
+ * Fetches all of the actions defined in this keymap.
+ *
+ * @return the list of actions
+ */
+ public Action[] getBoundActions();
+
+ /**
+ * Fetches the keystrokes that will result in
+ * the given action.
+ *
+ * @param a the action
+ * @return the list of keystrokes
+ */
+ public KeyStroke[] getKeyStrokesForAction(Action a);
+
+ /**
+ * Determines if the given key sequence is locally defined.
+ *
+ * @param key the key sequence
+ * @return true if the key sequence is locally defined else false
+ */
+ public boolean isLocallyDefined(KeyStroke key);
+
+ /**
+ * Adds a binding to the keymap.
+ *
+ * @param key the key sequence
+ * @param a the action
+ */
+ public void addActionForKeyStroke(KeyStroke key, Action a);
+
+ /**
+ * Removes a binding from the keymap.
+ *
+ * @param keys the key sequence
+ */
+ public void removeKeyStrokeBinding(KeyStroke keys);
+
+ /**
+ * Removes all bindings from the keymap.
+ */
+ public void removeBindings();
+
+ /**
+ * Fetches the parent keymap used to resolve key-bindings.
+ *
+ * @return the keymap
+ */
+ public Keymap getResolveParent();
+
+ /**
+ * Sets the parent keymap, which will be used to
+ * resolve key-bindings.
+ *
+ * @param parent the parent keymap
+ */
+ public void setResolveParent(Keymap parent);
+
+}
diff --git a/src/share/classes/javax/swing/text/LabelView.java b/src/share/classes/javax/swing/text/LabelView.java
new file mode 100644
index 000000000..524a7689d
--- /dev/null
+++ b/src/share/classes/javax/swing/text/LabelView.java
@@ -0,0 +1,316 @@
+/*
+ * 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;
+
+import java.awt.*;
+import javax.swing.event.*;
+
+/**
+ * A <code>LabelView</code> is a styled chunk of text
+ * that represents a view mapped over an element in the
+ * text model. It caches the character level attributes
+ * used for rendering.
+ *
+ * @author Timothy Prinzing
+ */
+public class LabelView extends GlyphView implements TabableView {
+
+ /**
+ * Constructs a new view wrapped on an element.
+ *
+ * @param elem the element
+ */
+ public LabelView(Element elem) {
+ super(elem);
+ }
+
+ /**
+ * Synchronize the view's cached values with the model.
+ * This causes the font, metrics, color, etc to be
+ * re-cached if the cache has been invalidated.
+ */
+ final void sync() {
+ if (font == null) {
+ setPropertiesFromAttributes();
+ }
+ }
+
+ /**
+ * Sets whether or not the view is underlined.
+ * Note that this setter is protected and is really
+ * only meant if you need to update some additional
+ * state when set.
+ *
+ * @param u true if the view is underlined, otherwise
+ * false
+ * @see #isUnderline
+ */
+ protected void setUnderline(boolean u) {
+ underline = u;
+ }
+
+ /**
+ * Sets whether or not the view has a strike/line
+ * through it.
+ * Note that this setter is protected and is really
+ * only meant if you need to update some additional
+ * state when set.
+ *
+ * @param s true if the view has a strike/line
+ * through it, otherwise false
+ * @see #isStrikeThrough
+ */
+ protected void setStrikeThrough(boolean s) {
+ strike = s;
+ }
+
+
+ /**
+ * Sets whether or not the view represents a
+ * superscript.
+ * Note that this setter is protected and is really
+ * only meant if you need to update some additional
+ * state when set.
+ *
+ * @param s true if the view represents a
+ * superscript, otherwise false
+ * @see #isSuperscript
+ */
+ protected void setSuperscript(boolean s) {
+ superscript = s;
+ }
+
+ /**
+ * Sets whether or not the view represents a
+ * subscript.
+ * Note that this setter is protected and is really
+ * only meant if you need to update some additional
+ * state when set.
+ *
+ * @param s true if the view represents a
+ * subscript, otherwise false
+ * @see #isSubscript
+ */
+ protected void setSubscript(boolean s) {
+ subscript = s;
+ }
+
+ /**
+ * Sets the background color for the view. This method is typically
+ * invoked as part of configuring this <code>View</code>. If you need
+ * to customize the background color you should override
+ * <code>setPropertiesFromAttributes</code> and invoke this method. A
+ * value of null indicates no background should be rendered, so that the
+ * background of the parent <code>View</code> will show through.
+ *
+ * @param bg background color, or null
+ * @see #setPropertiesFromAttributes
+ * @since 1.5
+ */
+ protected void setBackground(Color bg) {
+ this.bg = bg;
+ }
+
+ /**
+ * Sets the cached properties from the attributes.
+ */
+ protected void setPropertiesFromAttributes() {
+ AttributeSet attr = getAttributes();
+ if (attr != null) {
+ Document d = getDocument();
+ if (d instanceof StyledDocument) {
+ StyledDocument doc = (StyledDocument) d;
+ font = doc.getFont(attr);
+ fg = doc.getForeground(attr);
+ if (attr.isDefined(StyleConstants.Background)) {
+ bg = doc.getBackground(attr);
+ } else {
+ bg = null;
+ }
+ setUnderline(StyleConstants.isUnderline(attr));
+ setStrikeThrough(StyleConstants.isStrikeThrough(attr));
+ setSuperscript(StyleConstants.isSuperscript(attr));
+ setSubscript(StyleConstants.isSubscript(attr));
+ } else {
+ throw new StateInvariantError("LabelView needs StyledDocument");
+ }
+ }
+ }
+
+ /**
+ * Fetches the <code>FontMetrics</code> used for this view.
+ * @deprecated FontMetrics are not used for glyph rendering
+ * when running in the JDK.
+ */
+ @Deprecated
+ protected FontMetrics getFontMetrics() {
+ sync();
+ Container c = getContainer();
+ return (c != null) ? c.getFontMetrics(font) :
+ Toolkit.getDefaultToolkit().getFontMetrics(font);
+ }
+
+ /**
+ * Fetches the background color to use to render the glyphs.
+ * This is implemented to return a cached background color,
+ * which defaults to <code>null</code>.
+ *
+ * @return the cached background color
+ * @since 1.3
+ */
+ public Color getBackground() {
+ sync();
+ return bg;
+ }
+
+ /**
+ * Fetches the foreground color to use to render the glyphs.
+ * This is implemented to return a cached foreground color,
+ * which defaults to <code>null</code>.
+ *
+ * @return the cached foreground color
+ * @since 1.3
+ */
+ public Color getForeground() {
+ sync();
+ return fg;
+ }
+
+ /**
+ * Fetches the font that the glyphs should be based upon.
+ * This is implemented to return a cached font.
+ *
+ * @return the cached font
+ */
+ public Font getFont() {
+ sync();
+ return font;
+ }
+
+ /**
+ * Determines if the glyphs should be underlined. If true,
+ * an underline should be drawn through the baseline. This
+ * is implemented to return the cached underline property.
+ *
+ * <p>When you request this property, <code>LabelView</code>
+ * re-syncs its state with the properties of the
+ * <code>Element</code>'s <code>AttributeSet</code>.
+ * If <code>Element</code>'s <code>AttributeSet</code>
+ * does not have this property set, it will revert to false.
+ *
+ * @return the value of the cached
+ * <code>underline</code> property
+ * @since 1.3
+ */
+ public boolean isUnderline() {
+ sync();
+ return underline;
+ }
+
+ /**
+ * Determines if the glyphs should have a strikethrough
+ * line. If true, a line should be drawn through the center
+ * of the glyphs. This is implemented to return the
+ * cached <code>strikeThrough</code> property.
+ *
+ * <p>When you request this property, <code>LabelView</code>
+ * re-syncs its state with the properties of the
+ * <code>Element</code>'s <code>AttributeSet</code>.
+ * If <code>Element</code>'s <code>AttributeSet</code>
+ * does not have this property set, it will revert to false.
+ *
+ * @return the value of the cached
+ * <code>strikeThrough</code> property
+ * @since 1.3
+ */
+ public boolean isStrikeThrough() {
+ sync();
+ return strike;
+ }
+
+ /**
+ * Determines if the glyphs should be rendered as superscript.
+ * @return the value of the cached subscript property
+ *
+ * <p>When you request this property, <code>LabelView</code>
+ * re-syncs its state with the properties of the
+ * <code>Element</code>'s <code>AttributeSet</code>.
+ * If <code>Element</code>'s <code>AttributeSet</code>
+ * does not have this property set, it will revert to false.
+ *
+ * @return the value of the cached
+ * <code>subscript</code> property
+ * @since 1.3
+ */
+ public boolean isSubscript() {
+ sync();
+ return subscript;
+ }
+
+ /**
+ * Determines if the glyphs should be rendered as subscript.
+ *
+ * <p>When you request this property, <code>LabelView</code>
+ * re-syncs its state with the properties of the
+ * <code>Element</code>'s <code>AttributeSet</code>.
+ * If <code>Element</code>'s <code>AttributeSet</code>
+ * does not have this property set, it will revert to false.
+ *
+ * @return the value of the cached
+ * <code>superscript</code> property
+ * @since 1.3
+ */
+ public boolean isSuperscript() {
+ sync();
+ return superscript;
+ }
+
+ // --- View methods ---------------------------------------------
+
+ /**
+ * 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) {
+ font = null;
+ super.changedUpdate(e, a, f);
+ }
+
+ // --- variables ------------------------------------------------
+
+ private Font font;
+ private Color fg;
+ private Color bg;
+ private boolean underline;
+ private boolean strike;
+ private boolean superscript;
+ private boolean subscript;
+
+}
diff --git a/src/share/classes/javax/swing/text/LayeredHighlighter.java b/src/share/classes/javax/swing/text/LayeredHighlighter.java
new file mode 100644
index 000000000..f7e8ecedc
--- /dev/null
+++ b/src/share/classes/javax/swing/text/LayeredHighlighter.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;
+
+import java.awt.Graphics;
+import java.awt.Shape;
+
+/**
+ *
+ * @author Scott Violet
+ * @author Timothy Prinzing
+ * @see Highlighter
+ */
+public abstract class LayeredHighlighter implements Highlighter {
+ /**
+ * When leaf Views (such as LabelView) are rendering they should
+ * call into this method. If a highlight is in the given region it will
+ * be drawn immediately.
+ *
+ * @param g Graphics used to draw
+ * @param p0 starting offset of view
+ * @param p1 ending offset of view
+ * @param viewBounds Bounds of View
+ * @param editor JTextComponent
+ * @param view View instance being rendered
+ */
+ public abstract void paintLayeredHighlights(Graphics g, int p0, int p1,
+ Shape viewBounds,
+ JTextComponent editor,
+ View view);
+
+
+ /**
+ * Layered highlight renderer.
+ */
+ static public abstract class LayerPainter implements Highlighter.HighlightPainter {
+ public abstract Shape paintLayer(Graphics g, int p0, int p1,
+ Shape viewBounds,JTextComponent editor,
+ View view);
+ }
+}
diff --git a/src/share/classes/javax/swing/text/LayoutQueue.java b/src/share/classes/javax/swing/text/LayoutQueue.java
new file mode 100644
index 000000000..e4216f5fe
--- /dev/null
+++ b/src/share/classes/javax/swing/text/LayoutQueue.java
@@ -0,0 +1,121 @@
+/*
+ * 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;
+
+import java.util.Vector;
+
+/**
+ * A queue of text layout tasks.
+ *
+ * @author Timothy Prinzing
+ * @see AsyncBoxView
+ * @since 1.3
+ */
+public class LayoutQueue {
+
+ Vector tasks;
+ Thread worker;
+
+ static LayoutQueue defaultQueue;
+
+ /**
+ * Construct a layout queue.
+ */
+ public LayoutQueue() {
+ tasks = new Vector();
+ }
+
+ /**
+ * Fetch the default layout queue.
+ */
+ public static LayoutQueue getDefaultQueue() {
+ if (defaultQueue == null) {
+ defaultQueue = new LayoutQueue();
+ }
+ return defaultQueue;
+ }
+
+ /**
+ * Set the default layout queue.
+ *
+ * @param q the new queue.
+ */
+ public static void setDefaultQueue(LayoutQueue q) {
+ defaultQueue = q;
+ }
+
+ /**
+ * Add a task that is not needed immediately because
+ * the results are not believed to be visible.
+ */
+ public synchronized void addTask(Runnable task) {
+ if (worker == null) {
+ worker = new LayoutThread();
+ worker.start();
+ }
+ tasks.addElement(task);
+ notifyAll();
+ }
+
+ /**
+ * Used by the worker thread to get a new task to execute
+ */
+ protected synchronized Runnable waitForWork() {
+ while (tasks.size() == 0) {
+ try {
+ wait();
+ } catch (InterruptedException ie) {
+ return null;
+ }
+ }
+ Runnable work = (Runnable) tasks.firstElement();
+ tasks.removeElementAt(0);
+ return work;
+ }
+
+ /**
+ * low priority thread to perform layout work forever
+ */
+ class LayoutThread extends Thread {
+
+ LayoutThread() {
+ super("text-layout");
+ setPriority(Thread.MIN_PRIORITY);
+ }
+
+ public void run() {
+ Runnable work;
+ do {
+ work = waitForWork();
+ if (work != null) {
+ work.run();
+ }
+ } while (work != null);
+ }
+
+
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/MaskFormatter.java b/src/share/classes/javax/swing/text/MaskFormatter.java
new file mode 100644
index 000000000..78cc35b4b
--- /dev/null
+++ b/src/share/classes/javax/swing/text/MaskFormatter.java
@@ -0,0 +1,1015 @@
+/*
+ * Copyright 2000-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;
+
+import java.io.*;
+import java.text.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.text.*;
+
+/**
+ * <code>MaskFormatter</code> is used to format and edit strings. The behavior
+ * of a <code>MaskFormatter</code> is controlled by way of a String mask
+ * that specifies the valid characters that can be contained at a particular
+ * location in the <code>Document</code> model. The following characters can
+ * be specified:
+ *
+ * <table border=1 summary="Valid characters and their descriptions">
+ * <tr>
+ * <th>Character&nbsp;</th>
+ * <th><p align="left">Description</p></th>
+ * </tr>
+ * <tr>
+ * <td>#</td>
+ * <td>Any valid number, uses <code>Character.isDigit</code>.</td>
+ * </tr>
+ * <tr>
+ * <td>'</td>
+ * <td>Escape character, used to escape any of the
+ * special formatting characters.</td>
+ * </tr>
+ * <tr>
+ * <td>U</td><td>Any character (<code>Character.isLetter</code>). All
+ * lowercase letters are mapped to upper case.</td>
+ * </tr>
+ * <tr><td>L</td><td>Any character (<code>Character.isLetter</code>). All
+ * upper case letters are mapped to lower case.</td>
+ * </tr>
+ * <tr><td>A</td><td>Any character or number (<code>Character.isLetter</code>
+ * or <code>Character.isDigit</code>)</td>
+ * </tr>
+ * <tr><td>?</td><td>Any character
+ * (<code>Character.isLetter</code>).</td>
+ * </tr>
+ * <tr><td>*</td><td>Anything.</td></tr>
+ * <tr><td>H</td><td>Any hex character (0-9, a-f or A-F).</td></tr>
+ * </table>
+ *
+ * <p>
+ * Typically characters correspond to one char, but in certain languages this
+ * is not the case. The mask is on a per character basis, and will thus
+ * adjust to fit as many chars as are needed.
+ * <p>
+ * You can further restrict the characters that can be input by the
+ * <code>setInvalidCharacters</code> and <code>setValidCharacters</code>
+ * methods. <code>setInvalidCharacters</code> allows you to specify
+ * which characters are not legal. <code>setValidCharacters</code> allows
+ * you to specify which characters are valid. For example, the following
+ * code block is equivalent to a mask of '0xHHH' with no invalid/valid
+ * characters:
+ * <pre>
+ * MaskFormatter formatter = new MaskFormatter("0x***");
+ * formatter.setValidCharacters("0123456789abcdefABCDEF");
+ * </pre>
+ * <p>
+ * When initially formatting a value if the length of the string is
+ * less than the length of the mask, two things can happen. Either
+ * the placeholder string will be used, or the placeholder character will
+ * be used. Precedence is given to the placeholder string. For example:
+ * <pre>
+ * MaskFormatter formatter = new MaskFormatter("###-####");
+ * formatter.setPlaceholderCharacter('_');
+ * formatter.getDisplayValue(tf, "123");
+ * </pre>
+ * <p>
+ * Would result in the string '123-____'. If
+ * <code>setPlaceholder("555-1212")</code> was invoked '123-1212' would
+ * result. The placeholder String is only used on the initial format,
+ * on subsequent formats only the placeholder character will be used.
+ * <p>
+ * If a <code>MaskFormatter</code> is configured to only allow valid characters
+ * (<code>setAllowsInvalid(false)</code>) literal characters will be skipped as
+ * necessary when editing. Consider a <code>MaskFormatter</code> with
+ * the mask "###-####" and current value "555-1212". Using the right
+ * arrow key to navigate through the field will result in (| indicates the
+ * position of the caret):
+ * <pre>
+ * |555-1212
+ * 5|55-1212
+ * 55|5-1212
+ * 555-|1212
+ * 555-1|212
+ * </pre>
+ * The '-' is a literal (non-editable) character, and is skipped.
+ * <p>
+ * Similar behavior will result when editing. Consider inserting the string
+ * '123-45' and '12345' into the <code>MaskFormatter</code> in the
+ * previous example. Both inserts will result in the same String,
+ * '123-45__'. When <code>MaskFormatter</code>
+ * is processing the insert at character position 3 (the '-'), two things can
+ * happen:
+ * <ol>
+ * <li>If the inserted character is '-', it is accepted.
+ * <li>If the inserted character matches the mask for the next non-literal
+ * character, it is accepted at the new location.
+ * <li>Anything else results in an invalid edit
+ * </ol>
+ * <p>
+ * By default <code>MaskFormatter</code> will not allow invalid edits, you can
+ * change this with the <code>setAllowsInvalid</code> method, and will
+ * commit edits on valid edits (use the <code>setCommitsOnValidEdit</code> to
+ * change this).
+ * <p>
+ * By default, <code>MaskFormatter</code> is in overwrite mode. That is as
+ * characters are typed a new character is not inserted, rather the character
+ * at the current location is replaced with the newly typed character. You
+ * can change this behavior by way of the method <code>setOverwriteMode</code>.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @since 1.4
+ */
+public class MaskFormatter extends DefaultFormatter {
+ // Potential values in mask.
+ private static final char DIGIT_KEY = '#';
+ private static final char LITERAL_KEY = '\'';
+ private static final char UPPERCASE_KEY = 'U';
+ private static final char LOWERCASE_KEY = 'L';
+ private static final char ALPHA_NUMERIC_KEY = 'A';
+ private static final char CHARACTER_KEY = '?';
+ private static final char ANYTHING_KEY = '*';
+ private static final char HEX_KEY = 'H';
+
+ private static final MaskCharacter[] EmptyMaskChars = new MaskCharacter[0];
+
+ /** The user specified mask. */
+ private String mask;
+
+ private transient MaskCharacter[] maskChars;
+
+ /** List of valid characters. */
+ private String validCharacters;
+
+ /** List of invalid characters. */
+ private String invalidCharacters;
+
+ /** String used for the passed in value if it does not completely
+ * fill the mask. */
+ private String placeholderString;
+
+ /** String used to represent characters not present. */
+ private char placeholder;
+
+ /** Indicates if the value contains the literal characters. */
+ private boolean containsLiteralChars;
+
+
+ /**
+ * Creates a MaskFormatter with no mask.
+ */
+ public MaskFormatter() {
+ setAllowsInvalid(false);
+ containsLiteralChars = true;
+ maskChars = EmptyMaskChars;
+ placeholder = ' ';
+ }
+
+ /**
+ * Creates a <code>MaskFormatter</code> with the specified mask.
+ * A <code>ParseException</code>
+ * will be thrown if <code>mask</code> is an invalid mask.
+ *
+ * @throws ParseException if mask does not contain valid mask characters
+ */
+ public MaskFormatter(String mask) throws ParseException {
+ this();
+ setMask(mask);
+ }
+
+ /**
+ * Sets the mask dictating the legal characters.
+ * This will throw a <code>ParseException</code> if <code>mask</code> is
+ * not valid.
+ *
+ * @throws ParseException if mask does not contain valid mask characters
+ */
+ public void setMask(String mask) throws ParseException {
+ this.mask = mask;
+ updateInternalMask();
+ }
+
+ /**
+ * Returns the formatting mask.
+ *
+ * @return Mask dictating legal character values.
+ */
+ public String getMask() {
+ return mask;
+ }
+
+ /**
+ * Allows for further restricting of the characters that can be input.
+ * Only characters specified in the mask, not in the
+ * <code>invalidCharacters</code>, and in
+ * <code>validCharacters</code> will be allowed to be input. Passing
+ * in null (the default) implies the valid characters are only bound
+ * by the mask and the invalid characters.
+ *
+ * @param validCharacters If non-null, specifies legal characters.
+ */
+ public void setValidCharacters(String validCharacters) {
+ this.validCharacters = validCharacters;
+ }
+
+ /**
+ * Returns the valid characters that can be input.
+ *
+ * @return Legal characters
+ */
+ public String getValidCharacters() {
+ return validCharacters;
+ }
+
+ /**
+ * Allows for further restricting of the characters that can be input.
+ * Only characters specified in the mask, not in the
+ * <code>invalidCharacters</code>, and in
+ * <code>validCharacters</code> will be allowed to be input. Passing
+ * in null (the default) implies the valid characters are only bound
+ * by the mask and the valid characters.
+ *
+ * @param invalidCharacters If non-null, specifies illegal characters.
+ */
+ public void setInvalidCharacters(String invalidCharacters) {
+ this.invalidCharacters = invalidCharacters;
+ }
+
+ /**
+ * Returns the characters that are not valid for input.
+ *
+ * @return illegal characters.
+ */
+ public String getInvalidCharacters() {
+ return invalidCharacters;
+ }
+
+ /**
+ * Sets the string to use if the value does not completely fill in
+ * the mask. A null value implies the placeholder char should be used.
+ *
+ * @param placeholder String used when formatting if the value does not
+ * completely fill the mask
+ */
+ public void setPlaceholder(String placeholder) {
+ this.placeholderString = placeholder;
+ }
+
+ /**
+ * Returns the String to use if the value does not completely fill
+ * in the mask.
+ *
+ * @return String used when formatting if the value does not
+ * completely fill the mask
+ */
+ public String getPlaceholder() {
+ return placeholderString;
+ }
+
+ /**
+ * Sets the character to use in place of characters that are not present
+ * in the value, ie the user must fill them in. The default value is
+ * a space.
+ * <p>
+ * This is only applicable if the placeholder string has not been
+ * specified, or does not completely fill in the mask.
+ *
+ * @param placeholder Character used when formatting if the value does not
+ * completely fill the mask
+ */
+ public void setPlaceholderCharacter(char placeholder) {
+ this.placeholder = placeholder;
+ }
+
+ /**
+ * Returns the character to use in place of characters that are not present
+ * in the value, ie the user must fill them in.
+ *
+ * @return Character used when formatting if the value does not
+ * completely fill the mask
+ */
+ public char getPlaceholderCharacter() {
+ return placeholder;
+ }
+
+ /**
+ * If true, the returned value and set value will also contain the literal
+ * characters in mask.
+ * <p>
+ * For example, if the mask is <code>'(###) ###-####'</code>, the
+ * current value is <code>'(415) 555-1212'</code>, and
+ * <code>valueContainsLiteralCharacters</code> is
+ * true <code>stringToValue</code> will return
+ * <code>'(415) 555-1212'</code>. On the other hand, if
+ * <code>valueContainsLiteralCharacters</code> is false,
+ * <code>stringToValue</code> will return <code>'4155551212'</code>.
+ *
+ * @param containsLiteralChars Used to indicate if literal characters in
+ * mask should be returned in stringToValue
+ */
+ public void setValueContainsLiteralCharacters(
+ boolean containsLiteralChars) {
+ this.containsLiteralChars = containsLiteralChars;
+ }
+
+ /**
+ * Returns true if <code>stringToValue</code> should return literal
+ * characters in the mask.
+ *
+ * @return True if literal characters in mask should be returned in
+ * stringToValue
+ */
+ public boolean getValueContainsLiteralCharacters() {
+ return containsLiteralChars;
+ }
+
+ /**
+ * Parses the text, returning the appropriate Object representation of
+ * the String <code>value</code>. This strips the literal characters as
+ * necessary and invokes supers <code>stringToValue</code>, so that if
+ * you have specified a value class (<code>setValueClass</code>) an
+ * instance of it will be created. This will throw a
+ * <code>ParseException</code> if the value does not match the current
+ * mask. Refer to {@link #setValueContainsLiteralCharacters} for details
+ * on how literals are treated.
+ *
+ * @throws ParseException if there is an error in the conversion
+ * @param value String to convert
+ * @see #setValueContainsLiteralCharacters
+ * @return Object representation of text
+ */
+ public Object stringToValue(String value) throws ParseException {
+ return stringToValue(value, true);
+ }
+
+ /**
+ * Returns a String representation of the Object <code>value</code>
+ * based on the mask. Refer to
+ * {@link #setValueContainsLiteralCharacters} for details
+ * on how literals are treated.
+ *
+ * @throws ParseException if there is an error in the conversion
+ * @param value Value to convert
+ * @see #setValueContainsLiteralCharacters
+ * @return String representation of value
+ */
+ public String valueToString(Object value) throws ParseException {
+ String sValue = (value == null) ? "" : value.toString();
+ StringBuffer result = new StringBuffer();
+ String placeholder = getPlaceholder();
+ int[] valueCounter = { 0 };
+
+ append(result, sValue, valueCounter, placeholder, maskChars);
+ return result.toString();
+ }
+
+ /**
+ * Installs the <code>DefaultFormatter</code> onto a particular
+ * <code>JFormattedTextField</code>.
+ * This will invoke <code>valueToString</code> to convert the
+ * current value from the <code>JFormattedTextField</code> to
+ * a String. This will then install the <code>Action</code>s from
+ * <code>getActions</code>, the <code>DocumentFilter</code>
+ * returned from <code>getDocumentFilter</code> and the
+ * <code>NavigationFilter</code> returned from
+ * <code>getNavigationFilter</code> onto the
+ * <code>JFormattedTextField</code>.
+ * <p>
+ * Subclasses will typically only need to override this if they
+ * wish to install additional listeners on the
+ * <code>JFormattedTextField</code>.
+ * <p>
+ * If there is a <code>ParseException</code> in converting the
+ * current value to a String, this will set the text to an empty
+ * String, and mark the <code>JFormattedTextField</code> as being
+ * in an invalid state.
+ * <p>
+ * While this is a public method, this is typically only useful
+ * for subclassers of <code>JFormattedTextField</code>.
+ * <code>JFormattedTextField</code> will invoke this method at
+ * the appropriate times when the value changes, or its internal
+ * state changes.
+ *
+ * @param ftf JFormattedTextField to format for, may be null indicating
+ * uninstall from current JFormattedTextField.
+ */
+ public void install(JFormattedTextField ftf) {
+ super.install(ftf);
+ // valueToString doesn't throw, but stringToValue does, need to
+ // update the editValid state appropriately
+ if (ftf != null) {
+ Object value = ftf.getValue();
+
+ try {
+ stringToValue(valueToString(value));
+ } catch (ParseException pe) {
+ setEditValid(false);
+ }
+ }
+ }
+
+ /**
+ * Actual <code>stringToValue</code> implementation.
+ * If <code>completeMatch</code> is true, the value must exactly match
+ * the mask, on the other hand if <code>completeMatch</code> is false
+ * the string must match the mask or the placeholder string.
+ */
+ private Object stringToValue(String value, boolean completeMatch) throws
+ ParseException {
+ int errorOffset = -1;
+
+ if ((errorOffset = getInvalidOffset(value, completeMatch)) == -1) {
+ if (!getValueContainsLiteralCharacters()) {
+ value = stripLiteralChars(value);
+ }
+ return super.stringToValue(value);
+ }
+ throw new ParseException("stringToValue passed invalid value",
+ errorOffset);
+ }
+
+ /**
+ * Returns -1 if the passed in string is valid, otherwise the index of
+ * the first bogus character is returned.
+ */
+ private int getInvalidOffset(String string, boolean completeMatch) {
+ int iLength = string.length();
+
+ if (iLength != getMaxLength()) {
+ // trivially false
+ return iLength;
+ }
+ for (int counter = 0, max = string.length(); counter < max; counter++){
+ char aChar = string.charAt(counter);
+
+ if (!isValidCharacter(counter, aChar) &&
+ (completeMatch || !isPlaceholder(counter, aChar))) {
+ return counter;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Invokes <code>append</code> on the mask characters in
+ * <code>mask</code>.
+ */
+ private void append(StringBuffer result, String value, int[] index,
+ String placeholder, MaskCharacter[] mask)
+ throws ParseException {
+ for (int counter = 0, maxCounter = mask.length;
+ counter < maxCounter; counter++) {
+ mask[counter].append(result, value, index, placeholder);
+ }
+ }
+
+ /**
+ * Updates the internal representation of the mask.
+ */
+ private void updateInternalMask() throws ParseException {
+ String mask = getMask();
+ ArrayList fixed = new ArrayList();
+ ArrayList temp = fixed;
+
+ if (mask != null) {
+ for (int counter = 0, maxCounter = mask.length();
+ counter < maxCounter; counter++) {
+ char maskChar = mask.charAt(counter);
+
+ switch (maskChar) {
+ case DIGIT_KEY:
+ temp.add(new DigitMaskCharacter());
+ break;
+ case LITERAL_KEY:
+ if (++counter < maxCounter) {
+ maskChar = mask.charAt(counter);
+ temp.add(new LiteralCharacter(maskChar));
+ }
+ // else: Could actually throw if else
+ break;
+ case UPPERCASE_KEY:
+ temp.add(new UpperCaseCharacter());
+ break;
+ case LOWERCASE_KEY:
+ temp.add(new LowerCaseCharacter());
+ break;
+ case ALPHA_NUMERIC_KEY:
+ temp.add(new AlphaNumericCharacter());
+ break;
+ case CHARACTER_KEY:
+ temp.add(new CharCharacter());
+ break;
+ case ANYTHING_KEY:
+ temp.add(new MaskCharacter());
+ break;
+ case HEX_KEY:
+ temp.add(new HexCharacter());
+ break;
+ default:
+ temp.add(new LiteralCharacter(maskChar));
+ break;
+ }
+ }
+ }
+ if (fixed.size() == 0) {
+ maskChars = EmptyMaskChars;
+ }
+ else {
+ maskChars = new MaskCharacter[fixed.size()];
+ fixed.toArray(maskChars);
+ }
+ }
+
+ /**
+ * Returns the MaskCharacter at the specified location.
+ */
+ private MaskCharacter getMaskCharacter(int index) {
+ if (index >= maskChars.length) {
+ return null;
+ }
+ return maskChars[index];
+ }
+
+ /**
+ * Returns true if the placeholder character matches aChar.
+ */
+ private boolean isPlaceholder(int index, char aChar) {
+ return (getPlaceholderCharacter() == aChar);
+ }
+
+ /**
+ * Returns true if the passed in character matches the mask at the
+ * specified location.
+ */
+ private boolean isValidCharacter(int index, char aChar) {
+ return getMaskCharacter(index).isValidCharacter(aChar);
+ }
+
+ /**
+ * Returns true if the character at the specified location is a literal,
+ * that is it can not be edited.
+ */
+ private boolean isLiteral(int index) {
+ return getMaskCharacter(index).isLiteral();
+ }
+
+ /**
+ * Returns the maximum length the text can be.
+ */
+ private int getMaxLength() {
+ return maskChars.length;
+ }
+
+ /**
+ * Returns the literal character at the specified location.
+ */
+ private char getLiteral(int index) {
+ return getMaskCharacter(index).getChar((char)0);
+ }
+
+ /**
+ * Returns the character to insert at the specified location based on
+ * the passed in character. This provides a way to map certain sets
+ * of characters to alternative values (lowercase to
+ * uppercase...).
+ */
+ private char getCharacter(int index, char aChar) {
+ return getMaskCharacter(index).getChar(aChar);
+ }
+
+ /**
+ * Removes the literal characters from the passed in string.
+ */
+ private String stripLiteralChars(String string) {
+ StringBuffer sb = null;
+ int last = 0;
+
+ for (int counter = 0, max = string.length(); counter < max; counter++){
+ if (isLiteral(counter)) {
+ if (sb == null) {
+ sb = new StringBuffer();
+ if (counter > 0) {
+ sb.append(string.substring(0, counter));
+ }
+ last = counter + 1;
+ }
+ else if (last != counter) {
+ sb.append(string.substring(last, counter));
+ }
+ last = counter + 1;
+ }
+ }
+ if (sb == null) {
+ // Assume the mask isn't all literals.
+ return string;
+ }
+ else if (last != string.length()) {
+ if (sb == null) {
+ return string.substring(last);
+ }
+ sb.append(string.substring(last));
+ }
+ return sb.toString();
+ }
+
+
+ /**
+ * Subclassed to update the internal representation of the mask after
+ * the default read operation has completed.
+ */
+ private void readObject(ObjectInputStream s)
+ throws IOException, ClassNotFoundException {
+ s.defaultReadObject();
+ try {
+ updateInternalMask();
+ } catch (ParseException pe) {
+ // assert();
+ }
+ }
+
+ /**
+ * Returns true if the MaskFormatter allows invalid, or
+ * the offset is less than the max length and the character at
+ * <code>offset</code> is a literal.
+ */
+ boolean isNavigatable(int offset) {
+ if (!getAllowsInvalid()) {
+ return (offset < getMaxLength() && !isLiteral(offset));
+ }
+ return true;
+ }
+
+ /*
+ * Returns true if the operation described by <code>rh</code> will
+ * result in a legal edit. This may set the <code>value</code>
+ * field of <code>rh</code>.
+ * <p>
+ * This is overriden to return true for a partial match.
+ */
+ boolean isValidEdit(ReplaceHolder rh) {
+ if (!getAllowsInvalid()) {
+ String newString = getReplaceString(rh.offset, rh.length, rh.text);
+
+ try {
+ rh.value = stringToValue(newString, false);
+
+ return true;
+ } catch (ParseException pe) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * This method does the following (assuming !getAllowsInvalid()):
+ * iterate over the max of the deleted region or the text length, for
+ * each character:
+ * <ol>
+ * <li>If it is valid (matches the mask at the particular position, or
+ * matches the literal character at the position), allow it
+ * <li>Else if the position identifies a literal character, add it. This
+ * allows for the user to paste in text that may/may not contain
+ * the literals. For example, in pasing in 5551212 into ###-####
+ * when the 1 is evaluated it is illegal (by the first test), but there
+ * is a literal at this position (-), so it is used. NOTE: This has
+ * a problem that you can't tell (without looking ahead) if you should
+ * eat literals in the text. For example, if you paste '555' into
+ * #5##, should it result in '5555' or '555 '? The current code will
+ * result in the latter, which feels a little better as selecting
+ * text than pasting will always result in the same thing.
+ * <li>Else if at the end of the inserted text, the replace the item with
+ * the placeholder
+ * <li>Otherwise the insert is bogus and false is returned.
+ * </ol>
+ */
+ boolean canReplace(ReplaceHolder rh) {
+ // This method is rather long, but much of the burden is in
+ // maintaining a String and swapping to a StringBuffer only if
+ // absolutely necessary.
+ if (!getAllowsInvalid()) {
+ StringBuffer replace = null;
+ String text = rh.text;
+ int tl = (text != null) ? text.length() : 0;
+
+ if (tl == 0 && rh.length == 1 && getFormattedTextField().
+ getSelectionStart() != rh.offset) {
+ // Backspace, adjust to actually delete next non-literal.
+ while (rh.offset > 0 && isLiteral(rh.offset)) {
+ rh.offset--;
+ }
+ }
+ int max = Math.min(getMaxLength() - rh.offset,
+ Math.max(tl, rh.length));
+ for (int counter = 0, textIndex = 0; counter < max; counter++) {
+ if (textIndex < tl && isValidCharacter(rh.offset + counter,
+ text.charAt(textIndex))) {
+ char aChar = text.charAt(textIndex);
+ if (aChar != getCharacter(rh.offset + counter, aChar)) {
+ if (replace == null) {
+ replace = new StringBuffer();
+ if (textIndex > 0) {
+ replace.append(text.substring(0, textIndex));
+ }
+ }
+ }
+ if (replace != null) {
+ replace.append(getCharacter(rh.offset + counter,
+ aChar));
+ }
+ textIndex++;
+ }
+ else if (isLiteral(rh.offset + counter)) {
+ if (replace != null) {
+ replace.append(getLiteral(rh.offset + counter));
+ if (textIndex < tl) {
+ max = Math.min(max + 1, getMaxLength() -
+ rh.offset);
+ }
+ }
+ else if (textIndex > 0) {
+ replace = new StringBuffer(max);
+ replace.append(text.substring(0, textIndex));
+ replace.append(getLiteral(rh.offset + counter));
+ if (textIndex < tl) {
+ // Evaluate the character in text again.
+ max = Math.min(max + 1, getMaxLength() -
+ rh.offset);
+ }
+ else if (rh.cursorPosition == -1) {
+ rh.cursorPosition = rh.offset + counter;
+ }
+ }
+ else {
+ rh.offset++;
+ rh.length--;
+ counter--;
+ max--;
+ }
+ }
+ else if (textIndex >= tl) {
+ // placeholder
+ if (replace == null) {
+ replace = new StringBuffer();
+ if (text != null) {
+ replace.append(text);
+ }
+ }
+ replace.append(getPlaceholderCharacter());
+ if (tl > 0 && rh.cursorPosition == -1) {
+ rh.cursorPosition = rh.offset + counter;
+ }
+ }
+ else {
+ // Bogus character.
+ return false;
+ }
+ }
+ if (replace != null) {
+ rh.text = replace.toString();
+ }
+ else if (text != null && rh.offset + tl > getMaxLength()) {
+ rh.text = text.substring(0, getMaxLength() - rh.offset);
+ }
+ if (getOverwriteMode() && rh.text != null) {
+ rh.length = rh.text.length();
+ }
+ }
+ return super.canReplace(rh);
+ }
+
+
+ //
+ // Interal classes used to represent the mask.
+ //
+ private class MaskCharacter {
+ /**
+ * Subclasses should override this returning true if the instance
+ * represents a literal character. The default implementation
+ * returns false.
+ */
+ public boolean isLiteral() {
+ return false;
+ }
+
+ /**
+ * Returns true if <code>aChar</code> is a valid reprensentation of
+ * the receiver. The default implementation returns true if the
+ * receiver represents a literal character and <code>getChar</code>
+ * == aChar. Otherwise, this will return true is <code>aChar</code>
+ * is contained in the valid characters and not contained
+ * in the invalid characters.
+ */
+ public boolean isValidCharacter(char aChar) {
+ if (isLiteral()) {
+ return (getChar(aChar) == aChar);
+ }
+
+ aChar = getChar(aChar);
+
+ String filter = getValidCharacters();
+
+ if (filter != null && filter.indexOf(aChar) == -1) {
+ return false;
+ }
+ filter = getInvalidCharacters();
+ if (filter != null && filter.indexOf(aChar) != -1) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the character to insert for <code>aChar</code>. The
+ * default implementation returns <code>aChar</code>. Subclasses
+ * that wish to do some sort of mapping, perhaps lower case to upper
+ * case should override this and do the necessary mapping.
+ */
+ public char getChar(char aChar) {
+ return aChar;
+ }
+
+ /**
+ * Appends the necessary character in <code>formatting</code> at
+ * <code>index</code> to <code>buff</code>.
+ */
+ public void append(StringBuffer buff, String formatting, int[] index,
+ String placeholder)
+ throws ParseException {
+ boolean inString = index[0] < formatting.length();
+ char aChar = inString ? formatting.charAt(index[0]) : 0;
+
+ if (isLiteral()) {
+ buff.append(getChar(aChar));
+ if (getValueContainsLiteralCharacters()) {
+ if (inString && aChar != getChar(aChar)) {
+ throw new ParseException("Invalid character: " +
+ aChar, index[0]);
+ }
+ index[0] = index[0] + 1;
+ }
+ }
+ else if (index[0] >= formatting.length()) {
+ if (placeholder != null && index[0] < placeholder.length()) {
+ buff.append(placeholder.charAt(index[0]));
+ }
+ else {
+ buff.append(getPlaceholderCharacter());
+ }
+ index[0] = index[0] + 1;
+ }
+ else if (isValidCharacter(aChar)) {
+ buff.append(getChar(aChar));
+ index[0] = index[0] + 1;
+ }
+ else {
+ throw new ParseException("Invalid character: " + aChar,
+ index[0]);
+ }
+ }
+ }
+
+
+ /**
+ * Used to represent a fixed character in the mask.
+ */
+ private class LiteralCharacter extends MaskCharacter {
+ private char fixedChar;
+
+ public LiteralCharacter(char fixedChar) {
+ this.fixedChar = fixedChar;
+ }
+
+ public boolean isLiteral() {
+ return true;
+ }
+
+ public char getChar(char aChar) {
+ return fixedChar;
+ }
+ }
+
+
+ /**
+ * Represents a number, uses <code>Character.isDigit</code>.
+ */
+ private class DigitMaskCharacter extends MaskCharacter {
+ public boolean isValidCharacter(char aChar) {
+ return (Character.isDigit(aChar) &&
+ super.isValidCharacter(aChar));
+ }
+ }
+
+
+ /**
+ * Represents a character, lower case letters are mapped to upper case
+ * using <code>Character.toUpperCase</code>.
+ */
+ private class UpperCaseCharacter extends MaskCharacter {
+ public boolean isValidCharacter(char aChar) {
+ return (Character.isLetter(aChar) &&
+ super.isValidCharacter(aChar));
+ }
+
+ public char getChar(char aChar) {
+ return Character.toUpperCase(aChar);
+ }
+ }
+
+
+ /**
+ * Represents a character, upper case letters are mapped to lower case
+ * using <code>Character.toLowerCase</code>.
+ */
+ private class LowerCaseCharacter extends MaskCharacter {
+ public boolean isValidCharacter(char aChar) {
+ return (Character.isLetter(aChar) &&
+ super.isValidCharacter(aChar));
+ }
+
+ public char getChar(char aChar) {
+ return Character.toLowerCase(aChar);
+ }
+ }
+
+
+ /**
+ * Represents either a character or digit, uses
+ * <code>Character.isLetterOrDigit</code>.
+ */
+ private class AlphaNumericCharacter extends MaskCharacter {
+ public boolean isValidCharacter(char aChar) {
+ return (Character.isLetterOrDigit(aChar) &&
+ super.isValidCharacter(aChar));
+ }
+ }
+
+
+ /**
+ * Represents a letter, uses <code>Character.isLetter</code>.
+ */
+ private class CharCharacter extends MaskCharacter {
+ public boolean isValidCharacter(char aChar) {
+ return (Character.isLetter(aChar) &&
+ super.isValidCharacter(aChar));
+ }
+ }
+
+
+ /**
+ * Represents a hex character, 0-9a-fA-F. a-f is mapped to A-F
+ */
+ private class HexCharacter extends MaskCharacter {
+ public boolean isValidCharacter(char aChar) {
+ return ((aChar == '0' || aChar == '1' ||
+ aChar == '2' || aChar == '3' ||
+ aChar == '4' || aChar == '5' ||
+ aChar == '6' || aChar == '7' ||
+ aChar == '8' || aChar == '9' ||
+ aChar == 'a' || aChar == 'A' ||
+ aChar == 'b' || aChar == 'B' ||
+ aChar == 'c' || aChar == 'C' ||
+ aChar == 'd' || aChar == 'D' ||
+ aChar == 'e' || aChar == 'E' ||
+ aChar == 'f' || aChar == 'F') &&
+ super.isValidCharacter(aChar));
+ }
+
+ public char getChar(char aChar) {
+ if (Character.isDigit(aChar)) {
+ return aChar;
+ }
+ return Character.toUpperCase(aChar);
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/MutableAttributeSet.java b/src/share/classes/javax/swing/text/MutableAttributeSet.java
new file mode 100644
index 000000000..a95d7b1a5
--- /dev/null
+++ b/src/share/classes/javax/swing/text/MutableAttributeSet.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 1997-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;
+
+import java.util.Enumeration;
+
+/**
+ * A generic interface for a mutable collection of unique attributes.
+ *
+ * Implementations will probably want to provide a constructor of the
+ * form:<tt>
+ * public XXXAttributeSet(ConstAttributeSet source);</tt>
+ *
+ */
+public interface MutableAttributeSet extends AttributeSet {
+
+ /**
+ * Creates a new attribute set similar to this one except that it contains
+ * an attribute with the given name and value. The object must be
+ * immutable, or not mutated by any client.
+ *
+ * @param name the name
+ * @param value the value
+ */
+ public void addAttribute(Object name, Object value);
+
+ /**
+ * Creates a new attribute set similar to this one except that it contains
+ * the given attributes and values.
+ *
+ * @param attributes the set of attributes
+ */
+ public void addAttributes(AttributeSet attributes);
+
+ /**
+ * Removes an attribute with the given <code>name</code>.
+ *
+ * @param name the attribute name
+ */
+ public void removeAttribute(Object name);
+
+ /**
+ * Removes an attribute set with the given <code>names</code>.
+ *
+ * @param names the set of names
+ */
+ public void removeAttributes(Enumeration<?> names);
+
+ /**
+ * Removes a set of attributes with the given <code>name</code>.
+ *
+ * @param attributes the set of attributes
+ */
+ public void removeAttributes(AttributeSet attributes);
+
+ /**
+ * Sets the resolving parent. This is the set
+ * of attributes to resolve through if an attribute
+ * isn't defined locally.
+ *
+ * @param parent the parent
+ */
+ public void setResolveParent(AttributeSet parent);
+
+}
diff --git a/src/share/classes/javax/swing/text/NavigationFilter.java b/src/share/classes/javax/swing/text/NavigationFilter.java
new file mode 100644
index 000000000..338d3d83c
--- /dev/null
+++ b/src/share/classes/javax/swing/text/NavigationFilter.java
@@ -0,0 +1,147 @@
+/*
+ * 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;
+
+import java.awt.Shape;
+
+/**
+ * <code>NavigationFilter</code> can be used to restrict where the cursor can
+ * be positioned. When the default cursor positioning actions attempt to
+ * reposition the cursor they will call into the
+ * <code>NavigationFilter</code>, assuming
+ * the <code>JTextComponent</code> has a non-null
+ * <code>NavigationFilter</code> set. In this manner
+ * the <code>NavigationFilter</code> can effectively restrict where the
+ * cursor can be positioned. Similarly <code>DefaultCaret</code> will call
+ * into the <code>NavigationFilter</code> when the user is changing the
+ * selection to further restrict where the cursor can be positioned.
+ * <p>
+ * Subclasses can conditionally call into supers implementation to restrict
+ * where the cursor can be placed, or call directly into the
+ * <code>FilterBypass</code>.
+ *
+ * @see javax.swing.text.Caret
+ * @see javax.swing.text.DefaultCaret
+ * @see javax.swing.text.View
+ *
+ * @since 1.4
+ */
+public class NavigationFilter {
+ /**
+ * Invoked prior to the Caret setting the dot. The default implementation
+ * calls directly into the <code>FilterBypass</code> with the passed
+ * in arguments. Subclasses may wish to conditionally
+ * call super with a different location, or invoke the necessary method
+ * on the <code>FilterBypass</code>
+ *
+ * @param fb FilterBypass that can be used to mutate caret position
+ * @param dot the position >= 0
+ * @param bias Bias to place the dot at
+ */
+ public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
+ fb.setDot(dot, bias);
+ }
+
+ /**
+ * Invoked prior to the Caret moving the dot. The default implementation
+ * calls directly into the <code>FilterBypass</code> with the passed
+ * in arguments. Subclasses may wish to conditionally
+ * call super with a different location, or invoke the necessary
+ * methods on the <code>FilterBypass</code>.
+ *
+ * @param fb FilterBypass that can be used to mutate caret position
+ * @param dot the position >= 0
+ * @param bias Bias for new location
+ */
+ public void moveDot(FilterBypass fb, int dot, Position.Bias bias) {
+ fb.moveDot(dot, bias);
+ }
+
+ /**
+ * Returns the next visual position to place the caret at from an
+ * existing position. The default implementation simply forwards the
+ * method to the root View. Subclasses may wish to further restrict the
+ * location based on additional criteria.
+ *
+ * @param text JTextComponent containing text
+ * @param pos Position used in determining next position
+ * @param bias Bias used in determining next position
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard.
+ * This will be one of the following values:
+ * <ul>
+ * <li>SwingConstants.WEST
+ * <li>SwingConstants.EAST
+ * <li>SwingConstants.NORTH
+ * <li>SwingConstants.SOUTH
+ * </ul>
+ * @param biasRet Used to return resulting Bias of next position
+ * @return the location within the model that best represents the next
+ * location visual position
+ * @exception BadLocationException
+ * @exception IllegalArgumentException if <code>direction</code>
+ * doesn't have one of the legal values above
+ */
+ public int getNextVisualPositionFrom(JTextComponent text, int pos,
+ Position.Bias bias, int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+ return text.getUI().getNextVisualPositionFrom(text, pos, bias,
+ direction, biasRet);
+ }
+
+
+ /**
+ * Used as a way to circumvent calling back into the caret to
+ * position the cursor. Caret implementations that wish to support
+ * a NavigationFilter must provide an implementation that will
+ * not callback into the NavigationFilter.
+ * @since 1.4
+ */
+ public static abstract class FilterBypass {
+ /**
+ * Returns the Caret that is changing.
+ *
+ * @return Caret that is changing
+ */
+ public abstract Caret getCaret();
+
+ /**
+ * Sets the caret location, bypassing the NavigationFilter.
+ *
+ * @param dot the position >= 0
+ * @param bias Bias to place the dot at
+ */
+ public abstract void setDot(int dot, Position.Bias bias);
+
+ /**
+ * Moves the caret location, bypassing the NavigationFilter.
+ *
+ * @param dot the position >= 0
+ * @param bias Bias for new location
+ */
+ public abstract void moveDot(int dot, Position.Bias bias);
+ }
+}
diff --git a/src/share/classes/javax/swing/text/NumberFormatter.java b/src/share/classes/javax/swing/text/NumberFormatter.java
new file mode 100644
index 000000000..c9c471938
--- /dev/null
+++ b/src/share/classes/javax/swing/text/NumberFormatter.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2000-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;
+
+import java.lang.reflect.*;
+import java.text.*;
+import java.util.*;
+import javax.swing.text.*;
+
+/**
+ * <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
+ * adding special behavior for numbers. Among the specializations are
+ * (these are only used if the <code>NumberFormatter</code> does not display
+ * invalid nubers, eg <code>setAllowsInvalid(false)</code>):
+ * <ul>
+ * <li>Pressing +/- (- is determined from the
+ * <code>DecimalFormatSymbols</code> associated with the
+ * <code>DecimalFormat</code>) in any field but the exponent
+ * field will attempt to change the sign of the number to
+ * positive/negative.
+ * <li>Pressing +/- (- is determined from the
+ * <code>DecimalFormatSymbols</code> associated with the
+ * <code>DecimalFormat</code>) in the exponent field will
+ * attemp to change the sign of the exponent to positive/negative.
+ * </ul>
+ * <p>
+ * If you are displaying scientific numbers, you may wish to turn on
+ * overwrite mode, <code>setOverwriteMode(true)</code>. For example:
+ * <pre>
+ * DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
+ * NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
+ * textFormatter.setOverwriteMode(true);
+ * textFormatter.setAllowsInvalid(false);
+ * </pre>
+ * <p>
+ * If you are going to allow the user to enter decimal
+ * values, you should either force the DecimalFormat to contain at least
+ * one decimal (<code>#.0###</code>), or allow the value to be invalid
+ * <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
+ * input decimal values.
+ * <p>
+ * <code>NumberFormatter</code> provides slightly different behavior to
+ * <code>stringToValue</code> than that of its superclass. If you have
+ * specified a Class for values, {@link #setValueClass}, that is one of
+ * of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
+ * <code>Double</code>, <code>Byte</code> or <code>Short</code> and
+ * the Format's <code>parseObject</code> returns an instance of
+ * <code>Number</code>, the corresponding instance of the value class
+ * will be created using the constructor appropriate for the primitive
+ * type the value class represents. For example:
+ * <code>setValueClass(Integer.class)</code> will cause the resulting
+ * value to be created via
+ * <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
+ * This is typically useful if you
+ * wish to set a min/max value as the various <code>Number</code>
+ * implementations are generally not comparable to each other. This is also
+ * useful if for some reason you need a specific <code>Number</code>
+ * implementation for your values.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @since 1.4
+ */
+public class NumberFormatter extends InternationalFormatter {
+ /** The special characters from the Format instance. */
+ private String specialChars;
+
+ /**
+ * Creates a <code>NumberFormatter</code> with the a default
+ * <code>NumberFormat</code> instance obtained from
+ * <code>NumberFormat.getNumberInstance()</code>.
+ */
+ public NumberFormatter() {
+ this(NumberFormat.getNumberInstance());
+ }
+
+ /**
+ * Creates a NumberFormatter with the specified Format instance.
+ *
+ * @param format Format used to dictate legal values
+ */
+ public NumberFormatter(NumberFormat format) {
+ super(format);
+ setFormat(format);
+ setAllowsInvalid(true);
+ setCommitsOnValidEdit(false);
+ setOverwriteMode(false);
+ }
+
+ /**
+ * Sets the format that dictates the legal values that can be edited
+ * and displayed.
+ * <p>
+ * If you have used the nullary constructor the value of this property
+ * will be determined for the current locale by way of the
+ * <code>NumberFormat.getNumberInstance()</code> method.
+ *
+ * @param format NumberFormat instance used to dictate legal values
+ */
+ public void setFormat(Format format) {
+ super.setFormat(format);
+
+ DecimalFormatSymbols dfs = getDecimalFormatSymbols();
+
+ if (dfs != null) {
+ StringBuffer sb = new StringBuffer();
+
+ sb.append(dfs.getCurrencySymbol());
+ sb.append(dfs.getDecimalSeparator());
+ sb.append(dfs.getGroupingSeparator());
+ sb.append(dfs.getInfinity());
+ sb.append(dfs.getInternationalCurrencySymbol());
+ sb.append(dfs.getMinusSign());
+ sb.append(dfs.getMonetaryDecimalSeparator());
+ sb.append(dfs.getNaN());
+ sb.append(dfs.getPercent());
+ sb.append('+');
+ specialChars = sb.toString();
+ }
+ else {
+ specialChars = "";
+ }
+ }
+
+ /**
+ * Invokes <code>parseObject</code> on <code>f</code>, returning
+ * its value.
+ */
+ Object stringToValue(String text, Format f) throws ParseException {
+ if (f == null) {
+ return text;
+ }
+ Object value = f.parseObject(text);
+
+ return convertValueToValueClass(value, getValueClass());
+ }
+
+ /**
+ * Converts the passed in value to the passed in class. This only
+ * works if <code>valueClass</code> is one of <code>Integer</code>,
+ * <code>Long</code>, <code>Float</code>, <code>Double</code>,
+ * <code>Byte</code> or <code>Short</code> and <code>value</code>
+ * is an instanceof <code>Number</code>.
+ */
+ private Object convertValueToValueClass(Object value, Class valueClass) {
+ if (valueClass != null && (value instanceof Number)) {
+ if (valueClass == Integer.class) {
+ return new Integer(((Number)value).intValue());
+ }
+ else if (valueClass == Long.class) {
+ return new Long(((Number)value).longValue());
+ }
+ else if (valueClass == Float.class) {
+ return new Float(((Number)value).floatValue());
+ }
+ else if (valueClass == Double.class) {
+ return new Double(((Number)value).doubleValue());
+ }
+ else if (valueClass == Byte.class) {
+ return new Byte(((Number)value).byteValue());
+ }
+ else if (valueClass == Short.class) {
+ return new Short(((Number)value).shortValue());
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Returns the character that is used to toggle to positive values.
+ */
+ private char getPositiveSign() {
+ return '+';
+ }
+
+ /**
+ * Returns the character that is used to toggle to negative values.
+ */
+ private char getMinusSign() {
+ DecimalFormatSymbols dfs = getDecimalFormatSymbols();
+
+ if (dfs != null) {
+ return dfs.getMinusSign();
+ }
+ return '-';
+ }
+
+ /**
+ * Returns the character that is used to toggle to negative values.
+ */
+ private char getDecimalSeparator() {
+ DecimalFormatSymbols dfs = getDecimalFormatSymbols();
+
+ if (dfs != null) {
+ return dfs.getDecimalSeparator();
+ }
+ return '.';
+ }
+
+ /**
+ * Returns the DecimalFormatSymbols from the Format instance.
+ */
+ private DecimalFormatSymbols getDecimalFormatSymbols() {
+ Format f = getFormat();
+
+ if (f instanceof DecimalFormat) {
+ return ((DecimalFormat)f).getDecimalFormatSymbols();
+ }
+ return null;
+ }
+
+ /**
+ */
+ private boolean isValidInsertionCharacter(char aChar) {
+ return (Character.isDigit(aChar) || specialChars.indexOf(aChar) != -1);
+ }
+
+
+ /**
+ * Subclassed to return false if <code>text</code> contains in an invalid
+ * character to insert, that is, it is not a digit
+ * (<code>Character.isDigit()</code>) and
+ * not one of the characters defined by the DecimalFormatSymbols.
+ */
+ boolean isLegalInsertText(String text) {
+ if (getAllowsInvalid()) {
+ return true;
+ }
+ for (int counter = text.length() - 1; counter >= 0; counter--) {
+ char aChar = text.charAt(counter);
+
+ if (!Character.isDigit(aChar) &&
+ specialChars.indexOf(aChar) == -1){
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Subclassed to treat the decimal separator, grouping separator,
+ * exponent symbol, percent, permille, currency and sign as literals.
+ */
+ boolean isLiteral(Map attrs) {
+ if (!super.isLiteral(attrs)) {
+ if (attrs == null) {
+ return false;
+ }
+ int size = attrs.size();
+
+ if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
+ size--;
+ if (attrs.get(NumberFormat.Field.INTEGER) != null) {
+ size--;
+ }
+ }
+ if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
+ size--;
+ }
+ if (attrs.get(NumberFormat.Field.PERCENT) != null) {
+ size--;
+ }
+ if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
+ size--;
+ }
+ if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
+ size--;
+ }
+ if (attrs.get(NumberFormat.Field.SIGN) != null) {
+ size--;
+ }
+ if (size == 0) {
+ return true;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Subclassed to make the decimal separator navigatable, as well
+ * as making the character between the integer field and the next
+ * field navigatable.
+ */
+ boolean isNavigatable(int index) {
+ if (!super.isNavigatable(index)) {
+ // Don't skip the decimal, it causes wierd behavior
+ if (getBufferedChar(index) == getDecimalSeparator()) {
+ return true;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the first <code>NumberFormat.Field</code> starting
+ * <code>index</code> incrementing by <code>direction</code>.
+ */
+ private NumberFormat.Field getFieldFrom(int index, int direction) {
+ if (isValidMask()) {
+ int max = getFormattedTextField().getDocument().getLength();
+ AttributedCharacterIterator iterator = getIterator();
+
+ if (index >= max) {
+ index += direction;
+ }
+ while (index >= 0 && index < max) {
+ iterator.setIndex(index);
+
+ Map attrs = iterator.getAttributes();
+
+ if (attrs != null && attrs.size() > 0) {
+ Iterator keys = attrs.keySet().iterator();
+
+ while (keys.hasNext()) {
+ Object key = keys.next();
+
+ if (key instanceof NumberFormat.Field) {
+ return (NumberFormat.Field)key;
+ }
+ }
+ }
+ index += direction;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Overriden to toggle the value if the positive/minus sign
+ * is inserted.
+ */
+ void replace(DocumentFilter.FilterBypass fb, int offset, int length,
+ String string, AttributeSet attr) throws BadLocationException {
+ if (!getAllowsInvalid() && length == 0 && string != null &&
+ string.length() == 1 &&
+ toggleSignIfNecessary(fb, offset, string.charAt(0))) {
+ return;
+ }
+ super.replace(fb, offset, length, string, attr);
+ }
+
+ /**
+ * Will change the sign of the integer or exponent field if
+ * <code>aChar</code> is the positive or minus sign. Returns
+ * true if a sign change was attempted.
+ */
+ private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
+ int offset, char aChar) throws
+ BadLocationException {
+ if (aChar == getMinusSign() || aChar == getPositiveSign()) {
+ NumberFormat.Field field = getFieldFrom(offset, -1);
+ Object newValue;
+
+ try {
+ if (field == null ||
+ (field != NumberFormat.Field.EXPONENT &&
+ field != NumberFormat.Field.EXPONENT_SYMBOL &&
+ field != NumberFormat.Field.EXPONENT_SIGN)) {
+ newValue = toggleSign((aChar == getPositiveSign()));
+ }
+ else {
+ // exponent
+ newValue = toggleExponentSign(offset, aChar);
+ }
+ if (newValue != null && isValidValue(newValue, false)) {
+ int lc = getLiteralCountTo(offset);
+ String string = valueToString(newValue);
+
+ fb.remove(0, fb.getDocument().getLength());
+ fb.insertString(0, string, null);
+ updateValue(newValue);
+ repositionCursor(getLiteralCountTo(offset) -
+ lc + offset, 1);
+ return true;
+ }
+ } catch (ParseException pe) {
+ invalidEdit();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the range offset to length identifies the only
+ * integer field.
+ */
+ private boolean isOnlyIntegerField(int offset, int length) {
+ if (isValidMask()) {
+ int start = getAttributeStart(NumberFormat.Field.INTEGER);
+
+ if (start != -1) {
+ AttributedCharacterIterator iterator = getIterator();
+
+ iterator.setIndex(start);
+ if (offset > start || iterator.getRunLimit(
+ NumberFormat.Field.INTEGER) > (offset + length)) {
+ return false;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Invoked to toggle the sign. For this to work the value class
+ * must have a single arg constructor that takes a String.
+ */
+ private Object toggleSign(boolean positive) throws ParseException {
+ Object value = stringToValue(getFormattedTextField().getText());
+
+ if (value != null) {
+ // toString isn't localized, so that using +/- should work
+ // correctly.
+ String string = value.toString();
+
+ if (string != null && string.length() > 0) {
+ if (positive) {
+ if (string.charAt(0) == '-') {
+ string = string.substring(1);
+ }
+ }
+ else {
+ if (string.charAt(0) == '+') {
+ string = string.substring(1);
+ }
+ if (string.length() > 0 && string.charAt(0) != '-') {
+ string = "-" + string;
+ }
+ }
+ if (string != null) {
+ Class valueClass = getValueClass();
+
+ if (valueClass == null) {
+ valueClass = value.getClass();
+ }
+ try {
+ Constructor cons = valueClass.getConstructor(
+ new Class[] { String.class });
+
+ if (cons != null) {
+ return cons.newInstance(new Object[]{string});
+ }
+ } catch (Throwable ex) { }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Invoked to toggle the sign of the exponent (for scientific
+ * numbers).
+ */
+ private Object toggleExponentSign(int offset, char aChar) throws
+ BadLocationException, ParseException {
+ String string = getFormattedTextField().getText();
+ int replaceLength = 0;
+ int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
+
+ if (loc >= 0) {
+ replaceLength = 1;
+ offset = loc;
+ }
+ if (aChar == getPositiveSign()) {
+ string = getReplaceString(offset, replaceLength, null);
+ }
+ else {
+ string = getReplaceString(offset, replaceLength,
+ new String(new char[] { aChar }));
+ }
+ return stringToValue(string);
+ }
+}
diff --git a/src/share/classes/javax/swing/text/ParagraphView.java b/src/share/classes/javax/swing/text/ParagraphView.java
new file mode 100644
index 000000000..c7f6bb09c
--- /dev/null
+++ b/src/share/classes/javax/swing/text/ParagraphView.java
@@ -0,0 +1,1219 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.awt.*;
+import java.awt.font.TextAttribute;
+import javax.swing.event.*;
+import javax.swing.SizeRequirements;
+
+/**
+ * View of a simple line-wrapping paragraph that supports
+ * multiple fonts, colors, components, icons, etc. It is
+ * basically a vertical box with a margin around it. The
+ * contents of the box are a bunch of rows which are special
+ * horizontal boxes. This view creates a collection of
+ * views that represent the child elements of the paragraph
+ * element. Each of these views are placed into a row
+ * directly if they will fit, otherwise the <code>breakView</code>
+ * method is called to try and carve the view into pieces
+ * that fit.
+ *
+ * @author Timothy Prinzing
+ * @author Scott Violet
+ * @author Igor Kushnirskiy
+ * @see View
+ */
+public class ParagraphView extends FlowView implements TabExpander {
+
+ /**
+ * Constructs a <code>ParagraphView</code> for the given element.
+ *
+ * @param elem the element that this view is responsible for
+ */
+ public ParagraphView(Element elem) {
+ super(elem, View.Y_AXIS);
+ setPropertiesFromAttributes();
+ Document doc = elem.getDocument();
+ Object i18nFlag = doc.getProperty(AbstractDocument.I18NProperty);
+ if ((i18nFlag != null) && i18nFlag.equals(Boolean.TRUE)) {
+ try {
+ if (i18nStrategy == null) {
+ // the classname should probably come from a property file.
+ String classname = "javax.swing.text.TextLayoutStrategy";
+ ClassLoader loader = getClass().getClassLoader();
+ if (loader != null) {
+ i18nStrategy = loader.loadClass(classname);
+ } else {
+ i18nStrategy = Class.forName(classname);
+ }
+ }
+ Object o = i18nStrategy.newInstance();
+ if (o instanceof FlowStrategy) {
+ strategy = (FlowStrategy) o;
+ }
+ } catch (Throwable e) {
+ throw new StateInvariantError("ParagraphView: Can't create i18n strategy: "
+ + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Sets the type of justification.
+ *
+ * @param j one of the following values:
+ * <ul>
+ * <li><code>StyleConstants.ALIGN_LEFT</code>
+ * <li><code>StyleConstants.ALIGN_CENTER</code>
+ * <li><code>StyleConstants.ALIGN_RIGHT</code>
+ * </ul>
+ */
+ protected void setJustification(int j) {
+ justification = j;
+ }
+
+ /**
+ * Sets the line spacing.
+ *
+ * @param ls the value is a factor of the line hight
+ */
+ protected void setLineSpacing(float ls) {
+ lineSpacing = ls;
+ }
+
+ /**
+ * Sets the indent on the first line.
+ *
+ * @param fi the value in points
+ */
+ protected void setFirstLineIndent(float fi) {
+ firstLineIndent = (int) fi;
+ }
+
+ /**
+ * Set the cached properties from the attributes.
+ */
+ protected void setPropertiesFromAttributes() {
+ AttributeSet attr = getAttributes();
+ if (attr != null) {
+ setParagraphInsets(attr);
+ Integer a = (Integer)attr.getAttribute(StyleConstants.Alignment);
+ int alignment;
+ if (a == null) {
+ Document doc = getElement().getDocument();
+ Object o = doc.getProperty(TextAttribute.RUN_DIRECTION);
+ if ((o != null) && o.equals(TextAttribute.RUN_DIRECTION_RTL)) {
+ alignment = StyleConstants.ALIGN_RIGHT;
+ } else {
+ alignment = StyleConstants.ALIGN_LEFT;
+ }
+ } else {
+ alignment = a.intValue();
+ }
+ setJustification(alignment);
+ setLineSpacing(StyleConstants.getLineSpacing(attr));
+ setFirstLineIndent(StyleConstants.getFirstLineIndent(attr));
+ }
+ }
+
+ /**
+ * Returns the number of views that this view is
+ * responsible for.
+ * The child views of the paragraph are rows which
+ * have been used to arrange pieces of the <code>View</code>s
+ * that represent the child elements. This is the number
+ * of views that have been tiled in two dimensions,
+ * and should be equivalent to the number of child elements
+ * to the element this view is responsible for.
+ *
+ * @return the number of views that this <code>ParagraphView</code>
+ * is responsible for
+ */
+ protected int getLayoutViewCount() {
+ return layoutPool.getViewCount();
+ }
+
+ /**
+ * Returns the view at a given <code>index</code>.
+ * The child views of the paragraph are rows which
+ * have been used to arrange pieces of the <code>Views</code>
+ * that represent the child elements. This methods returns
+ * the view responsible for the child element index
+ * (prior to breaking). These are the Views that were
+ * produced from a factory (to represent the child
+ * elements) and used for layout.
+ *
+ * @param index the <code>index</code> of the desired view
+ * @return the view at <code>index</code>
+ */
+ protected View getLayoutView(int index) {
+ return layoutPool.getView(index);
+ }
+
+ /**
+ * Adjusts the given row if possible to fit within the
+ * layout span. By default this will try to find the
+ * highest break weight possible nearest the end of
+ * the row. If a forced break is encountered, the
+ * break will be positioned there.
+ * <p>
+ * This is meant for internal usage, and should not be used directly.
+ *
+ * @param r the row to adjust to the current layout
+ * span
+ * @param desiredSpan the current layout span >= 0
+ * @param x the location r starts at
+ */
+ protected void adjustRow(Row r, int desiredSpan, int x) {
+ }
+
+ /**
+ * Returns the next visual position for the cursor, in
+ * either the east or west direction.
+ * Overridden from <code>CompositeView</code>.
+ * @param pos position into the model
+ * @param b either <code>Position.Bias.Forward</code> or
+ * <code>Position.Bias.Backward</code>
+ * @param a the allocated region to render into
+ * @param direction either <code>SwingConstants.NORTH</code>
+ * or <code>SwingConstants.SOUTH</code>
+ * @param biasRet an array containing the bias that were checked
+ * in this method
+ * @return the location in the model that represents the
+ * next location visual position
+ */
+ protected int getNextNorthSouthVisualPositionFrom(int pos, Position.Bias b,
+ Shape a, int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+ int vIndex;
+ if(pos == -1) {
+ vIndex = (direction == NORTH) ?
+ getViewCount() - 1 : 0;
+ }
+ else {
+ if(b == Position.Bias.Backward && pos > 0) {
+ vIndex = getViewIndexAtPosition(pos - 1);
+ }
+ else {
+ vIndex = getViewIndexAtPosition(pos);
+ }
+ if(direction == NORTH) {
+ if(vIndex == 0) {
+ return -1;
+ }
+ vIndex--;
+ }
+ else if(++vIndex >= getViewCount()) {
+ return -1;
+ }
+ }
+ // vIndex gives index of row to look in.
+ JTextComponent text = (JTextComponent)getContainer();
+ Caret c = text.getCaret();
+ Point magicPoint;
+ magicPoint = (c != null) ? c.getMagicCaretPosition() : null;
+ int x;
+ if(magicPoint == null) {
+ Shape posBounds;
+ try {
+ posBounds = text.getUI().modelToView(text, pos, b);
+ } catch (BadLocationException exc) {
+ posBounds = null;
+ }
+ if(posBounds == null) {
+ x = 0;
+ }
+ else {
+ x = posBounds.getBounds().x;
+ }
+ }
+ else {
+ x = magicPoint.x;
+ }
+ return getClosestPositionTo(pos, b, a, direction, biasRet, vIndex, x);
+ }
+
+ /**
+ * Returns the closest model position to <code>x</code>.
+ * <code>rowIndex</code> gives the index of the view that corresponds
+ * that should be looked in.
+ * @param pos position into the model
+ * @param a the allocated region to render into
+ * @param direction one of the following values:
+ * <ul>
+ * <li><code>SwingConstants.NORTH</code>
+ * <li><code>SwingConstants.SOUTH</code>
+ * </ul>
+ * @param biasRet an array containing the bias that were checked
+ * in this method
+ * @param rowIndex the index of the view
+ * @param x the x coordinate of interest
+ * @return the closest model position to <code>x</code>
+ */
+ // NOTE: This will not properly work if ParagraphView contains
+ // other ParagraphViews. It won't raise, but this does not message
+ // the children views with getNextVisualPositionFrom.
+ protected int getClosestPositionTo(int pos, Position.Bias b, Shape a,
+ int direction, Position.Bias[] biasRet,
+ int rowIndex, int x)
+ throws BadLocationException {
+ JTextComponent text = (JTextComponent)getContainer();
+ Document doc = getDocument();
+ AbstractDocument aDoc = (doc instanceof AbstractDocument) ?
+ (AbstractDocument)doc : null;
+ View row = getView(rowIndex);
+ int lastPos = -1;
+ // This could be made better to check backward positions too.
+ biasRet[0] = Position.Bias.Forward;
+ for(int vc = 0, numViews = row.getViewCount(); vc < numViews; vc++) {
+ View v = row.getView(vc);
+ int start = v.getStartOffset();
+ boolean ltr = (aDoc != null) ? aDoc.isLeftToRight
+ (start, start + 1) : true;
+ if(ltr) {
+ lastPos = start;
+ for(int end = v.getEndOffset(); lastPos < end; lastPos++) {
+ float xx = text.modelToView(lastPos).getBounds().x;
+ if(xx >= x) {
+ while (++lastPos < end &&
+ text.modelToView(lastPos).getBounds().x == xx) {
+ }
+ return --lastPos;
+ }
+ }
+ lastPos--;
+ }
+ else {
+ for(lastPos = v.getEndOffset() - 1; lastPos >= start;
+ lastPos--) {
+ float xx = text.modelToView(lastPos).getBounds().x;
+ if(xx >= x) {
+ while (--lastPos >= start &&
+ text.modelToView(lastPos).getBounds().x == xx) {
+ }
+ return ++lastPos;
+ }
+ }
+ lastPos++;
+ }
+ }
+ if(lastPos == -1) {
+ return getStartOffset();
+ }
+ return lastPos;
+ }
+
+ /**
+ * Determines in which direction the next view lays.
+ * Consider the <code>View</code> at index n.
+ * Typically the <code>View</code>s are layed out
+ * from left to right, so that the <code>View</code>
+ * to the EAST will be at index n + 1, and the
+ * <code>View</code> to the WEST will be at index n - 1.
+ * In certain situations, such as with bidirectional text,
+ * it is possible that the <code>View</code> to EAST is not
+ * at index n + 1, but rather at index n - 1,
+ * or that the <code>View</code> to the WEST is not at
+ * index n - 1, but index n + 1. In this case this method
+ * would return true, indicating the <code>View</code>s are
+ * layed out in descending order.
+ * <p>
+ * This will return true if the text is layed out right
+ * to left at position, otherwise false.
+ *
+ * @param position position into the model
+ * @param bias either <code>Position.Bias.Forward</code> or
+ * <code>Position.Bias.Backward</code>
+ * @return true if the text is layed out right to left at
+ * position, otherwise false.
+ */
+ protected boolean flipEastAndWestAtEnds(int position,
+ Position.Bias bias) {
+ Document doc = getDocument();
+ if(doc instanceof AbstractDocument &&
+ !((AbstractDocument)doc).isLeftToRight(getStartOffset(),
+ getStartOffset() + 1)) {
+ return true;
+ }
+ return false;
+ }
+
+ // --- FlowView methods ---------------------------------------------
+
+ /**
+ * Fetches the constraining span to flow against for
+ * the given child index.
+ * @param index the index of the view being queried
+ * @return the constraining span for the given view at
+ * <code>index</code>
+ * @since 1.3
+ */
+ public int getFlowSpan(int index) {
+ View child = getView(index);
+ int adjust = 0;
+ if (child instanceof Row) {
+ Row row = (Row) child;
+ adjust = row.getLeftInset() + row.getRightInset();
+ }
+ return (layoutSpan == Integer.MAX_VALUE) ? layoutSpan
+ : (layoutSpan - adjust);
+ }
+
+ /**
+ * Fetches the location along the flow axis that the
+ * flow span will start at.
+ * @param index the index of the view being queried
+ * @return the location for the given view at
+ * <code>index</code>
+ * @since 1.3
+ */
+ public int getFlowStart(int index) {
+ View child = getView(index);
+ int adjust = 0;
+ if (child instanceof Row) {
+ Row row = (Row) child;
+ adjust = row.getLeftInset();
+ }
+ return tabBase + adjust;
+ }
+
+ /**
+ * Create a <code>View</code> that should be used to hold a
+ * a row's worth of children in a flow.
+ * @return the new <code>View</code>
+ * @since 1.3
+ */
+ protected View createRow() {
+ return new Row(getElement());
+ }
+
+ // --- TabExpander methods ------------------------------------------
+
+ /**
+ * Returns the next tab stop position given a reference position.
+ * This view implements the tab coordinate system, and calls
+ * <code>getTabbedSpan</code> 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.
+ * <code>LabelView</code> is an example of a view that delegates
+ * its tab expansion needs upward to the paragraph.
+ * <p>
+ * This is implemented to try and locate a <code>TabSet</code>
+ * 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(justification != StyleConstants.ALIGN_LEFT)
+ return x + 10.0f;
+ x -= tabBase;
+ TabSet tabs = getTabSet();
+ if(tabs == null) {
+ // a tab every 72 pixels.
+ return (float)(tabBase + (((int)x / 72 + 1) * 72));
+ }
+ TabStop tab = tabs.getTabAfter(x + .01f);
+ if(tab == null) {
+ // no tab, do a default of 5 pixels.
+ // Should this cause a wrapping of the line?
+ return tabBase + x + 5.0f;
+ }
+ int alignment = tab.getAlignment();
+ int offset;
+ switch(alignment) {
+ default:
+ case TabStop.ALIGN_LEFT:
+ // Simple case, left tab.
+ return tabBase + tab.getPosition();
+ case TabStop.ALIGN_BAR:
+ // PENDING: what does this mean?
+ return tabBase + tab.getPosition();
+ case TabStop.ALIGN_RIGHT:
+ case TabStop.ALIGN_CENTER:
+ offset = findOffsetToCharactersInString(tabChars,
+ tabOffset + 1);
+ break;
+ case TabStop.ALIGN_DECIMAL:
+ offset = findOffsetToCharactersInString(tabDecimalChars,
+ tabOffset + 1);
+ break;
+ }
+ if (offset == -1) {
+ offset = getEndOffset();
+ }
+ float charsSize = getPartialSize(tabOffset + 1, offset);
+ switch(alignment) {
+ case TabStop.ALIGN_RIGHT:
+ case TabStop.ALIGN_DECIMAL:
+ // right and decimal are treated the same way, the new
+ // position will be the location of the tab less the
+ // partialSize.
+ return tabBase + Math.max(x, tab.getPosition() - charsSize);
+ case TabStop.ALIGN_CENTER:
+ // Similar to right, but half the partialSize.
+ return tabBase + Math.max(x, tab.getPosition() - charsSize / 2.0f);
+ }
+ // will never get here!
+ return x;
+ }
+
+ /**
+ * Gets the <code>Tabset</code> to be used in calculating tabs.
+ *
+ * @return the <code>TabSet</code>
+ */
+ protected TabSet getTabSet() {
+ return StyleConstants.getTabSet(getElement().getAttributes());
+ }
+
+ /**
+ * Returns the size used by the views between
+ * <code>startOffset</code> and <code>endOffset</code>.
+ * This uses <code>getPartialView</code> to calculate the
+ * size if the child view implements the
+ * <code>TabableView</code> interface. If a
+ * size is needed and a <code>View</code> does not implement
+ * the <code>TabableView</code> interface,
+ * the <code>preferredSpan</code> will be used.
+ *
+ * @param startOffset the starting document offset >= 0
+ * @param endOffset the ending document offset >= startOffset
+ * @return the size >= 0
+ */
+ protected float getPartialSize(int startOffset, int endOffset) {
+ float size = 0.0f;
+ int viewIndex;
+ int numViews = getViewCount();
+ View view;
+ int viewEnd;
+ int tempEnd;
+
+ // Have to search layoutPool!
+ // PENDING: when ParagraphView supports breaking location
+ // into layoutPool will have to change!
+ viewIndex = getElement().getElementIndex(startOffset);
+ numViews = layoutPool.getViewCount();
+ while(startOffset < endOffset && viewIndex < numViews) {
+ view = layoutPool.getView(viewIndex++);
+ viewEnd = view.getEndOffset();
+ tempEnd = Math.min(endOffset, viewEnd);
+ if(view instanceof TabableView)
+ size += ((TabableView)view).getPartialSpan(startOffset, tempEnd);
+ else if(startOffset == view.getStartOffset() &&
+ tempEnd == view.getEndOffset())
+ size += view.getPreferredSpan(View.X_AXIS);
+ else
+ // PENDING: should we handle this better?
+ return 0.0f;
+ startOffset = viewEnd;
+ }
+ return size;
+ }
+
+ /**
+ * Finds the next character in the document with a character in
+ * <code>string</code>, starting at offset <code>start</code>. If
+ * there are no characters found, -1 will be returned.
+ *
+ * @param string the string of characters
+ * @param start where to start in the model >= 0
+ * @return the document offset, or -1 if no characters found
+ */
+ protected int findOffsetToCharactersInString(char[] string,
+ int start) {
+ int stringLength = string.length;
+ int end = getEndOffset();
+ Segment seg = new Segment();
+ try {
+ getDocument().getText(start, end - start, seg);
+ } catch (BadLocationException ble) {
+ return -1;
+ }
+ for(int counter = seg.offset, maxCounter = seg.offset + seg.count;
+ counter < maxCounter; counter++) {
+ char currentChar = seg.array[counter];
+ for(int subCounter = 0; subCounter < stringLength;
+ subCounter++) {
+ if(currentChar == string[subCounter])
+ return counter - seg.offset + start;
+ }
+ }
+ // No match.
+ return -1;
+ }
+
+ /**
+ * Returns where the tabs are calculated from.
+ * @return where tabs are calculated from
+ */
+ protected float getTabBase() {
+ return (float)tabBase;
+ }
+
+ // ---- View methods ----------------------------------------------------
+
+ /**
+ * 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) {
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
+ tabBase = alloc.x + getLeftInset();
+ super.paint(g, a);
+
+ // line with the negative firstLineIndent value needs
+ // special handling
+ if (firstLineIndent < 0) {
+ Shape sh = getChildAllocation(0, a);
+ if ((sh != null) && sh.intersects(alloc)) {
+ int x = alloc.x + getLeftInset() + firstLineIndent;
+ int y = alloc.y + getTopInset();
+
+ Rectangle clip = g.getClipBounds();
+ tempRect.x = x + getOffset(X_AXIS, 0);
+ tempRect.y = y + getOffset(Y_AXIS, 0);
+ tempRect.width = getSpan(X_AXIS, 0) - firstLineIndent;
+ tempRect.height = getSpan(Y_AXIS, 0);
+ if (tempRect.intersects(clip)) {
+ tempRect.x = tempRect.x - firstLineIndent;
+ paintChild(g, tempRect, 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * Determines the desired alignment for this view along an
+ * axis. This is implemented to give the alignment to the
+ * center of the first row along the y axis, and the default
+ * along the x axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @return the desired alignment. This should be a value
+ * between 0.0 and 1.0 inclusive, 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 Y_AXIS:
+ float a = 0.5f;
+ if (getViewCount() != 0) {
+ int paragraphSpan = (int) getPreferredSpan(View.Y_AXIS);
+ View v = getView(0);
+ int rowSpan = (int) v.getPreferredSpan(View.Y_AXIS);
+ a = (paragraphSpan != 0) ? ((float)(rowSpan / 2)) / paragraphSpan : 0;
+ }
+ return a;
+ case X_AXIS:
+ return 0.5f;
+ default:
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ }
+
+ /**
+ * Breaks this view on the given axis at the given length.
+ * <p>
+ * <code>ParagraphView</code> instances are breakable
+ * along the <code>Y_AXIS</code> only, and only if
+ * <code>len</code> is after the first line.
+ *
+ * @param axis may be either <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @param len specifies where a potential break is desired
+ * along the given axis >= 0
+ * @param a the current allocation of the view
+ * @return the fragment of the view that represents the
+ * given span, if the view can be broken; if the view
+ * doesn't support breaking behavior, the view itself is
+ * returned
+ * @see View#breakView
+ */
+ public View breakView(int axis, float len, Shape a) {
+ if(axis == View.Y_AXIS) {
+ if(a != null) {
+ Rectangle alloc = a.getBounds();
+ setSize(alloc.width, alloc.height);
+ }
+ // Determine what row to break on.
+
+ // PENDING(prinz) add break support
+ return this;
+ }
+ return this;
+ }
+
+ /**
+ * Gets the break weight for a given location.
+ * <p>
+ * <code>ParagraphView</code> instances are breakable
+ * along the <code>Y_AXIS</code> only, and only if
+ * <code>len</code> is after the first row. If the length
+ * is less than one row, a value of <code>BadBreakWeight</code>
+ * is returned.
+ *
+ * @param axis may be either <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @param len specifies where a potential break is desired >= 0
+ * @return a value indicating the attractiveness of breaking here;
+ * either <code>GoodBreakWeight</code> or <code>BadBreakWeight</code>
+ * @see View#getBreakWeight
+ */
+ public int getBreakWeight(int axis, float len) {
+ if(axis == View.Y_AXIS) {
+ // PENDING(prinz) make this return a reasonable value
+ // when paragraph breaking support is re-implemented.
+ // If less than one row, bad weight value should be
+ // returned.
+ //return GoodBreakWeight;
+ return BadBreakWeight;
+ }
+ return BadBreakWeight;
+ }
+
+ /**
+ * Calculate the needs for the paragraph along the minor axis.
+ *
+ * <p>This uses size requirements of the superclass, modified to take into
+ * account the non-breakable areas at the adjacent views edges. The minimal
+ * size requirements for such views should be no less than the sum of all
+ * adjacent fragments.</p>
+ *
+ * <p>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.</p>
+ *
+ * @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
+ */
+ @Override
+ protected SizeRequirements calculateMinorAxisRequirements(int axis,
+ SizeRequirements r) {
+ r = super.calculateMinorAxisRequirements(axis, r);
+
+ float min = 0;
+ float glue = 0;
+ int n = getLayoutViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getLayoutView(i);
+ float span = v.getMinimumSpan(axis);
+ if (v.getBreakWeight(axis, 0, v.getMaximumSpan(axis))
+ > View.BadBreakWeight) {
+ // find the longest non-breakable fragments at the view edges
+ int p0 = v.getStartOffset();
+ int p1 = v.getEndOffset();
+ float start = findEdgeSpan(v, axis, p0, p0, p1);
+ float end = findEdgeSpan(v, axis, p1, p0, p1);
+ glue += start;
+ min = Math.max(min, Math.max(span, glue));
+ glue = end;
+ } else {
+ // non-breakable view
+ glue += span;
+ min = Math.max(min, glue);
+ }
+ }
+ r.minimum = Math.max(r.minimum, (int) min);
+ r.preferred = Math.max(r.minimum, r.preferred);
+ r.maximum = Math.max(r.preferred, r.maximum);
+
+ return r;
+ }
+
+ /**
+ * Binary search for the longest non-breakable fragment at the view edge.
+ */
+ private float findEdgeSpan(View v, int axis, int fp, int p0, int p1) {
+ int len = p1 - p0;
+ if (len <= 1) {
+ // further fragmentation is not possible
+ return v.getMinimumSpan(axis);
+ } else {
+ int mid = p0 + len / 2;
+ boolean startEdge = mid > fp;
+ // initial view is breakable hence must support fragmentation
+ View f = startEdge ?
+ v.createFragment(fp, mid) : v.createFragment(mid, fp);
+ boolean breakable = f.getBreakWeight(
+ axis, 0, f.getMaximumSpan(axis)) > View.BadBreakWeight;
+ if (breakable == startEdge) {
+ p1 = mid;
+ } else {
+ p0 = mid;
+ }
+ return findEdgeSpan(f, axis, fp, p0, p1);
+ }
+ }
+
+ /**
+ * Gives notification from the document that attributes were changed
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ // update any property settings stored, and layout should be
+ // recomputed
+ setPropertiesFromAttributes();
+ layoutChanged(X_AXIS);
+ layoutChanged(Y_AXIS);
+ super.changedUpdate(changes, a, f);
+ }
+
+
+ // --- variables -----------------------------------------------
+
+ private int justification;
+ private float lineSpacing;
+ /** Indentation for the first line, from the left inset. */
+ protected int firstLineIndent = 0;
+
+ /**
+ * Used by the TabExpander functionality to determine
+ * where to base the tab calculations. This is basically
+ * the location of the left side of the paragraph.
+ */
+ private int tabBase;
+
+ /**
+ * Used to create an i18n-based layout strategy
+ */
+ static Class i18nStrategy;
+
+ /** Used for searching for a tab. */
+ static char[] tabChars;
+ /** Used for searching for a tab or decimal character. */
+ static char[] tabDecimalChars;
+
+ static {
+ tabChars = new char[1];
+ tabChars[0] = '\t';
+ tabDecimalChars = new char[2];
+ tabDecimalChars[0] = '\t';
+ tabDecimalChars[1] = '.';
+ }
+
+ /**
+ * Internally created view that has the purpose of holding
+ * the views that represent the children of the paragraph
+ * that have been arranged in rows.
+ */
+ class Row extends BoxView {
+
+ Row(Element elem) {
+ super(elem, View.X_AXIS);
+ }
+
+ /**
+ * This is reimplemented to do nothing since the
+ * paragraph fills in the row with its needed
+ * children.
+ */
+ protected void loadChildren(ViewFactory f) {
+ }
+
+ /**
+ * Fetches the attributes to use when rendering. This view
+ * isn't directly responsible for an element so it returns
+ * the outer classes attributes.
+ */
+ public AttributeSet getAttributes() {
+ View p = getParent();
+ return (p != null) ? p.getAttributes() : null;
+ }
+
+ public float getAlignment(int axis) {
+ if (axis == View.X_AXIS) {
+ switch (justification) {
+ case StyleConstants.ALIGN_LEFT:
+ return 0;
+ case StyleConstants.ALIGN_RIGHT:
+ return 1;
+ case StyleConstants.ALIGN_CENTER:
+ return 0.5f;
+ case StyleConstants.ALIGN_JUSTIFIED:
+ float rv = 0.5f;
+ //if we can justifiy the content always align to
+ //the left.
+ if (isJustifiableDocument()) {
+ rv = 0f;
+ }
+ return rv;
+ }
+ }
+ return super.getAlignment(axis);
+ }
+
+ /**
+ * Provides a mapping from the document model coordinate space
+ * to the coordinate space of the view mapped to it. This is
+ * implemented to let the superclass find the position along
+ * the major axis and the allocation of the row is used
+ * along the minor axis, so that even though the children
+ * are different heights they all get the same caret height.
+ *
+ * @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 {
+ Rectangle r = a.getBounds();
+ View v = getViewAtPosition(pos, r);
+ if ((v != null) && (!v.getElement().isLeaf())) {
+ // Don't adjust the height if the view represents a branch.
+ return super.modelToView(pos, a, b);
+ }
+ r = a.getBounds();
+ int height = r.height;
+ int y = r.y;
+ Shape loc = super.modelToView(pos, a, b);
+ r = loc.getBounds();
+ r.height = height;
+ r.y = y;
+ return r;
+ }
+
+ /**
+ * Range represented by a row in the paragraph is only
+ * a subset of the total range of the paragraph element.
+ * @see View#getRange
+ */
+ public int getStartOffset() {
+ int offs = Integer.MAX_VALUE;
+ int n = getViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ offs = Math.min(offs, v.getStartOffset());
+ }
+ return offs;
+ }
+
+ public int getEndOffset() {
+ int offs = 0;
+ int n = getViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ offs = Math.max(offs, v.getEndOffset());
+ }
+ return offs;
+ }
+
+ /**
+ * 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.
+ * <p>
+ * This is implemented to do a baseline layout of the children
+ * by calling BoxView.baselineLayout.
+ *
+ * @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) {
+ baselineLayout(targetSpan, axis, offsets, spans);
+ }
+
+ protected SizeRequirements calculateMinorAxisRequirements(int axis,
+ SizeRequirements r) {
+ return baselineRequirements(axis, r);
+ }
+
+
+ private boolean isLastRow() {
+ View parent;
+ return ((parent = getParent()) == null
+ || this == parent.getView(parent.getViewCount() - 1));
+ }
+
+ private boolean isBrokenRow() {
+ boolean rv = false;
+ int viewsCount = getViewCount();
+ if (viewsCount > 0) {
+ View lastView = getView(viewsCount - 1);
+ if (lastView.getBreakWeight(X_AXIS, 0, 0) >=
+ ForcedBreakWeight) {
+ rv = true;
+ }
+ }
+ return rv;
+ }
+
+ private boolean isJustifiableDocument() {
+ return (! Boolean.TRUE.equals(getDocument().getProperty(
+ AbstractDocument.I18NProperty)));
+ }
+
+ /**
+ * Whether we need to justify this {@code Row}.
+ * At this time (jdk1.6) we support justification on for non
+ * 18n text.
+ *
+ * @return {@code true} if this {@code Row} should be justified.
+ */
+ private boolean isJustifyEnabled() {
+ boolean ret = (justification == StyleConstants.ALIGN_JUSTIFIED);
+
+ //no justification for i18n documents
+ ret = ret && isJustifiableDocument();
+
+ //no justification for the last row
+ ret = ret && ! isLastRow();
+
+ //no justification for the broken rows
+ ret = ret && ! isBrokenRow();
+
+ return ret;
+ }
+
+
+ //Calls super method after setting spaceAddon to 0.
+ //Justification should not affect MajorAxisRequirements
+ @Override
+ protected SizeRequirements calculateMajorAxisRequirements(int axis,
+ SizeRequirements r) {
+ int oldJustficationData[] = justificationData;
+ justificationData = null;
+ SizeRequirements ret = super.calculateMajorAxisRequirements(axis, r);
+ if (isJustifyEnabled()) {
+ justificationData = oldJustficationData;
+ }
+ return ret;
+ }
+
+ @Override
+ protected void layoutMajorAxis(int targetSpan, int axis,
+ int[] offsets, int[] spans) {
+ int oldJustficationData[] = justificationData;
+ justificationData = null;
+ super.layoutMajorAxis(targetSpan, axis, offsets, spans);
+ if (! isJustifyEnabled()) {
+ return;
+ }
+
+ int currentSpan = 0;
+ for (int span : spans) {
+ currentSpan += span;
+ }
+ if (currentSpan == targetSpan) {
+ //no need to justify
+ return;
+ }
+
+ // we justify text by enlarging spaces by the {@code spaceAddon}.
+ // justification is started to the right of the rightmost TAB.
+ // leading and trailing spaces are not extendable.
+ //
+ // GlyphPainter1 uses
+ // justificationData
+ // for all painting and measurement.
+
+ int extendableSpaces = 0;
+ int startJustifiableContent = -1;
+ int endJustifiableContent = -1;
+ int lastLeadingSpaces = 0;
+
+ int rowStartOffset = getStartOffset();
+ int rowEndOffset = getEndOffset();
+ int spaceMap[] = new int[rowEndOffset - rowStartOffset];
+ Arrays.fill(spaceMap, 0);
+ for (int i = getViewCount() - 1; i >= 0 ; i--) {
+ View view = getView(i);
+ if (view instanceof GlyphView) {
+ GlyphView.JustificationInfo justificationInfo =
+ ((GlyphView) view).getJustificationInfo(rowStartOffset);
+ final int viewStartOffset = view.getStartOffset();
+ final int offset = viewStartOffset - rowStartOffset;
+ for (int j = 0; j < justificationInfo.spaceMap.length(); j++) {
+ if (justificationInfo.spaceMap.get(j)) {
+ spaceMap[j + offset] = 1;
+ }
+ }
+ if (startJustifiableContent > 0) {
+ if (justificationInfo.end >= 0) {
+ extendableSpaces += justificationInfo.trailingSpaces;
+ } else {
+ lastLeadingSpaces += justificationInfo.trailingSpaces;
+ }
+ }
+ if (justificationInfo.start >= 0) {
+ startJustifiableContent =
+ justificationInfo.start + viewStartOffset;
+ extendableSpaces += lastLeadingSpaces;
+ }
+ if (justificationInfo.end >= 0
+ && endJustifiableContent < 0) {
+ endJustifiableContent =
+ justificationInfo.end + viewStartOffset;
+ }
+ extendableSpaces += justificationInfo.contentSpaces;
+ lastLeadingSpaces = justificationInfo.leadingSpaces;
+ if (justificationInfo.hasTab) {
+ break;
+ }
+ }
+ }
+ if (extendableSpaces <= 0) {
+ //there is nothing we can do to justify
+ return;
+ }
+ int adjustment = (targetSpan - currentSpan);
+ int spaceAddon = (extendableSpaces > 0)
+ ? adjustment / extendableSpaces
+ : 0;
+ int spaceAddonLeftoverEnd = -1;
+ for (int i = startJustifiableContent - rowStartOffset,
+ leftover = adjustment - spaceAddon * extendableSpaces;
+ leftover > 0;
+ leftover -= spaceMap[i],
+ i++) {
+ spaceAddonLeftoverEnd = i;
+ }
+ if (spaceAddon > 0 || spaceAddonLeftoverEnd >= 0) {
+ justificationData = (oldJustficationData != null)
+ ? oldJustficationData
+ : new int[END_JUSTIFIABLE + 1];
+ justificationData[SPACE_ADDON] = spaceAddon;
+ justificationData[SPACE_ADDON_LEFTOVER_END] =
+ spaceAddonLeftoverEnd;
+ justificationData[START_JUSTIFIABLE] =
+ startJustifiableContent - rowStartOffset;
+ justificationData[END_JUSTIFIABLE] =
+ endJustifiableContent - rowStartOffset;
+ super.layoutMajorAxis(targetSpan, axis, offsets, spans);
+ }
+ }
+
+ //for justified row we assume the maximum horizontal span
+ //is MAX_VALUE.
+ @Override
+ public float getMaximumSpan(int axis) {
+ float ret;
+ if (View.X_AXIS == axis
+ && isJustifyEnabled()) {
+ ret = Float.MAX_VALUE;
+ } else {
+ ret = super.getMaximumSpan(axis);
+ }
+ return ret;
+ }
+
+ /**
+ * Fetches the child view index representing the given position in
+ * the model.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ */
+ protected int getViewIndexAtPosition(int pos) {
+ // This is expensive, but are views are not necessarily layed
+ // out in model order.
+ if(pos < getStartOffset() || pos >= getEndOffset())
+ return -1;
+ for(int counter = getViewCount() - 1; counter >= 0; counter--) {
+ View v = getView(counter);
+ if(pos >= v.getStartOffset() &&
+ pos < v.getEndOffset()) {
+ return counter;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Gets the left inset.
+ *
+ * @return the inset
+ */
+ protected short getLeftInset() {
+ View parentView;
+ int adjustment = 0;
+ if ((parentView = getParent()) != null) { //use firstLineIdent for the first row
+ if (this == parentView.getView(0)) {
+ adjustment = firstLineIndent;
+ }
+ }
+ return (short)(super.getLeftInset() + adjustment);
+ }
+
+ protected short getBottomInset() {
+ return (short)(super.getBottomInset() +
+ ((minorRequest != null) ? minorRequest.preferred : 0) *
+ lineSpacing);
+ }
+
+ final static int SPACE_ADDON = 0;
+ final static int SPACE_ADDON_LEFTOVER_END = 1;
+ final static int START_JUSTIFIABLE = 2;
+ //this should be the last index in justificationData
+ final static int END_JUSTIFIABLE = 3;
+
+ int justificationData[] = null;
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/PasswordView.java b/src/share/classes/javax/swing/text/PasswordView.java
new file mode 100644
index 000000000..fb937fb09
--- /dev/null
+++ b/src/share/classes/javax/swing/text/PasswordView.java
@@ -0,0 +1,236 @@
+/*
+ * 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;
+
+import sun.swing.SwingUtilities2;
+import java.awt.*;
+import javax.swing.JPasswordField;
+
+/**
+ * Implements a View suitable for use in JPasswordField
+ * UI implementations. This is basically a field ui that
+ * renders its contents as the echo character specified
+ * in the associated component (if it can narrow the
+ * component to a JPasswordField).
+ *
+ * @author Timothy Prinzing
+ * @see View
+ */
+public class PasswordView extends FieldView {
+
+ /**
+ * Constructs a new view wrapped on an element.
+ *
+ * @param elem the element
+ */
+ public PasswordView(Element elem) {
+ super(elem);
+ }
+
+ /**
+ * Renders the given range in the model as normal unselected
+ * text. This sets the foreground color and echos the characters
+ * using the value returned by getEchoChar().
+ *
+ * @param g the graphics context
+ * @param x the starting X coordinate >= 0
+ * @param y the starting Y coordinate >= 0
+ * @param p0 the starting offset in the model >= 0
+ * @param p1 the ending offset in the model >= p0
+ * @return the X location of the end of the range >= 0
+ * @exception BadLocationException if p0 or p1 are out of range
+ */
+ protected int drawUnselectedText(Graphics g, int x, int y,
+ int p0, int p1) throws BadLocationException {
+
+ Container c = getContainer();
+ if (c instanceof JPasswordField) {
+ JPasswordField f = (JPasswordField) c;
+ if (! f.echoCharIsSet()) {
+ return super.drawUnselectedText(g, x, y, p0, p1);
+ }
+ if (f.isEnabled()) {
+ g.setColor(f.getForeground());
+ }
+ else {
+ g.setColor(f.getDisabledTextColor());
+ }
+ char echoChar = f.getEchoChar();
+ int n = p1 - p0;
+ for (int i = 0; i < n; i++) {
+ x = drawEchoCharacter(g, x, y, echoChar);
+ }
+ }
+ return x;
+ }
+
+ /**
+ * Renders the given range in the model as selected text. This
+ * is implemented to render the text in the color specified in
+ * the hosting component. It assumes the highlighter will render
+ * the selected background. Uses the result of getEchoChar() to
+ * display the characters.
+ *
+ * @param g the graphics context
+ * @param x the starting X coordinate >= 0
+ * @param y the starting Y coordinate >= 0
+ * @param p0 the starting offset in the model >= 0
+ * @param p1 the ending offset in the model >= p0
+ * @return the X location of the end of the range >= 0
+ * @exception BadLocationException if p0 or p1 are out of range
+ */
+ protected int drawSelectedText(Graphics g, int x,
+ int y, int p0, int p1) throws BadLocationException {
+ g.setColor(selected);
+ Container c = getContainer();
+ if (c instanceof JPasswordField) {
+ JPasswordField f = (JPasswordField) c;
+ if (! f.echoCharIsSet()) {
+ return super.drawSelectedText(g, x, y, p0, p1);
+ }
+ char echoChar = f.getEchoChar();
+ int n = p1 - p0;
+ for (int i = 0; i < n; i++) {
+ x = drawEchoCharacter(g, x, y, echoChar);
+ }
+ }
+ return x;
+ }
+
+ /**
+ * Renders the echo character, or whatever graphic should be used
+ * to display the password characters. The color in the Graphics
+ * object is set to the appropriate foreground color for selected
+ * or unselected text.
+ *
+ * @param g the graphics context
+ * @param x the starting X coordinate >= 0
+ * @param y the starting Y coordinate >= 0
+ * @param c the echo character
+ * @return the updated X position >= 0
+ */
+ protected int drawEchoCharacter(Graphics g, int x, int y, char c) {
+ ONE[0] = c;
+ SwingUtilities2.drawChars(Utilities.getJComponent(this),
+ g, ONE, 0, 1, x, y);
+ return x + g.getFontMetrics().charWidth(c);
+ }
+
+ /**
+ * 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 >= 0
+ * @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 {
+ Container c = getContainer();
+ if (c instanceof JPasswordField) {
+ JPasswordField f = (JPasswordField) c;
+ if (! f.echoCharIsSet()) {
+ return super.modelToView(pos, a, b);
+ }
+ char echoChar = f.getEchoChar();
+ FontMetrics m = f.getFontMetrics(f.getFont());
+
+ Rectangle alloc = adjustAllocation(a).getBounds();
+ int dx = (pos - getStartOffset()) * m.charWidth(echoChar);
+ alloc.x += dx;
+ alloc.width = 1;
+ return alloc;
+ }
+ return null;
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param fx the X coordinate >= 0.0f
+ * @param fy the Y coordinate >= 0.0f
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents the
+ * given point in the view
+ * @see View#viewToModel
+ */
+ public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
+ bias[0] = Position.Bias.Forward;
+ int n = 0;
+ Container c = getContainer();
+ if (c instanceof JPasswordField) {
+ JPasswordField f = (JPasswordField) c;
+ if (! f.echoCharIsSet()) {
+ return super.viewToModel(fx, fy, a, bias);
+ }
+ char echoChar = f.getEchoChar();
+ int charWidth = f.getFontMetrics(f.getFont()).charWidth(echoChar);
+ a = adjustAllocation(a);
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a :
+ a.getBounds();
+ n = (charWidth > 0 ?
+ ((int)fx - alloc.x) / charWidth : Integer.MAX_VALUE);
+ if (n < 0) {
+ n = 0;
+ }
+ else if (n > (getStartOffset() + getDocument().getLength())) {
+ n = getDocument().getLength() - getStartOffset();
+ }
+ }
+ return getStartOffset() + n;
+ }
+
+ /**
+ * 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.
+ */
+ public float getPreferredSpan(int axis) {
+ switch (axis) {
+ case View.X_AXIS:
+ Container c = getContainer();
+ if (c instanceof JPasswordField) {
+ JPasswordField f = (JPasswordField) c;
+ if (f.echoCharIsSet()) {
+ char echoChar = f.getEchoChar();
+ FontMetrics m = f.getFontMetrics(f.getFont());
+ Document doc = getDocument();
+ return m.charWidth(echoChar) * getDocument().getLength();
+ }
+ }
+ }
+ return super.getPreferredSpan(axis);
+ }
+
+ static char[] ONE = new char[1];
+}
diff --git a/src/share/classes/javax/swing/text/PlainDocument.java b/src/share/classes/javax/swing/text/PlainDocument.java
new file mode 100644
index 000000000..85ea6c8d2
--- /dev/null
+++ b/src/share/classes/javax/swing/text/PlainDocument.java
@@ -0,0 +1,324 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import javax.swing.event.*;
+
+/**
+ * A plain document that maintains no character attributes. The
+ * default element structure for this document is a map of the lines in
+ * the text. The Element returned by getDefaultRootElement is
+ * a map of the lines, and each child element represents a line.
+ * This model does not maintain any character level attributes,
+ * but each line can be tagged with an arbitrary set of attributes.
+ * Line to offset, and offset to line translations can be quickly
+ * performed using the default root element. The structure information
+ * of the DocumentEvent's fired by edits will indicate the line
+ * structure changes.
+ * <p>
+ * The default content storage management is performed by a
+ * gapped buffer implementation (GapContent). It supports
+ * editing reasonably large documents with good efficiency when
+ * the edits are contiguous or clustered, as is typical.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Timothy Prinzing
+ * @see Document
+ * @see AbstractDocument
+ */
+public class PlainDocument extends AbstractDocument {
+
+ /**
+ * Name of the attribute that specifies the tab
+ * size for tabs contained in the content. The
+ * type for the value is Integer.
+ */
+ public static final String tabSizeAttribute = "tabSize";
+
+ /**
+ * Name of the attribute that specifies the maximum
+ * length of a line, if there is a maximum length.
+ * The type for the value is Integer.
+ */
+ public static final String lineLimitAttribute = "lineLimit";
+
+ /**
+ * Constructs a plain text document. A default model using
+ * <code>GapContent</code> is constructed and set.
+ */
+ public PlainDocument() {
+ this(new GapContent());
+ }
+
+ /**
+ * Constructs a plain text document. A default root element is created,
+ * and the tab size set to 8.
+ *
+ * @param c the container for the content
+ */
+ public PlainDocument(Content c) {
+ super(c);
+ putProperty(tabSizeAttribute, new Integer(8));
+ defaultRoot = createDefaultRoot();
+ }
+
+ /**
+ * Inserts some content into the document.
+ * Inserting content causes a write lock to be held while the
+ * actual changes are taking place, followed by notification
+ * to the observers on the thread that grabbed the write lock.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param offs the starting offset >= 0
+ * @param str the string to insert; does nothing with null/empty strings
+ * @param a the attributes for the inserted content
+ * @exception BadLocationException the given insert position is not a valid
+ * position within the document
+ * @see Document#insertString
+ */
+ public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
+ // fields don't want to have multiple lines. We may provide a field-specific
+ // model in the future in which case the filtering logic here will no longer
+ // be needed.
+ Object filterNewlines = getProperty("filterNewlines");
+ if ((filterNewlines instanceof Boolean) && filterNewlines.equals(Boolean.TRUE)) {
+ if ((str != null) && (str.indexOf('\n') >= 0)) {
+ StringBuffer filtered = new StringBuffer(str);
+ int n = filtered.length();
+ for (int i = 0; i < n; i++) {
+ if (filtered.charAt(i) == '\n') {
+ filtered.setCharAt(i, ' ');
+ }
+ }
+ str = filtered.toString();
+ }
+ }
+ super.insertString(offs, str, a);
+ }
+
+ /**
+ * Gets the default root element for the document model.
+ *
+ * @return the root
+ * @see Document#getDefaultRootElement
+ */
+ public Element getDefaultRootElement() {
+ return defaultRoot;
+ }
+
+ /**
+ * Creates the root element to be used to represent the
+ * default document structure.
+ *
+ * @return the element base
+ */
+ protected AbstractElement createDefaultRoot() {
+ BranchElement map = (BranchElement) createBranchElement(null, null);
+ Element line = createLeafElement(map, null, 0, 1);
+ Element[] lines = new Element[1];
+ lines[0] = line;
+ map.replace(0, 0, lines);
+ return map;
+ }
+
+ /**
+ * Get the paragraph element containing the given position. Since this
+ * document only models lines, it returns the line instead.
+ */
+ public Element getParagraphElement(int pos){
+ Element lineMap = getDefaultRootElement();
+ return lineMap.getElement( lineMap.getElementIndex( pos ) );
+ }
+
+ /**
+ * Updates document structure as a result of text insertion. This
+ * will happen within a write lock. Since this document simply
+ * maps out lines, we refresh the line map.
+ *
+ * @param chng the change event describing the dit
+ * @param attr the set of attributes for the inserted text
+ */
+ protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
+ removed.removeAllElements();
+ added.removeAllElements();
+ BranchElement lineMap = (BranchElement) getDefaultRootElement();
+ int offset = chng.getOffset();
+ int length = chng.getLength();
+ if (offset > 0) {
+ offset -= 1;
+ length += 1;
+ }
+ int index = lineMap.getElementIndex(offset);
+ Element rmCandidate = lineMap.getElement(index);
+ int rmOffs0 = rmCandidate.getStartOffset();
+ int rmOffs1 = rmCandidate.getEndOffset();
+ int lastOffset = rmOffs0;
+ try {
+ if (s == null) {
+ s = new Segment();
+ }
+ getContent().getChars(offset, length, s);
+ boolean hasBreaks = false;
+ for (int i = 0; i < length; i++) {
+ char c = s.array[s.offset + i];
+ if (c == '\n') {
+ int breakOffset = offset + i + 1;
+ added.addElement(createLeafElement(lineMap, null, lastOffset, breakOffset));
+ lastOffset = breakOffset;
+ hasBreaks = true;
+ }
+ }
+ if (hasBreaks) {
+ int rmCount = 1;
+ removed.addElement(rmCandidate);
+ if ((offset + length == rmOffs1) && (lastOffset != rmOffs1) &&
+ ((index+1) < lineMap.getElementCount())) {
+ rmCount += 1;
+ Element e = lineMap.getElement(index+1);
+ removed.addElement(e);
+ rmOffs1 = e.getEndOffset();
+ }
+ if (lastOffset < rmOffs1) {
+ added.addElement(createLeafElement(lineMap, null, lastOffset, rmOffs1));
+ }
+
+ Element[] aelems = new Element[added.size()];
+ added.copyInto(aelems);
+ Element[] relems = new Element[removed.size()];
+ removed.copyInto(relems);
+ ElementEdit ee = new ElementEdit(lineMap, index, relems, aelems);
+ chng.addEdit(ee);
+ lineMap.replace(index, relems.length, aelems);
+ }
+ if (Utilities.isComposedTextAttributeDefined(attr)) {
+ insertComposedTextUpdate(chng, attr);
+ }
+ } catch (BadLocationException e) {
+ throw new Error("Internal error: " + e.toString());
+ }
+ super.insertUpdate(chng, attr);
+ }
+
+ /**
+ * Updates any document structure as a result of text removal.
+ * This will happen within a write lock. Since the structure
+ * represents a line map, this just checks to see if the
+ * removal spans lines. If it does, the two lines outside
+ * of the removal area are joined together.
+ *
+ * @param chng the change event describing the edit
+ */
+ protected void removeUpdate(DefaultDocumentEvent chng) {
+ removed.removeAllElements();
+ BranchElement map = (BranchElement) getDefaultRootElement();
+ int offset = chng.getOffset();
+ int length = chng.getLength();
+ int line0 = map.getElementIndex(offset);
+ int line1 = map.getElementIndex(offset + length);
+ if (line0 != line1) {
+ // a line was removed
+ for (int i = line0; i <= line1; i++) {
+ removed.addElement(map.getElement(i));
+ }
+ int p0 = map.getElement(line0).getStartOffset();
+ int p1 = map.getElement(line1).getEndOffset();
+ Element[] aelems = new Element[1];
+ aelems[0] = createLeafElement(map, null, p0, p1);
+ Element[] relems = new Element[removed.size()];
+ removed.copyInto(relems);
+ ElementEdit ee = new ElementEdit(map, line0, relems, aelems);
+ chng.addEdit(ee);
+ map.replace(line0, relems.length, aelems);
+ } else {
+ //Check for the composed text element
+ Element line = map.getElement(line0);
+ if (!line.isLeaf()) {
+ Element leaf = line.getElement(line.getElementIndex(offset));
+ if (Utilities.isComposedTextElement(leaf)) {
+ Element[] aelem = new Element[1];
+ aelem[0] = createLeafElement(map, null,
+ line.getStartOffset(), line.getEndOffset());
+ Element[] relem = new Element[1];
+ relem[0] = line;
+ ElementEdit ee = new ElementEdit(map, line0, relem, aelem);
+ chng.addEdit(ee);
+ map.replace(line0, 1, aelem);
+ }
+ }
+ }
+ super.removeUpdate(chng);
+ }
+
+ //
+ // Inserts the composed text of an input method. The line element
+ // where the composed text is inserted into becomes an branch element
+ // which contains leaf elements of the composed text and the text
+ // backing store.
+ //
+ private void insertComposedTextUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
+ added.removeAllElements();
+ BranchElement lineMap = (BranchElement) getDefaultRootElement();
+ int offset = chng.getOffset();
+ int length = chng.getLength();
+ int index = lineMap.getElementIndex(offset);
+ Element elem = lineMap.getElement(index);
+ int elemStart = elem.getStartOffset();
+ int elemEnd = elem.getEndOffset();
+ BranchElement[] abelem = new BranchElement[1];
+ abelem[0] = (BranchElement) createBranchElement(lineMap, null);
+ Element[] relem = new Element[1];
+ relem[0] = elem;
+ if (elemStart != offset)
+ added.addElement(createLeafElement(abelem[0], null, elemStart, offset));
+ added.addElement(createLeafElement(abelem[0], attr, offset, offset+length));
+ if (elemEnd != offset+length)
+ added.addElement(createLeafElement(abelem[0], null, offset+length, elemEnd));
+ Element[] alelem = new Element[added.size()];
+ added.copyInto(alelem);
+ ElementEdit ee = new ElementEdit(lineMap, index, relem, abelem);
+ chng.addEdit(ee);
+
+ abelem[0].replace(0, 0, alelem);
+ lineMap.replace(index, 1, abelem);
+ }
+
+ private AbstractElement defaultRoot;
+ private Vector added = new Vector(); // Vector<Element>
+ private Vector removed = new Vector(); // Vector<Element>
+ private transient Segment s;
+}
diff --git a/src/share/classes/javax/swing/text/PlainView.java b/src/share/classes/javax/swing/text/PlainView.java
new file mode 100644
index 000000000..a35fa5164
--- /dev/null
+++ b/src/share/classes/javax/swing/text/PlainView.java
@@ -0,0 +1,715 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import java.util.Properties;
+import java.awt.*;
+import javax.swing.event.*;
+
+/**
+ * Implements View interface for a simple multi-line text view
+ * that has text in one font and color. The view represents each
+ * child element as a line of text.
+ *
+ * @author Timothy Prinzing
+ * @see View
+ */
+public class PlainView extends View implements TabExpander {
+
+ /**
+ * Constructs a new PlainView wrapped on an element.
+ *
+ * @param elem the element
+ */
+ public PlainView(Element elem) {
+ super(elem);
+ }
+
+ /**
+ * Returns the tab size set for the document, defaulting to 8.
+ *
+ * @return the tab size
+ */
+ protected int getTabSize() {
+ Integer i = (Integer) getDocument().getProperty(PlainDocument.tabSizeAttribute);
+ int size = (i != null) ? i.intValue() : 8;
+ return size;
+ }
+
+ /**
+ * Renders a line of text, suppressing whitespace at the end
+ * and expanding any tabs. This is implemented to make calls
+ * to the methods <code>drawUnselectedText</code> and
+ * <code>drawSelectedText</code> so that the way selected and
+ * unselected text are rendered can be customized.
+ *
+ * @param lineIndex the line to draw >= 0
+ * @param g the <code>Graphics</code> context
+ * @param x the starting X position >= 0
+ * @param y the starting Y position >= 0
+ * @see #drawUnselectedText
+ * @see #drawSelectedText
+ */
+ protected void drawLine(int lineIndex, Graphics g, int x, int y) {
+ Element line = getElement().getElement(lineIndex);
+ Element elem;
+
+ try {
+ if (line.isLeaf()) {
+ drawElement(lineIndex, line, g, x, y);
+ } else {
+ // this line contains the composed text.
+ int count = line.getElementCount();
+ for(int i = 0; i < count; i++) {
+ elem = line.getElement(i);
+ x = drawElement(lineIndex, elem, g, x, y);
+ }
+ }
+ } catch (BadLocationException e) {
+ throw new StateInvariantError("Can't render line: " + lineIndex);
+ }
+ }
+
+ private int drawElement(int lineIndex, Element elem, Graphics g, int x, int y) throws BadLocationException {
+ int p0 = elem.getStartOffset();
+ int p1 = elem.getEndOffset();
+ p1 = Math.min(getDocument().getLength(), p1);
+
+ if (lineIndex == 0) {
+ x += firstLineOffset;
+ }
+ AttributeSet attr = elem.getAttributes();
+ if (Utilities.isComposedTextAttributeDefined(attr)) {
+ g.setColor(unselected);
+ x = Utilities.drawComposedText(this, attr, g, x, y,
+ p0-elem.getStartOffset(),
+ p1-elem.getStartOffset());
+ } else {
+ if (sel0 == sel1 || selected == unselected) {
+ // no selection, or it is invisible
+ x = drawUnselectedText(g, x, y, p0, p1);
+ } else if ((p0 >= sel0 && p0 <= sel1) && (p1 >= sel0 && p1 <= sel1)) {
+ x = drawSelectedText(g, x, y, p0, p1);
+ } else if (sel0 >= p0 && sel0 <= p1) {
+ if (sel1 >= p0 && sel1 <= p1) {
+ x = drawUnselectedText(g, x, y, p0, sel0);
+ x = drawSelectedText(g, x, y, sel0, sel1);
+ x = drawUnselectedText(g, x, y, sel1, p1);
+ } else {
+ x = drawUnselectedText(g, x, y, p0, sel0);
+ x = drawSelectedText(g, x, y, sel0, p1);
+ }
+ } else if (sel1 >= p0 && sel1 <= p1) {
+ x = drawSelectedText(g, x, y, p0, sel1);
+ x = drawUnselectedText(g, x, y, sel1, p1);
+ } else {
+ x = drawUnselectedText(g, x, y, p0, p1);
+ }
+ }
+
+ return x;
+ }
+
+ /**
+ * Renders the given range in the model as normal unselected
+ * text. Uses the foreground or disabled color to render the text.
+ *
+ * @param g the graphics context
+ * @param x the starting X coordinate >= 0
+ * @param y the starting Y coordinate >= 0
+ * @param p0 the beginning position in the model >= 0
+ * @param p1 the ending position in the model >= 0
+ * @return the X location of the end of the range >= 0
+ * @exception BadLocationException if the range is invalid
+ */
+ protected int drawUnselectedText(Graphics g, int x, int y,
+ int p0, int p1) throws BadLocationException {
+ g.setColor(unselected);
+ Document doc = getDocument();
+ Segment s = SegmentCache.getSharedSegment();
+ doc.getText(p0, p1 - p0, s);
+ int ret = Utilities.drawTabbedText(this, s, x, y, g, this, p0);
+ SegmentCache.releaseSharedSegment(s);
+ return ret;
+ }
+
+ /**
+ * Renders the given range in the model as selected text. This
+ * is implemented to render the text in the color specified in
+ * the hosting component. It assumes the highlighter will render
+ * the selected background.
+ *
+ * @param g the graphics context
+ * @param x the starting X coordinate >= 0
+ * @param y the starting Y coordinate >= 0
+ * @param p0 the beginning position in the model >= 0
+ * @param p1 the ending position in the model >= 0
+ * @return the location of the end of the range
+ * @exception BadLocationException if the range is invalid
+ */
+ protected int drawSelectedText(Graphics g, int x,
+ int y, int p0, int p1) throws BadLocationException {
+ g.setColor(selected);
+ Document doc = getDocument();
+ Segment s = SegmentCache.getSharedSegment();
+ doc.getText(p0, p1 - p0, s);
+ int ret = Utilities.drawTabbedText(this, s, x, y, g, this, p0);
+ SegmentCache.releaseSharedSegment(s);
+ return ret;
+ }
+
+ /**
+ * Gives access to a buffer that can be used to fetch
+ * text from the associated document.
+ *
+ * @return the buffer
+ */
+ protected final Segment getLineBuffer() {
+ if (lineBuffer == null) {
+ lineBuffer = new Segment();
+ }
+ return lineBuffer;
+ }
+
+ /**
+ * Checks to see if the font metrics and longest line
+ * are up-to-date.
+ *
+ * @since 1.4
+ */
+ protected void updateMetrics() {
+ Component host = getContainer();
+ Font f = host.getFont();
+ if (font != f) {
+ // The font changed, we need to recalculate the
+ // longest line.
+ calculateLongestLine();
+ tabSize = getTabSize() * metrics.charWidth('m');
+ }
+ }
+
+ // ---- View methods ----------------------------------------------------
+
+ /**
+ * 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
+ */
+ public float getPreferredSpan(int axis) {
+ updateMetrics();
+ switch (axis) {
+ case View.X_AXIS:
+ return getLineWidth(longLine);
+ case View.Y_AXIS:
+ return getElement().getElementCount() * metrics.getHeight();
+ default:
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ }
+
+ /**
+ * Renders using the given rendering surface and area on that surface.
+ * The view may need to do layout and create child views to enable
+ * itself to render into the given allocation.
+ *
+ * @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) {
+ Shape originalA = a;
+ a = adjustPaintRegion(a);
+ Rectangle alloc = (Rectangle) a;
+ tabBase = alloc.x;
+ JTextComponent host = (JTextComponent) getContainer();
+ Highlighter h = host.getHighlighter();
+ g.setFont(host.getFont());
+ sel0 = host.getSelectionStart();
+ sel1 = host.getSelectionEnd();
+ unselected = (host.isEnabled()) ?
+ host.getForeground() : host.getDisabledTextColor();
+ Caret c = host.getCaret();
+ selected = c.isSelectionVisible() && h != null ?
+ host.getSelectedTextColor() : unselected;
+ updateMetrics();
+
+ // If the lines are clipped then we don't expend the effort to
+ // try and paint them. Since all of the lines are the same height
+ // with this object, determination of what lines need to be repainted
+ // is quick.
+ Rectangle clip = g.getClipBounds();
+ int fontHeight = metrics.getHeight();
+ int heightBelow = (alloc.y + alloc.height) - (clip.y + clip.height);
+ int heightAbove = clip.y - alloc.y;
+ int linesBelow, linesAbove, linesTotal;
+
+ if (fontHeight > 0) {
+ linesBelow = Math.max(0, heightBelow / fontHeight);
+ linesAbove = Math.max(0, heightAbove / fontHeight);
+ linesTotal = alloc.height / fontHeight;
+ if (alloc.height % fontHeight != 0) {
+ linesTotal++;
+ }
+ } else {
+ linesBelow = linesAbove = linesTotal = 0;
+ }
+
+ // update the visible lines
+ Rectangle lineArea = lineToRect(a, linesAbove);
+ int y = lineArea.y + metrics.getAscent();
+ int x = lineArea.x;
+ Element map = getElement();
+ int lineCount = map.getElementCount();
+ int endLine = Math.min(lineCount, linesTotal - linesBelow);
+ lineCount--;
+ LayeredHighlighter dh = (h instanceof LayeredHighlighter) ?
+ (LayeredHighlighter)h : null;
+ for (int line = linesAbove; line < endLine; line++) {
+ if (dh != null) {
+ Element lineElement = map.getElement(line);
+ if (line == lineCount) {
+ dh.paintLayeredHighlights(g, lineElement.getStartOffset(),
+ lineElement.getEndOffset(),
+ originalA, host, this);
+ }
+ else {
+ dh.paintLayeredHighlights(g, lineElement.getStartOffset(),
+ lineElement.getEndOffset() - 1,
+ originalA, host, this);
+ }
+ }
+ drawLine(line, g, x, y);
+ y += fontHeight;
+ if (line == 0) {
+ // This should never really happen, in so far as if
+ // firstLineOffset is non 0, there should only be one
+ // line of text.
+ x -= firstLineOffset;
+ }
+ }
+ }
+
+ /**
+ * Should return a shape ideal for painting based on the passed in
+ * Shape <code>a</code>. This is useful if painting in a different
+ * region. The default implementation returns <code>a</code>.
+ */
+ Shape adjustPaintRegion(Shape a) {
+ return a;
+ }
+
+ /**
+ * 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 >= 0
+ * @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 {
+ // line coordinates
+ Document doc = getDocument();
+ Element map = getElement();
+ int lineIndex = map.getElementIndex(pos);
+ if (lineIndex < 0) {
+ return lineToRect(a, 0);
+ }
+ Rectangle lineArea = lineToRect(a, lineIndex);
+
+ // determine span from the start of the line
+ tabBase = lineArea.x;
+ Element line = map.getElement(lineIndex);
+ int p0 = line.getStartOffset();
+ Segment s = SegmentCache.getSharedSegment();
+ doc.getText(p0, pos - p0, s);
+ int xOffs = Utilities.getTabbedTextWidth(s, metrics, tabBase, this,p0);
+ SegmentCache.releaseSharedSegment(s);
+
+ // fill in the results and return
+ lineArea.x += xOffs;
+ lineArea.width = 1;
+ lineArea.height = metrics.getHeight();
+ return lineArea;
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param fx the X coordinate >= 0
+ * @param fy the Y coordinate >= 0
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents the
+ * given point in the view >= 0
+ * @see View#viewToModel
+ */
+ public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
+ // PENDING(prinz) properly calculate bias
+ bias[0] = Position.Bias.Forward;
+
+ Rectangle alloc = a.getBounds();
+ Document doc = getDocument();
+ int x = (int) fx;
+ int y = (int) fy;
+ if (y < alloc.y) {
+ // above the area covered by this icon, so the the position
+ // is assumed to be the start of the coverage for this view.
+ return getStartOffset();
+ } else if (y > alloc.y + alloc.height) {
+ // below the area covered by this icon, so the the position
+ // is assumed to be the end of the coverage for this view.
+ return getEndOffset() - 1;
+ } else {
+ // positioned within the coverage of this view vertically,
+ // so we figure out which line the point corresponds to.
+ // if the line is greater than the number of lines contained, then
+ // simply use the last line as it represents the last possible place
+ // we can position to.
+ Element map = doc.getDefaultRootElement();
+ int fontHeight = metrics.getHeight();
+ int lineIndex = (fontHeight > 0 ?
+ Math.abs((y - alloc.y) / fontHeight) :
+ map.getElementCount() - 1);
+ if (lineIndex >= map.getElementCount()) {
+ return getEndOffset() - 1;
+ }
+ Element line = map.getElement(lineIndex);
+ int dx = 0;
+ if (lineIndex == 0) {
+ alloc.x += firstLineOffset;
+ alloc.width -= firstLineOffset;
+ }
+ if (x < alloc.x) {
+ // point is to the left of the line
+ return line.getStartOffset();
+ } else if (x > alloc.x + alloc.width) {
+ // point is to the right of the line
+ return line.getEndOffset() - 1;
+ } else {
+ // Determine the offset into the text
+ try {
+ int p0 = line.getStartOffset();
+ int p1 = line.getEndOffset() - 1;
+ Segment s = SegmentCache.getSharedSegment();
+ doc.getText(p0, p1 - p0, s);
+ tabBase = alloc.x;
+ int offs = p0 + Utilities.getTabbedTextOffset(s, metrics,
+ tabBase, x, this, p0);
+ SegmentCache.releaseSharedSegment(s);
+ return offs;
+ } catch (BadLocationException e) {
+ // should not happen
+ return -1;
+ }
+ }
+ }
+ }
+
+ /**
+ * Gives notification that something was inserted into the document
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ updateDamage(changes, a, f);
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ updateDamage(changes, a, f);
+ }
+
+ /**
+ * Gives notification from the document that attributes were changed
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ updateDamage(changes, a, f);
+ }
+
+ /**
+ * Sets the size of the view. This should cause
+ * layout of the view along the given axis, if it
+ * has any layout duties.
+ *
+ * @param width the width >= 0
+ * @param height the height >= 0
+ */
+ public void setSize(float width, float height) {
+ super.setSize(width, height);
+ updateMetrics();
+ }
+
+ // --- TabExpander methods ------------------------------------------
+
+ /**
+ * Returns the next tab stop position after a given reference position.
+ * This implementation does not support things like centering so it
+ * ignores the tabOffset argument.
+ *
+ * @param x the current position >= 0
+ * @param tabOffset the position within the text stream
+ * that the tab occurred at >= 0.
+ * @return the tab stop, measured in points >= 0
+ */
+ public float nextTabStop(float x, int tabOffset) {
+ if (tabSize == 0) {
+ return x;
+ }
+ int ntabs = (((int) x) - tabBase) / tabSize;
+ return tabBase + ((ntabs + 1) * tabSize);
+ }
+
+ // --- local methods ------------------------------------------------
+
+ /**
+ * Repaint the region of change covered by the given document
+ * event. Damages the line that begins the range to cover
+ * the case when the insert/remove is only on one line.
+ * If lines are added or removed, damages the whole
+ * view. The longest line is checked to see if it has
+ * changed.
+ *
+ * @since 1.4
+ */
+ protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f) {
+ Component host = getContainer();
+ updateMetrics();
+ Element elem = getElement();
+ DocumentEvent.ElementChange ec = changes.getChange(elem);
+
+ Element[] added = (ec != null) ? ec.getChildrenAdded() : null;
+ Element[] removed = (ec != null) ? ec.getChildrenRemoved() : null;
+ if (((added != null) && (added.length > 0)) ||
+ ((removed != null) && (removed.length > 0))) {
+ // lines were added or removed...
+ if (added != null) {
+ int currWide = getLineWidth(longLine);
+ for (int i = 0; i < added.length; i++) {
+ int w = getLineWidth(added[i]);
+ if (w > currWide) {
+ currWide = w;
+ longLine = added[i];
+ }
+ }
+ }
+ if (removed != null) {
+ for (int i = 0; i < removed.length; i++) {
+ if (removed[i] == longLine) {
+ calculateLongestLine();
+ break;
+ }
+ }
+ }
+ preferenceChanged(null, true, true);
+ host.repaint();
+ } else {
+ Element map = getElement();
+ int line = map.getElementIndex(changes.getOffset());
+ damageLineRange(line, line, a, host);
+ if (changes.getType() == DocumentEvent.EventType.INSERT) {
+ // check to see if the line is longer than current
+ // longest line.
+ int w = getLineWidth(longLine);
+ Element e = map.getElement(line);
+ if (e == longLine) {
+ preferenceChanged(null, true, false);
+ } else if (getLineWidth(e) > w) {
+ longLine = e;
+ preferenceChanged(null, true, false);
+ }
+ } else if (changes.getType() == DocumentEvent.EventType.REMOVE) {
+ if (map.getElement(line) == longLine) {
+ // removed from longest line... recalc
+ calculateLongestLine();
+ preferenceChanged(null, true, false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Repaint the given line range.
+ *
+ * @param host the component hosting the view (used to call repaint)
+ * @param a the region allocated for the view to render into
+ * @param line0 the starting line number to repaint. This must
+ * be a valid line number in the model.
+ * @param line1 the ending line number to repaint. This must
+ * be a valid line number in the model.
+ * @since 1.4
+ */
+ protected void damageLineRange(int line0, int line1, Shape a, Component host) {
+ if (a != null) {
+ Rectangle area0 = lineToRect(a, line0);
+ Rectangle area1 = lineToRect(a, line1);
+ if ((area0 != null) && (area1 != null)) {
+ Rectangle damage = area0.union(area1);
+ host.repaint(damage.x, damage.y, damage.width, damage.height);
+ } else {
+ host.repaint();
+ }
+ }
+ }
+
+ /**
+ * Determine the rectangle that represents the given line.
+ *
+ * @param a the region allocated for the view to render into
+ * @param line the line number to find the region of. This must
+ * be a valid line number in the model.
+ * @since 1.4
+ */
+ protected Rectangle lineToRect(Shape a, int line) {
+ Rectangle r = null;
+ updateMetrics();
+ if (metrics != null) {
+ Rectangle alloc = a.getBounds();
+ if (line == 0) {
+ alloc.x += firstLineOffset;
+ alloc.width -= firstLineOffset;
+ }
+ r = new Rectangle(alloc.x, alloc.y + (line * metrics.getHeight()),
+ alloc.width, metrics.getHeight());
+ }
+ return r;
+ }
+
+ /**
+ * Iterate over the lines represented by the child elements
+ * of the element this view represents, looking for the line
+ * that is the longest. The <em>longLine</em> variable is updated to
+ * represent the longest line contained. The <em>font</em> variable
+ * is updated to indicate the font used to calculate the
+ * longest line.
+ */
+ private void calculateLongestLine() {
+ Component c = getContainer();
+ font = c.getFont();
+ metrics = c.getFontMetrics(font);
+ Document doc = getDocument();
+ Element lines = getElement();
+ int n = lines.getElementCount();
+ int maxWidth = -1;
+ for (int i = 0; i < n; i++) {
+ Element line = lines.getElement(i);
+ int w = getLineWidth(line);
+ if (w > maxWidth) {
+ maxWidth = w;
+ longLine = line;
+ }
+ }
+ }
+
+ /**
+ * Calculate the width of the line represented by
+ * the given element. It is assumed that the font
+ * and font metrics are up-to-date.
+ */
+ private int getLineWidth(Element line) {
+ if (line == null) {
+ return 0;
+ }
+ int p0 = line.getStartOffset();
+ int p1 = line.getEndOffset();
+ int w;
+ Segment s = SegmentCache.getSharedSegment();
+ try {
+ line.getDocument().getText(p0, p1 - p0, s);
+ w = Utilities.getTabbedTextWidth(s, metrics, tabBase, this, p0);
+ } catch (BadLocationException ble) {
+ w = 0;
+ }
+ SegmentCache.releaseSharedSegment(s);
+ return w;
+ }
+
+ // --- member variables -----------------------------------------------
+
+ /**
+ * Font metrics for the current font.
+ */
+ protected FontMetrics metrics;
+
+ /**
+ * The current longest line. This is used to calculate
+ * the preferred width of the view. Since the calculation
+ * is potentially expensive we try to avoid it by stashing
+ * which line is currently the longest.
+ */
+ Element longLine;
+
+ /**
+ * Font used to calculate the longest line... if this
+ * changes we need to recalculate the longest line
+ */
+ Font font;
+
+ Segment lineBuffer;
+ int tabSize;
+ int tabBase;
+
+ int sel0;
+ int sel1;
+ Color unselected;
+ Color selected;
+
+ /**
+ * Offset of where to draw the first character on the first line.
+ * This is a hack and temporary until we can better address the problem
+ * of text measuring. This field is actually never set directly in
+ * PlainView, but by FieldView.
+ */
+ int firstLineOffset;
+
+}
diff --git a/src/share/classes/javax/swing/text/Position.java b/src/share/classes/javax/swing/text/Position.java
new file mode 100644
index 000000000..4ffcb1379
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Position.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+/**
+ * Represents a location within a document. It is intended to abstract away
+ * implementation details of the document and enable specification of
+ * positions within the document that are capable of tracking of change as
+ * the document is edited.
+ * <p>
+ * A {@code Position} object points at a location between two characters.
+ * As the surrounding content is altered, the {@code Position} object
+ * adjusts its offset automatically to reflect the changes. If content is
+ * inserted or removed before the {@code Position} object's location, then the
+ * {@code Position} increments or decrements its offset, respectively,
+ * so as to point to the same location. If a portion of the document is removed
+ * that contains a {@code Position}'s offset, then the {@code Position}'s
+ * offset becomes that of the beginning of the removed region. For example, if
+ * a {@code Position} has an offset of 5 and the region 2-10 is removed, then
+ * the {@code Position}'s offset becomes 2.
+ * <p>
+ * {@code Position} with an offset of 0 is a special case. It never changes its
+ * offset while document content is altered.
+ *
+ * @author Timothy Prinzing
+ */
+public interface Position {
+
+ /**
+ * Fetches the current offset within the document.
+ *
+ * @return the offset >= 0
+ */
+ public int getOffset();
+
+ /**
+ * A typesafe enumeration to indicate bias to a position
+ * in the model. A position indicates a location between
+ * two characters. The bias can be used to indicate an
+ * interest toward one of the two sides of the position
+ * in boundary conditions where a simple offset is
+ * ambiguous.
+ */
+ public static final class Bias {
+
+ /**
+ * Indicates to bias toward the next character
+ * in the model.
+ */
+ public static final Bias Forward = new Bias("Forward");
+
+ /**
+ * Indicates a bias toward the previous character
+ * in the model.
+ */
+ public static final Bias Backward = new Bias("Backward");
+
+ /**
+ * string representation
+ */
+ public String toString() {
+ return name;
+ }
+
+ private Bias(String name) {
+ this.name = name;
+ }
+
+ private String name;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/Segment.java b/src/share/classes/javax/swing/text/Segment.java
new file mode 100644
index 000000000..0186c900d
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Segment.java
@@ -0,0 +1,316 @@
+/*
+ * 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;
+
+import java.text.CharacterIterator;
+
+/**
+ * A segment of a character array representing a fragment
+ * of text. It should be treated as immutable even though
+ * the array is directly accessible. This gives fast access
+ * to fragments of text without the overhead of copying
+ * around characters. This is effectively an unprotected
+ * String.
+ * <p>
+ * The Segment implements the java.text.CharacterIterator
+ * interface to support use with the i18n support without
+ * copying text into a string.
+ *
+ * @author Timothy Prinzing
+ */
+public class Segment implements Cloneable, CharacterIterator, CharSequence {
+
+ /**
+ * This is the array containing the text of
+ * interest. This array should never be modified;
+ * it is available only for efficiency.
+ */
+ public char[] array;
+
+ /**
+ * This is the offset into the array that
+ * the desired text begins.
+ */
+ public int offset;
+
+ /**
+ * This is the number of array elements that
+ * make up the text of interest.
+ */
+ public int count;
+
+ private boolean partialReturn;
+
+ /**
+ * Creates a new segment.
+ */
+ public Segment() {
+ this(null, 0, 0);
+ }
+
+ /**
+ * Creates a new segment referring to an existing array.
+ *
+ * @param array the array to refer to
+ * @param offset the offset into the array
+ * @param count the number of characters
+ */
+ public Segment(char[] array, int offset, int count) {
+ this.array = array;
+ this.offset = offset;
+ this.count = count;
+ partialReturn = false;
+ }
+
+ /**
+ * Flag to indicate that partial returns are valid. If the flag is true,
+ * an implementation of the interface method Document.getText(position,length,Segment)
+ * should return as much text as possible without making a copy. The default
+ * state of the flag is false which will cause Document.getText(position,length,Segment)
+ * to provide the same return behavior it always had, which may or may not
+ * make a copy of the text depending upon the request.
+ *
+ * @param p whether or not partial returns are valid.
+ * @since 1.4
+ */
+ public void setPartialReturn(boolean p) {
+ partialReturn = p;
+ }
+
+ /**
+ * Flag to indicate that partial returns are valid.
+ *
+ * @return whether or not partial returns are valid.
+ * @since 1.4
+ */
+ public boolean isPartialReturn() {
+ return partialReturn;
+ }
+
+ /**
+ * Converts a segment into a String.
+ *
+ * @return the string
+ */
+ public String toString() {
+ if (array != null) {
+ return new String(array, offset, count);
+ }
+ return new String();
+ }
+
+ // --- CharacterIterator methods -------------------------------------
+
+ /**
+ * Sets the position to getBeginIndex() and returns the character at that
+ * position.
+ * @return the first character in the text, or DONE if the text is empty
+ * @see #getBeginIndex
+ * @since 1.3
+ */
+ public char first() {
+ pos = offset;
+ if (count != 0) {
+ return array[pos];
+ }
+ return DONE;
+ }
+
+ /**
+ * Sets the position to getEndIndex()-1 (getEndIndex() if the text is empty)
+ * and returns the character at that position.
+ * @return the last character in the text, or DONE if the text is empty
+ * @see #getEndIndex
+ * @since 1.3
+ */
+ public char last() {
+ pos = offset + count;
+ if (count != 0) {
+ pos -= 1;
+ return array[pos];
+ }
+ return DONE;
+ }
+
+ /**
+ * Gets the character at the current position (as returned by getIndex()).
+ * @return the character at the current position or DONE if the current
+ * position is off the end of the text.
+ * @see #getIndex
+ * @since 1.3
+ */
+ public char current() {
+ if (count != 0 && pos < offset + count) {
+ return array[pos];
+ }
+ return DONE;
+ }
+
+ /**
+ * Increments the iterator's index by one and returns the character
+ * at the new index. If the resulting index is greater or equal
+ * to getEndIndex(), the current index is reset to getEndIndex() and
+ * a value of DONE is returned.
+ * @return the character at the new position or DONE if the new
+ * position is off the end of the text range.
+ * @since 1.3
+ */
+ public char next() {
+ pos += 1;
+ int end = offset + count;
+ if (pos >= end) {
+ pos = end;
+ return DONE;
+ }
+ return current();
+ }
+
+ /**
+ * Decrements the iterator's index by one and returns the character
+ * at the new index. If the current index is getBeginIndex(), the index
+ * remains at getBeginIndex() and a value of DONE is returned.
+ * @return the character at the new position or DONE if the current
+ * position is equal to getBeginIndex().
+ * @since 1.3
+ */
+ public char previous() {
+ if (pos == offset) {
+ return DONE;
+ }
+ pos -= 1;
+ return current();
+ }
+
+ /**
+ * Sets the position to the specified position in the text and returns that
+ * character.
+ * @param position the position within the text. Valid values range from
+ * getBeginIndex() to getEndIndex(). An IllegalArgumentException is thrown
+ * if an invalid value is supplied.
+ * @return the character at the specified position or DONE if the specified position is equal to getEndIndex()
+ * @since 1.3
+ */
+ public char setIndex(int position) {
+ int end = offset + count;
+ if ((position < offset) || (position > end)) {
+ throw new IllegalArgumentException("bad position: " + position);
+ }
+ pos = position;
+ if ((pos != end) && (count != 0)) {
+ return array[pos];
+ }
+ return DONE;
+ }
+
+ /**
+ * Returns the start index of the text.
+ * @return the index at which the text begins.
+ * @since 1.3
+ */
+ public int getBeginIndex() {
+ return offset;
+ }
+
+ /**
+ * Returns the end index of the text. This index is the index of the first
+ * character following the end of the text.
+ * @return the index after the last character in the text
+ * @since 1.3
+ */
+ public int getEndIndex() {
+ return offset + count;
+ }
+
+ /**
+ * Returns the current index.
+ * @return the current index.
+ * @since 1.3
+ */
+ public int getIndex() {
+ return pos;
+ }
+
+ // --- CharSequence methods -------------------------------------
+
+ /**
+ * {@inheritDoc}
+ * @since 1.6
+ */
+ public char charAt(int index) {
+ if (index < 0
+ || index >= count) {
+ throw new StringIndexOutOfBoundsException(index);
+ }
+ return array[offset + index];
+ }
+
+ /**
+ * {@inheritDoc}
+ * @since 1.6
+ */
+ public int length() {
+ return count;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @since 1.6
+ */
+ public CharSequence subSequence(int start, int end) {
+ if (start < 0) {
+ throw new StringIndexOutOfBoundsException(start);
+ }
+ if (end > count) {
+ throw new StringIndexOutOfBoundsException(end);
+ }
+ if (start > end) {
+ throw new StringIndexOutOfBoundsException(end - start);
+ }
+ Segment segment = new Segment();
+ segment.array = this.array;
+ segment.offset = this.offset + start;
+ segment.count = end - start;
+ return segment;
+ }
+
+ /**
+ * Creates a shallow copy.
+ *
+ * @return the copy
+ */
+ public Object clone() {
+ Object o;
+ try {
+ o = super.clone();
+ } catch (CloneNotSupportedException cnse) {
+ o = null;
+ }
+ return o;
+ }
+
+ private int pos;
+
+
+}
diff --git a/src/share/classes/javax/swing/text/SegmentCache.java b/src/share/classes/javax/swing/text/SegmentCache.java
new file mode 100644
index 000000000..a2bf89f91
--- /dev/null
+++ b/src/share/classes/javax/swing/text/SegmentCache.java
@@ -0,0 +1,127 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * SegmentCache caches <code>Segment</code>s to avoid continually creating
+ * and destroying of <code>Segment</code>s. A common use of this class would
+ * be:
+ * <pre>
+ * Segment segment = segmentCache.getSegment();
+ * // do something with segment
+ * ...
+ * segmentCache.releaseSegment(segment);
+ * </pre>
+ *
+ */
+class SegmentCache {
+ /**
+ * A global cache.
+ */
+ private static SegmentCache sharedCache = new SegmentCache();
+
+ /**
+ * A list of the currently unused Segments.
+ */
+ private List segments;
+
+
+ /**
+ * Returns the shared SegmentCache.
+ */
+ public static SegmentCache getSharedInstance() {
+ return sharedCache;
+ }
+
+ /**
+ * A convenience method to get a Segment from the shared
+ * <code>SegmentCache</code>.
+ */
+ public static Segment getSharedSegment() {
+ return getSharedInstance().getSegment();
+ }
+
+ /**
+ * A convenience method to release a Segment to the shared
+ * <code>SegmentCache</code>.
+ */
+ public static void releaseSharedSegment(Segment segment) {
+ getSharedInstance().releaseSegment(segment);
+ }
+
+
+
+ /**
+ * Creates and returns a SegmentCache.
+ */
+ public SegmentCache() {
+ segments = new ArrayList(11);
+ }
+
+ /**
+ * Returns a <code>Segment</code>. When done, the <code>Segment</code>
+ * should be recycled by invoking <code>releaseSegment</code>.
+ */
+ public Segment getSegment() {
+ synchronized(this) {
+ int size = segments.size();
+
+ if (size > 0) {
+ return (Segment)segments.remove(size - 1);
+ }
+ }
+ return new CachedSegment();
+ }
+
+ /**
+ * Releases a Segment. You should not use a Segment after you release it,
+ * and you should NEVER release the same Segment more than once, eg:
+ * <pre>
+ * segmentCache.releaseSegment(segment);
+ * segmentCache.releaseSegment(segment);
+ * </pre>
+ * Will likely result in very bad things happening!
+ */
+ public void releaseSegment(Segment segment) {
+ if (segment instanceof CachedSegment) {
+ synchronized(this) {
+ segment.array = null;
+ segment.count = 0;
+ segments.add(segment);
+ }
+ }
+ }
+
+
+ /**
+ * CachedSegment is used as a tagging interface to determine if
+ * a Segment can successfully be shared.
+ */
+ private static class CachedSegment extends Segment {
+ }
+}
diff --git a/src/share/classes/javax/swing/text/SimpleAttributeSet.java b/src/share/classes/javax/swing/text/SimpleAttributeSet.java
new file mode 100644
index 000000000..b659dcf50
--- /dev/null
+++ b/src/share/classes/javax/swing/text/SimpleAttributeSet.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 1997-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;
+
+import java.util.Hashtable;
+import java.util.Enumeration;
+import java.util.Collections;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+/**
+ * A straightforward implementation of MutableAttributeSet using a
+ * hash table.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Tim Prinzing
+ */
+public class SimpleAttributeSet implements MutableAttributeSet, Serializable, Cloneable
+{
+ private static final long serialVersionUID = -6631553454711782652L;
+
+ /**
+ * An empty attribute set.
+ */
+ public static final AttributeSet EMPTY = new EmptyAttributeSet();
+
+ private transient Hashtable table = new Hashtable(3);
+
+ /**
+ * Creates a new attribute set.
+ */
+ public SimpleAttributeSet() {
+ }
+
+ /**
+ * Creates a new attribute set based on a supplied set of attributes.
+ *
+ * @param source the set of attributes
+ */
+ public SimpleAttributeSet(AttributeSet source) {
+ addAttributes(source);
+ }
+
+ private SimpleAttributeSet(Hashtable table) {
+ this.table = table;
+ }
+
+ /**
+ * Checks whether the set of attributes is empty.
+ *
+ * @return true if the set is empty else false
+ */
+ public boolean isEmpty()
+ {
+ return table.isEmpty();
+ }
+
+ /**
+ * Gets a count of the number of attributes.
+ *
+ * @return the count
+ */
+ public int getAttributeCount() {
+ return table.size();
+ }
+
+ /**
+ * Tells whether a given attribute is defined.
+ *
+ * @param attrName the attribute name
+ * @return true if the attribute is defined
+ */
+ public boolean isDefined(Object attrName) {
+ return table.containsKey(attrName);
+ }
+
+ /**
+ * Compares two attribute sets.
+ *
+ * @param attr the second attribute set
+ * @return true if the sets are equal, false otherwise
+ */
+ public boolean isEqual(AttributeSet attr) {
+ return ((getAttributeCount() == attr.getAttributeCount()) &&
+ containsAttributes(attr));
+ }
+
+ /**
+ * Makes a copy of the attributes.
+ *
+ * @return the copy
+ */
+ public AttributeSet copyAttributes() {
+ return (AttributeSet) clone();
+ }
+
+ /**
+ * Gets the names of the attributes in the set.
+ *
+ * @return the names as an <code>Enumeration</code>
+ */
+ public Enumeration<?> getAttributeNames() {
+ return table.keys();
+ }
+
+ /**
+ * Gets the value of an attribute.
+ *
+ * @param name the attribute name
+ * @return the value
+ */
+ public Object getAttribute(Object name) {
+ Object value = table.get(name);
+ if (value == null) {
+ AttributeSet parent = getResolveParent();
+ if (parent != null) {
+ value = parent.getAttribute(name);
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Checks whether the attribute list contains a
+ * specified attribute name/value pair.
+ *
+ * @param name the name
+ * @param value the value
+ * @return true if the name/value pair is in the list
+ */
+ public boolean containsAttribute(Object name, Object value) {
+ return value.equals(getAttribute(name));
+ }
+
+ /**
+ * Checks whether the attribute list contains all the
+ * specified name/value pairs.
+ *
+ * @param attributes the attribute list
+ * @return true if the list contains all the name/value pairs
+ */
+ public boolean containsAttributes(AttributeSet attributes) {
+ boolean result = true;
+
+ Enumeration names = attributes.getAttributeNames();
+ while (result && names.hasMoreElements()) {
+ Object name = names.nextElement();
+ result = attributes.getAttribute(name).equals(getAttribute(name));
+ }
+
+ return result;
+ }
+
+ /**
+ * Adds an attribute to the list.
+ *
+ * @param name the attribute name
+ * @param value the attribute value
+ */
+ public void addAttribute(Object name, Object value) {
+ table.put(name, value);
+ }
+
+ /**
+ * Adds a set of attributes to the list.
+ *
+ * @param attributes the set of attributes to add
+ */
+ public void addAttributes(AttributeSet attributes) {
+ Enumeration names = attributes.getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object name = names.nextElement();
+ addAttribute(name, attributes.getAttribute(name));
+ }
+ }
+
+ /**
+ * Removes an attribute from the list.
+ *
+ * @param name the attribute name
+ */
+ public void removeAttribute(Object name) {
+ table.remove(name);
+ }
+
+ /**
+ * Removes a set of attributes from the list.
+ *
+ * @param names the set of names to remove
+ */
+ public void removeAttributes(Enumeration<?> names) {
+ while (names.hasMoreElements())
+ removeAttribute(names.nextElement());
+ }
+
+ /**
+ * Removes a set of attributes from the list.
+ *
+ * @param attributes the set of attributes to remove
+ */
+ public void removeAttributes(AttributeSet attributes) {
+ if (attributes == this) {
+ table.clear();
+ }
+ else {
+ Enumeration names = attributes.getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object name = names.nextElement();
+ Object value = attributes.getAttribute(name);
+ if (value.equals(getAttribute(name)))
+ removeAttribute(name);
+ }
+ }
+ }
+
+ /**
+ * Gets the resolving parent. This is the set
+ * of attributes to resolve through if an attribute
+ * isn't defined locally. This is null if there
+ * are no other sets of attributes to resolve
+ * through.
+ *
+ * @return the parent
+ */
+ public AttributeSet getResolveParent() {
+ return (AttributeSet) table.get(StyleConstants.ResolveAttribute);
+ }
+
+ /**
+ * Sets the resolving parent.
+ *
+ * @param parent the parent
+ */
+ public void setResolveParent(AttributeSet parent) {
+ addAttribute(StyleConstants.ResolveAttribute, parent);
+ }
+
+ // --- Object methods ---------------------------------
+
+ /**
+ * Clones a set of attributes.
+ *
+ * @return the new set of attributes
+ */
+ public Object clone() {
+ SimpleAttributeSet attr;
+ try {
+ attr = (SimpleAttributeSet) super.clone();
+ attr.table = (Hashtable) table.clone();
+ } catch (CloneNotSupportedException cnse) {
+ attr = null;
+ }
+ return attr;
+ }
+
+ /**
+ * Returns a hashcode for this set of attributes.
+ * @return a hashcode value for this set of attributes.
+ */
+ public int hashCode() {
+ return table.hashCode();
+ }
+
+ /**
+ * Compares this object to the specified object.
+ * The result is <code>true</code> if the object is an equivalent
+ * set of attributes.
+ * @param obj the object to compare this attribute set with
+ * @return <code>true</code> if the objects are equal;
+ * <code>false</code> otherwise
+ */
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof AttributeSet) {
+ AttributeSet attrs = (AttributeSet) obj;
+ return isEqual(attrs);
+ }
+ return false;
+ }
+
+ /**
+ * Converts the attribute set to a String.
+ *
+ * @return the string
+ */
+ public String toString() {
+ String s = "";
+ Enumeration names = getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object key = names.nextElement();
+ Object value = getAttribute(key);
+ if (value instanceof AttributeSet) {
+ // don't go recursive
+ s = s + key + "=**AttributeSet** ";
+ } else {
+ s = s + key + "=" + value + " ";
+ }
+ }
+ return s;
+ }
+
+ private void writeObject(java.io.ObjectOutputStream s) throws IOException {
+ s.defaultWriteObject();
+ StyleContext.writeAttributeSet(s, this);
+ }
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException {
+ s.defaultReadObject();
+ table = new Hashtable(3);
+ StyleContext.readAttributeSet(s, this);
+ }
+
+ /**
+ * An AttributeSet that is always empty.
+ */
+ static class EmptyAttributeSet implements AttributeSet, Serializable {
+ static final long serialVersionUID = -8714803568785904228L;
+
+ public int getAttributeCount() {
+ return 0;
+ }
+ public boolean isDefined(Object attrName) {
+ return false;
+ }
+ public boolean isEqual(AttributeSet attr) {
+ return (attr.getAttributeCount() == 0);
+ }
+ public AttributeSet copyAttributes() {
+ return this;
+ }
+ public Object getAttribute(Object key) {
+ return null;
+ }
+ public Enumeration getAttributeNames() {
+ return Collections.emptyEnumeration();
+ }
+ public boolean containsAttribute(Object name, Object value) {
+ return false;
+ }
+ public boolean containsAttributes(AttributeSet attributes) {
+ return (attributes.getAttributeCount() == 0);
+ }
+ public AttributeSet getResolveParent() {
+ return null;
+ }
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ return ((obj instanceof AttributeSet) &&
+ (((AttributeSet)obj).getAttributeCount() == 0));
+ }
+ public int hashCode() {
+ return 0;
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/StateInvariantError.java b/src/share/classes/javax/swing/text/StateInvariantError.java
new file mode 100644
index 000000000..90417acb0
--- /dev/null
+++ b/src/share/classes/javax/swing/text/StateInvariantError.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 1997-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;
+
+/**
+ * This exception is to report the failure of state invarient
+ * assertion that was made. This indicates an internal error
+ * has occurred.
+ *
+ * @author Timothy Prinzing
+ */
+class StateInvariantError extends Error
+{
+ /**
+ * Creates a new StateInvariantFailure object.
+ *
+ * @param s a string indicating the assertion that failed
+ */
+ public StateInvariantError(String s) {
+ super(s);
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/StringContent.java b/src/share/classes/javax/swing/text/StringContent.java
new file mode 100644
index 000000000..d51ca95d2
--- /dev/null
+++ b/src/share/classes/javax/swing/text/StringContent.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright 1997-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;
+
+import java.util.Vector;
+import java.io.Serializable;
+import javax.swing.undo.*;
+import javax.swing.SwingUtilities;
+
+/**
+ * An implementation of the AbstractDocument.Content interface that is
+ * a brute force implementation that is useful for relatively small
+ * documents and/or debugging. It manages the character content
+ * as a simple character array. It is also quite inefficient.
+ * <p>
+ * It is generally recommended that the gap buffer or piece table
+ * implementations be used instead. This buffer does not scale up
+ * to large sizes.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Timothy Prinzing
+ */
+public final class StringContent implements AbstractDocument.Content, Serializable {
+
+ /**
+ * Creates a new StringContent object. Initial size defaults to 10.
+ */
+ public StringContent() {
+ this(10);
+ }
+
+ /**
+ * Creates a new StringContent object, with the initial
+ * size specified. If the length is < 1, a size of 1 is used.
+ *
+ * @param initialLength the initial size
+ */
+ public StringContent(int initialLength) {
+ if (initialLength < 1) {
+ initialLength = 1;
+ }
+ data = new char[initialLength];
+ data[0] = '\n';
+ count = 1;
+ }
+
+ /**
+ * Returns the length of the content.
+ *
+ * @return the length >= 1
+ * @see AbstractDocument.Content#length
+ */
+ public int length() {
+ return count;
+ }
+
+ /**
+ * Inserts a string into the content.
+ *
+ * @param where the starting position >= 0 && < length()
+ * @param str the non-null string to insert
+ * @return an UndoableEdit object for undoing
+ * @exception BadLocationException if the specified position is invalid
+ * @see AbstractDocument.Content#insertString
+ */
+ public UndoableEdit insertString(int where, String str) throws BadLocationException {
+ if (where >= count || where < 0) {
+ throw new BadLocationException("Invalid location", count);
+ }
+ char[] chars = str.toCharArray();
+ replace(where, 0, chars, 0, chars.length);
+ if (marks != null) {
+ updateMarksForInsert(where, str.length());
+ }
+ return new InsertUndo(where, str.length());
+ }
+
+ /**
+ * Removes part of the content. where + nitems must be < length().
+ *
+ * @param where the starting position >= 0
+ * @param nitems the number of characters to remove >= 0
+ * @return an UndoableEdit object for undoing
+ * @exception BadLocationException if the specified position is invalid
+ * @see AbstractDocument.Content#remove
+ */
+ public UndoableEdit remove(int where, int nitems) throws BadLocationException {
+ if (where + nitems >= count) {
+ throw new BadLocationException("Invalid range", count);
+ }
+ String removedString = getString(where, nitems);
+ UndoableEdit edit = new RemoveUndo(where, removedString);
+ replace(where, nitems, empty, 0, 0);
+ if (marks != null) {
+ updateMarksForRemove(where, nitems);
+ }
+ return edit;
+
+ }
+
+ /**
+ * Retrieves a portion of the content. where + len must be <= length().
+ *
+ * @param where the starting position >= 0
+ * @param len the length to retrieve >= 0
+ * @return a string representing the content; may be empty
+ * @exception BadLocationException if the specified position is invalid
+ * @see AbstractDocument.Content#getString
+ */
+ public String getString(int where, int len) throws BadLocationException {
+ if (where + len > count) {
+ throw new BadLocationException("Invalid range", count);
+ }
+ return new String(data, where, len);
+ }
+
+ /**
+ * Retrieves a portion of the content. where + len must be <= length()
+ *
+ * @param where the starting position >= 0
+ * @param len the number of characters to retrieve >= 0
+ * @param chars the Segment object to return the characters in
+ * @exception BadLocationException if the specified position is invalid
+ * @see AbstractDocument.Content#getChars
+ */
+ public void getChars(int where, int len, Segment chars) throws BadLocationException {
+ if (where + len > count) {
+ throw new BadLocationException("Invalid location", count);
+ }
+ chars.array = data;
+ chars.offset = where;
+ chars.count = len;
+ }
+
+ /**
+ * Creates a position within the content that will
+ * track change as the content is mutated.
+ *
+ * @param offset the offset to create a position for >= 0
+ * @return the position
+ * @exception BadLocationException if the specified position is invalid
+ */
+ public Position createPosition(int offset) throws BadLocationException {
+ // some small documents won't have any sticky positions
+ // at all, so the buffer is created lazily.
+ if (marks == null) {
+ marks = new Vector();
+ }
+ return new StickyPosition(offset);
+ }
+
+ // --- local methods ---------------------------------------
+
+ /**
+ * Replaces some of the characters in the array
+ * @param offset offset into the array to start the replace
+ * @param length number of characters to remove
+ * @param replArray replacement array
+ * @param replOffset offset into the replacement array
+ * @param replLength number of character to use from the
+ * replacement array.
+ */
+ void replace(int offset, int length,
+ char[] replArray, int replOffset, int replLength) {
+ int delta = replLength - length;
+ int src = offset + length;
+ int nmove = count - src;
+ int dest = src + delta;
+ if ((count + delta) >= data.length) {
+ // need to grow the array
+ int newLength = Math.max(2*data.length, count + delta);
+ char[] newData = new char[newLength];
+ System.arraycopy(data, 0, newData, 0, offset);
+ System.arraycopy(replArray, replOffset, newData, offset, replLength);
+ System.arraycopy(data, src, newData, dest, nmove);
+ data = newData;
+ } else {
+ // patch the existing array
+ System.arraycopy(data, src, data, dest, nmove);
+ System.arraycopy(replArray, replOffset, data, offset, replLength);
+ }
+ count = count + delta;
+ }
+
+ void resize(int ncount) {
+ char[] ndata = new char[ncount];
+ System.arraycopy(data, 0, ndata, 0, Math.min(ncount, count));
+ data = ndata;
+ }
+
+ synchronized void updateMarksForInsert(int offset, int length) {
+ if (offset == 0) {
+ // zero is a special case where we update only
+ // marks after it.
+ offset = 1;
+ }
+ int n = marks.size();
+ for (int i = 0; i < n; i++) {
+ PosRec mark = (PosRec) marks.elementAt(i);
+ if (mark.unused) {
+ // this record is no longer used, get rid of it
+ marks.removeElementAt(i);
+ i -= 1;
+ n -= 1;
+ } else if (mark.offset >= offset) {
+ mark.offset += length;
+ }
+ }
+ }
+
+ synchronized void updateMarksForRemove(int offset, int length) {
+ int n = marks.size();
+ for (int i = 0; i < n; i++) {
+ PosRec mark = (PosRec) marks.elementAt(i);
+ if (mark.unused) {
+ // this record is no longer used, get rid of it
+ marks.removeElementAt(i);
+ i -= 1;
+ n -= 1;
+ } else if (mark.offset >= (offset + length)) {
+ mark.offset -= length;
+ } else if (mark.offset >= offset) {
+ mark.offset = offset;
+ }
+ }
+ }
+
+ /**
+ * Returns a Vector containing instances of UndoPosRef for the
+ * Positions in the range
+ * <code>offset</code> to <code>offset</code> + <code>length</code>.
+ * If <code>v</code> is not null the matching Positions are placed in
+ * there. The vector with the resulting Positions are returned.
+ * <p>
+ * This is meant for internal usage, and is generally not of interest
+ * to subclasses.
+ *
+ * @param v the Vector to use, with a new one created on null
+ * @param offset the starting offset >= 0
+ * @param length the length >= 0
+ * @return the set of instances
+ */
+ protected Vector getPositionsInRange(Vector v, int offset,
+ int length) {
+ int n = marks.size();
+ int end = offset + length;
+ Vector placeIn = (v == null) ? new Vector() : v;
+ for (int i = 0; i < n; i++) {
+ PosRec mark = (PosRec) marks.elementAt(i);
+ if (mark.unused) {
+ // this record is no longer used, get rid of it
+ marks.removeElementAt(i);
+ i -= 1;
+ n -= 1;
+ } else if(mark.offset >= offset && mark.offset <= end)
+ placeIn.addElement(new UndoPosRef(mark));
+ }
+ return placeIn;
+ }
+
+ /**
+ * Resets the location for all the UndoPosRef instances
+ * in <code>positions</code>.
+ * <p>
+ * This is meant for internal usage, and is generally not of interest
+ * to subclasses.
+ *
+ * @param positions the positions of the instances
+ */
+ protected void updateUndoPositions(Vector positions) {
+ for(int counter = positions.size() - 1; counter >= 0; counter--) {
+ UndoPosRef ref = (UndoPosRef)positions.elementAt(counter);
+ // Check if the Position is still valid.
+ if(ref.rec.unused) {
+ positions.removeElementAt(counter);
+ }
+ else
+ ref.resetLocation();
+ }
+ }
+
+ private static final char[] empty = new char[0];
+ private char[] data;
+ private int count;
+ transient Vector marks;
+
+ /**
+ * holds the data for a mark... separately from
+ * the real mark so that the real mark can be
+ * collected if there are no more references to
+ * it.... the update table holds only a reference
+ * to this grungy thing.
+ */
+ final class PosRec {
+
+ PosRec(int offset) {
+ this.offset = offset;
+ }
+
+ int offset;
+ boolean unused;
+ }
+
+ /**
+ * This really wants to be a weak reference but
+ * in 1.1 we don't have a 100% pure solution for
+ * this... so this class trys to hack a solution
+ * to causing the marks to be collected.
+ */
+ final class StickyPosition implements Position {
+
+ StickyPosition(int offset) {
+ rec = new PosRec(offset);
+ marks.addElement(rec);
+ }
+
+ public int getOffset() {
+ return rec.offset;
+ }
+
+ protected void finalize() throws Throwable {
+ // schedule the record to be removed later
+ // on another thread.
+ rec.unused = true;
+ }
+
+ public String toString() {
+ return Integer.toString(getOffset());
+ }
+
+ PosRec rec;
+ }
+
+ /**
+ * Used to hold a reference to a Position that is being reset as the
+ * result of removing from the content.
+ */
+ final class UndoPosRef {
+ UndoPosRef(PosRec rec) {
+ this.rec = rec;
+ this.undoLocation = rec.offset;
+ }
+
+ /**
+ * Resets the location of the Position to the offset when the
+ * receiver was instantiated.
+ */
+ protected void resetLocation() {
+ rec.offset = undoLocation;
+ }
+
+ /** Location to reset to when resetLocatino is invoked. */
+ protected int undoLocation;
+ /** Position to reset offset. */
+ protected PosRec rec;
+ }
+
+ /**
+ * UnoableEdit created for inserts.
+ */
+ class InsertUndo extends AbstractUndoableEdit {
+ protected InsertUndo(int offset, int length) {
+ super();
+ this.offset = offset;
+ this.length = length;
+ }
+
+ public void undo() throws CannotUndoException {
+ super.undo();
+ try {
+ synchronized(StringContent.this) {
+ // Get the Positions in the range being removed.
+ if(marks != null)
+ posRefs = getPositionsInRange(null, offset, length);
+ string = getString(offset, length);
+ remove(offset, length);
+ }
+ } catch (BadLocationException bl) {
+ throw new CannotUndoException();
+ }
+ }
+
+ public void redo() throws CannotRedoException {
+ super.redo();
+ try {
+ synchronized(StringContent.this) {
+ insertString(offset, string);
+ string = null;
+ // Update the Positions that were in the range removed.
+ if(posRefs != null) {
+ updateUndoPositions(posRefs);
+ posRefs = null;
+ }
+ }
+ } catch (BadLocationException bl) {
+ throw new CannotRedoException();
+ }
+ }
+
+ // Where the string goes.
+ protected int offset;
+ // Length of the string.
+ protected int length;
+ // The string that was inserted. To cut down on space needed this
+ // will only be valid after an undo.
+ protected String string;
+ // An array of instances of UndoPosRef for the Positions in the
+ // range that was removed, valid after undo.
+ protected Vector posRefs;
+ }
+
+
+ /**
+ * UndoableEdit created for removes.
+ */
+ class RemoveUndo extends AbstractUndoableEdit {
+ protected RemoveUndo(int offset, String string) {
+ super();
+ this.offset = offset;
+ this.string = string;
+ this.length = string.length();
+ if(marks != null)
+ posRefs = getPositionsInRange(null, offset, length);
+ }
+
+ public void undo() throws CannotUndoException {
+ super.undo();
+ try {
+ synchronized(StringContent.this) {
+ insertString(offset, string);
+ // Update the Positions that were in the range removed.
+ if(posRefs != null) {
+ updateUndoPositions(posRefs);
+ posRefs = null;
+ }
+ string = null;
+ }
+ } catch (BadLocationException bl) {
+ throw new CannotUndoException();
+ }
+ }
+
+ public void redo() throws CannotRedoException {
+ super.redo();
+ try {
+ synchronized(StringContent.this) {
+ string = getString(offset, length);
+ // Get the Positions in the range being removed.
+ if(marks != null)
+ posRefs = getPositionsInRange(null, offset, length);
+ remove(offset, length);
+ }
+ } catch (BadLocationException bl) {
+ throw new CannotRedoException();
+ }
+ }
+
+ // Where the string goes.
+ protected int offset;
+ // Length of the string.
+ protected int length;
+ // The string that was inserted. This will be null after an undo.
+ protected String string;
+ // An array of instances of UndoPosRef for the Positions in the
+ // range that was removed, valid before undo.
+ protected Vector posRefs;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/Style.java b/src/share/classes/javax/swing/text/Style.java
new file mode 100644
index 000000000..ec31c5bfb
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Style.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 1997-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;
+
+import java.awt.Component;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ChangeEvent;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+
+
+/**
+ * A collection of attributes to associate with an element in a document.
+ * Since these are typically used to associate character and paragraph
+ * styles with the element, operations for this are provided. Other
+ * customized attributes that get associated with the element will
+ * effectively be name-value pairs that live in a hierarchy and if a name
+ * (key) is not found locally, the request is forwarded to the parent.
+ * Commonly used attributes are separated out to facilitate alternative
+ * implementations that are more efficient.
+ *
+ * @author Timothy Prinzing
+ */
+public interface Style extends MutableAttributeSet {
+
+ /**
+ * Fetches the name of the style. A style is not required to be named,
+ * so <code>null</code> is returned if there is no name
+ * associated with the style.
+ *
+ * @return the name
+ */
+ public String getName();
+
+ /**
+ * Adds a listener to track whenever an attribute
+ * has been changed.
+ *
+ * @param l the change listener
+ */
+ public void addChangeListener(ChangeListener l);
+
+ /**
+ * Removes a listener that was tracking attribute changes.
+ *
+ * @param l the change listener
+ */
+ public void removeChangeListener(ChangeListener l);
+
+
+}
diff --git a/src/share/classes/javax/swing/text/StyleConstants.java b/src/share/classes/javax/swing/text/StyleConstants.java
new file mode 100644
index 000000000..fa59a783f
--- /dev/null
+++ b/src/share/classes/javax/swing/text/StyleConstants.java
@@ -0,0 +1,852 @@
+/*
+ * 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;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Toolkit;
+import javax.swing.Icon;
+
+/**
+ * <p>
+ * A collection of <em>well known</em> or common attribute keys
+ * and methods to apply to an AttributeSet or MutableAttributeSet
+ * to get/set the properties in a typesafe manner.
+ * <p>
+ * The paragraph attributes form the definition of a paragraph to be rendered.
+ * All sizes are specified in points (such as found in postscript), a
+ * device independent measure.
+ * </p>
+ * <p align=center><img src="doc-files/paragraph.gif"
+ * alt="Diagram shows SpaceAbove, FirstLineIndent, LeftIndent, RightIndent,
+ * and SpaceBelow a paragraph."></p>
+ * <p>
+ *
+ * @author Timothy Prinzing
+ */
+public class StyleConstants {
+
+ /**
+ * Name of elements used to represent components.
+ */
+ public static final String ComponentElementName = "component";
+
+ /**
+ * Name of elements used to represent icons.
+ */
+ public static final String IconElementName = "icon";
+
+ /**
+ * Attribute name used to name the collection of
+ * attributes.
+ */
+ public static final Object NameAttribute = new StyleConstants("name");
+
+ /**
+ * Attribute name used to identifiy the resolving parent
+ * set of attributes, if one is defined.
+ */
+ public static final Object ResolveAttribute = new StyleConstants("resolver");
+
+ /**
+ * Attribute used to identify the model for embedded
+ * objects that have a model view separation.
+ */
+ public static final Object ModelAttribute = new StyleConstants("model");
+
+ /**
+ * Returns the string representation.
+ *
+ * @return the string
+ */
+ public String toString() {
+ return representation;
+ }
+
+ // ---- character constants -----------------------------------
+
+ /**
+ * Bidirectional level of a character as assigned by the Unicode bidi
+ * algorithm.
+ */
+ public static final Object BidiLevel = new CharacterConstants("bidiLevel");
+
+ /**
+ * Name of the font family.
+ */
+ public static final Object FontFamily = new FontConstants("family");
+
+ /**
+ * Name of the font family.
+ *
+ * @since 1.5
+ */
+ public static final Object Family = FontFamily;
+
+ /**
+ * Name of the font size.
+ */
+ public static final Object FontSize = new FontConstants("size");
+
+ /**
+ * Name of the font size.
+ *
+ * @since 1.5
+ */
+ public static final Object Size = FontSize;
+
+ /**
+ * Name of the bold attribute.
+ */
+ public static final Object Bold = new FontConstants("bold");
+
+ /**
+ * Name of the italic attribute.
+ */
+ public static final Object Italic = new FontConstants("italic");
+
+ /**
+ * Name of the underline attribute.
+ */
+ public static final Object Underline = new CharacterConstants("underline");
+
+ /**
+ * Name of the Strikethrough attribute.
+ */
+ public static final Object StrikeThrough = new CharacterConstants("strikethrough");
+
+ /**
+ * Name of the Superscript attribute.
+ */
+ public static final Object Superscript = new CharacterConstants("superscript");
+
+ /**
+ * Name of the Subscript attribute.
+ */
+ public static final Object Subscript = new CharacterConstants("subscript");
+
+ /**
+ * Name of the foreground color attribute.
+ */
+ public static final Object Foreground = new ColorConstants("foreground");
+
+ /**
+ * Name of the background color attribute.
+ */
+ public static final Object Background = new ColorConstants("background");
+
+ /**
+ * Name of the component attribute.
+ */
+ public static final Object ComponentAttribute = new CharacterConstants("component");
+
+ /**
+ * Name of the icon attribute.
+ */
+ public static final Object IconAttribute = new CharacterConstants("icon");
+
+ /**
+ * Name of the input method composed text attribute. The value of
+ * this attribute is an instance of AttributedString which represents
+ * the composed text.
+ */
+ public static final Object ComposedTextAttribute = new StyleConstants("composed text");
+
+ /**
+ * The amount of space to indent the first
+ * line of the paragraph. This value may be negative
+ * to offset in the reverse direction. The type
+ * is Float and specifies the size of the space
+ * in points.
+ */
+ public static final Object FirstLineIndent = new ParagraphConstants("FirstLineIndent");
+
+ /**
+ * The amount to indent the left side
+ * of the paragraph.
+ * Type is float and specifies the size in points.
+ */
+ public static final Object LeftIndent = new ParagraphConstants("LeftIndent");
+
+ /**
+ * The amount to indent the right side
+ * of the paragraph.
+ * Type is float and specifies the size in points.
+ */
+ public static final Object RightIndent = new ParagraphConstants("RightIndent");
+
+ /**
+ * The amount of space between lines
+ * of the paragraph.
+ * Type is float and specifies the size as a factor of the line height
+ */
+ public static final Object LineSpacing = new ParagraphConstants("LineSpacing");
+
+ /**
+ * The amount of space above the paragraph.
+ * Type is float and specifies the size in points.
+ */
+ public static final Object SpaceAbove = new ParagraphConstants("SpaceAbove");
+
+ /**
+ * The amount of space below the paragraph.
+ * Type is float and specifies the size in points.
+ */
+ public static final Object SpaceBelow = new ParagraphConstants("SpaceBelow");
+
+ /**
+ * Alignment for the paragraph. The type is
+ * Integer. Valid values are:
+ * <ul>
+ * <li>ALIGN_LEFT
+ * <li>ALIGN_RIGHT
+ * <li>ALIGN_CENTER
+ * <li>ALIGN_JUSTIFED
+ * </ul>
+ *
+ */
+ public static final Object Alignment = new ParagraphConstants("Alignment");
+
+ /**
+ * TabSet for the paragraph, type is a TabSet containing
+ * TabStops.
+ */
+ public static final Object TabSet = new ParagraphConstants("TabSet");
+
+ /**
+ * Orientation for a paragraph.
+ */
+ public static final Object Orientation = new ParagraphConstants("Orientation");
+ /**
+ * A possible value for paragraph alignment. This
+ * specifies that the text is aligned to the left
+ * indent and extra whitespace should be placed on
+ * the right.
+ */
+ public static final int ALIGN_LEFT = 0;
+
+ /**
+ * A possible value for paragraph alignment. This
+ * specifies that the text is aligned to the center
+ * and extra whitespace should be placed equally on
+ * the left and right.
+ */
+ public static final int ALIGN_CENTER = 1;
+
+ /**
+ * A possible value for paragraph alignment. This
+ * specifies that the text is aligned to the right
+ * indent and extra whitespace should be placed on
+ * the left.
+ */
+ public static final int ALIGN_RIGHT = 2;
+
+ /**
+ * A possible value for paragraph alignment. This
+ * specifies that extra whitespace should be spread
+ * out through the rows of the paragraph with the
+ * text lined up with the left and right indent
+ * except on the last line which should be aligned
+ * to the left.
+ */
+ public static final int ALIGN_JUSTIFIED = 3;
+
+ // --- character attribute accessors ---------------------------
+
+ /**
+ * Gets the BidiLevel setting.
+ *
+ * @param a the attribute set
+ * @return the value
+ */
+ public static int getBidiLevel(AttributeSet a) {
+ Integer o = (Integer) a.getAttribute(BidiLevel);
+ if (o != null) {
+ return o.intValue();
+ }
+ return 0; // Level 0 is base level (non-embedded) left-to-right
+ }
+
+ /**
+ * Sets the BidiLevel.
+ *
+ * @param a the attribute set
+ * @param o the bidi level value
+ */
+ public static void setBidiLevel(MutableAttributeSet a, int o) {
+ a.addAttribute(BidiLevel, new Integer(o));
+ }
+
+ /**
+ * Gets the component setting from the attribute list.
+ *
+ * @param a the attribute set
+ * @return the component, null if none
+ */
+ public static Component getComponent(AttributeSet a) {
+ return (Component) a.getAttribute(ComponentAttribute);
+ }
+
+ /**
+ * Sets the component attribute.
+ *
+ * @param a the attribute set
+ * @param c the component
+ */
+ public static void setComponent(MutableAttributeSet a, Component c) {
+ a.addAttribute(AbstractDocument.ElementNameAttribute, ComponentElementName);
+ a.addAttribute(ComponentAttribute, c);
+ }
+
+ /**
+ * Gets the icon setting from the attribute list.
+ *
+ * @param a the attribute set
+ * @return the icon, null if none
+ */
+ public static Icon getIcon(AttributeSet a) {
+ return (Icon) a.getAttribute(IconAttribute);
+ }
+
+ /**
+ * Sets the icon attribute.
+ *
+ * @param a the attribute set
+ * @param c the icon
+ */
+ public static void setIcon(MutableAttributeSet a, Icon c) {
+ a.addAttribute(AbstractDocument.ElementNameAttribute, IconElementName);
+ a.addAttribute(IconAttribute, c);
+ }
+
+ /**
+ * Gets the font family setting from the attribute list.
+ *
+ * @param a the attribute set
+ * @return the font family, "Monospaced" as the default
+ */
+ public static String getFontFamily(AttributeSet a) {
+ String family = (String) a.getAttribute(FontFamily);
+ if (family == null) {
+ family = "Monospaced";
+ }
+ return family;
+ }
+
+ /**
+ * Sets the font attribute.
+ *
+ * @param a the attribute set
+ * @param fam the font
+ */
+ public static void setFontFamily(MutableAttributeSet a, String fam) {
+ a.addAttribute(FontFamily, fam);
+ }
+
+ /**
+ * Gets the font size setting from the attribute list.
+ *
+ * @param a the attribute set
+ * @return the font size, 12 as the default
+ */
+ public static int getFontSize(AttributeSet a) {
+ Integer size = (Integer) a.getAttribute(FontSize);
+ if (size != null) {
+ return size.intValue();
+ }
+ return 12;
+ }
+
+ /**
+ * Sets the font size attribute.
+ *
+ * @param a the attribute set
+ * @param s the font size
+ */
+ public static void setFontSize(MutableAttributeSet a, int s) {
+ a.addAttribute(FontSize, new Integer(s));
+ }
+
+ /**
+ * Checks whether the bold attribute is set.
+ *
+ * @param a the attribute set
+ * @return true if set else false
+ */
+ public static boolean isBold(AttributeSet a) {
+ Boolean bold = (Boolean) a.getAttribute(Bold);
+ if (bold != null) {
+ return bold.booleanValue();
+ }
+ return false;
+ }
+
+ /**
+ * Sets the bold attribute.
+ *
+ * @param a the attribute set
+ * @param b specifies true/false for setting the attribute
+ */
+ public static void setBold(MutableAttributeSet a, boolean b) {
+ a.addAttribute(Bold, Boolean.valueOf(b));
+ }
+
+ /**
+ * Checks whether the italic attribute is set.
+ *
+ * @param a the attribute set
+ * @return true if set else false
+ */
+ public static boolean isItalic(AttributeSet a) {
+ Boolean italic = (Boolean) a.getAttribute(Italic);
+ if (italic != null) {
+ return italic.booleanValue();
+ }
+ return false;
+ }
+
+ /**
+ * Sets the italic attribute.
+ *
+ * @param a the attribute set
+ * @param b specifies true/false for setting the attribute
+ */
+ public static void setItalic(MutableAttributeSet a, boolean b) {
+ a.addAttribute(Italic, Boolean.valueOf(b));
+ }
+
+ /**
+ * Checks whether the underline attribute is set.
+ *
+ * @param a the attribute set
+ * @return true if set else false
+ */
+ public static boolean isUnderline(AttributeSet a) {
+ Boolean underline = (Boolean) a.getAttribute(Underline);
+ if (underline != null) {
+ return underline.booleanValue();
+ }
+ return false;
+ }
+
+ /**
+ * Checks whether the strikethrough attribute is set.
+ *
+ * @param a the attribute set
+ * @return true if set else false
+ */
+ public static boolean isStrikeThrough(AttributeSet a) {
+ Boolean strike = (Boolean) a.getAttribute(StrikeThrough);
+ if (strike != null) {
+ return strike.booleanValue();
+ }
+ return false;
+ }
+
+
+ /**
+ * Checks whether the superscript attribute is set.
+ *
+ * @param a the attribute set
+ * @return true if set else false
+ */
+ public static boolean isSuperscript(AttributeSet a) {
+ Boolean superscript = (Boolean) a.getAttribute(Superscript);
+ if (superscript != null) {
+ return superscript.booleanValue();
+ }
+ return false;
+ }
+
+
+ /**
+ * Checks whether the subscript attribute is set.
+ *
+ * @param a the attribute set
+ * @return true if set else false
+ */
+ public static boolean isSubscript(AttributeSet a) {
+ Boolean subscript = (Boolean) a.getAttribute(Subscript);
+ if (subscript != null) {
+ return subscript.booleanValue();
+ }
+ return false;
+ }
+
+
+ /**
+ * Sets the underline attribute.
+ *
+ * @param a the attribute set
+ * @param b specifies true/false for setting the attribute
+ */
+ public static void setUnderline(MutableAttributeSet a, boolean b) {
+ a.addAttribute(Underline, Boolean.valueOf(b));
+ }
+
+ /**
+ * Sets the strikethrough attribute.
+ *
+ * @param a the attribute set
+ * @param b specifies true/false for setting the attribute
+ */
+ public static void setStrikeThrough(MutableAttributeSet a, boolean b) {
+ a.addAttribute(StrikeThrough, Boolean.valueOf(b));
+ }
+
+ /**
+ * Sets the superscript attribute.
+ *
+ * @param a the attribute set
+ * @param b specifies true/false for setting the attribute
+ */
+ public static void setSuperscript(MutableAttributeSet a, boolean b) {
+ a.addAttribute(Superscript, Boolean.valueOf(b));
+ }
+
+ /**
+ * Sets the subscript attribute.
+ *
+ * @param a the attribute set
+ * @param b specifies true/false for setting the attribute
+ */
+ public static void setSubscript(MutableAttributeSet a, boolean b) {
+ a.addAttribute(Subscript, Boolean.valueOf(b));
+ }
+
+
+ /**
+ * Gets the foreground color setting from the attribute list.
+ *
+ * @param a the attribute set
+ * @return the color, Color.black as the default
+ */
+ public static Color getForeground(AttributeSet a) {
+ Color fg = (Color) a.getAttribute(Foreground);
+ if (fg == null) {
+ fg = Color.black;
+ }
+ return fg;
+ }
+
+ /**
+ * Sets the foreground color.
+ *
+ * @param a the attribute set
+ * @param fg the color
+ */
+ public static void setForeground(MutableAttributeSet a, Color fg) {
+ a.addAttribute(Foreground, fg);
+ }
+
+ /**
+ * Gets the background color setting from the attribute list.
+ *
+ * @param a the attribute set
+ * @return the color, Color.black as the default
+ */
+ public static Color getBackground(AttributeSet a) {
+ Color fg = (Color) a.getAttribute(Background);
+ if (fg == null) {
+ fg = Color.black;
+ }
+ return fg;
+ }
+
+ /**
+ * Sets the background color.
+ *
+ * @param a the attribute set
+ * @param fg the color
+ */
+ public static void setBackground(MutableAttributeSet a, Color fg) {
+ a.addAttribute(Background, fg);
+ }
+
+
+ // --- paragraph attribute accessors ----------------------------
+
+ /**
+ * Gets the first line indent setting.
+ *
+ * @param a the attribute set
+ * @return the value, 0 if not set
+ */
+ public static float getFirstLineIndent(AttributeSet a) {
+ Float indent = (Float) a.getAttribute(FirstLineIndent);
+ if (indent != null) {
+ return indent.floatValue();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets the first line indent.
+ *
+ * @param a the attribute set
+ * @param i the value
+ */
+ public static void setFirstLineIndent(MutableAttributeSet a, float i) {
+ a.addAttribute(FirstLineIndent, new Float(i));
+ }
+
+ /**
+ * Gets the right indent setting.
+ *
+ * @param a the attribute set
+ * @return the value, 0 if not set
+ */
+ public static float getRightIndent(AttributeSet a) {
+ Float indent = (Float) a.getAttribute(RightIndent);
+ if (indent != null) {
+ return indent.floatValue();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets right indent.
+ *
+ * @param a the attribute set
+ * @param i the value
+ */
+ public static void setRightIndent(MutableAttributeSet a, float i) {
+ a.addAttribute(RightIndent, new Float(i));
+ }
+
+ /**
+ * Gets the left indent setting.
+ *
+ * @param a the attribute set
+ * @return the value, 0 if not set
+ */
+ public static float getLeftIndent(AttributeSet a) {
+ Float indent = (Float) a.getAttribute(LeftIndent);
+ if (indent != null) {
+ return indent.floatValue();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets left indent.
+ *
+ * @param a the attribute set
+ * @param i the value
+ */
+ public static void setLeftIndent(MutableAttributeSet a, float i) {
+ a.addAttribute(LeftIndent, new Float(i));
+ }
+
+ /**
+ * Gets the line spacing setting.
+ *
+ * @param a the attribute set
+ * @return the value, 0 if not set
+ */
+ public static float getLineSpacing(AttributeSet a) {
+ Float space = (Float) a.getAttribute(LineSpacing);
+ if (space != null) {
+ return space.floatValue();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets line spacing.
+ *
+ * @param a the attribute set
+ * @param i the value
+ */
+ public static void setLineSpacing(MutableAttributeSet a, float i) {
+ a.addAttribute(LineSpacing, new Float(i));
+ }
+
+ /**
+ * Gets the space above setting.
+ *
+ * @param a the attribute set
+ * @return the value, 0 if not set
+ */
+ public static float getSpaceAbove(AttributeSet a) {
+ Float space = (Float) a.getAttribute(SpaceAbove);
+ if (space != null) {
+ return space.floatValue();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets space above.
+ *
+ * @param a the attribute set
+ * @param i the value
+ */
+ public static void setSpaceAbove(MutableAttributeSet a, float i) {
+ a.addAttribute(SpaceAbove, new Float(i));
+ }
+
+ /**
+ * Gets the space below setting.
+ *
+ * @param a the attribute set
+ * @return the value, 0 if not set
+ */
+ public static float getSpaceBelow(AttributeSet a) {
+ Float space = (Float) a.getAttribute(SpaceBelow);
+ if (space != null) {
+ return space.floatValue();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets space below.
+ *
+ * @param a the attribute set
+ * @param i the value
+ */
+ public static void setSpaceBelow(MutableAttributeSet a, float i) {
+ a.addAttribute(SpaceBelow, new Float(i));
+ }
+
+ /**
+ * Gets the alignment setting.
+ *
+ * @param a the attribute set
+ * @return the value <code>StyleConstants.ALIGN_LEFT</code> if not set
+ */
+ public static int getAlignment(AttributeSet a) {
+ Integer align = (Integer) a.getAttribute(Alignment);
+ if (align != null) {
+ return align.intValue();
+ }
+ return ALIGN_LEFT;
+ }
+
+ /**
+ * Sets alignment.
+ *
+ * @param a the attribute set
+ * @param align the alignment value
+ */
+ public static void setAlignment(MutableAttributeSet a, int align) {
+ a.addAttribute(Alignment, new Integer(align));
+ }
+
+ /**
+ * Gets the TabSet.
+ *
+ * @param a the attribute set
+ * @return the <code>TabSet</code>
+ */
+ public static TabSet getTabSet(AttributeSet a) {
+ TabSet tabs = (TabSet)a.getAttribute(TabSet);
+ // PENDING: should this return a default?
+ return tabs;
+ }
+
+ /**
+ * Sets the TabSet.
+ *
+ * @param a the attribute set.
+ * @param tabs the TabSet
+ */
+ public static void setTabSet(MutableAttributeSet a, TabSet tabs) {
+ a.addAttribute(TabSet, tabs);
+ }
+
+ // --- privates ---------------------------------------------
+
+ static Object[] keys = {
+ NameAttribute, ResolveAttribute, BidiLevel,
+ FontFamily, FontSize, Bold, Italic, Underline,
+ StrikeThrough, Superscript, Subscript, Foreground,
+ Background, ComponentAttribute, IconAttribute,
+ FirstLineIndent, LeftIndent, RightIndent, LineSpacing,
+ SpaceAbove, SpaceBelow, Alignment, TabSet, Orientation,
+ ModelAttribute, ComposedTextAttribute
+ };
+
+ StyleConstants(String representation) {
+ this.representation = representation;
+ }
+
+ private String representation;
+
+ /**
+ * This is a typesafe enumeration of the <em>well-known</em>
+ * attributes that contribute to a paragraph style. These are
+ * aliased by the outer class for general presentation.
+ */
+ public static class ParagraphConstants extends StyleConstants
+ implements AttributeSet.ParagraphAttribute {
+
+ private ParagraphConstants(String representation) {
+ super(representation);
+ }
+ }
+
+ /**
+ * This is a typesafe enumeration of the <em>well-known</em>
+ * attributes that contribute to a character style. These are
+ * aliased by the outer class for general presentation.
+ */
+ public static class CharacterConstants extends StyleConstants
+ implements AttributeSet.CharacterAttribute {
+
+ private CharacterConstants(String representation) {
+ super(representation);
+ }
+ }
+
+ /**
+ * This is a typesafe enumeration of the <em>well-known</em>
+ * attributes that contribute to a color. These are aliased
+ * by the outer class for general presentation.
+ */
+ public static class ColorConstants extends StyleConstants
+ implements AttributeSet.ColorAttribute, AttributeSet.CharacterAttribute {
+
+ private ColorConstants(String representation) {
+ super(representation);
+ }
+ }
+
+ /**
+ * This is a typesafe enumeration of the <em>well-known</em>
+ * attributes that contribute to a font. These are aliased
+ * by the outer class for general presentation.
+ */
+ public static class FontConstants extends StyleConstants
+ implements AttributeSet.FontAttribute, AttributeSet.CharacterAttribute {
+
+ private FontConstants(String representation) {
+ super(representation);
+ }
+ }
+
+
+}
diff --git a/src/share/classes/javax/swing/text/StyleContext.java b/src/share/classes/javax/swing/text/StyleContext.java
new file mode 100644
index 000000000..1b48db67e
--- /dev/null
+++ b/src/share/classes/javax/swing/text/StyleContext.java
@@ -0,0 +1,1625 @@
+/*
+ * 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;
+
+import java.awt.*;
+import java.util.*;
+import java.io.*;
+
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+import javax.swing.event.ChangeEvent;
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
+
+import sun.font.FontManager;
+
+/**
+ * A pool of styles and their associated resources. This class determines
+ * the lifetime of a group of resources by being a container that holds
+ * caches for various resources such as font and color that get reused
+ * by the various style definitions. This can be shared by multiple
+ * documents if desired to maximize the sharing of related resources.
+ * <p>
+ * This class also provides efficient support for small sets of attributes
+ * and compresses them by sharing across uses and taking advantage of
+ * their immutable nature. Since many styles are replicated, the potential
+ * for sharing is significant, and copies can be extremely cheap.
+ * Larger sets reduce the possibility of sharing, and therefore revert
+ * automatically to a less space-efficient implementation.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Timothy Prinzing
+ */
+public class StyleContext implements Serializable, AbstractDocument.AttributeContext {
+
+ /**
+ * Returns default AttributeContext shared by all documents that
+ * don't bother to define/supply their own context.
+ *
+ * @return the context
+ */
+ public static final StyleContext getDefaultStyleContext() {
+ if (defaultContext == null) {
+ defaultContext = new StyleContext();
+ }
+ return defaultContext;
+ }
+
+ private static StyleContext defaultContext;
+
+ /**
+ * Creates a new StyleContext object.
+ */
+ public StyleContext() {
+ styles = new NamedStyle(null);
+ addStyle(DEFAULT_STYLE, null);
+ }
+
+ /**
+ * Adds a new style into the style hierarchy. Style attributes
+ * resolve from bottom up so an attribute specified in a child
+ * will override an attribute specified in the parent.
+ *
+ * @param nm the name of the style (must be unique within the
+ * collection of named styles in the document). The name may
+ * be null if the style is unnamed, but the caller is responsible
+ * for managing the reference returned as an unnamed style can't
+ * be fetched by name. An unnamed style may be useful for things
+ * like character attribute overrides such as found in a style
+ * run.
+ * @param parent the parent style. This may be null if unspecified
+ * attributes need not be resolved in some other style.
+ * @return the created style
+ */
+ public Style addStyle(String nm, Style parent) {
+ Style style = new NamedStyle(nm, parent);
+ if (nm != null) {
+ // add a named style, a class of attributes
+ styles.addAttribute(nm, style);
+ }
+ return style;
+ }
+
+ /**
+ * Removes a named style previously added to the document.
+ *
+ * @param nm the name of the style to remove
+ */
+ public void removeStyle(String nm) {
+ styles.removeAttribute(nm);
+ }
+
+ /**
+ * Fetches a named style previously added to the document
+ *
+ * @param nm the name of the style
+ * @return the style
+ */
+ public Style getStyle(String nm) {
+ return (Style) styles.getAttribute(nm);
+ }
+
+ /**
+ * Fetches the names of the styles defined.
+ *
+ * @return the list of names as an enumeration
+ */
+ public Enumeration<?> getStyleNames() {
+ return styles.getAttributeNames();
+ }
+
+ /**
+ * Adds a listener to track when styles are added
+ * or removed.
+ *
+ * @param l the change listener
+ */
+ public void addChangeListener(ChangeListener l) {
+ styles.addChangeListener(l);
+ }
+
+ /**
+ * Removes a listener that was tracking styles being
+ * added or removed.
+ *
+ * @param l the change listener
+ */
+ public void removeChangeListener(ChangeListener l) {
+ styles.removeChangeListener(l);
+ }
+
+ /**
+ * Returns an array of all the <code>ChangeListener</code>s added
+ * to this StyleContext with addChangeListener().
+ *
+ * @return all of the <code>ChangeListener</code>s added or an empty
+ * array if no listeners have been added
+ * @since 1.4
+ */
+ public ChangeListener[] getChangeListeners() {
+ return ((NamedStyle)styles).getChangeListeners();
+ }
+
+ /**
+ * Gets the font from an attribute set. This is
+ * implemented to try and fetch a cached font
+ * for the given AttributeSet, and if that fails
+ * the font features are resolved and the
+ * font is fetched from the low-level font cache.
+ *
+ * @param attr the attribute set
+ * @return the font
+ */
+ public Font getFont(AttributeSet attr) {
+ // PENDING(prinz) add cache behavior
+ int style = Font.PLAIN;
+ if (StyleConstants.isBold(attr)) {
+ style |= Font.BOLD;
+ }
+ if (StyleConstants.isItalic(attr)) {
+ style |= Font.ITALIC;
+ }
+ String family = StyleConstants.getFontFamily(attr);
+ int size = StyleConstants.getFontSize(attr);
+
+ /**
+ * if either superscript or subscript is
+ * is set, we need to reduce the font size
+ * by 2.
+ */
+ if (StyleConstants.isSuperscript(attr) ||
+ StyleConstants.isSubscript(attr)) {
+ size -= 2;
+ }
+
+ return getFont(family, style, size);
+ }
+
+ /**
+ * 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. By default it simply returns
+ * the value specified by the StyleConstants.Foreground attribute.
+ *
+ * @param attr the set of attributes
+ * @return the color
+ */
+ public Color getForeground(AttributeSet attr) {
+ return StyleConstants.getForeground(attr);
+ }
+
+ /**
+ * 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. By default it simply returns
+ * the value specified by the StyleConstants.Background attribute.
+ *
+ * @param attr the set of attributes
+ * @return the color
+ */
+ public Color getBackground(AttributeSet attr) {
+ return StyleConstants.getBackground(attr);
+ }
+
+ /**
+ * Gets a new font. This returns a Font from a cache
+ * if a cached font exists. If not, a Font is added to
+ * the cache. This is basically a low-level cache for
+ * 1.1 font features.
+ *
+ * @param family the font family (such as "Monospaced")
+ * @param style the style of the font (such as Font.PLAIN)
+ * @param size the point size >= 1
+ * @return the new font
+ */
+ public Font getFont(String family, int style, int size) {
+ fontSearch.setValue(family, style, size);
+ Font f = (Font) fontTable.get(fontSearch);
+ if (f == null) {
+ // haven't seen this one yet.
+ Style defaultStyle =
+ getStyle(StyleContext.DEFAULT_STYLE);
+ if (defaultStyle != null) {
+ final String FONT_ATTRIBUTE_KEY = "FONT_ATTRIBUTE_KEY";
+ Font defaultFont =
+ (Font) defaultStyle.getAttribute(FONT_ATTRIBUTE_KEY);
+ if (defaultFont != null
+ && defaultFont.getFamily().equalsIgnoreCase(family)) {
+ f = defaultFont.deriveFont(style, size);
+ }
+ }
+ if (f == null) {
+ f = new Font(family, style, size);
+ }
+ if (! FontManager.fontSupportsDefaultEncoding(f)) {
+ f = FontManager.getCompositeFontUIResource(f);
+ }
+ FontKey key = new FontKey(family, style, size);
+ fontTable.put(key, f);
+ }
+ return f;
+ }
+
+ /**
+ * Returns font metrics for a font.
+ *
+ * @param f the font
+ * @return the metrics
+ */
+ public FontMetrics getFontMetrics(Font f) {
+ // The Toolkit implementations cache, so we just forward
+ // to the default toolkit.
+ return Toolkit.getDefaultToolkit().getFontMetrics(f);
+ }
+
+ // --- AttributeContext methods --------------------
+
+ /**
+ * Adds an attribute to the given set, and returns
+ * the new representative set.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param old the old attribute set
+ * @param name the non-null attribute name
+ * @param value the attribute value
+ * @return the updated attribute set
+ * @see MutableAttributeSet#addAttribute
+ */
+ public synchronized AttributeSet addAttribute(AttributeSet old, Object name, Object value) {
+ if ((old.getAttributeCount() + 1) <= getCompressionThreshold()) {
+ // build a search key and find/create an immutable and unique
+ // set.
+ search.removeAttributes(search);
+ search.addAttributes(old);
+ search.addAttribute(name, value);
+ reclaim(old);
+ return getImmutableUniqueSet();
+ }
+ MutableAttributeSet ma = getMutableAttributeSet(old);
+ ma.addAttribute(name, value);
+ return ma;
+ }
+
+ /**
+ * Adds a set of attributes to the element.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param old the old attribute set
+ * @param attr the attributes to add
+ * @return the updated attribute set
+ * @see MutableAttributeSet#addAttribute
+ */
+ public synchronized AttributeSet addAttributes(AttributeSet old, AttributeSet attr) {
+ if ((old.getAttributeCount() + attr.getAttributeCount()) <= getCompressionThreshold()) {
+ // build a search key and find/create an immutable and unique
+ // set.
+ search.removeAttributes(search);
+ search.addAttributes(old);
+ search.addAttributes(attr);
+ reclaim(old);
+ return getImmutableUniqueSet();
+ }
+ MutableAttributeSet ma = getMutableAttributeSet(old);
+ ma.addAttributes(attr);
+ return ma;
+ }
+
+ /**
+ * Removes an attribute from the set.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param old the old set of attributes
+ * @param name the non-null attribute name
+ * @return the updated attribute set
+ * @see MutableAttributeSet#removeAttribute
+ */
+ public synchronized AttributeSet removeAttribute(AttributeSet old, Object name) {
+ if ((old.getAttributeCount() - 1) <= getCompressionThreshold()) {
+ // build a search key and find/create an immutable and unique
+ // set.
+ search.removeAttributes(search);
+ search.addAttributes(old);
+ search.removeAttribute(name);
+ reclaim(old);
+ return getImmutableUniqueSet();
+ }
+ MutableAttributeSet ma = getMutableAttributeSet(old);
+ ma.removeAttribute(name);
+ return ma;
+ }
+
+ /**
+ * Removes a set of attributes for the element.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param old the old attribute set
+ * @param names the attribute names
+ * @return the updated attribute set
+ * @see MutableAttributeSet#removeAttributes
+ */
+ public synchronized AttributeSet removeAttributes(AttributeSet old, Enumeration<?> names) {
+ if (old.getAttributeCount() <= getCompressionThreshold()) {
+ // build a search key and find/create an immutable and unique
+ // set.
+ search.removeAttributes(search);
+ search.addAttributes(old);
+ search.removeAttributes(names);
+ reclaim(old);
+ return getImmutableUniqueSet();
+ }
+ MutableAttributeSet ma = getMutableAttributeSet(old);
+ ma.removeAttributes(names);
+ return ma;
+ }
+
+ /**
+ * Removes a set of attributes for the element.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param old the old attribute set
+ * @param attrs the attributes
+ * @return the updated attribute set
+ * @see MutableAttributeSet#removeAttributes
+ */
+ public synchronized AttributeSet removeAttributes(AttributeSet old, AttributeSet attrs) {
+ if (old.getAttributeCount() <= getCompressionThreshold()) {
+ // build a search key and find/create an immutable and unique
+ // set.
+ search.removeAttributes(search);
+ search.addAttributes(old);
+ search.removeAttributes(attrs);
+ reclaim(old);
+ return getImmutableUniqueSet();
+ }
+ MutableAttributeSet ma = getMutableAttributeSet(old);
+ ma.removeAttributes(attrs);
+ return ma;
+ }
+
+ /**
+ * Fetches an empty AttributeSet.
+ *
+ * @return the set
+ */
+ public AttributeSet getEmptySet() {
+ return SimpleAttributeSet.EMPTY;
+ }
+
+ /**
+ * Returns a set no longer needed by the MutableAttributeSet implmentation.
+ * This is useful for operation under 1.1 where there are no weak
+ * references. This would typically be called by the finalize method
+ * of the MutableAttributeSet implementation.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> for more information.
+ *
+ * @param a the set to reclaim
+ */
+ public void reclaim(AttributeSet a) {
+ if (SwingUtilities.isEventDispatchThread()) {
+ attributesPool.size(); // force WeakHashMap to expunge stale entries
+ }
+ // if current thread is not event dispatching thread
+ // do not bother with expunging stale entries.
+ }
+
+ // --- local methods -----------------------------------------------
+
+ /**
+ * Returns the maximum number of key/value pairs to try and
+ * compress into unique/immutable sets. Any sets above this
+ * limit will use hashtables and be a MutableAttributeSet.
+ *
+ * @return the threshold
+ */
+ protected int getCompressionThreshold() {
+ return THRESHOLD;
+ }
+
+ /**
+ * Create 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 SmallAttributeSet(a);
+ }
+
+ /**
+ * Create 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 SimpleAttributeSet(a);
+ }
+
+ /**
+ * Clean the unused immutable sets out of the hashtable.
+ */
+ synchronized void removeUnusedSets() {
+ attributesPool.size(); // force WeakHashMap to expunge stale entries
+ }
+
+ /**
+ * Search for an existing attribute set using the current search
+ * parameters. If a matching set is found, return it. If a match
+ * is not found, we create a new set and add it to the pool.
+ */
+ AttributeSet getImmutableUniqueSet() {
+ // PENDING(prinz) should consider finding a alternative to
+ // generating extra garbage on search key.
+ SmallAttributeSet key = createSmallAttributeSet(search);
+ WeakReference reference = (WeakReference)attributesPool.get(key);
+ SmallAttributeSet a;
+ if (reference == null
+ || (a = (SmallAttributeSet)reference.get()) == null) {
+ a = key;
+ attributesPool.put(a, new WeakReference(a));
+ }
+ return a;
+ }
+
+ /**
+ * Creates a mutable attribute set to hand out because the current
+ * needs are too big to try and use a shared version.
+ */
+ MutableAttributeSet getMutableAttributeSet(AttributeSet a) {
+ if (a instanceof MutableAttributeSet &&
+ a != SimpleAttributeSet.EMPTY) {
+ return (MutableAttributeSet) a;
+ }
+ return createLargeAttributeSet(a);
+ }
+
+ /**
+ * Converts a StyleContext to a String.
+ *
+ * @return the string
+ */
+ public String toString() {
+ removeUnusedSets();
+ String s = "";
+ Iterator iterator = attributesPool.keySet().iterator();
+ while (iterator.hasNext()) {
+ SmallAttributeSet set = (SmallAttributeSet)iterator.next();
+ s = s + set + "\n";
+ }
+ return s;
+ }
+
+ // --- serialization ---------------------------------------------
+
+ /**
+ * Context-specific handling of writing out attributes
+ */
+ public void writeAttributes(ObjectOutputStream out,
+ AttributeSet a) throws IOException {
+ writeAttributeSet(out, a);
+ }
+
+ /**
+ * Context-specific handling of reading in attributes
+ */
+ public void readAttributes(ObjectInputStream in,
+ MutableAttributeSet a) throws ClassNotFoundException, IOException {
+ readAttributeSet(in, a);
+ }
+
+ /**
+ * Writes a set of attributes to the given object stream
+ * for the purpose of serialization. This will take
+ * special care to deal with static attribute keys that
+ * have been registered wit the
+ * <code>registerStaticAttributeKey</code> method.
+ * Any attribute key not regsitered as a static key
+ * will be serialized directly. All values are expected
+ * to be serializable.
+ *
+ * @param out the output stream
+ * @param a the attribute set
+ * @exception IOException on any I/O error
+ */
+ public static void writeAttributeSet(ObjectOutputStream out,
+ AttributeSet a) throws IOException {
+ int n = a.getAttributeCount();
+ out.writeInt(n);
+ Enumeration keys = a.getAttributeNames();
+ while (keys.hasMoreElements()) {
+ Object key = keys.nextElement();
+ if (key instanceof Serializable) {
+ out.writeObject(key);
+ } else {
+ Object ioFmt = freezeKeyMap.get(key);
+ if (ioFmt == null) {
+ throw new NotSerializableException(key.getClass().
+ getName() + " is not serializable as a key in an AttributeSet");
+ }
+ out.writeObject(ioFmt);
+ }
+ Object value = a.getAttribute(key);
+ Object ioFmt = freezeKeyMap.get(value);
+ if (value instanceof Serializable) {
+ out.writeObject((ioFmt != null) ? ioFmt : value);
+ } else {
+ if (ioFmt == null) {
+ throw new NotSerializableException(value.getClass().
+ getName() + " is not serializable as a value in an AttributeSet");
+ }
+ out.writeObject(ioFmt);
+ }
+ }
+ }
+
+ /**
+ * Reads a set of attributes from the given object input
+ * stream that have been previously written out with
+ * <code>writeAttributeSet</code>. This will try to restore
+ * keys that were static objects to the static objects in
+ * the current virtual machine considering only those keys
+ * that have been registered with the
+ * <code>registerStaticAttributeKey</code> method.
+ * The attributes retrieved from the stream will be placed
+ * into the given mutable set.
+ *
+ * @param in the object stream to read the attribute data from.
+ * @param a the attribute set to place the attribute
+ * definitions in.
+ * @exception ClassNotFoundException passed upward if encountered
+ * when reading the object stream.
+ * @exception IOException passed upward if encountered when
+ * reading the object stream.
+ */
+ public static void readAttributeSet(ObjectInputStream in,
+ MutableAttributeSet a) throws ClassNotFoundException, IOException {
+
+ int n = in.readInt();
+ for (int i = 0; i < n; i++) {
+ Object key = in.readObject();
+ Object value = in.readObject();
+ if (thawKeyMap != null) {
+ Object staticKey = thawKeyMap.get(key);
+ if (staticKey != null) {
+ key = staticKey;
+ }
+ Object staticValue = thawKeyMap.get(value);
+ if (staticValue != null) {
+ value = staticValue;
+ }
+ }
+ a.addAttribute(key, value);
+ }
+ }
+
+ /**
+ * Registers an object as a static object that is being
+ * used as a key in attribute sets. This allows the key
+ * to be treated specially for serialization.
+ * <p>
+ * For operation under a 1.1 virtual machine, this
+ * uses the value returned by <code>toString</code>
+ * concatenated to the classname. The value returned
+ * by toString should not have the class reference
+ * in it (ie it should be reimplemented from the
+ * definition in Object) in order to be the same when
+ * recomputed later.
+ *
+ * @param key the non-null object key
+ */
+ public static void registerStaticAttributeKey(Object key) {
+ String ioFmt = key.getClass().getName() + "." + key.toString();
+ if (freezeKeyMap == null) {
+ freezeKeyMap = new Hashtable();
+ thawKeyMap = new Hashtable();
+ }
+ freezeKeyMap.put(key, ioFmt);
+ thawKeyMap.put(ioFmt, key);
+ }
+
+ /**
+ * Returns the object previously registered with
+ * <code>registerStaticAttributeKey</code>.
+ */
+ public static Object getStaticAttribute(Object key) {
+ if (thawKeyMap == null || key == null) {
+ return null;
+ }
+ return thawKeyMap.get(key);
+ }
+
+ /**
+ * Returns the String that <code>key</code> will be registered with
+ * @see #getStaticAttribute
+ * @see #registerStaticAttributeKey
+ */
+ public static Object getStaticAttributeKey(Object key) {
+ return key.getClass().getName() + "." + key.toString();
+ }
+
+ private void writeObject(java.io.ObjectOutputStream s)
+ throws IOException
+ {
+ // clean out unused sets before saving
+ removeUnusedSets();
+
+ s.defaultWriteObject();
+ }
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException
+ {
+ fontSearch = new FontKey(null, 0, 0);
+ fontTable = new Hashtable();
+ search = new SimpleAttributeSet();
+ attributesPool = Collections.
+ synchronizedMap(new WeakHashMap());
+ s.defaultReadObject();
+ }
+
+ // --- variables ---------------------------------------------------
+
+ /**
+ * The name given to the default logical style attached
+ * to paragraphs.
+ */
+ public static final String DEFAULT_STYLE = "default";
+
+ private static Hashtable freezeKeyMap;
+ private static Hashtable thawKeyMap;
+
+ private Style styles;
+ private transient FontKey fontSearch = new FontKey(null, 0, 0);
+ private transient Hashtable fontTable = new Hashtable();
+
+ private transient Map attributesPool = Collections.
+ synchronizedMap(new WeakHashMap());
+ private transient MutableAttributeSet search = new SimpleAttributeSet();
+
+ /**
+ * Number of immutable sets that are not currently
+ * being used. This helps indicate when the sets need
+ * to be cleaned out of the hashtable they are stored
+ * in.
+ */
+ private int unusedSets;
+
+ /**
+ * The threshold for no longer sharing the set of attributes
+ * in an immutable table.
+ */
+ static final int THRESHOLD = 9;
+
+ /**
+ * This class holds a small number of attributes in an array.
+ * The storage format is key, value, key, value, etc. The size
+ * of the set is the length of the array divided by two. By
+ * default, this is the class that will be used to store attributes
+ * when held in the compact sharable form.
+ */
+ public class SmallAttributeSet implements AttributeSet {
+
+ public SmallAttributeSet(Object[] attributes) {
+ this.attributes = attributes;
+ updateResolveParent();
+ }
+
+ public SmallAttributeSet(AttributeSet attrs) {
+ int n = attrs.getAttributeCount();
+ Object[] tbl = new Object[2 * n];
+ Enumeration names = attrs.getAttributeNames();
+ int i = 0;
+ while (names.hasMoreElements()) {
+ tbl[i] = names.nextElement();
+ tbl[i+1] = attrs.getAttribute(tbl[i]);
+ i += 2;
+ }
+ attributes = tbl;
+ updateResolveParent();
+ }
+
+ private void updateResolveParent() {
+ resolveParent = null;
+ Object[] tbl = attributes;
+ for (int i = 0; i < tbl.length; i += 2) {
+ if (tbl[i] == StyleConstants.ResolveAttribute) {
+ resolveParent = (AttributeSet)tbl[i + 1];
+ break;
+ }
+ }
+ }
+
+ Object getLocalAttribute(Object nm) {
+ if (nm == StyleConstants.ResolveAttribute) {
+ return resolveParent;
+ }
+ Object[] tbl = attributes;
+ for (int i = 0; i < tbl.length; i += 2) {
+ if (nm.equals(tbl[i])) {
+ return tbl[i+1];
+ }
+ }
+ return null;
+ }
+
+ // --- Object methods -------------------------
+
+ /**
+ * Returns a string showing the key/value pairs
+ */
+ public String toString() {
+ String s = "{";
+ Object[] tbl = attributes;
+ for (int i = 0; i < tbl.length; i += 2) {
+ if (tbl[i+1] instanceof AttributeSet) {
+ // don't recurse
+ s = s + tbl[i] + "=" + "AttributeSet" + ",";
+ } else {
+ s = s + tbl[i] + "=" + tbl[i+1] + ",";
+ }
+ }
+ s = s + "}";
+ return s;
+ }
+
+ /**
+ * Returns a hashcode for this set of attributes.
+ * @return a hashcode value for this set of attributes.
+ */
+ public int hashCode() {
+ int code = 0;
+ Object[] tbl = attributes;
+ for (int i = 1; i < tbl.length; i += 2) {
+ code ^= tbl[i].hashCode();
+ }
+ return code;
+ }
+
+ /**
+ * Compares this object to the specifed object.
+ * The result is <code>true</code> if the object is an equivalent
+ * set of attributes.
+ * @param obj the object to compare with.
+ * @return <code>true</code> if the objects are equal;
+ * <code>false</code> otherwise.
+ */
+ public boolean equals(Object obj) {
+ if (obj instanceof AttributeSet) {
+ AttributeSet attrs = (AttributeSet) obj;
+ return ((getAttributeCount() == attrs.getAttributeCount()) &&
+ containsAttributes(attrs));
+ }
+ return false;
+ }
+
+ /**
+ * Clones a set of attributes. Since the set is immutable, a
+ * clone is basically the same set.
+ *
+ * @return the set of attributes
+ */
+ public Object clone() {
+ return this;
+ }
+
+ // --- AttributeSet methods ----------------------------
+
+ /**
+ * Gets the number of attributes that are defined.
+ *
+ * @return the number of attributes
+ * @see AttributeSet#getAttributeCount
+ */
+ public int getAttributeCount() {
+ return attributes.length / 2;
+ }
+
+ /**
+ * 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) {
+ Object[] a = attributes;
+ int n = a.length;
+ for (int i = 0; i < n; i += 2) {
+ if (key.equals(a[i])) {
+ 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) {
+ if (attr instanceof SmallAttributeSet) {
+ return attr == this;
+ }
+ return ((getAttributeCount() == attr.getAttributeCount()) &&
+ containsAttributes(attr));
+ }
+
+ /**
+ * Copies a set of attributes.
+ *
+ * @return the copy
+ * @see AttributeSet#copyAttributes
+ */
+ public AttributeSet copyAttributes() {
+ return this;
+ }
+
+ /**
+ * Gets the value of an attribute.
+ *
+ * @param key the attribute name
+ * @return the attribute value
+ * @see AttributeSet#getAttribute
+ */
+ public Object getAttribute(Object key) {
+ Object value = getLocalAttribute(key);
+ if (value == null) {
+ AttributeSet parent = getResolveParent();
+ if (parent != null)
+ value = parent.getAttribute(key);
+ }
+ return value;
+ }
+
+ /**
+ * Gets the names of all attributes.
+ *
+ * @return the attribute names
+ * @see AttributeSet#getAttributeNames
+ */
+ public Enumeration<?> getAttributeNames() {
+ return new KeyEnumeration(attributes);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * If not overriden, the resolving parent defaults to
+ * the parent element.
+ *
+ * @return the attributes from the parent
+ * @see AttributeSet#getResolveParent
+ */
+ public AttributeSet getResolveParent() {
+ return resolveParent;
+ }
+
+ // --- variables -----------------------------------------
+
+ Object[] attributes;
+ // This is also stored in attributes
+ AttributeSet resolveParent;
+ }
+
+ /**
+ * An enumeration of the keys in a SmallAttributeSet.
+ */
+ class KeyEnumeration implements Enumeration<Object> {
+
+ KeyEnumeration(Object[] attr) {
+ this.attr = attr;
+ i = 0;
+ }
+
+ /**
+ * Tests if this enumeration contains more elements.
+ *
+ * @return <code>true</code> if this enumeration contains more elements;
+ * <code>false</code> otherwise.
+ * @since JDK1.0
+ */
+ public boolean hasMoreElements() {
+ return i < attr.length;
+ }
+
+ /**
+ * Returns the next element of this enumeration.
+ *
+ * @return the next element of this enumeration.
+ * @exception NoSuchElementException if no more elements exist.
+ * @since JDK1.0
+ */
+ public Object nextElement() {
+ if (i < attr.length) {
+ Object o = attr[i];
+ i += 2;
+ return o;
+ }
+ throw new NoSuchElementException();
+ }
+
+ Object[] attr;
+ int i;
+ }
+
+ /**
+ * Sorts the key strings so that they can be very quickly compared
+ * in the attribute set searchs.
+ */
+ class KeyBuilder {
+
+ public void initialize(AttributeSet a) {
+ if (a instanceof SmallAttributeSet) {
+ initialize(((SmallAttributeSet)a).attributes);
+ } else {
+ keys.removeAllElements();
+ data.removeAllElements();
+ Enumeration names = a.getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object name = names.nextElement();
+ addAttribute(name, a.getAttribute(name));
+ }
+ }
+ }
+
+ /**
+ * Initialize with a set of already sorted
+ * keys (data from an existing SmallAttributeSet).
+ */
+ private void initialize(Object[] sorted) {
+ keys.removeAllElements();
+ data.removeAllElements();
+ int n = sorted.length;
+ for (int i = 0; i < n; i += 2) {
+ keys.addElement(sorted[i]);
+ data.addElement(sorted[i+1]);
+ }
+ }
+
+ /**
+ * Creates a table of sorted key/value entries
+ * suitable for creation of an instance of
+ * SmallAttributeSet.
+ */
+ public Object[] createTable() {
+ int n = keys.size();
+ Object[] tbl = new Object[2 * n];
+ for (int i = 0; i < n; i ++) {
+ int offs = 2 * i;
+ tbl[offs] = keys.elementAt(i);
+ tbl[offs + 1] = data.elementAt(i);
+ }
+ return tbl;
+ }
+
+ /**
+ * The number of key/value pairs contained
+ * in the current key being forged.
+ */
+ int getCount() {
+ return keys.size();
+ }
+
+ /**
+ * Adds a key/value to the set.
+ */
+ public void addAttribute(Object key, Object value) {
+ keys.addElement(key);
+ data.addElement(value);
+ }
+
+ /**
+ * Adds a set of key/value pairs to the set.
+ */
+ public void addAttributes(AttributeSet attr) {
+ if (attr instanceof SmallAttributeSet) {
+ // avoid searching the keys, they are already interned.
+ Object[] tbl = ((SmallAttributeSet)attr).attributes;
+ int n = tbl.length;
+ for (int i = 0; i < n; i += 2) {
+ addAttribute(tbl[i], tbl[i+1]);
+ }
+ } else {
+ Enumeration names = attr.getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object name = names.nextElement();
+ addAttribute(name, attr.getAttribute(name));
+ }
+ }
+ }
+
+ /**
+ * Removes the given name from the set.
+ */
+ public void removeAttribute(Object key) {
+ int n = keys.size();
+ for (int i = 0; i < n; i++) {
+ if (keys.elementAt(i).equals(key)) {
+ keys.removeElementAt(i);
+ data.removeElementAt(i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Removes the set of keys from the set.
+ */
+ public void removeAttributes(Enumeration names) {
+ while (names.hasMoreElements()) {
+ Object name = names.nextElement();
+ removeAttribute(name);
+ }
+ }
+
+ /**
+ * Removes the set of matching attributes from the set.
+ */
+ public void removeAttributes(AttributeSet attr) {
+ Enumeration names = attr.getAttributeNames();
+ while (names.hasMoreElements()) {
+ Object name = names.nextElement();
+ Object value = attr.getAttribute(name);
+ removeSearchAttribute(name, value);
+ }
+ }
+
+ private void removeSearchAttribute(Object ikey, Object value) {
+ int n = keys.size();
+ for (int i = 0; i < n; i++) {
+ if (keys.elementAt(i).equals(ikey)) {
+ if (data.elementAt(i).equals(value)) {
+ keys.removeElementAt(i);
+ data.removeElementAt(i);
+ }
+ return;
+ }
+ }
+ }
+
+ private Vector keys = new Vector();
+ private Vector data = new Vector();
+ }
+
+ /**
+ * key for a font table
+ */
+ static class FontKey {
+
+ private String family;
+ private int style;
+ private int size;
+
+ /**
+ * Constructs a font key.
+ */
+ public FontKey(String family, int style, int size) {
+ setValue(family, style, size);
+ }
+
+ public void setValue(String family, int style, int size) {
+ this.family = (family != null) ? family.intern() : null;
+ this.style = style;
+ this.size = size;
+ }
+
+ /**
+ * Returns a hashcode for this font.
+ * @return a hashcode value for this font.
+ */
+ public int hashCode() {
+ int fhash = (family != null) ? family.hashCode() : 0;
+ return fhash ^ style ^ size;
+ }
+
+ /**
+ * Compares this object to the specifed object.
+ * The result is <code>true</code> if and only if the argument is not
+ * <code>null</code> and is a <code>Font</code> object with the same
+ * name, style, and point size as this font.
+ * @param obj the object to compare this font with.
+ * @return <code>true</code> if the objects are equal;
+ * <code>false</code> otherwise.
+ */
+ public boolean equals(Object obj) {
+ if (obj instanceof FontKey) {
+ FontKey font = (FontKey)obj;
+ return (size == font.size) && (style == font.style) && (family == font.family);
+ }
+ return false;
+ }
+
+ }
+
+ /**
+ * A collection of attributes, typically used to represent
+ * character and paragraph styles. This is an implementation
+ * of MutableAttributeSet that can be observed if desired.
+ * These styles will take advantage of immutability while
+ * the sets are small enough, and may be substantially more
+ * efficient than something like SimpleAttributeSet.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public class NamedStyle implements Style, Serializable {
+
+ /**
+ * Creates a new named style.
+ *
+ * @param name the style name, null for unnamed
+ * @param parent the parent style, null if none
+ * @since 1.4
+ */
+ public NamedStyle(String name, Style parent) {
+ attributes = getEmptySet();
+ if (name != null) {
+ setName(name);
+ }
+ if (parent != null) {
+ setResolveParent(parent);
+ }
+ }
+
+ /**
+ * Creates a new named style.
+ *
+ * @param parent the parent style, null if none
+ * @since 1.4
+ */
+ public NamedStyle(Style parent) {
+ this(null, parent);
+ }
+
+ /**
+ * Creates a new named style, with a null name and parent.
+ */
+ public NamedStyle() {
+ attributes = getEmptySet();
+ }
+
+ /**
+ * Converts the style to a string.
+ *
+ * @return the string
+ */
+ public String toString() {
+ return "NamedStyle:" + getName() + " " + attributes;
+ }
+
+ /**
+ * Fetches the name of the style. A style is not required to be named,
+ * so null is returned if there is no name associated with the style.
+ *
+ * @return the name
+ */
+ public String getName() {
+ if (isDefined(StyleConstants.NameAttribute)) {
+ return getAttribute(StyleConstants.NameAttribute).toString();
+ }
+ return null;
+ }
+
+ /**
+ * Changes the name of the style. Does nothing with a null name.
+ *
+ * @param name the new name
+ */
+ public void setName(String name) {
+ if (name != null) {
+ this.addAttribute(StyleConstants.NameAttribute, name);
+ }
+ }
+
+ /**
+ * Adds a change listener.
+ *
+ * @param l the change listener
+ */
+ public void addChangeListener(ChangeListener l) {
+ listenerList.add(ChangeListener.class, l);
+ }
+
+ /**
+ * Removes a change listener.
+ *
+ * @param l the change listener
+ */
+ public void removeChangeListener(ChangeListener l) {
+ listenerList.remove(ChangeListener.class, l);
+ }
+
+
+ /**
+ * Returns an array of all the <code>ChangeListener</code>s added
+ * to this NamedStyle with addChangeListener().
+ *
+ * @return all of the <code>ChangeListener</code>s added or an empty
+ * array if no listeners have been added
+ * @since 1.4
+ */
+ public ChangeListener[] getChangeListeners() {
+ return (ChangeListener[])listenerList.getListeners(
+ ChangeListener.class);
+ }
+
+
+ /**
+ * 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.
+ *
+ * @see EventListenerList
+ */
+ protected void fireStateChanged() {
+ // Guaranteed to return a non-null array
+ Object[] listeners = listenerList.getListenerList();
+ // Process the listeners last to first, notifying
+ // those that are interested in this event
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==ChangeListener.class) {
+ // Lazily create the event:
+ if (changeEvent == null)
+ changeEvent = new ChangeEvent(this);
+ ((ChangeListener)listeners[i+1]).stateChanged(changeEvent);
+ }
+ }
+ }
+
+ /**
+ * Return an array of all the listeners of the given type that
+ * were added to this model.
+ *
+ * @return all of the objects receiving <em>listenerType</em> notifications
+ * from this model
+ *
+ * @since 1.3
+ */
+ public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
+ return listenerList.getListeners(listenerType);
+ }
+
+ // --- AttributeSet ----------------------------
+ // delegated to the immutable field "attributes"
+
+ /**
+ * Gets the number of attributes that are defined.
+ *
+ * @return the number of attributes >= 0
+ * @see AttributeSet#getAttributeCount
+ */
+ public int getAttributeCount() {
+ return attributes.getAttributeCount();
+ }
+
+ /**
+ * Checks whether a given attribute is defined.
+ *
+ * @param attrName the non-null attribute name
+ * @return true if the attribute is defined
+ * @see AttributeSet#isDefined
+ */
+ public boolean isDefined(Object attrName) {
+ return attributes.isDefined(attrName);
+ }
+
+ /**
+ * 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 attributes.isEqual(attr);
+ }
+
+ /**
+ * Copies a set of attributes.
+ *
+ * @return the copy
+ * @see AttributeSet#copyAttributes
+ */
+ public AttributeSet copyAttributes() {
+ NamedStyle a = new NamedStyle();
+ a.attributes = attributes.copyAttributes();
+ return a;
+ }
+
+ /**
+ * Gets the value of an attribute.
+ *
+ * @param attrName the non-null attribute name
+ * @return the attribute value
+ * @see AttributeSet#getAttribute
+ */
+ public Object getAttribute(Object attrName) {
+ return attributes.getAttribute(attrName);
+ }
+
+ /**
+ * Gets the names of all attributes.
+ *
+ * @return the attribute names as an enumeration
+ * @see AttributeSet#getAttributeNames
+ */
+ public Enumeration<?> getAttributeNames() {
+ return attributes.getAttributeNames();
+ }
+
+ /**
+ * Checks whether a given attribute name/value is defined.
+ *
+ * @param name the non-null 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 attributes.containsAttribute(name, value);
+ }
+
+
+ /**
+ * Checks whether the element contains all the attributes.
+ *
+ * @param attrs the attributes to check
+ * @return true if the element contains all the attributes
+ * @see AttributeSet#containsAttributes
+ */
+ public boolean containsAttributes(AttributeSet attrs) {
+ return attributes.containsAttributes(attrs);
+ }
+
+ /**
+ * Gets attributes from the parent.
+ * If not overriden, the resolving parent defaults to
+ * the parent element.
+ *
+ * @return the attributes from the parent
+ * @see AttributeSet#getResolveParent
+ */
+ public AttributeSet getResolveParent() {
+ return attributes.getResolveParent();
+ }
+
+ // --- MutableAttributeSet ----------------------------------
+ // should fetch a new immutable record for the field
+ // "attributes".
+
+ /**
+ * Adds an attribute.
+ *
+ * @param name the non-null attribute name
+ * @param value the attribute value
+ * @see MutableAttributeSet#addAttribute
+ */
+ public void addAttribute(Object name, Object value) {
+ StyleContext context = StyleContext.this;
+ attributes = context.addAttribute(attributes, name, value);
+ fireStateChanged();
+ }
+
+ /**
+ * Adds a set of attributes to the element.
+ *
+ * @param attr the attributes to add
+ * @see MutableAttributeSet#addAttribute
+ */
+ public void addAttributes(AttributeSet attr) {
+ StyleContext context = StyleContext.this;
+ attributes = context.addAttributes(attributes, attr);
+ fireStateChanged();
+ }
+
+ /**
+ * Removes an attribute from the set.
+ *
+ * @param name the non-null attribute name
+ * @see MutableAttributeSet#removeAttribute
+ */
+ public void removeAttribute(Object name) {
+ StyleContext context = StyleContext.this;
+ attributes = context.removeAttribute(attributes, name);
+ fireStateChanged();
+ }
+
+ /**
+ * Removes a set of attributes for the element.
+ *
+ * @param names the attribute names
+ * @see MutableAttributeSet#removeAttributes
+ */
+ public void removeAttributes(Enumeration<?> names) {
+ StyleContext context = StyleContext.this;
+ attributes = context.removeAttributes(attributes, names);
+ fireStateChanged();
+ }
+
+ /**
+ * Removes a set of attributes for the element.
+ *
+ * @param attrs the attributes
+ * @see MutableAttributeSet#removeAttributes
+ */
+ public void removeAttributes(AttributeSet attrs) {
+ StyleContext context = StyleContext.this;
+ if (attrs == this) {
+ attributes = context.getEmptySet();
+ } else {
+ attributes = context.removeAttributes(attributes, attrs);
+ }
+ fireStateChanged();
+ }
+
+ /**
+ * Sets the resolving parent.
+ *
+ * @param parent the parent, null if none
+ * @see MutableAttributeSet#setResolveParent
+ */
+ public void setResolveParent(AttributeSet parent) {
+ if (parent != null) {
+ addAttribute(StyleConstants.ResolveAttribute, parent);
+ } else {
+ removeAttribute(StyleConstants.ResolveAttribute);
+ }
+ }
+
+ // --- serialization ---------------------------------------------
+
+ private void writeObject(ObjectOutputStream s) throws IOException {
+ s.defaultWriteObject();
+ writeAttributeSet(s, attributes);
+ }
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException
+ {
+ s.defaultReadObject();
+ attributes = SimpleAttributeSet.EMPTY;
+ readAttributeSet(s, this);
+ }
+
+ // --- member variables -----------------------------------------------
+
+ /**
+ * The change listeners for the model.
+ */
+ protected EventListenerList listenerList = new EventListenerList();
+
+ /**
+ * Only one ChangeEvent is needed per model instance since the
+ * event's only (read-only) state is the source property. The source
+ * of events generated here is always "this".
+ */
+ protected transient ChangeEvent changeEvent = null;
+
+ /**
+ * Inner AttributeSet implementation, which may be an
+ * immutable unique set being shared.
+ */
+ private transient AttributeSet attributes;
+
+ }
+
+ static {
+ // initialize the static key registry with the StyleConstants keys
+ try {
+ int n = StyleConstants.keys.length;
+ for (int i = 0; i < n; i++) {
+ StyleContext.registerStaticAttributeKey(StyleConstants.keys[i]);
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+
+}
diff --git a/src/share/classes/javax/swing/text/StyledDocument.java b/src/share/classes/javax/swing/text/StyledDocument.java
new file mode 100644
index 000000000..b0763c14f
--- /dev/null
+++ b/src/share/classes/javax/swing/text/StyledDocument.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 1997-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;
+
+import java.awt.Font;
+import java.awt.Color;
+
+/**
+ * Interface for a generic styled document.
+ *
+ * @author Timothy Prinzing
+ */
+public interface StyledDocument extends Document {
+
+ /**
+ * Adds a new style into the logical style hierarchy. Style attributes
+ * resolve from bottom up so an attribute specified in a child
+ * will override an attribute specified in the parent.
+ *
+ * @param nm the name of the style (must be unique within the
+ * collection of named styles). The name may be null if the style
+ * is unnamed, but the caller is responsible
+ * for managing the reference returned as an unnamed style can't
+ * be fetched by name. An unnamed style may be useful for things
+ * like character attribute overrides such as found in a style
+ * run.
+ * @param parent the parent style. This may be null if unspecified
+ * attributes need not be resolved in some other style.
+ * @return the style
+ */
+ public Style addStyle(String nm, Style parent);
+
+ /**
+ * Removes a named style previously added to the document.
+ *
+ * @param nm the name of the style to remove
+ */
+ public void removeStyle(String nm);
+
+ /**
+ * Fetches a named style previously added.
+ *
+ * @param nm the name of the style
+ * @return the style
+ */
+ public Style getStyle(String nm);
+
+ /**
+ * Changes the content element attributes used for the given range of
+ * existing content in the document. All of the attributes
+ * defined in the given Attributes argument are applied to the
+ * given range. This method can be used to completely remove
+ * all content level attributes for the given range by
+ * giving an Attributes argument that has no attributes defined
+ * and setting replace to true.
+ *
+ * @param offset the start of the change >= 0
+ * @param length the length of the change >= 0
+ * @param s the non-null attributes to change to. Any attributes
+ * defined will be applied to the text for the given range.
+ * @param replace indicates whether or not the previous
+ * attributes should be cleared before the new attributes
+ * as set. If true, the operation will replace the
+ * previous attributes entirely. If false, the new
+ * attributes will be merged with the previous attributes.
+ */
+ public void setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace);
+
+ /**
+ * Sets paragraph attributes.
+ *
+ * @param offset the start of the change >= 0
+ * @param length the length of the change >= 0
+ * @param s the non-null attributes to change to. Any attributes
+ * defined will be applied to the text for the given range.
+ * @param replace indicates whether or not the previous
+ * attributes should be cleared before the new attributes
+ * are set. If true, the operation will replace the
+ * previous attributes entirely. If false, the new
+ * attributes will be merged with the previous attributes.
+ */
+ public void setParagraphAttributes(int offset, int length, AttributeSet s, boolean replace);
+
+ /**
+ * Sets the logical style to use for the paragraph at the
+ * given position. If attributes aren't explicitly set
+ * for character and paragraph attributes they will resolve
+ * through the logical style assigned to the paragraph, which
+ * in turn may resolve through some hierarchy completely
+ * independent of the element hierarchy in the document.
+ *
+ * @param pos the starting position >= 0
+ * @param s the style to set
+ */
+ public void setLogicalStyle(int pos, Style s);
+
+ /**
+ * Gets a logical style for a given position in a paragraph.
+ *
+ * @param p the position >= 0
+ * @return the style
+ */
+ public Style getLogicalStyle(int p);
+
+ /**
+ * Gets the element that represents the paragraph that
+ * encloses the given offset within the document.
+ *
+ * @param pos the offset >= 0
+ * @return the element
+ */
+ public Element getParagraphElement(int pos);
+
+ /**
+ * Gets the element that represents the character that
+ * is at the given offset within the document.
+ *
+ * @param pos the offset >= 0
+ * @return the element
+ */
+ public Element getCharacterElement(int pos);
+
+
+ /**
+ * 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 attr the set of attributes
+ * @return the color
+ */
+ public Color getForeground(AttributeSet attr);
+
+ /**
+ * 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 attr the set of attributes
+ * @return the color
+ */
+ public Color getBackground(AttributeSet attr);
+
+ /**
+ * Takes a set of attributes and turn it into a font
+ * specification. This can be used to turn things like
+ * family, style, size, etc into a font that is available
+ * on the system the document is currently being used on.
+ *
+ * @param attr the set of attributes
+ * @return the font
+ */
+ public Font getFont(AttributeSet attr);
+
+}
diff --git a/src/share/classes/javax/swing/text/StyledEditorKit.java b/src/share/classes/javax/swing/text/StyledEditorKit.java
new file mode 100644
index 000000000..d841344a0
--- /dev/null
+++ b/src/share/classes/javax/swing/text/StyledEditorKit.java
@@ -0,0 +1,895 @@
+/*
+ * Copyright 1997-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;
+
+import java.io.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import javax.swing.event.*;
+import javax.swing.Action;
+import javax.swing.JEditorPane;
+import javax.swing.KeyStroke;
+import javax.swing.UIManager;
+
+/**
+ * This is the set of things needed by a text component
+ * to be a reasonably functioning editor for some <em>type</em>
+ * of text document. This implementation provides a default
+ * implementation which treats text as styled text and
+ * provides a minimal set of actions for editing styled text.
+ *
+ * @author Timothy Prinzing
+ */
+public class StyledEditorKit extends DefaultEditorKit {
+
+ /**
+ * Creates a new EditorKit used for styled documents.
+ */
+ public StyledEditorKit() {
+ createInputAttributeUpdated();
+ createInputAttributes();
+ }
+
+ /**
+ * Gets the input attributes for the pane. When
+ * the caret moves and there is no selection, the
+ * input attributes are automatically mutated to
+ * reflect the character attributes of the current
+ * caret location. The styled editing actions
+ * use the input attributes to carry out their
+ * actions.
+ *
+ * @return the attribute set
+ */
+ public MutableAttributeSet getInputAttributes() {
+ return inputAttributes;
+ }
+
+ /**
+ * Fetches the element representing the current
+ * run of character attributes for the caret.
+ *
+ * @return the element
+ */
+ public Element getCharacterAttributeRun() {
+ return currentRun;
+ }
+
+ // --- EditorKit methods ---------------------------
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Creates an uninitialized text storage model
+ * that is appropriate for this type of editor.
+ *
+ * @return the model
+ */
+ public Document createDefaultDocument() {
+ return new DefaultStyledDocument();
+ }
+
+ /**
+ * Called when the kit is being installed into
+ * a JEditorPane.
+ *
+ * @param c the JEditorPane
+ */
+ public void install(JEditorPane c) {
+ c.addCaretListener(inputAttributeUpdater);
+ c.addPropertyChangeListener(inputAttributeUpdater);
+ Caret caret = c.getCaret();
+ if (caret != null) {
+ inputAttributeUpdater.updateInputAttributes
+ (caret.getDot(), caret.getMark(), 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.removeCaretListener(inputAttributeUpdater);
+ c.removePropertyChangeListener(inputAttributeUpdater);
+
+ // remove references to current document so it can be collected.
+ currentRun = null;
+ currentParagraph = null;
+ }
+
+ /**
+ * Fetches a factory that is suitable for producing
+ * views of any models that are produced by this
+ * kit. This is implemented to return View implementations
+ * for the following kinds of elements:
+ * <ul>
+ * <li>AbstractDocument.ContentElementName
+ * <li>AbstractDocument.ParagraphElementName
+ * <li>AbstractDocument.SectionElementName
+ * <li>StyleConstants.ComponentElementName
+ * <li>StyleConstants.IconElementName
+ * </ul>
+ *
+ * @return the factory
+ */
+ public ViewFactory getViewFactory() {
+ return defaultFactory;
+ }
+
+ /**
+ * Creates a copy of the editor kit.
+ *
+ * @return the copy
+ */
+ public Object clone() {
+ StyledEditorKit o = (StyledEditorKit)super.clone();
+ o.currentRun = o.currentParagraph = null;
+ o.createInputAttributeUpdated();
+ o.createInputAttributes();
+ return o;
+ }
+
+ /**
+ * Creates the AttributeSet used for the selection.
+ */
+ private void createInputAttributes() {
+ inputAttributes = new SimpleAttributeSet() {
+ public AttributeSet getResolveParent() {
+ return (currentParagraph != null) ?
+ currentParagraph.getAttributes() : null;
+ }
+
+ public Object clone() {
+ return new SimpleAttributeSet(this);
+ }
+ };
+ }
+
+ /**
+ * Creates a new <code>AttributeTracker</code>.
+ */
+ private void createInputAttributeUpdated() {
+ inputAttributeUpdater = new AttributeTracker();
+ }
+
+
+ private static final ViewFactory defaultFactory = new StyledViewFactory();
+
+ Element currentRun;
+ Element currentParagraph;
+
+ /**
+ * This is the set of attributes used to store the
+ * input attributes.
+ */
+ MutableAttributeSet inputAttributes;
+
+ /**
+ * This listener will be attached to the caret of
+ * the text component that the EditorKit gets installed
+ * into. This should keep the input attributes updated
+ * for use by the styled actions.
+ */
+ private AttributeTracker inputAttributeUpdater;
+
+ /**
+ * Tracks caret movement and keeps the input attributes set
+ * to reflect the current set of attribute definitions at the
+ * caret position.
+ * <p>This implements PropertyChangeListener to update the
+ * input attributes when the Document changes, as if the Document
+ * changes the attributes will almost certainly change.
+ */
+ class AttributeTracker implements CaretListener, PropertyChangeListener, Serializable {
+
+ /**
+ * Updates the attributes. <code>dot</code> and <code>mark</code>
+ * mark give the positions of the selection in <code>c</code>.
+ */
+ void updateInputAttributes(int dot, int mark, JTextComponent c) {
+ // EditorKit might not have installed the StyledDocument yet.
+ Document aDoc = c.getDocument();
+ if (!(aDoc instanceof StyledDocument)) {
+ return ;
+ }
+ int start = Math.min(dot, mark);
+ // record current character attributes.
+ StyledDocument doc = (StyledDocument)aDoc;
+ // If nothing is selected, get the attributes from the character
+ // before the start of the selection, otherwise get the attributes
+ // from the character element at the start of the selection.
+ Element run;
+ currentParagraph = doc.getParagraphElement(start);
+ if (currentParagraph.getStartOffset() == start || dot != mark) {
+ // Get the attributes from the character at the selection
+ // if in a different paragrah!
+ run = doc.getCharacterElement(start);
+ }
+ else {
+ run = doc.getCharacterElement(Math.max(start-1, 0));
+ }
+ if (run != currentRun) {
+ /*
+ * PENDING(prinz) All attributes that represent a single
+ * glyph position and can't be inserted into should be
+ * removed from the input attributes... this requires
+ * mixing in an interface to indicate that condition.
+ * When we can add things again this logic needs to be
+ * improved!!
+ */
+ currentRun = run;
+ createInputAttributes(currentRun, getInputAttributes());
+ }
+ }
+
+ public void propertyChange(PropertyChangeEvent evt) {
+ Object newValue = evt.getNewValue();
+ Object source = evt.getSource();
+
+ if ((source instanceof JTextComponent) &&
+ (newValue instanceof Document)) {
+ // New document will have changed selection to 0,0.
+ updateInputAttributes(0, 0, (JTextComponent)source);
+ }
+ }
+
+ public void caretUpdate(CaretEvent e) {
+ updateInputAttributes(e.getDot(), e.getMark(),
+ (JTextComponent)e.getSource());
+ }
+ }
+
+ /**
+ * Copies the key/values in <code>element</code>s AttributeSet into
+ * <code>set</code>. This does not copy component, icon, or element
+ * names attributes. Subclasses may wish to refine what is and what
+ * isn't copied here. But be sure to first remove all the attributes that
+ * are in <code>set</code>.<p>
+ * This is called anytime the caret moves over a different location.
+ *
+ */
+ protected void createInputAttributes(Element element,
+ MutableAttributeSet set) {
+ if (element.getAttributes().getAttributeCount() > 0
+ || element.getEndOffset() - element.getStartOffset() > 1
+ || element.getEndOffset() < element.getDocument().getLength()) {
+ set.removeAttributes(set);
+ set.addAttributes(element.getAttributes());
+ set.removeAttribute(StyleConstants.ComponentAttribute);
+ set.removeAttribute(StyleConstants.IconAttribute);
+ set.removeAttribute(AbstractDocument.ElementNameAttribute);
+ set.removeAttribute(StyleConstants.ComposedTextAttribute);
+ }
+ }
+
+ // ---- default ViewFactory implementation ---------------------
+
+ static class StyledViewFactory implements ViewFactory {
+
+ public View create(Element elem) {
+ String kind = elem.getName();
+ if (kind != null) {
+ if (kind.equals(AbstractDocument.ContentElementName)) {
+ return new LabelView(elem);
+ } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
+ return new ParagraphView(elem);
+ } else if (kind.equals(AbstractDocument.SectionElementName)) {
+ return new BoxView(elem, View.Y_AXIS);
+ } else if (kind.equals(StyleConstants.ComponentElementName)) {
+ return new ComponentView(elem);
+ } else if (kind.equals(StyleConstants.IconElementName)) {
+ return new IconView(elem);
+ }
+ }
+
+ // default to text display
+ return new LabelView(elem);
+ }
+
+ }
+
+ // --- Action implementations ---------------------------------
+
+ private static final Action[] defaultActions = {
+ new FontFamilyAction("font-family-SansSerif", "SansSerif"),
+ new FontFamilyAction("font-family-Monospaced", "Monospaced"),
+ new FontFamilyAction("font-family-Serif", "Serif"),
+ new FontSizeAction("font-size-8", 8),
+ new FontSizeAction("font-size-10", 10),
+ new FontSizeAction("font-size-12", 12),
+ new FontSizeAction("font-size-14", 14),
+ new FontSizeAction("font-size-16", 16),
+ new FontSizeAction("font-size-18", 18),
+ new FontSizeAction("font-size-24", 24),
+ new FontSizeAction("font-size-36", 36),
+ new FontSizeAction("font-size-48", 48),
+ new AlignmentAction("left-justify", StyleConstants.ALIGN_LEFT),
+ new AlignmentAction("center-justify", StyleConstants.ALIGN_CENTER),
+ new AlignmentAction("right-justify", StyleConstants.ALIGN_RIGHT),
+ new BoldAction(),
+ new ItalicAction(),
+ new StyledInsertBreakAction(),
+ new UnderlineAction()
+ };
+
+ /**
+ * An action that assumes it's being fired on a JEditorPane
+ * with a StyledEditorKit (or subclass) installed. This has
+ * some convenience methods for causing character or paragraph
+ * level attribute changes. The convenience methods will
+ * throw an IllegalArgumentException if the assumption of
+ * a StyledDocument, a JEditorPane, or a StyledEditorKit
+ * fail to be true.
+ * <p>
+ * The component that gets acted upon by the action
+ * will be the source of the ActionEvent if the source
+ * can be narrowed to a JEditorPane type. If the source
+ * can't be narrowed, the most recently focused text
+ * component is changed. If neither of these are the
+ * case, the action cannot be performed.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public abstract static class StyledTextAction extends TextAction {
+
+ /**
+ * Creates a new StyledTextAction from a string action name.
+ *
+ * @param nm the name of the action
+ */
+ public StyledTextAction(String nm) {
+ super(nm);
+ }
+
+ /**
+ * Gets the target editor for an action.
+ *
+ * @param e the action event
+ * @return the editor
+ */
+ protected final JEditorPane getEditor(ActionEvent e) {
+ JTextComponent tcomp = getTextComponent(e);
+ if (tcomp instanceof JEditorPane) {
+ return (JEditorPane) tcomp;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the document associated with an editor pane.
+ *
+ * @param e the editor
+ * @return the document
+ * @exception IllegalArgumentException for the wrong document type
+ */
+ protected final StyledDocument getStyledDocument(JEditorPane e) {
+ Document d = e.getDocument();
+ if (d instanceof StyledDocument) {
+ return (StyledDocument) d;
+ }
+ throw new IllegalArgumentException("document must be StyledDocument");
+ }
+
+ /**
+ * Gets the editor kit associated with an editor pane.
+ *
+ * @param e the editor pane
+ * @return the kit
+ * @exception IllegalArgumentException for the wrong document type
+ */
+ protected final StyledEditorKit getStyledEditorKit(JEditorPane e) {
+ EditorKit k = e.getEditorKit();
+ if (k instanceof StyledEditorKit) {
+ return (StyledEditorKit) k;
+ }
+ throw new IllegalArgumentException("EditorKit must be StyledEditorKit");
+ }
+
+ /**
+ * Applies the given attributes to character
+ * content. If there is a selection, the attributes
+ * are applied to the selection range. If there
+ * is no selection, the attributes are applied to
+ * the input attribute set which defines the attributes
+ * for any new text that gets inserted.
+ *
+ * @param editor the editor
+ * @param attr the attributes
+ * @param replace if true, then replace the existing attributes first
+ */
+ protected final void setCharacterAttributes(JEditorPane editor,
+ AttributeSet attr, boolean replace) {
+ int p0 = editor.getSelectionStart();
+ int p1 = editor.getSelectionEnd();
+ if (p0 != p1) {
+ StyledDocument doc = getStyledDocument(editor);
+ doc.setCharacterAttributes(p0, p1 - p0, attr, replace);
+ }
+ StyledEditorKit k = getStyledEditorKit(editor);
+ MutableAttributeSet inputAttributes = k.getInputAttributes();
+ if (replace) {
+ inputAttributes.removeAttributes(inputAttributes);
+ }
+ inputAttributes.addAttributes(attr);
+ }
+
+ /**
+ * Applies the given attributes to paragraphs. If
+ * there is a selection, the attributes are applied
+ * to the paragraphs that intersect the selection.
+ * if there is no selection, the attributes are applied
+ * to the paragraph at the current caret position.
+ *
+ * @param editor the editor
+ * @param attr the attributes
+ * @param replace if true, replace the existing attributes first
+ */
+ protected final void setParagraphAttributes(JEditorPane editor,
+ AttributeSet attr, boolean replace) {
+ int p0 = editor.getSelectionStart();
+ int p1 = editor.getSelectionEnd();
+ StyledDocument doc = getStyledDocument(editor);
+ doc.setParagraphAttributes(p0, p1 - p0, attr, replace);
+ }
+
+ }
+
+ /**
+ * An action to set the font family in the associated
+ * JEditorPane. This will use the family specified as
+ * the command string on the ActionEvent if there is one,
+ * otherwise the family that was initialized with will be used.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class FontFamilyAction extends StyledTextAction {
+
+ /**
+ * Creates a new FontFamilyAction.
+ *
+ * @param nm the action name
+ * @param family the font family
+ */
+ public FontFamilyAction(String nm, String family) {
+ super(nm);
+ this.family = family;
+ }
+
+ /**
+ * Sets the font family.
+ *
+ * @param e the event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JEditorPane editor = getEditor(e);
+ if (editor != null) {
+ String family = this.family;
+ if ((e != null) && (e.getSource() == editor)) {
+ String s = e.getActionCommand();
+ if (s != null) {
+ family = s;
+ }
+ }
+ if (family != null) {
+ MutableAttributeSet attr = new SimpleAttributeSet();
+ StyleConstants.setFontFamily(attr, family);
+ setCharacterAttributes(editor, attr, false);
+ } else {
+ UIManager.getLookAndFeel().provideErrorFeedback(editor);
+ }
+ }
+ }
+
+ private String family;
+ }
+
+ /**
+ * An action to set the font size in the associated
+ * JEditorPane. This will use the size specified as
+ * the command string on the ActionEvent if there is one,
+ * otherwise the size that was initialized with will be used.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class FontSizeAction extends StyledTextAction {
+
+ /**
+ * Creates a new FontSizeAction.
+ *
+ * @param nm the action name
+ * @param size the font size
+ */
+ public FontSizeAction(String nm, int size) {
+ super(nm);
+ this.size = size;
+ }
+
+ /**
+ * Sets the font size.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JEditorPane editor = getEditor(e);
+ if (editor != null) {
+ int size = this.size;
+ if ((e != null) && (e.getSource() == editor)) {
+ String s = e.getActionCommand();
+ try {
+ size = Integer.parseInt(s, 10);
+ } catch (NumberFormatException nfe) {
+ }
+ }
+ if (size != 0) {
+ MutableAttributeSet attr = new SimpleAttributeSet();
+ StyleConstants.setFontSize(attr, size);
+ setCharacterAttributes(editor, attr, false);
+ } else {
+ UIManager.getLookAndFeel().provideErrorFeedback(editor);
+ }
+ }
+ }
+
+ private int size;
+ }
+
+ /**
+ * An action to set foreground color. This sets the
+ * <code>StyleConstants.Foreground</code> attribute for the
+ * currently selected range of the target JEditorPane.
+ * This is done by calling
+ * <code>StyledDocument.setCharacterAttributes</code>
+ * on the styled document associated with the target
+ * JEditorPane.
+ * <p>
+ * If the target text component is specified as the
+ * source of the ActionEvent and there is a command string,
+ * the command string will be interpreted as the foreground
+ * color. It will be interpreted by called
+ * <code>Color.decode</code>, and should therefore be
+ * legal input for that method.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class ForegroundAction extends StyledTextAction {
+
+ /**
+ * Creates a new ForegroundAction.
+ *
+ * @param nm the action name
+ * @param fg the foreground color
+ */
+ public ForegroundAction(String nm, Color fg) {
+ super(nm);
+ this.fg = fg;
+ }
+
+ /**
+ * Sets the foreground color.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JEditorPane editor = getEditor(e);
+ if (editor != null) {
+ Color fg = this.fg;
+ if ((e != null) && (e.getSource() == editor)) {
+ String s = e.getActionCommand();
+ try {
+ fg = Color.decode(s);
+ } catch (NumberFormatException nfe) {
+ }
+ }
+ if (fg != null) {
+ MutableAttributeSet attr = new SimpleAttributeSet();
+ StyleConstants.setForeground(attr, fg);
+ setCharacterAttributes(editor, attr, false);
+ } else {
+ UIManager.getLookAndFeel().provideErrorFeedback(editor);
+ }
+ }
+ }
+
+ private Color fg;
+ }
+
+ /**
+ * An action to set paragraph alignment. This sets the
+ * <code>StyleConstants.Alignment</code> attribute for the
+ * currently selected range of the target JEditorPane.
+ * This is done by calling
+ * <code>StyledDocument.setParagraphAttributes</code>
+ * on the styled document associated with the target
+ * JEditorPane.
+ * <p>
+ * If the target text component is specified as the
+ * source of the ActionEvent and there is a command string,
+ * the command string will be interpreted as an integer
+ * that should be one of the legal values for the
+ * <code>StyleConstants.Alignment</code> attribute.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class AlignmentAction extends StyledTextAction {
+
+ /**
+ * Creates a new AlignmentAction.
+ *
+ * @param nm the action name
+ * @param a the alignment >= 0
+ */
+ public AlignmentAction(String nm, int a) {
+ super(nm);
+ this.a = a;
+ }
+
+ /**
+ * Sets the alignment.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JEditorPane editor = getEditor(e);
+ if (editor != null) {
+ int a = this.a;
+ if ((e != null) && (e.getSource() == editor)) {
+ String s = e.getActionCommand();
+ try {
+ a = Integer.parseInt(s, 10);
+ } catch (NumberFormatException nfe) {
+ }
+ }
+ MutableAttributeSet attr = new SimpleAttributeSet();
+ StyleConstants.setAlignment(attr, a);
+ setParagraphAttributes(editor, attr, false);
+ }
+ }
+
+ private int a;
+ }
+
+ /**
+ * An action to toggle the bold attribute.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class BoldAction extends StyledTextAction {
+
+ /**
+ * Constructs a new BoldAction.
+ */
+ public BoldAction() {
+ super("font-bold");
+ }
+
+ /**
+ * Toggles the bold attribute.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JEditorPane editor = getEditor(e);
+ if (editor != null) {
+ StyledEditorKit kit = getStyledEditorKit(editor);
+ MutableAttributeSet attr = kit.getInputAttributes();
+ boolean bold = (StyleConstants.isBold(attr)) ? false : true;
+ SimpleAttributeSet sas = new SimpleAttributeSet();
+ StyleConstants.setBold(sas, bold);
+ setCharacterAttributes(editor, sas, false);
+ }
+ }
+ }
+
+ /**
+ * An action to toggle the italic attribute.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class ItalicAction extends StyledTextAction {
+
+ /**
+ * Constructs a new ItalicAction.
+ */
+ public ItalicAction() {
+ super("font-italic");
+ }
+
+ /**
+ * Toggles the italic attribute.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JEditorPane editor = getEditor(e);
+ if (editor != null) {
+ StyledEditorKit kit = getStyledEditorKit(editor);
+ MutableAttributeSet attr = kit.getInputAttributes();
+ boolean italic = (StyleConstants.isItalic(attr)) ? false : true;
+ SimpleAttributeSet sas = new SimpleAttributeSet();
+ StyleConstants.setItalic(sas, italic);
+ setCharacterAttributes(editor, sas, false);
+ }
+ }
+ }
+
+ /**
+ * An action to toggle the underline attribute.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ */
+ public static class UnderlineAction extends StyledTextAction {
+
+ /**
+ * Constructs a new UnderlineAction.
+ */
+ public UnderlineAction() {
+ super("font-underline");
+ }
+
+ /**
+ * Toggles the Underline attribute.
+ *
+ * @param e the action event
+ */
+ public void actionPerformed(ActionEvent e) {
+ JEditorPane editor = getEditor(e);
+ if (editor != null) {
+ StyledEditorKit kit = getStyledEditorKit(editor);
+ MutableAttributeSet attr = kit.getInputAttributes();
+ boolean underline = (StyleConstants.isUnderline(attr)) ? false : true;
+ SimpleAttributeSet sas = new SimpleAttributeSet();
+ StyleConstants.setUnderline(sas, underline);
+ setCharacterAttributes(editor, sas, false);
+ }
+ }
+ }
+
+
+ /**
+ * StyledInsertBreakAction has similar behavior to that of
+ * <code>DefaultEditorKit.InsertBreakAction</code>. That is when
+ * its <code>actionPerformed</code> method is invoked, a newline
+ * is inserted. Beyond that, this will reset the input attributes to
+ * what they were before the newline was inserted.
+ */
+ static class StyledInsertBreakAction extends StyledTextAction {
+ private SimpleAttributeSet tempSet;
+
+ StyledInsertBreakAction() {
+ super(insertBreakAction);
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ JEditorPane target = getEditor(e);
+
+ if (target != null) {
+ if ((!target.isEditable()) || (!target.isEnabled())) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ return;
+ }
+ StyledEditorKit sek = getStyledEditorKit(target);
+
+ if (tempSet != null) {
+ tempSet.removeAttributes(tempSet);
+ }
+ else {
+ tempSet = new SimpleAttributeSet();
+ }
+ tempSet.addAttributes(sek.getInputAttributes());
+ target.replaceSelection("\n");
+
+ MutableAttributeSet ia = sek.getInputAttributes();
+
+ ia.removeAttributes(ia);
+ ia.addAttributes(tempSet);
+ tempSet.removeAttributes(tempSet);
+ }
+ else {
+ // See if we are in a JTextComponent.
+ JTextComponent text = getTextComponent(e);
+
+ if (text != null) {
+ if ((!text.isEditable()) || (!text.isEnabled())) {
+ UIManager.getLookAndFeel().provideErrorFeedback(target);
+ return;
+ }
+ text.replaceSelection("\n");
+ }
+ }
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/TabExpander.java b/src/share/classes/javax/swing/text/TabExpander.java
new file mode 100644
index 000000000..c3a23d455
--- /dev/null
+++ b/src/share/classes/javax/swing/text/TabExpander.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 1997-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;
+
+
+/**
+ * Simple interface to allow for different types of
+ * implementations of tab expansion.
+ *
+ * @author Timothy Prinzing
+ */
+public interface TabExpander {
+
+ /**
+ * Returns the next tab stop position given a reference
+ * position. Values are expressed in points.
+ *
+ * @param x the position in points >= 0
+ * @param tabOffset the position within the text stream
+ * that the tab occurred at >= 0.
+ * @return the next tab stop >= 0
+ */
+ float nextTabStop(float x, int tabOffset);
+
+}
diff --git a/src/share/classes/javax/swing/text/TabSet.java b/src/share/classes/javax/swing/text/TabSet.java
new file mode 100644
index 000000000..6b2cc9041
--- /dev/null
+++ b/src/share/classes/javax/swing/text/TabSet.java
@@ -0,0 +1,212 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+
+/**
+ * A TabSet is comprised of many TabStops. It offers methods for locating the
+ * closest TabStop to a given position and finding all the potential TabStops.
+ * It is also immutable.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Scott Violet
+ */
+public class TabSet implements Serializable
+{
+ /** TabStops this TabSet contains. */
+ private TabStop[] tabs;
+ /**
+ * Since this class is immutable the hash code could be
+ * calculated once. MAX_VALUE means that it was not initialized
+ * yet. Hash code shouldn't has MAX_VALUE value.
+ */
+ private int hashCode = Integer.MAX_VALUE;
+
+ /**
+ * Creates and returns an instance of TabSet. The array of Tabs
+ * passed in must be sorted in ascending order.
+ */
+ public TabSet(TabStop[] tabs) {
+ // PENDING(sky): If this becomes a problem, make it sort.
+ if(tabs != null) {
+ int tabCount = tabs.length;
+
+ this.tabs = new TabStop[tabCount];
+ System.arraycopy(tabs, 0, this.tabs, 0, tabCount);
+ }
+ else
+ this.tabs = null;
+ }
+
+ /**
+ * Returns the number of Tab instances the receiver contains.
+ */
+ public int getTabCount() {
+ return (tabs == null) ? 0 : tabs.length;
+ }
+
+ /**
+ * Returns the TabStop at index <code>index</code>. This will throw an
+ * IllegalArgumentException if <code>index</code> is outside the range
+ * of tabs.
+ */
+ public TabStop getTab(int index) {
+ int numTabs = getTabCount();
+
+ if(index < 0 || index >= numTabs)
+ throw new IllegalArgumentException(index +
+ " is outside the range of tabs");
+ return tabs[index];
+ }
+
+ /**
+ * Returns the Tab instance after <code>location</code>. This will
+ * return null if there are no tabs after <code>location</code>.
+ */
+ public TabStop getTabAfter(float location) {
+ int index = getTabIndexAfter(location);
+
+ return (index == -1) ? null : tabs[index];
+ }
+
+ /**
+ * @return the index of the TabStop <code>tab</code>, or -1 if
+ * <code>tab</code> is not contained in the receiver.
+ */
+ public int getTabIndex(TabStop tab) {
+ for(int counter = getTabCount() - 1; counter >= 0; counter--)
+ // should this use .equals?
+ if(getTab(counter) == tab)
+ return counter;
+ return -1;
+ }
+
+ /**
+ * Returns the index of the Tab to be used after <code>location</code>.
+ * This will return -1 if there are no tabs after <code>location</code>.
+ */
+ public int getTabIndexAfter(float location) {
+ int current, min, max;
+
+ min = 0;
+ max = getTabCount();
+ while(min != max) {
+ current = (max - min) / 2 + min;
+ if(location > tabs[current].getPosition()) {
+ if(min == current)
+ min = max;
+ else
+ min = current;
+ }
+ else {
+ if(current == 0 || location > tabs[current - 1].getPosition())
+ return current;
+ max = current;
+ }
+ }
+ // no tabs after the passed in location.
+ return -1;
+ }
+
+ /**
+ * Indicates whether this <code>TabSet</code> is equal to another one.
+ * @param o the <code>TabSet</code> instance which this instance
+ * should be compared to.
+ * @return <code>true</code> if <code>o</code> is the instance of
+ * <code>TabSet</code>, has the same number of <code>TabStop</code>s
+ * and they are all equal, <code>false</code> otherwise.
+ *
+ * @since 1.5
+ */
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof TabSet) {
+ TabSet ts = (TabSet) o;
+ int count = getTabCount();
+ if (ts.getTabCount() != count) {
+ return false;
+ }
+ for (int i=0; i < count; i++) {
+ TabStop ts1 = getTab(i);
+ TabStop ts2 = ts.getTab(i);
+ if ((ts1 == null && ts2 != null) ||
+ (ts1 != null && !getTab(i).equals(ts.getTab(i)))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a hashcode for this set of TabStops.
+ * @return a hashcode value for this set of TabStops.
+ *
+ * @since 1.5
+ */
+ public int hashCode() {
+ if (hashCode == Integer.MAX_VALUE) {
+ hashCode = 0;
+ int len = getTabCount();
+ for (int i = 0; i < len; i++) {
+ TabStop ts = getTab(i);
+ hashCode ^= ts != null ? getTab(i).hashCode() : 0;
+ }
+ if (hashCode == Integer.MAX_VALUE) {
+ hashCode -= 1;
+ }
+ }
+ return hashCode;
+ }
+
+ /**
+ * Returns the string representation of the set of tabs.
+ */
+ public String toString() {
+ int tabCount = getTabCount();
+ StringBuffer buffer = new StringBuffer("[ ");
+
+ for(int counter = 0; counter < tabCount; counter++) {
+ if(counter > 0)
+ buffer.append(" - ");
+ buffer.append(getTab(counter).toString());
+ }
+ buffer.append(" ]");
+ return buffer.toString();
+ }
+}
diff --git a/src/share/classes/javax/swing/text/TabStop.java b/src/share/classes/javax/swing/text/TabStop.java
new file mode 100644
index 000000000..c74c35963
--- /dev/null
+++ b/src/share/classes/javax/swing/text/TabStop.java
@@ -0,0 +1,177 @@
+/*
+ * 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;
+
+import java.io.Serializable;
+
+/**
+ * This class encapsulates a single tab stop (basically as tab stops
+ * are thought of by RTF). A tab stop is at a specified distance from the
+ * left margin, aligns text in a specified way, and has a specified leader.
+ * TabStops are immutable, and usually contained in TabSets.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ */
+public class TabStop implements Serializable {
+
+ /** Character following tab is positioned at location. */
+ public static final int ALIGN_LEFT = 0;
+ /** Characters following tab are positioned such that all following
+ * characters up to next tab/newline end at location. */
+ public static final int ALIGN_RIGHT = 1;
+ /** Characters following tab are positioned such that all following
+ * characters up to next tab/newline are centered around the tabs
+ * location. */
+ public static final int ALIGN_CENTER = 2;
+ /** Characters following tab are aligned such that next
+ * decimal/tab/newline is at the tab location, very similar to
+ * RIGHT_TAB, just includes decimal as additional character to look for.
+ */
+ public static final int ALIGN_DECIMAL = 4;
+ public static final int ALIGN_BAR = 5;
+
+ /* Bar tabs (whatever they are) are actually a separate kind of tab
+ in the RTF spec. However, being a bar tab and having alignment
+ properties are mutually exclusive, so the reader treats barness
+ as being a kind of alignment. */
+
+ public static final int LEAD_NONE = 0;
+ public static final int LEAD_DOTS = 1;
+ public static final int LEAD_HYPHENS = 2;
+ public static final int LEAD_UNDERLINE = 3;
+ public static final int LEAD_THICKLINE = 4;
+ public static final int LEAD_EQUALS = 5;
+
+ /** Tab type. */
+ private int alignment;
+ /** Location, from the left margin, that tab is at. */
+ private float position;
+ private int leader;
+
+ /**
+ * Creates a tab at position <code>pos</code> with a default alignment
+ * and default leader.
+ */
+ public TabStop(float pos) {
+ this(pos, ALIGN_LEFT, LEAD_NONE);
+ }
+
+ /**
+ * Creates a tab with the specified position <code>pos</code>,
+ * alignment <code>align</code> and leader <code>leader</code>.
+ */
+ public TabStop(float pos, int align, int leader) {
+ alignment = align;
+ this.leader = leader;
+ position = pos;
+ }
+
+ /**
+ * Returns the position, as a float, of the tab.
+ * @return the position of the tab
+ */
+ public float getPosition() {
+ return position;
+ }
+
+ /**
+ * Returns the alignment, as an integer, of the tab.
+ * @return the alignment of the tab
+ */
+ public int getAlignment() {
+ return alignment;
+ }
+
+ /**
+ * Returns the leader of the tab.
+ * @return the leader of the tab
+ */
+ public int getLeader() {
+ return leader;
+ }
+
+ /**
+ * Returns true if the tabs are equal.
+ * @return true if the tabs are equal, otherwise false
+ */
+ public boolean equals(Object other)
+ {
+ if (other == this) {
+ return true;
+ }
+ if (other instanceof TabStop) {
+ TabStop o = (TabStop)other;
+ return ( (alignment == o.alignment) &&
+ (leader == o.leader) &&
+ (position == o.position) ); /* TODO: epsilon */
+ }
+ return false;
+ }
+
+ /**
+ * Returns the hashCode for the object. This must be defined
+ * here to ensure 100% pure.
+ *
+ * @return the hashCode for the object
+ */
+ public int hashCode() {
+ return alignment ^ leader ^ Math.round(position);
+ }
+
+ /* This is for debugging; perhaps it should be removed before release */
+ public String toString() {
+ String buf;
+ switch(alignment) {
+ default:
+ case ALIGN_LEFT:
+ buf = "";
+ break;
+ case ALIGN_RIGHT:
+ buf = "right ";
+ break;
+ case ALIGN_CENTER:
+ buf = "center ";
+ break;
+ case ALIGN_DECIMAL:
+ buf = "decimal ";
+ break;
+ case ALIGN_BAR:
+ buf = "bar ";
+ break;
+ }
+ buf = buf + "tab @" + String.valueOf(position);
+ if (leader != LEAD_NONE)
+ buf = buf + " (w/leaders)";
+ return buf;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/TabableView.java b/src/share/classes/javax/swing/text/TabableView.java
new file mode 100644
index 000000000..b52825503
--- /dev/null
+++ b/src/share/classes/javax/swing/text/TabableView.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+
+/**
+ * Interface for <code>View</code>s that have size dependent upon tabs.
+ *
+ * @author Timothy Prinzing
+ * @author Scott Violet
+ * @see TabExpander
+ * @see LabelView
+ * @see ParagraphView
+ */
+public interface TabableView {
+
+ /**
+ * Determines the desired span when using the given
+ * tab expansion implementation. If a container
+ * calls this method, it will do so prior to the
+ * normal layout which would call getPreferredSpan.
+ * A view implementing this should give the same
+ * result in any subsequent calls to getPreferredSpan
+ * along the axis of tab expansion.
+ *
+ * @param x the position the view would be located
+ * at for the purpose of tab expansion >= 0.
+ * @param e how to expand the tabs when encountered.
+ * @return the desired span >= 0
+ */
+ float getTabbedSpan(float x, TabExpander e);
+
+ /**
+ * Determines the span along the same axis as tab
+ * expansion for a portion of the view. This is
+ * intended for use by the TabExpander for cases
+ * where the tab expansion involves aligning the
+ * portion of text that doesn't have whitespace
+ * relative to the tab stop. There is therefore
+ * an assumption that the range given does not
+ * contain tabs.
+ *
+ * @param p0 the starting location in the text document >= 0
+ * @param p1 the ending location in the text document >= p0
+ * @return the span >= 0
+ */
+ float getPartialSpan(int p0, int p1);
+}
diff --git a/src/share/classes/javax/swing/text/TableView.java b/src/share/classes/javax/swing/text/TableView.java
new file mode 100644
index 000000000..b2d8ee057
--- /dev/null
+++ b/src/share/classes/javax/swing/text/TableView.java
@@ -0,0 +1,908 @@
+/*
+ * 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;
+
+import java.awt.*;
+import java.util.BitSet;
+import java.util.Vector;
+import javax.swing.SizeRequirements;
+import javax.swing.event.DocumentEvent;
+
+import javax.swing.text.html.HTML;
+
+/**
+ * <p>
+ * Implements View interface for a table, that is composed of an
+ * element structure where the child elements of the element
+ * this view is responsible for represent rows and the child
+ * elements of the row elements are cells. The cell elements can
+ * have an arbitrary element structure under them, which will
+ * be built with the ViewFactory returned by the getViewFactory
+ * method.
+ * <pre>
+ *
+ * &nbsp; TABLE
+ * &nbsp; ROW
+ * &nbsp; CELL
+ * &nbsp; CELL
+ * &nbsp; ROW
+ * &nbsp; CELL
+ * &nbsp; CELL
+ *
+ * </pre>
+ * <p>
+ * This is implemented as a hierarchy of boxes, the table itself
+ * is a vertical box, the rows are horizontal boxes, and the cells
+ * are vertical boxes. The cells are allowed to span multiple
+ * columns and rows. By default, the table can be thought of as
+ * being formed over a grid (i.e. somewhat like one would find in
+ * gridbag layout), where table cells can request to span more
+ * than one grid cell. The default horizontal span of table cells
+ * will be based upon this grid, but can be changed by reimplementing
+ * the requested span of the cell (i.e. table cells can have independant
+ * spans if desired).
+ *
+ * @author Timothy Prinzing
+ * @see View
+ */
+public abstract class TableView extends BoxView {
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Creates a new table row.
+ *
+ * @param elem an element
+ * @return the row
+ */
+ protected TableRow createTableRow(Element elem) {
+ return new TableRow(elem);
+ }
+
+ /**
+ * @deprecated Table cells can now be any arbitrary
+ * View implementation and should be produced by the
+ * ViewFactory rather than the table.
+ *
+ * @param elem an element
+ * @return the cell
+ */
+ @Deprecated
+ protected TableCell createTableCell(Element elem) {
+ return new TableCell(elem);
+ }
+
+ /**
+ * The number of columns in the table.
+ */
+ 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.
+ */
+ int getColumnSpan(int col) {
+ return columnSpans[col];
+ }
+
+ /**
+ * The number of rows in the table.
+ */
+ int getRowCount() {
+ return rows.size();
+ }
+
+ /**
+ * Fetches the span (height) of the given row.
+ */
+ int getRowSpan(int row) {
+ View rv = getRow(row);
+ if (rv != null) {
+ return (int) rv.getPreferredSpan(Y_AXIS);
+ }
+ return 0;
+ }
+
+ TableRow getRow(int row) {
+ if (row < rows.size()) {
+ return (TableRow) rows.elementAt(row);
+ }
+ return null;
+ }
+
+ /**
+ * Determines the number of columns occupied by
+ * the table cell represented by given element.
+ */
+ /*protected*/ int getColumnsOccupied(View v) {
+ // PENDING(prinz) this code should be in the html
+ // paragraph, but we can't add api to enable it.
+ AttributeSet a = v.getElement().getAttributes();
+ 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) {
+ // PENDING(prinz) this code should be in the html
+ // paragraph, but we can't add api to enable it.
+ AttributeSet a = v.getElement().getAttributes();
+ 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 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();
+ }
+
+ /**
+ * Fill in the grid locations that are placeholders
+ * for multi-column, multi-row, and missing grid
+ * locations.
+ */
+ void updateGrid() {
+ if (! gridValid) {
+ // determine which views are table rows and clear out
+ // grid points marked filled.
+ rows.removeAllElements();
+ int n = getViewCount();
+ for (int i = 0; i < n; i++) {
+ View v = getView(i);
+ if (v instanceof TableRow) {
+ rows.addElement(v);
+ TableRow rv = (TableRow) v;
+ rv.clearFilledColumns();
+ rv.setRow(i);
+ }
+ }
+
+ int maxColumns = 0;
+ int nrows = rows.size();
+ for (int row = 0; row < nrows; row++) {
+ TableRow rv = getRow(row);
+ int col = 0;
+ for (int cell = 0; cell < rv.getViewCount(); cell++, col++) {
+ View cv = rv.getView(cell);
+ // advance to a free column
+ for (; rv.isFilled(col); col++);
+ int rowSpan = getRowsOccupied(cv);
+ 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();
+ }
+ gridValid = true;
+ }
+ }
+
+ /**
+ * Mark a grid location as filled in for a cells overflow.
+ */
+ void addFill(int row, int col) {
+ TableRow rv = getRow(row);
+ if (rv != null) {
+ rv.fillColumn(col);
+ }
+ }
+
+ /**
+ * Lays out the columns to fit within the given target span.
+ * Returns the results through {@code offsets} and {@code spans}.
+ *
+ * @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
+ */
+ protected void layoutColumns(int targetSpan, int[] offsets, int[] spans,
+ SizeRequirements[] reqs) {
+ // allocate using the convenience method on SizeRequirements
+ SizeRequirements.calculateTiledPositions(targetSpan, null, reqs,
+ offsets, spans);
+ }
+
+ /**
+ * 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.
+ * <p>
+ * This is implemented to call the
+ * <a href="#layoutColumns">layoutColumns</a> 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.
+ */
+ 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++) {
+ TableRow row = getRow(i);
+ row.layoutChanged(axis);
+ }
+
+ // calculate column spans
+ layoutColumns(targetSpan, columnOffsets, columnSpans, columnRequirements);
+
+ // continue normal layout
+ super.layoutMinorAxis(targetSpan, axis, offsets, spans);
+ }
+
+ /**
+ * 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).
+ * <p>
+ * This is implemented to calculate the requirements as the sum of the
+ * requirements of the columns.
+ */
+ 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;
+ long max = 0;
+ for (int i = 0; i < columnRequirements.length; i++) {
+ SizeRequirements req = columnRequirements[i];
+ min += req.minimum;
+ pref += req.preferred;
+ max += req.maximum;
+ }
+ r.minimum = (int) min;
+ r.preferred = (int) pref;
+ r.maximum = (int) max;
+ r.alignment = 0;
+ return r;
+ }
+
+ /*
+ boolean shouldTrace() {
+ AttributeSet a = getElement().getAttributes();
+ Object o = a.getAttribute(HTML.Attribute.ID);
+ if ((o != null) && o.equals("debug")) {
+ return true;
+ }
+ return false;
+ }
+ */
+
+ /**
+ * 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) {
+ // pass 1 - single column cells
+ boolean hasMultiColumn = false;
+ int nrows = getRowCount();
+ for (int i = 0; i < nrows; i++) {
+ TableRow row = getRow(i);
+ 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);
+ if (colSpan == 1) {
+ checkSingleColumnCell(axis, col, cv);
+ } else {
+ hasMultiColumn = true;
+ col += colSpan - 1;
+ }
+ }
+ }
+
+ // pass 2 - multi-column cells
+ if (hasMultiColumn) {
+ for (int i = 0; i < nrows; i++) {
+ TableRow row = getRow(i);
+ 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 colSpan = getColumnsOccupied(cv);
+ if (colSpan > 1) {
+ checkMultiColumnCell(axis, col, colSpan, cv);
+ col += colSpan - 1;
+ }
+ }
+ }
+ }
+
+ /*
+ if (shouldTrace()) {
+ System.err.println("calc:");
+ for (int i = 0; i < columnRequirements.length; i++) {
+ System.err.println(" " + i + ": " + columnRequirements[i]);
+ }
+ }
+ */
+ }
+
+ /**
+ * 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);
+ req.maximum = Math.max((int) v.getMaximumSpan(axis), req.maximum);
+ }
+
+ /**
+ * 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. The
+ * maximum for each cell is the maximum of the existing
+ * maximum or the amount needed by the cell.
+ */
+ SizeRequirements[] reqs = new SizeRequirements[ncols];
+ for (int i = 0; i < ncols; i++) {
+ SizeRequirements r = reqs[i] = columnRequirements[col + i];
+ r.maximum = Math.max(r.maximum, (int) v.getMaximumSpan(axis));
+ }
+ 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. The
+ * maximum for each cell is the maximum of the existing
+ * maximum or the amount needed by the cell.
+ */
+ SizeRequirements[] reqs = new SizeRequirements[ncols];
+ for (int i = 0; i < ncols; i++) {
+ SizeRequirements r = 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);
+ }
+ }
+
+ }
+
+ /**
+ * 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
+ * <code>null</code> 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;
+ }
+
+ // ---- variables ----------------------------------------------------
+
+ int[] columnSpans;
+ int[] columnOffsets;
+ SizeRequirements[] columnRequirements;
+ Vector rows;
+ boolean gridValid;
+ static final private BitSet EMPTY = new BitSet();
+
+ /**
+ * View of a row in a row-centric table.
+ */
+ public class TableRow extends BoxView {
+
+ /**
+ * Constructs a TableView for the given element.
+ *
+ * @param elem the element that this view is responsible for
+ * @since 1.4
+ */
+ public TableRow(Element elem) {
+ super(elem, View.X_AXIS);
+ fillColumns = new BitSet();
+ }
+
+ void clearFilledColumns() {
+ fillColumns.and(EMPTY);
+ }
+
+ void fillColumn(int col) {
+ fillColumns.set(col);
+ }
+
+ boolean isFilled(int col) {
+ return fillColumns.get(col);
+ }
+
+ /** get location in the overall set of rows */
+ int getRow() {
+ return row;
+ }
+
+ /**
+ * set location in the overall set of rows, this is
+ * set by the TableView.updateGrid() method.
+ */
+ void setRow(int row) {
+ this.row = row;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * 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.
+ * <p>
+ * 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.
+ */
+ protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] 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);
+ 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];
+ }
+ }
+ col += colSpan - 1;
+ }
+ }
+ }
+
+ /**
+ * 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.
+ * <p>
+ * 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.
+ */
+ 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) {
+ for (int j = 1; j < rowSpan; j++) {
+ // test bounds of each row because it may not exist
+ // either because of error or because the table isn't
+ // fully loaded yet.
+ int row = getRow() + j;
+ if (row < TableView.this.getViewCount()) {
+ int span = TableView.this.getSpan(Y_AXIS, getRow()+j);
+ spans[cell] += span;
+ }
+ }
+ }
+ 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
+ * <code>null</code> 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;
+ }
+
+ /** columns filled by multi-column or multi-row cells */
+ BitSet fillColumns;
+ /** the row within the overall grid */
+ int row;
+ }
+
+ /**
+ * @deprecated A table cell can now be any View implementation.
+ */
+ @Deprecated
+ public class TableCell extends BoxView implements GridCell {
+
+ /**
+ * Constructs a TableCell for the given element.
+ *
+ * @param elem the element that this view is responsible for
+ * @since 1.4
+ */
+ public TableCell(Element elem) {
+ super(elem, View.Y_AXIS);
+ }
+
+ // --- GridCell methods -------------------------------------
+
+ /**
+ * Gets the number of columns this cell spans (e.g. the
+ * grid width).
+ *
+ * @return the number of columns
+ */
+ public int getColumnCount() {
+ return 1;
+ }
+
+ /**
+ * Gets the number of rows this cell spans (that is, the
+ * grid height).
+ *
+ * @return the number of rows
+ */
+ public int getRowCount() {
+ return 1;
+ }
+
+
+ /**
+ * Sets the grid location.
+ *
+ * @param row the row >= 0
+ * @param col the column >= 0
+ */
+ public void setGridLocation(int row, int col) {
+ this.row = row;
+ this.col = col;
+ }
+
+ /**
+ * Gets the row of the grid location
+ */
+ public int getGridRow() {
+ return row;
+ }
+
+ /**
+ * Gets the column of the grid location
+ */
+ public int getGridColumn() {
+ return col;
+ }
+
+ int row;
+ int col;
+ }
+
+ /**
+ * <em>
+ * THIS IS NO LONGER USED, AND WILL BE REMOVED IN THE
+ * NEXT RELEASE. THE JCK SIGNATURE TEST THINKS THIS INTERFACE
+ * SHOULD EXIST
+ * </em>
+ */
+ interface GridCell {
+
+ /**
+ * Sets the grid location.
+ *
+ * @param row the row >= 0
+ * @param col the column >= 0
+ */
+ public void setGridLocation(int row, int col);
+
+ /**
+ * Gets the row of the grid location
+ */
+ public int getGridRow();
+
+ /**
+ * Gets the column of the grid location
+ */
+ public int getGridColumn();
+
+ /**
+ * Gets the number of columns this cell spans (e.g. the
+ * grid width).
+ *
+ * @return the number of columns
+ */
+ public int getColumnCount();
+
+ /**
+ * Gets the number of rows this cell spans (that is, the
+ * grid height).
+ *
+ * @return the number of rows
+ */
+ public int getRowCount();
+
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/TextAction.java b/src/share/classes/javax/swing/text/TextAction.java
new file mode 100644
index 000000000..5d5da7d97
--- /dev/null
+++ b/src/share/classes/javax/swing/text/TextAction.java
@@ -0,0 +1,137 @@
+/*
+ * 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;
+
+import java.awt.event.ActionEvent;
+import java.awt.KeyboardFocusManager;
+import java.awt.Component;
+import java.util.Hashtable;
+import java.util.Enumeration;
+import javax.swing.Action;
+import javax.swing.AbstractAction;
+import javax.swing.KeyStroke;
+
+/**
+ * An Action implementation useful for key bindings that are
+ * shared across a number of different text components. Because
+ * the action is shared, it must have a way of getting it's
+ * target to act upon. This class provides support to try and
+ * find a text component to operate on. The preferred way of
+ * getting the component to act upon is through the ActionEvent
+ * that is received. If the Object returned by getSource can
+ * be narrowed to a text component, it will be used. If the
+ * action event is null or can't be narrowed, the last focused
+ * text component is tried. This is determined by being
+ * used in conjunction with a JTextController which
+ * arranges to share that information with a TextAction.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> package.
+ * Please see {@link java.beans.XMLEncoder}.
+ *
+ * @author Timothy Prinzing
+ */
+public abstract class TextAction extends AbstractAction {
+
+ /**
+ * Creates a new JTextAction object.
+ *
+ * @param name the name of the action
+ */
+ public TextAction(String name) {
+ super(name);
+ }
+
+ /**
+ * Determines the component to use for the action.
+ * This if fetched from the source of the ActionEvent
+ * if it's not null and can be narrowed. Otherwise,
+ * the last focused component is used.
+ *
+ * @param e the ActionEvent
+ * @return the component
+ */
+ protected final JTextComponent getTextComponent(ActionEvent e) {
+ if (e != null) {
+ Object o = e.getSource();
+ if (o instanceof JTextComponent) {
+ return (JTextComponent) o;
+ }
+ }
+ return getFocusedComponent();
+ }
+
+ /**
+ * Takes one list of
+ * commands and augments it with another list
+ * of commands. The second list takes precedence
+ * over the first list; that is, when both lists
+ * contain a command with the same name, the command
+ * from the second list is used.
+ *
+ * @param list1 the first list, may be empty but not
+ * <code>null</code>
+ * @param list2 the second list, may be empty but not
+ * <code>null</code>
+ * @return the augmented list
+ */
+ public static final Action[] augmentList(Action[] list1, Action[] list2) {
+ Hashtable h = new Hashtable();
+ for (int i = 0; i < list1.length; i++) {
+ Action a = list1[i];
+ String value = (String)a.getValue(Action.NAME);
+ h.put((value!=null ? value:""), a);
+ }
+ for (int i = 0; i < list2.length; i++) {
+ Action a = list2[i];
+ String value = (String)a.getValue(Action.NAME);
+ h.put((value!=null ? value:""), a);
+ }
+ Action[] actions = new Action[h.size()];
+ int index = 0;
+ for (Enumeration e = h.elements() ; e.hasMoreElements() ;) {
+ actions[index++] = (Action) e.nextElement();
+ }
+ return actions;
+ }
+
+ /**
+ * Fetches the text component that currently has focus.
+ * This allows actions to be shared across text components
+ * which is useful for key-bindings where a large set of
+ * actions are defined, but generally used the same way
+ * across many different components.
+ *
+ * @return the component
+ */
+ protected final JTextComponent getFocusedComponent() {
+ return JTextComponent.getFocusedComponent();
+ }
+}
diff --git a/src/share/classes/javax/swing/text/TextLayoutStrategy.java b/src/share/classes/javax/swing/text/TextLayoutStrategy.java
new file mode 100644
index 000000000..956cf31f0
--- /dev/null
+++ b/src/share/classes/javax/swing/text/TextLayoutStrategy.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright 1999-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;
+
+import java.util.*;
+import java.awt.*;
+import java.text.AttributedCharacterIterator;
+import java.text.BreakIterator;
+import java.awt.font.*;
+import java.awt.geom.AffineTransform;
+import javax.swing.event.DocumentEvent;
+import sun.font.BidiUtils;
+
+/**
+ * A flow strategy that uses java.awt.font.LineBreakMeasureer to
+ * produce java.awt.font.TextLayout for i18n capable rendering.
+ * If the child view being placed into the flow is of type
+ * GlyphView and can be rendered by TextLayout, a GlyphPainter
+ * that uses TextLayout is plugged into the GlyphView.
+ *
+ * @author Timothy Prinzing
+ */
+class TextLayoutStrategy extends FlowView.FlowStrategy {
+
+ /**
+ * Constructs a layout strategy for paragraphs based
+ * upon java.awt.font.LineBreakMeasurer.
+ */
+ public TextLayoutStrategy() {
+ text = new AttributedSegment();
+ }
+
+ // --- FlowStrategy methods --------------------------------------------
+
+ /**
+ * Gives notification that something was inserted into the document
+ * in a location that the given flow view is responsible for. The
+ * strategy should update the appropriate changed region (which
+ * depends upon the strategy used for repair).
+ *
+ * @param e the change information from the associated document
+ * @param alloc the current allocation of the view inside of the insets.
+ * This value will be null if the view has not yet been displayed.
+ * @see View#insertUpdate
+ */
+ public void insertUpdate(FlowView fv, DocumentEvent e, Rectangle alloc) {
+ sync(fv);
+ super.insertUpdate(fv, e, alloc);
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that the given flow view is responsible for.
+ *
+ * @param e the change information from the associated document
+ * @param alloc the current allocation of the view inside of the insets.
+ * @see View#removeUpdate
+ */
+ public void removeUpdate(FlowView fv, DocumentEvent e, Rectangle alloc) {
+ sync(fv);
+ super.removeUpdate(fv, e, alloc);
+ }
+
+ /**
+ * Gives notification from the document that attributes were changed
+ * in a location that this view is responsible for.
+ *
+ * @param changes 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(FlowView fv, DocumentEvent e, Rectangle alloc) {
+ sync(fv);
+ super.changedUpdate(fv, e, alloc);
+ }
+
+ /**
+ * Does a a full layout on the given View. This causes all of
+ * the rows (child views) to be rebuilt to match the given
+ * constraints for each row. This is called by a FlowView.layout
+ * to update the child views in the flow.
+ *
+ * @param v the view to reflow
+ */
+ public void layout(FlowView fv) {
+ super.layout(fv);
+ }
+
+ /**
+ * Creates a row of views that will fit within the
+ * layout span of the row. This is implemented to execute the
+ * superclass functionality (which fills the row with child
+ * views or view fragments) and follow that with bidi reordering
+ * of the unidirectional view fragments.
+ *
+ * @param row the row to fill in with views. This is assumed
+ * to be empty on entry.
+ * @param pos The current position in the children of
+ * this views element from which to start.
+ * @return the position to start the next row
+ */
+ protected int layoutRow(FlowView fv, int rowIndex, int p0) {
+ int p1 = super.layoutRow(fv, rowIndex, p0);
+ View row = fv.getView(rowIndex);
+ Document doc = fv.getDocument();
+ Object i18nFlag = doc.getProperty(AbstractDocument.I18NProperty);
+ if ((i18nFlag != null) && i18nFlag.equals(Boolean.TRUE)) {
+ int n = row.getViewCount();
+ if (n > 1) {
+ AbstractDocument d = (AbstractDocument)fv.getDocument();
+ Element bidiRoot = d.getBidiRootElement();
+ byte[] levels = new byte[n];
+ View[] reorder = new View[n];
+
+ for( int i=0; i<n; i++ ) {
+ View v = row.getView(i);
+ int bidiIndex =bidiRoot.getElementIndex(v.getStartOffset());
+ Element bidiElem = bidiRoot.getElement( bidiIndex );
+ levels[i] = (byte)StyleConstants.getBidiLevel(bidiElem.getAttributes());
+ reorder[i] = v;
+ }
+
+ BidiUtils.reorderVisually( levels, reorder );
+ row.replace(0, n, reorder);
+ }
+ }
+ return p1;
+ }
+
+ /**
+ * Adjusts the given row if possible to fit within the
+ * layout span. Since all adjustments were already
+ * calculated by the LineBreakMeasurer, this is implemented
+ * to do nothing.
+ *
+ * @param r the row to adjust to the current layout
+ * span.
+ * @param desiredSpan the current layout span >= 0
+ * @param x the location r starts at.
+ */
+ protected void adjustRow(FlowView fv, int rowIndex, int desiredSpan, int x) {
+ }
+
+ /**
+ * Creates a unidirectional view that can be used to represent the
+ * current chunk. This can be either an entire view from the
+ * logical view, or a fragment of the view.
+ *
+ * @param fv the view holding the flow
+ * @param startOffset the start location for the view being created
+ * @param spanLeft the about of span left to fill in the row
+ * @param rowIndex the row the view will be placed into
+ */
+ protected View createView(FlowView fv, int startOffset, int spanLeft, int rowIndex) {
+ // Get the child view that contains the given starting position
+ View lv = getLogicalView(fv);
+ View row = fv.getView(rowIndex);
+ boolean requireNextWord = (viewBuffer.size() == 0) ? false : true;
+ int childIndex = lv.getViewIndex(startOffset, Position.Bias.Forward);
+ View v = lv.getView(childIndex);
+
+ int endOffset = getLimitingOffset(v, startOffset, spanLeft, requireNextWord);
+ if (endOffset == startOffset) {
+ return null;
+ }
+
+ View frag;
+ if ((startOffset==v.getStartOffset()) && (endOffset == v.getEndOffset())) {
+ // return the entire view
+ frag = v;
+ } else {
+ // return a unidirectional fragment.
+ frag = v.createFragment(startOffset, endOffset);
+ }
+
+ if ((frag instanceof GlyphView) && (measurer != null)) {
+ // install a TextLayout based renderer if the view is responsible
+ // for glyphs. If the view represents a tab, the default
+ // glyph painter is used (may want to handle tabs differently).
+ boolean isTab = false;
+ int p0 = frag.getStartOffset();
+ int p1 = frag.getEndOffset();
+ if ((p1 - p0) == 1) {
+ // check for tab
+ Segment s = ((GlyphView)frag).getText(p0, p1);
+ char ch = s.first();
+ if (ch == '\t') {
+ isTab = true;
+ }
+ }
+ TextLayout tl = (isTab) ? null :
+ measurer.nextLayout(spanLeft, text.toIteratorIndex(endOffset),
+ requireNextWord);
+ if (tl != null) {
+ ((GlyphView)frag).setGlyphPainter(new GlyphPainter2(tl));
+ }
+ }
+ return frag;
+ }
+
+ /**
+ * Calculate the limiting offset for the next view fragment.
+ * At most this would be the entire view (i.e. the limiting
+ * offset would be the end offset in that case). If the range
+ * contains a tab or a direction change, that will limit the
+ * offset to something less. This value is then fed to the
+ * LineBreakMeasurer as a limit to consider in addition to the
+ * remaining span.
+ *
+ * @param v the logical view representing the starting offset.
+ * @param startOffset the model location to start at.
+ */
+ int getLimitingOffset(View v, int startOffset, int spanLeft, boolean requireNextWord) {
+ int endOffset = v.getEndOffset();
+
+ // check for direction change
+ Document doc = v.getDocument();
+ if (doc instanceof AbstractDocument) {
+ AbstractDocument d = (AbstractDocument) doc;
+ Element bidiRoot = d.getBidiRootElement();
+ if( bidiRoot.getElementCount() > 1 ) {
+ int bidiIndex = bidiRoot.getElementIndex( startOffset );
+ Element bidiElem = bidiRoot.getElement( bidiIndex );
+ endOffset = Math.min( bidiElem.getEndOffset(), endOffset );
+ }
+ }
+
+ // check for tab
+ if (v instanceof GlyphView) {
+ Segment s = ((GlyphView)v).getText(startOffset, endOffset);
+ char ch = s.first();
+ if (ch == '\t') {
+ // if the first character is a tab, create a dedicated
+ // view for just the tab
+ endOffset = startOffset + 1;
+ } else {
+ for (ch = s.next(); ch != Segment.DONE; ch = s.next()) {
+ if (ch == '\t') {
+ // found a tab, don't include it in the text
+ endOffset = startOffset + s.getIndex() - s.getBeginIndex();
+ break;
+ }
+ }
+ }
+ }
+
+ // determine limit from LineBreakMeasurer
+ int limitIndex = text.toIteratorIndex(endOffset);
+ if (measurer != null) {
+ int index = text.toIteratorIndex(startOffset);
+ if (measurer.getPosition() != index) {
+ measurer.setPosition(index);
+ }
+ limitIndex = measurer.nextOffset(spanLeft, limitIndex, requireNextWord);
+ }
+ int pos = text.toModelPosition(limitIndex);
+ return pos;
+ }
+
+ /**
+ * Synchronize the strategy with its FlowView. Allows the strategy
+ * to update its state to account for changes in that portion of the
+ * model represented by the FlowView. Also allows the strategy
+ * to update the FlowView in response to these changes.
+ */
+ void sync(FlowView fv) {
+ View lv = getLogicalView(fv);
+ text.setView(lv);
+
+ Container container = fv.getContainer();
+ FontRenderContext frc = sun.swing.SwingUtilities2.
+ getFontRenderContext(container);
+ BreakIterator iter;
+ Container c = fv.getContainer();
+ if (c != null) {
+ iter = BreakIterator.getLineInstance(c.getLocale());
+ } else {
+ iter = BreakIterator.getLineInstance();
+ }
+
+ measurer = new LineBreakMeasurer(text, iter, frc);
+
+ // If the children of the FlowView's logical view are GlyphViews, they
+ // need to have their painters updated.
+ int n = lv.getViewCount();
+ for( int i=0; i<n; i++ ) {
+ View child = lv.getView(i);
+ if( child instanceof GlyphView ) {
+ int p0 = child.getStartOffset();
+ int p1 = child.getEndOffset();
+ measurer.setPosition(text.toIteratorIndex(p0));
+ TextLayout layout
+ = measurer.nextLayout( Float.MAX_VALUE,
+ text.toIteratorIndex(p1), false );
+ ((GlyphView)child).setGlyphPainter(new GlyphPainter2(layout));
+ }
+ }
+
+ // Reset measurer.
+ measurer.setPosition(text.getBeginIndex());
+
+ }
+
+ // --- variables -------------------------------------------------------
+
+ private LineBreakMeasurer measurer;
+ private AttributedSegment text;
+
+ /**
+ * Implementation of AttributedCharacterIterator that supports
+ * the GlyphView attributes for rendering the glyphs through a
+ * TextLayout.
+ */
+ static class AttributedSegment extends Segment implements AttributedCharacterIterator {
+
+ AttributedSegment() {
+ }
+
+ View getView() {
+ return v;
+ }
+
+ void setView(View v) {
+ this.v = v;
+ Document doc = v.getDocument();
+ int p0 = v.getStartOffset();
+ int p1 = v.getEndOffset();
+ try {
+ doc.getText(p0, p1 - p0, this);
+ } catch (BadLocationException bl) {
+ throw new IllegalArgumentException("Invalid view");
+ }
+ first();
+ }
+
+ /**
+ * Get a boundary position for the font.
+ * This is implemented to assume that two fonts are
+ * equal if their references are equal (i.e. that the
+ * font came from a cache).
+ *
+ * @return the location in model coordinates. This is
+ * not the same as the Segment coordinates.
+ */
+ int getFontBoundary(int childIndex, int dir) {
+ View child = v.getView(childIndex);
+ Font f = getFont(childIndex);
+ for (childIndex += dir; (childIndex >= 0) && (childIndex < v.getViewCount());
+ childIndex += dir) {
+ Font next = getFont(childIndex);
+ if (next != f) {
+ // this run is different
+ break;
+ }
+ child = v.getView(childIndex);
+ }
+ return (dir < 0) ? child.getStartOffset() : child.getEndOffset();
+ }
+
+ /**
+ * Get the font at the given child index.
+ */
+ Font getFont(int childIndex) {
+ View child = v.getView(childIndex);
+ if (child instanceof GlyphView) {
+ return ((GlyphView)child).getFont();
+ }
+ return null;
+ }
+
+ int toModelPosition(int index) {
+ return v.getStartOffset() + (index - getBeginIndex());
+ }
+
+ int toIteratorIndex(int pos) {
+ return pos - v.getStartOffset() + getBeginIndex();
+ }
+
+ // --- AttributedCharacterIterator methods -------------------------
+
+ /**
+ * Returns the index of the first character of the run
+ * with respect to all attributes containing the current character.
+ */
+ public int getRunStart() {
+ int pos = toModelPosition(getIndex());
+ int i = v.getViewIndex(pos, Position.Bias.Forward);
+ View child = v.getView(i);
+ return toIteratorIndex(child.getStartOffset());
+ }
+
+ /**
+ * Returns the index of the first character of the run
+ * with respect to the given attribute containing the current character.
+ */
+ public int getRunStart(AttributedCharacterIterator.Attribute attribute) {
+ if (attribute instanceof TextAttribute) {
+ int pos = toModelPosition(getIndex());
+ int i = v.getViewIndex(pos, Position.Bias.Forward);
+ if (attribute == TextAttribute.FONT) {
+ return toIteratorIndex(getFontBoundary(i, -1));
+ }
+ }
+ return getBeginIndex();
+ }
+
+ /**
+ * Returns the index of the first character of the run
+ * with respect to the given attributes containing the current character.
+ */
+ public int getRunStart(Set<? extends Attribute> attributes) {
+ int index = getBeginIndex();
+ Object[] a = attributes.toArray();
+ for (int i = 0; i < a.length; i++) {
+ TextAttribute attr = (TextAttribute) a[i];
+ index = Math.max(getRunStart(attr), index);
+ }
+ return Math.min(getIndex(), index);
+ }
+
+ /**
+ * Returns the index of the first character following the run
+ * with respect to all attributes containing the current character.
+ */
+ public int getRunLimit() {
+ int pos = toModelPosition(getIndex());
+ int i = v.getViewIndex(pos, Position.Bias.Forward);
+ View child = v.getView(i);
+ return toIteratorIndex(child.getEndOffset());
+ }
+
+ /**
+ * Returns the index of the first character following the run
+ * with respect to the given attribute containing the current character.
+ */
+ public int getRunLimit(AttributedCharacterIterator.Attribute attribute) {
+ if (attribute instanceof TextAttribute) {
+ int pos = toModelPosition(getIndex());
+ int i = v.getViewIndex(pos, Position.Bias.Forward);
+ if (attribute == TextAttribute.FONT) {
+ return toIteratorIndex(getFontBoundary(i, 1));
+ }
+ }
+ return getEndIndex();
+ }
+
+ /**
+ * Returns the index of the first character following the run
+ * with respect to the given attributes containing the current character.
+ */
+ public int getRunLimit(Set<? extends Attribute> attributes) {
+ int index = getEndIndex();
+ Object[] a = attributes.toArray();
+ for (int i = 0; i < a.length; i++) {
+ TextAttribute attr = (TextAttribute) a[i];
+ index = Math.min(getRunLimit(attr), index);
+ }
+ return Math.max(getIndex(), index);
+ }
+
+ /**
+ * Returns a map with the attributes defined on the current
+ * character.
+ */
+ public Map getAttributes() {
+ Object[] ka = keys.toArray();
+ Hashtable h = new Hashtable();
+ for (int i = 0; i < ka.length; i++) {
+ TextAttribute a = (TextAttribute) ka[i];
+ Object value = getAttribute(a);
+ if (value != null) {
+ h.put(a, value);
+ }
+ }
+ return h;
+ }
+
+ /**
+ * Returns the value of the named attribute for the current character.
+ * Returns null if the attribute is not defined.
+ * @param attribute the key of the attribute whose value is requested.
+ */
+ public Object getAttribute(AttributedCharacterIterator.Attribute attribute) {
+ int pos = toModelPosition(getIndex());
+ int childIndex = v.getViewIndex(pos, Position.Bias.Forward);
+ if (attribute == TextAttribute.FONT) {
+ return getFont(childIndex);
+ } else if( attribute == TextAttribute.RUN_DIRECTION ) {
+ return
+ v.getDocument().getProperty(TextAttribute.RUN_DIRECTION);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the keys of all attributes defined on the
+ * iterator's text range. The set is empty if no
+ * attributes are defined.
+ */
+ public Set getAllAttributeKeys() {
+ return keys;
+ }
+
+ View v;
+
+ static Set keys;
+
+ static {
+ keys = new HashSet();
+ keys.add(TextAttribute.FONT);
+ keys.add(TextAttribute.RUN_DIRECTION);
+ }
+
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/Utilities.java b/src/share/classes/javax/swing/text/Utilities.java
new file mode 100644
index 000000000..68c7c2269
--- /dev/null
+++ b/src/share/classes/javax/swing/text/Utilities.java
@@ -0,0 +1,1050 @@
+/*
+ * 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;
+
+import java.lang.reflect.Method;
+
+import java.awt.Component;
+import java.awt.Rectangle;
+import java.awt.Graphics;
+import java.awt.FontMetrics;
+import java.awt.Shape;
+import java.awt.Toolkit;
+import java.awt.Graphics2D;
+import java.awt.font.FontRenderContext;
+import java.awt.font.TextLayout;
+import java.awt.font.TextAttribute;
+
+import java.text.*;
+import javax.swing.JComponent;
+import javax.swing.SwingConstants;
+import javax.swing.text.ParagraphView.Row;
+import sun.swing.SwingUtilities2;
+
+/**
+ * A collection of methods to deal with various text
+ * related activities.
+ *
+ * @author Timothy Prinzing
+ */
+public class Utilities {
+ /**
+ * If <code>view</code>'s container is a <code>JComponent</code> it
+ * is returned, after casting.
+ */
+ static JComponent getJComponent(View view) {
+ if (view != null) {
+ Component component = view.getContainer();
+ if (component instanceof JComponent) {
+ return (JComponent)component;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Draws the given text, expanding any tabs that are contained
+ * using the given tab expansion technique. This particular
+ * implementation renders in a 1.1 style coordinate system
+ * where ints are used and 72dpi is assumed.
+ *
+ * @param s the source of the text
+ * @param x the X origin >= 0
+ * @param y the Y origin >= 0
+ * @param g the graphics context
+ * @param e how to expand the tabs. If this value is null,
+ * tabs will be expanded as a space character.
+ * @param startOffset starting offset of the text in the document >= 0
+ * @return the X location at the end of the rendered text
+ */
+ public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
+ TabExpander e, int startOffset) {
+ return drawTabbedText(null, s, x, y, g, e, startOffset);
+ }
+
+ /**
+ * Draws the given text, expanding any tabs that are contained
+ * using the given tab expansion technique. This particular
+ * implementation renders in a 1.1 style coordinate system
+ * where ints are used and 72dpi is assumed.
+ *
+ * @param view View requesting rendering, may be null.
+ * @param s the source of the text
+ * @param x the X origin >= 0
+ * @param y the Y origin >= 0
+ * @param g the graphics context
+ * @param e how to expand the tabs. If this value is null,
+ * tabs will be expanded as a space character.
+ * @param startOffset starting offset of the text in the document >= 0
+ * @return the X location at the end of the rendered text
+ */
+ static final int drawTabbedText(View view,
+ Segment s, int x, int y, Graphics g,
+ TabExpander e, int startOffset) {
+ return drawTabbedText(view, s, x, y, g, e, startOffset, null);
+ }
+
+ // In addition to the previous method it can extend spaces for
+ // justification.
+ //
+ // all params are the same as in the preious method except the last
+ // one:
+ // @param justificationData justificationData for the row.
+ // if null not justification is needed
+ static final int drawTabbedText(View view,
+ Segment s, int x, int y, Graphics g,
+ TabExpander e, int startOffset,
+ int [] justificationData) {
+ JComponent component = getJComponent(view);
+ FontMetrics metrics = SwingUtilities2.getFontMetrics(component, g);
+ int nextX = x;
+ char[] txt = s.array;
+ int txtOffset = s.offset;
+ int flushLen = 0;
+ int flushIndex = s.offset;
+ int spaceAddon = 0;
+ int spaceAddonLeftoverEnd = -1;
+ int startJustifiableContent = 0;
+ int endJustifiableContent = 0;
+ if (justificationData != null) {
+ int offset = - startOffset + txtOffset;
+ View parent = null;
+ if (view != null
+ && (parent = view.getParent()) != null) {
+ offset += parent.getStartOffset();
+ }
+ spaceAddon =
+ justificationData[Row.SPACE_ADDON];
+ spaceAddonLeftoverEnd =
+ justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
+ startJustifiableContent =
+ justificationData[Row.START_JUSTIFIABLE] + offset;
+ endJustifiableContent =
+ justificationData[Row.END_JUSTIFIABLE] + offset;
+ }
+ int n = s.offset + s.count;
+ for (int i = txtOffset; i < n; i++) {
+ if (txt[i] == '\t'
+ || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
+ && (txt[i] == ' ')
+ && startJustifiableContent <= i
+ && i <= endJustifiableContent
+ )) {
+ if (flushLen > 0) {
+ nextX = SwingUtilities2.drawChars(component, g, txt,
+ flushIndex, flushLen, x, y);
+ flushLen = 0;
+ }
+ flushIndex = i + 1;
+ if (txt[i] == '\t') {
+ if (e != null) {
+ nextX = (int) e.nextTabStop((float) nextX, startOffset + i - txtOffset);
+ } else {
+ nextX += metrics.charWidth(' ');
+ }
+ } else if (txt[i] == ' ') {
+ nextX += metrics.charWidth(' ') + spaceAddon;
+ if (i <= spaceAddonLeftoverEnd) {
+ nextX++;
+ }
+ }
+ x = nextX;
+ } else if ((txt[i] == '\n') || (txt[i] == '\r')) {
+ if (flushLen > 0) {
+ nextX = SwingUtilities2.drawChars(component, g, txt,
+ flushIndex, flushLen, x, y);
+ flushLen = 0;
+ }
+ flushIndex = i + 1;
+ x = nextX;
+ } else {
+ flushLen += 1;
+ }
+ }
+ if (flushLen > 0) {
+ nextX = SwingUtilities2.drawChars(component, g,txt, flushIndex,
+ flushLen, x, y);
+ }
+ return nextX;
+ }
+
+ /**
+ * Determines the width of the given segment of text taking tabs
+ * into consideration. This is implemented in a 1.1 style coordinate
+ * system where ints are used and 72dpi is assumed.
+ *
+ * @param s the source of the text
+ * @param metrics the font metrics to use for the calculation
+ * @param x the X origin >= 0
+ * @param e how to expand the tabs. If this value is null,
+ * tabs will be expanded as a space character.
+ * @param startOffset starting offset of the text in the document >= 0
+ * @return the width of the text
+ */
+ public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, int x,
+ TabExpander e, int startOffset) {
+ return getTabbedTextWidth(null, s, metrics, x, e, startOffset, null);
+ }
+
+
+ // In addition to the previous method it can extend spaces for
+ // justification.
+ //
+ // all params are the same as in the preious method except the last
+ // one:
+ // @param justificationData justificationData for the row.
+ // if null not justification is needed
+ static final int getTabbedTextWidth(View view, Segment s, FontMetrics metrics, int x,
+ TabExpander e, int startOffset,
+ int[] justificationData) {
+ int nextX = x;
+ char[] txt = s.array;
+ int txtOffset = s.offset;
+ int n = s.offset + s.count;
+ int charCount = 0;
+ int spaceAddon = 0;
+ int spaceAddonLeftoverEnd = -1;
+ int startJustifiableContent = 0;
+ int endJustifiableContent = 0;
+ if (justificationData != null) {
+ int offset = - startOffset + txtOffset;
+ View parent = null;
+ if (view != null
+ && (parent = view.getParent()) != null) {
+ offset += parent.getStartOffset();
+ }
+ spaceAddon =
+ justificationData[Row.SPACE_ADDON];
+ spaceAddonLeftoverEnd =
+ justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
+ startJustifiableContent =
+ justificationData[Row.START_JUSTIFIABLE] + offset;
+ endJustifiableContent =
+ justificationData[Row.END_JUSTIFIABLE] + offset;
+ }
+
+ for (int i = txtOffset; i < n; i++) {
+ if (txt[i] == '\t'
+ || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
+ && (txt[i] == ' ')
+ && startJustifiableContent <= i
+ && i <= endJustifiableContent
+ )) {
+ nextX += metrics.charsWidth(txt, i-charCount, charCount);
+ charCount = 0;
+ if (txt[i] == '\t') {
+ if (e != null) {
+ nextX = (int) e.nextTabStop((float) nextX,
+ startOffset + i - txtOffset);
+ } else {
+ nextX += metrics.charWidth(' ');
+ }
+ } else if (txt[i] == ' ') {
+ nextX += metrics.charWidth(' ') + spaceAddon;
+ if (i <= spaceAddonLeftoverEnd) {
+ nextX++;
+ }
+ }
+ } else if(txt[i] == '\n') {
+ // Ignore newlines, they take up space and we shouldn't be
+ // counting them.
+ nextX += metrics.charsWidth(txt, i - charCount, charCount);
+ charCount = 0;
+ } else {
+ charCount++;
+ }
+ }
+ nextX += metrics.charsWidth(txt, n - charCount, charCount);
+ return nextX - x;
+ }
+
+ /**
+ * Determines the relative offset into the given text that
+ * best represents the given span in the view coordinate
+ * system. This is implemented in a 1.1 style coordinate
+ * system where ints are used and 72dpi is assumed.
+ *
+ * @param s the source of the text
+ * @param metrics the font metrics to use for the calculation
+ * @param x0 the starting view location representing the start
+ * of the given text >= 0.
+ * @param x the target view location to translate to an
+ * offset into the text >= 0.
+ * @param e how to expand the tabs. If this value is null,
+ * tabs will be expanded as a space character.
+ * @param startOffset starting offset of the text in the document >= 0
+ * @return the offset into the text >= 0
+ */
+ public static final int getTabbedTextOffset(Segment s, FontMetrics metrics,
+ int x0, int x, TabExpander e,
+ int startOffset) {
+ return getTabbedTextOffset(s, metrics, x0, x, e, startOffset, true);
+ }
+
+ static final int getTabbedTextOffset(View view, Segment s, FontMetrics metrics,
+ int x0, int x, TabExpander e,
+ int startOffset,
+ int[] justificationData) {
+ return getTabbedTextOffset(view, s, metrics, x0, x, e, startOffset, true,
+ justificationData);
+ }
+
+ public static final int getTabbedTextOffset(Segment s,
+ FontMetrics metrics,
+ int x0, int x, TabExpander e,
+ int startOffset,
+ boolean round) {
+ return getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, round, null);
+ }
+
+ // In addition to the previous method it can extend spaces for
+ // justification.
+ //
+ // all params are the same as in the preious method except the last
+ // one:
+ // @param justificationData justificationData for the row.
+ // if null not justification is needed
+ static final int getTabbedTextOffset(View view,
+ Segment s,
+ FontMetrics metrics,
+ int x0, int x, TabExpander e,
+ int startOffset,
+ boolean round,
+ int[] justificationData) {
+ if (x0 >= x) {
+ // x before x0, return.
+ return 0;
+ }
+ int currX = x0;
+ int nextX = currX;
+ // s may be a shared segment, so it is copied prior to calling
+ // the tab expander
+ char[] txt = s.array;
+ int txtOffset = s.offset;
+ int txtCount = s.count;
+ int spaceAddon = 0 ;
+ int spaceAddonLeftoverEnd = -1;
+ int startJustifiableContent = 0 ;
+ int endJustifiableContent = 0;
+ if (justificationData != null) {
+ int offset = - startOffset + txtOffset;
+ View parent = null;
+ if (view != null
+ && (parent = view.getParent()) != null) {
+ offset += parent.getStartOffset();
+ }
+ spaceAddon =
+ justificationData[Row.SPACE_ADDON];
+ spaceAddonLeftoverEnd =
+ justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
+ startJustifiableContent =
+ justificationData[Row.START_JUSTIFIABLE] + offset;
+ endJustifiableContent =
+ justificationData[Row.END_JUSTIFIABLE] + offset;
+ }
+ int n = s.offset + s.count;
+ for (int i = s.offset; i < n; i++) {
+ if (txt[i] == '\t'
+ || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
+ && (txt[i] == ' ')
+ && startJustifiableContent <= i
+ && i <= endJustifiableContent
+ )){
+ if (txt[i] == '\t') {
+ if (e != null) {
+ nextX = (int) e.nextTabStop((float) nextX,
+ startOffset + i - txtOffset);
+ } else {
+ nextX += metrics.charWidth(' ');
+ }
+ } else if (txt[i] == ' ') {
+ nextX += metrics.charWidth(' ') + spaceAddon;
+ if (i <= spaceAddonLeftoverEnd) {
+ nextX++;
+ }
+ }
+ } else {
+ nextX += metrics.charWidth(txt[i]);
+ }
+ if ((x >= currX) && (x < nextX)) {
+ // found the hit position... return the appropriate side
+ if ((round == false) || ((x - currX) < (nextX - x))) {
+ return i - txtOffset;
+ } else {
+ return i + 1 - txtOffset;
+ }
+ }
+ currX = nextX;
+ }
+
+ // didn't find, return end offset
+ return txtCount;
+ }
+
+ /**
+ * Determine where to break the given text to fit
+ * within the given span. This tries to find a word boundary.
+ * @param s the source of the text
+ * @param metrics the font metrics to use for the calculation
+ * @param x0 the starting view location representing the start
+ * of the given text.
+ * @param x the target view location to translate to an
+ * offset into the text.
+ * @param e how to expand the tabs. If this value is null,
+ * tabs will be expanded as a space character.
+ * @param startOffset starting offset in the document of the text
+ * @return the offset into the given text
+ */
+ public static final int getBreakLocation(Segment s, FontMetrics metrics,
+ int x0, int x, TabExpander e,
+ int startOffset) {
+ char[] txt = s.array;
+ int txtOffset = s.offset;
+ int txtCount = s.count;
+ int index = Utilities.getTabbedTextOffset(s, metrics, x0, x,
+ e, startOffset, false);
+
+
+ if (index >= txtCount - 1) {
+ return txtCount;
+ }
+
+ for (int i = txtOffset + index; i >= txtOffset; i--) {
+ char ch = txt[i];
+ if (ch < 256) {
+ // break on whitespace
+ if (Character.isWhitespace(ch)) {
+ index = i - txtOffset + 1;
+ break;
+ }
+ } else {
+ // a multibyte char found; use BreakIterator to find line break
+ BreakIterator bit = BreakIterator.getLineInstance();
+ bit.setText(s);
+ int breakPos = bit.preceding(i + 1);
+ if (breakPos > txtOffset) {
+ index = breakPos - txtOffset;
+ }
+ break;
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Determines the starting row model position of the row that contains
+ * the specified model position. The component given must have a
+ * size to compute the result. If the component doesn't have a size
+ * a value of -1 will be returned.
+ *
+ * @param c the editor
+ * @param offs the offset in the document >= 0
+ * @return the position >= 0 if the request can be computed, otherwise
+ * a value of -1 will be returned.
+ * @exception BadLocationException if the offset is out of range
+ */
+ public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException {
+ Rectangle r = c.modelToView(offs);
+ if (r == null) {
+ return -1;
+ }
+ int lastOffs = offs;
+ int y = r.y;
+ while ((r != null) && (y == r.y)) {
+ // Skip invisible elements
+ if(r.height !=0) {
+ offs = lastOffs;
+ }
+ lastOffs -= 1;
+ r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
+ }
+ return offs;
+ }
+
+ /**
+ * Determines the ending row model position of the row that contains
+ * the specified model position. The component given must have a
+ * size to compute the result. If the component doesn't have a size
+ * a value of -1 will be returned.
+ *
+ * @param c the editor
+ * @param offs the offset in the document >= 0
+ * @return the position >= 0 if the request can be computed, otherwise
+ * a value of -1 will be returned.
+ * @exception BadLocationException if the offset is out of range
+ */
+ public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException {
+ Rectangle r = c.modelToView(offs);
+ if (r == null) {
+ return -1;
+ }
+ int n = c.getDocument().getLength();
+ int lastOffs = offs;
+ int y = r.y;
+ while ((r != null) && (y == r.y)) {
+ // Skip invisible elements
+ if (r.height !=0) {
+ offs = lastOffs;
+ }
+ lastOffs += 1;
+ r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
+ }
+ return offs;
+ }
+
+ /**
+ * Determines the position in the model that is closest to the given
+ * view location in the row above. The component given must have a
+ * size to compute the result. If the component doesn't have a size
+ * a value of -1 will be returned.
+ *
+ * @param c the editor
+ * @param offs the offset in the document >= 0
+ * @param x the X coordinate >= 0
+ * @return the position >= 0 if the request can be computed, otherwise
+ * a value of -1 will be returned.
+ * @exception BadLocationException if the offset is out of range
+ */
+ public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException {
+ int lastOffs = getRowStart(c, offs) - 1;
+ if (lastOffs < 0) {
+ return -1;
+ }
+ int bestSpan = Integer.MAX_VALUE;
+ int y = 0;
+ Rectangle r = null;
+ if (lastOffs >= 0) {
+ r = c.modelToView(lastOffs);
+ y = r.y;
+ }
+ while ((r != null) && (y == r.y)) {
+ int span = Math.abs(r.x - x);
+ if (span < bestSpan) {
+ offs = lastOffs;
+ bestSpan = span;
+ }
+ lastOffs -= 1;
+ r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
+ }
+ return offs;
+ }
+
+ /**
+ * Determines the position in the model that is closest to the given
+ * view location in the row below. The component given must have a
+ * size to compute the result. If the component doesn't have a size
+ * a value of -1 will be returned.
+ *
+ * @param c the editor
+ * @param offs the offset in the document >= 0
+ * @param x the X coordinate >= 0
+ * @return the position >= 0 if the request can be computed, otherwise
+ * a value of -1 will be returned.
+ * @exception BadLocationException if the offset is out of range
+ */
+ public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException {
+ int lastOffs = getRowEnd(c, offs) + 1;
+ if (lastOffs <= 0) {
+ return -1;
+ }
+ int bestSpan = Integer.MAX_VALUE;
+ int n = c.getDocument().getLength();
+ int y = 0;
+ Rectangle r = null;
+ if (lastOffs <= n) {
+ r = c.modelToView(lastOffs);
+ y = r.y;
+ }
+ while ((r != null) && (y == r.y)) {
+ int span = Math.abs(x - r.x);
+ if (span < bestSpan) {
+ offs = lastOffs;
+ bestSpan = span;
+ }
+ lastOffs += 1;
+ r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
+ }
+ return offs;
+ }
+
+ /**
+ * Determines the start of a word for the given model location.
+ * Uses BreakIterator.getWordInstance() to actually get the words.
+ *
+ * @param c the editor
+ * @param offs the offset in the document >= 0
+ * @return the location in the model of the word start >= 0
+ * @exception BadLocationException if the offset is out of range
+ */
+ public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException {
+ Document doc = c.getDocument();
+ Element line = getParagraphElement(c, offs);
+ if (line == null) {
+ throw new BadLocationException("No word at " + offs, offs);
+ }
+ int lineStart = line.getStartOffset();
+ int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
+
+ Segment seg = SegmentCache.getSharedSegment();
+ doc.getText(lineStart, lineEnd - lineStart, seg);
+ if(seg.count > 0) {
+ BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
+ words.setText(seg);
+ int wordPosition = seg.offset + offs - lineStart;
+ if(wordPosition >= words.last()) {
+ wordPosition = words.last() - 1;
+ }
+ words.following(wordPosition);
+ offs = lineStart + words.previous() - seg.offset;
+ }
+ SegmentCache.releaseSharedSegment(seg);
+ return offs;
+ }
+
+ /**
+ * Determines the end of a word for the given location.
+ * Uses BreakIterator.getWordInstance() to actually get the words.
+ *
+ * @param c the editor
+ * @param offs the offset in the document >= 0
+ * @return the location in the model of the word end >= 0
+ * @exception BadLocationException if the offset is out of range
+ */
+ public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException {
+ Document doc = c.getDocument();
+ Element line = getParagraphElement(c, offs);
+ if (line == null) {
+ throw new BadLocationException("No word at " + offs, offs);
+ }
+ int lineStart = line.getStartOffset();
+ int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
+
+ Segment seg = SegmentCache.getSharedSegment();
+ doc.getText(lineStart, lineEnd - lineStart, seg);
+ if(seg.count > 0) {
+ BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
+ words.setText(seg);
+ int wordPosition = offs - lineStart + seg.offset;
+ if(wordPosition >= words.last()) {
+ wordPosition = words.last() - 1;
+ }
+ offs = lineStart + words.following(wordPosition) - seg.offset;
+ }
+ SegmentCache.releaseSharedSegment(seg);
+ return offs;
+ }
+
+ /**
+ * Determines the start of the next word for the given location.
+ * Uses BreakIterator.getWordInstance() to actually get the words.
+ *
+ * @param c the editor
+ * @param offs the offset in the document >= 0
+ * @return the location in the model of the word start >= 0
+ * @exception BadLocationException if the offset is out of range
+ */
+ public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException {
+ int nextWord;
+ Element line = getParagraphElement(c, offs);
+ for (nextWord = getNextWordInParagraph(c, line, offs, false);
+ nextWord == BreakIterator.DONE;
+ nextWord = getNextWordInParagraph(c, line, offs, true)) {
+
+ // didn't find in this line, try the next line
+ offs = line.getEndOffset();
+ line = getParagraphElement(c, offs);
+ }
+ return nextWord;
+ }
+
+ /**
+ * Finds the next word in the given elements text. The first
+ * parameter allows searching multiple paragraphs where even
+ * the first offset is desired.
+ * Returns the offset of the next word, or BreakIterator.DONE
+ * if there are no more words in the element.
+ */
+ static int getNextWordInParagraph(JTextComponent c, Element line, int offs, boolean first) throws BadLocationException {
+ if (line == null) {
+ throw new BadLocationException("No more words", offs);
+ }
+ Document doc = line.getDocument();
+ int lineStart = line.getStartOffset();
+ int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
+ if ((offs >= lineEnd) || (offs < lineStart)) {
+ throw new BadLocationException("No more words", offs);
+ }
+ Segment seg = SegmentCache.getSharedSegment();
+ doc.getText(lineStart, lineEnd - lineStart, seg);
+ BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
+ words.setText(seg);
+ if ((first && (words.first() == (seg.offset + offs - lineStart))) &&
+ (! Character.isWhitespace(seg.array[words.first()]))) {
+
+ return offs;
+ }
+ int wordPosition = words.following(seg.offset + offs - lineStart);
+ if ((wordPosition == BreakIterator.DONE) ||
+ (wordPosition >= seg.offset + seg.count)) {
+ // there are no more words on this line.
+ return BreakIterator.DONE;
+ }
+ // if we haven't shot past the end... check to
+ // see if the current boundary represents whitespace.
+ // if so, we need to try again
+ char ch = seg.array[wordPosition];
+ if (! Character.isWhitespace(ch)) {
+ return lineStart + wordPosition - seg.offset;
+ }
+
+ // it was whitespace, try again. The assumption
+ // is that it must be a word start if the last
+ // one had whitespace following it.
+ wordPosition = words.next();
+ if (wordPosition != BreakIterator.DONE) {
+ offs = lineStart + wordPosition - seg.offset;
+ if (offs != lineEnd) {
+ return offs;
+ }
+ }
+ SegmentCache.releaseSharedSegment(seg);
+ return BreakIterator.DONE;
+ }
+
+
+ /**
+ * Determine the start of the prev word for the given location.
+ * Uses BreakIterator.getWordInstance() to actually get the words.
+ *
+ * @param c the editor
+ * @param offs the offset in the document >= 0
+ * @return the location in the model of the word start >= 0
+ * @exception BadLocationException if the offset is out of range
+ */
+ public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException {
+ int prevWord;
+ Element line = getParagraphElement(c, offs);
+ for (prevWord = getPrevWordInParagraph(c, line, offs);
+ prevWord == BreakIterator.DONE;
+ prevWord = getPrevWordInParagraph(c, line, offs)) {
+
+ // didn't find in this line, try the prev line
+ offs = line.getStartOffset() - 1;
+ line = getParagraphElement(c, offs);
+ }
+ return prevWord;
+ }
+
+ /**
+ * Finds the previous word in the given elements text. The first
+ * parameter allows searching multiple paragraphs where even
+ * the first offset is desired.
+ * Returns the offset of the next word, or BreakIterator.DONE
+ * if there are no more words in the element.
+ */
+ static int getPrevWordInParagraph(JTextComponent c, Element line, int offs) throws BadLocationException {
+ if (line == null) {
+ throw new BadLocationException("No more words", offs);
+ }
+ Document doc = line.getDocument();
+ int lineStart = line.getStartOffset();
+ int lineEnd = line.getEndOffset();
+ if ((offs > lineEnd) || (offs < lineStart)) {
+ throw new BadLocationException("No more words", offs);
+ }
+ Segment seg = SegmentCache.getSharedSegment();
+ doc.getText(lineStart, lineEnd - lineStart, seg);
+ BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
+ words.setText(seg);
+ if (words.following(seg.offset + offs - lineStart) == BreakIterator.DONE) {
+ words.last();
+ }
+ int wordPosition = words.previous();
+ if (wordPosition == (seg.offset + offs - lineStart)) {
+ wordPosition = words.previous();
+ }
+
+ if (wordPosition == BreakIterator.DONE) {
+ // there are no more words on this line.
+ return BreakIterator.DONE;
+ }
+ // if we haven't shot past the end... check to
+ // see if the current boundary represents whitespace.
+ // if so, we need to try again
+ char ch = seg.array[wordPosition];
+ if (! Character.isWhitespace(ch)) {
+ return lineStart + wordPosition - seg.offset;
+ }
+
+ // it was whitespace, try again. The assumption
+ // is that it must be a word start if the last
+ // one had whitespace following it.
+ wordPosition = words.previous();
+ if (wordPosition != BreakIterator.DONE) {
+ return lineStart + wordPosition - seg.offset;
+ }
+ SegmentCache.releaseSharedSegment(seg);
+ return BreakIterator.DONE;
+ }
+
+ /**
+ * Determines the element to use for a paragraph/line.
+ *
+ * @param c the editor
+ * @param offs the starting offset in the document >= 0
+ * @return the element
+ */
+ public static final Element getParagraphElement(JTextComponent c, int offs) {
+ Document doc = c.getDocument();
+ if (doc instanceof StyledDocument) {
+ return ((StyledDocument)doc).getParagraphElement(offs);
+ }
+ Element map = doc.getDefaultRootElement();
+ int index = map.getElementIndex(offs);
+ Element paragraph = map.getElement(index);
+ if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) {
+ return paragraph;
+ }
+ return null;
+ }
+
+ static boolean isComposedTextElement(Document doc, int offset) {
+ Element elem = doc.getDefaultRootElement();
+ while (!elem.isLeaf()) {
+ elem = elem.getElement(elem.getElementIndex(offset));
+ }
+ return isComposedTextElement(elem);
+ }
+
+ static boolean isComposedTextElement(Element elem) {
+ AttributeSet as = elem.getAttributes();
+ return isComposedTextAttributeDefined(as);
+ }
+
+ static boolean isComposedTextAttributeDefined(AttributeSet as) {
+ return ((as != null) &&
+ (as.isDefined(StyleConstants.ComposedTextAttribute)));
+ }
+
+ /**
+ * Draws the given composed text passed from an input method.
+ *
+ * @param view View hosting text
+ * @param attr the attributes containing the composed text
+ * @param g the graphics context
+ * @param x the X origin
+ * @param y the Y origin
+ * @param p0 starting offset in the composed text to be rendered
+ * @param p1 ending offset in the composed text to be rendered
+ * @return the new insertion position
+ */
+ static int drawComposedText(View view, AttributeSet attr, Graphics g,
+ int x, int y, int p0, int p1)
+ throws BadLocationException {
+ Graphics2D g2d = (Graphics2D)g;
+ AttributedString as = (AttributedString)attr.getAttribute(
+ StyleConstants.ComposedTextAttribute);
+ as.addAttribute(TextAttribute.FONT, g.getFont());
+
+ if (p0 >= p1)
+ return x;
+
+ AttributedCharacterIterator aci = as.getIterator(null, p0, p1);
+ return x + (int)SwingUtilities2.drawString(
+ getJComponent(view), g2d,aci,x,y);
+ }
+
+ /**
+ * Paints the composed text in a GlyphView
+ */
+ static void paintComposedText(Graphics g, Rectangle alloc, GlyphView v) {
+ if (g instanceof Graphics2D) {
+ Graphics2D g2d = (Graphics2D) g;
+ int p0 = v.getStartOffset();
+ int p1 = v.getEndOffset();
+ AttributeSet attrSet = v.getElement().getAttributes();
+ AttributedString as =
+ (AttributedString)attrSet.getAttribute(StyleConstants.ComposedTextAttribute);
+ int start = v.getElement().getStartOffset();
+ int y = alloc.y + alloc.height - (int)v.getGlyphPainter().getDescent(v);
+ int x = alloc.x;
+
+ //Add text attributes
+ as.addAttribute(TextAttribute.FONT, v.getFont());
+ as.addAttribute(TextAttribute.FOREGROUND, v.getForeground());
+ if (StyleConstants.isBold(v.getAttributes())) {
+ as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
+ }
+ if (StyleConstants.isItalic(v.getAttributes())) {
+ as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
+ }
+ if (v.isUnderline()) {
+ as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
+ }
+ if (v.isStrikeThrough()) {
+ as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
+ }
+ if (v.isSuperscript()) {
+ as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
+ }
+ if (v.isSubscript()) {
+ as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);
+ }
+
+ // draw
+ AttributedCharacterIterator aci = as.getIterator(null, p0 - start, p1 - start);
+ SwingUtilities2.drawString(getJComponent(v),
+ g2d,aci,x,y);
+ }
+ }
+
+ /*
+ * Convenience function for determining ComponentOrientation. Helps us
+ * avoid having Munge directives throughout the code.
+ */
+ static boolean isLeftToRight( java.awt.Component c ) {
+ return c.getComponentOrientation().isLeftToRight();
+ }
+
+
+ /**
+ * Provides a way to determine the next visually represented model
+ * location that one might place a caret. Some views may not be visible,
+ * they might not be in the same order found in the model, or they just
+ * might not allow access to some of the locations in the model.
+ * <p>
+ * This implementation assumes the views are layed out in a logical
+ * manner. That is, that the view at index x + 1 is visually after
+ * the View at index x, and that the View at index x - 1 is visually
+ * before the View at x. There is support for reversing this behavior
+ * only if the passed in <code>View</code> is an instance of
+ * <code>CompositeView</code>. The <code>CompositeView</code>
+ * must then override the <code>flipEastAndWestAtEnds</code> method.
+ *
+ * @param v View to query
+ * @param pos the position to convert >= 0
+ * @param a the allocated region to render into
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard;
+ * this may be one of the following:
+ * <ul>
+ * <li><code>SwingConstants.WEST</code>
+ * <li><code>SwingConstants.EAST</code>
+ * <li><code>SwingConstants.NORTH</code>
+ * <li><code>SwingConstants.SOUTH</code>
+ * </ul>
+ * @param biasRet an array contain the bias that was checked
+ * @return the location within the model that best represents the next
+ * location visual position
+ * @exception BadLocationException
+ * @exception IllegalArgumentException if <code>direction</code> is invalid
+ */
+ static int getNextVisualPositionFrom(View v, int pos, Position.Bias b,
+ Shape alloc, int direction,
+ Position.Bias[] biasRet)
+ throws BadLocationException {
+ if (v.getViewCount() == 0) {
+ // Nothing to do.
+ return pos;
+ }
+ boolean top = (direction == SwingConstants.NORTH ||
+ direction == SwingConstants.WEST);
+ int retValue;
+ if (pos == -1) {
+ // Start from the first View.
+ int childIndex = (top) ? v.getViewCount() - 1 : 0;
+ View child = v.getView(childIndex);
+ Shape childBounds = v.getChildAllocation(childIndex, alloc);
+ retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
+ direction, biasRet);
+ if (retValue == -1 && !top && v.getViewCount() > 1) {
+ // Special case that should ONLY happen if first view
+ // isn't valid (can happen when end position is put at
+ // beginning of line.
+ child = v.getView(1);
+ childBounds = v.getChildAllocation(1, alloc);
+ retValue = child.getNextVisualPositionFrom(-1, biasRet[0],
+ childBounds,
+ direction, biasRet);
+ }
+ }
+ else {
+ int increment = (top) ? -1 : 1;
+ int childIndex;
+ if (b == Position.Bias.Backward && pos > 0) {
+ childIndex = v.getViewIndex(pos - 1, Position.Bias.Forward);
+ }
+ else {
+ childIndex = v.getViewIndex(pos, Position.Bias.Forward);
+ }
+ View child = v.getView(childIndex);
+ Shape childBounds = v.getChildAllocation(childIndex, alloc);
+ retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
+ direction, biasRet);
+ if ((direction == SwingConstants.EAST ||
+ direction == SwingConstants.WEST) &&
+ (v instanceof CompositeView) &&
+ ((CompositeView)v).flipEastAndWestAtEnds(pos, b)) {
+ increment *= -1;
+ }
+ childIndex += increment;
+ if (retValue == -1 && childIndex >= 0 &&
+ childIndex < v.getViewCount()) {
+ child = v.getView(childIndex);
+ childBounds = v.getChildAllocation(childIndex, alloc);
+ retValue = child.getNextVisualPositionFrom(
+ -1, b, childBounds, direction, biasRet);
+ // If there is a bias change, it is a fake position
+ // and we should skip it. This is usually the result
+ // of two elements side be side flowing the same way.
+ if (retValue == pos && biasRet[0] != b) {
+ return getNextVisualPositionFrom(v, pos, biasRet[0],
+ alloc, direction,
+ biasRet);
+ }
+ }
+ else if (retValue != -1 && biasRet[0] != b &&
+ ((increment == 1 && child.getEndOffset() == retValue) ||
+ (increment == -1 &&
+ child.getStartOffset() == retValue)) &&
+ childIndex >= 0 && childIndex < v.getViewCount()) {
+ // Reached the end of a view, make sure the next view
+ // is a different direction.
+ child = v.getView(childIndex);
+ childBounds = v.getChildAllocation(childIndex, alloc);
+ Position.Bias originalBias = biasRet[0];
+ int nextPos = child.getNextVisualPositionFrom(
+ -1, b, childBounds, direction, biasRet);
+ if (biasRet[0] == b) {
+ retValue = nextPos;
+ }
+ else {
+ biasRet[0] = originalBias;
+ }
+ }
+ }
+ return retValue;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/View.java b/src/share/classes/javax/swing/text/View.java
new file mode 100644
index 000000000..f4ccc8707
--- /dev/null
+++ b/src/share/classes/javax/swing/text/View.java
@@ -0,0 +1,1343 @@
+/*
+ * 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;
+
+import java.awt.*;
+import javax.swing.SwingConstants;
+import javax.swing.event.*;
+
+/**
+ * <p>
+ * A very important part of the text package is the <code>View</code> class.
+ * As the name suggests it represents a view of the text model,
+ * or a piece of the text model.
+ * It is this class that is responsible for the look of the text component.
+ * The view is not intended to be some completely new thing that one must
+ * learn, but rather is much like a lightweight component.
+ * <p>
+By default, a view is very light. It contains a reference to the parent
+view from which it can fetch many things without holding state, and it
+contains a reference to a portion of the model (<code>Element</code>).
+A view does not
+have to exactly represent an element in the model, that is simply a typical
+and therefore convenient mapping. A view can alternatively maintain a couple
+of Position objects to maintain its location in the model (i.e. represent
+a fragment of an element). This is typically the result of formatting where
+views have been broken down into pieces. The convenience of a substantial
+relationship to the element makes it easier to build factories to produce the
+views, and makes it easier to keep track of the view pieces as the model is
+changed and the view must be changed to reflect the model. Simple views
+therefore represent an Element directly and complex views do not.
+<p>
+A view has the following responsibilities:
+ <dl>
+
+ <dt><b>Participate in layout.</b>
+ <dd>
+ <p>The view has a <code>setSize</code> method which is like
+ <code>doLayout</code> and <code>setSize</code> in <code>Component</code> combined.
+ The view has a <code>preferenceChanged</code> method which is
+ like <code>invalidate</code> in <code>Component</code> except that one can
+ invalidate just one axis
+ and the child requesting the change is identified.
+ <p>A View expresses the size that it would like to be in terms of three
+ values, a minimum, a preferred, and a maximum span. Layout in a view is
+ can be done independently upon each axis. For a properly functioning View
+ implementation, the minimum span will be &lt;= the preferred span which in turn
+ will be &lt;= the maximum span.
+ </p>
+ <p align=center><img src="doc-files/View-flexibility.jpg"
+ alt="The above text describes this graphic.">
+ <p>The minimum set of methods for layout are:
+ <ul>
+ <li><a href="#getMinimumSpan(int)">getMinimumSpan</a>
+ <li><a href="#getPreferredSpan(int)">getPreferredSpan</a>
+ <li><a href="#getMaximumSpan(int)">getMaximumSpan</a>
+ <li><a href="#getAlignment(int)">getAlignment</a>
+ <li><a href="#preferenceChanged(javax.swing.text.View, boolean, boolean)">preferenceChanged</a>
+ <li><a href="#setSize(float, float)">setSize</a>
+ </ul>
+
+ <p>The <code>setSize</code> method should be prepared to be called a number of times
+ (i.e. It may be called even if the size didn't change).
+ The <code>setSize</code> method
+ is generally called to make sure the View layout is complete prior to trying
+ to perform an operation on it that requires an up-to-date layout. A view's
+ size should <em>always</em> be set to a value within the minimum and maximum
+ span specified by that view. Additionally, the view must always call the
+ <code>preferenceChanged</code> method on the parent if it has changed the
+ values for the
+ layout it would like, and expects the parent to honor. The parent View is
+ not required to recognize a change until the <code>preferenceChanged</code>
+ has been sent.
+ This allows parent View implementations to cache the child requirements if
+ desired. The calling sequence looks something like the following:
+ </p>
+ <p align=center>
+ <img src="doc-files/View-layout.jpg"
+ alt="Sample calling sequence between parent view and child view:
+ setSize, getMinimum, getPreferred, getMaximum, getAlignment, setSize">
+ <p>The exact calling sequence is up to the layout functionality of
+ the parent view (if the view has any children). The view may collect
+ the preferences of the children prior to determining what it will give
+ each child, or it might iteratively update the children one at a time.
+ </p>
+
+ <dt><b>Render a portion of the model.</b>
+ <dd>
+ <p>This is done in the paint method, which is pretty much like a component
+ paint method. Views are expected to potentially populate a fairly large
+ tree. A <code>View</code> has the following semantics for rendering:
+ </p>
+ <ul>
+ <li>The view gets its allocation from the parent at paint time, so it
+ must be prepared to redo layout if the allocated area is different from
+ what it is prepared to deal with.
+ <li>The coordinate system is the same as the hosting <code>Component</code>
+ (i.e. the <code>Component</code> returned by the
+ <a href="#getContainer">getContainer</a> method).
+ This means a child view lives in the same coordinate system as the parent
+ view unless the parent has explicitly changed the coordinate system.
+ To schedule itself to be repainted a view can call repaint on the hosting
+ <code>Component</code>.
+ <li>The default is to <em>not clip</em> the children. It is more efficient
+ to allow a view to clip only if it really feels it needs clipping.
+ <li>The <code>Graphics</code> object given is not initialized in any way.
+ A view should set any settings needed.
+ <li>A <code>View</code> is inherently transparent. While a view may render into its
+ entire allocation, typically a view does not. Rendering is performed by
+ tranversing down the tree of <code>View</code> implementations.
+ Each <code>View</code> is responsible
+ for rendering its children. This behavior is depended upon for thread
+ safety. While view implementations do not necessarily have to be implemented
+ with thread safety in mind, other view implementations that do make use of
+ concurrency can depend upon a tree traversal to guarantee thread safety.
+ <li>The order of views relative to the model is up to the implementation.
+ Although child views will typically be arranged in the same order that they
+ occur in the model, they may be visually arranged in an entirely different
+ order. View implementations may have Z-Order associated with them if the
+ children are overlapping.
+ </ul>
+ <p>The methods for rendering are:
+ <ul>
+ <li><a href="#paint(java.awt.Graphics, java.awt.Shape)">paint</a>
+ </ul>
+ <p>
+
+ <dt><b>Translate between the model and view coordinate systems.</b>
+ <dd>
+ <p>Because the view objects are produced from a factory and therefore cannot
+ necessarily be counted upon to be in a particular pattern, one must be able
+ to perform translation to properly locate spatial representation of the model.
+ The methods for doing this are:
+ <ul>
+ <li><a href="#modelToView(int, javax.swing.text.Position.Bias, int, javax.swing.text.Position.Bias, java.awt.Shape)">modelToView</a>
+ <li><a href="#viewToModel(float, float, java.awt.Shape, javax.swing.text.Position.Bias[])">viewToModel</a>
+ <li><a href="#getDocument()">getDocument</a>
+ <li><a href="#getElement()">getElement</a>
+ <li><a href="#getStartOffset()">getStartOffset</a>
+ <li><a href="#getEndOffset()">getEndOffset</a>
+ </ul>
+ <p>The layout must be valid prior to attempting to make the translation.
+ The translation is not valid, and must not be attempted while changes
+ are being broadcasted from the model via a <code>DocumentEvent</code>.
+ </p>
+
+ <dt><b>Respond to changes from the model.</b>
+ <dd>
+ <p>If the overall view is represented by many pieces (which is the best situation
+ if one want to be able to change the view and write the least amount of new code),
+ it would be impractical to have a huge number of <code>DocumentListener</code>s.
+ If each
+ view listened to the model, only a few would actually be interested in the
+ changes broadcasted at any given time. Since the model has no knowledge of
+ views, it has no way to filter the broadcast of change information. The view
+ hierarchy itself is instead responsible for propagating the change information.
+ At any level in the view hierarchy, that view knows enough about its children to
+ best distribute the change information further. Changes are therefore broadcasted
+ starting from the root of the view hierarchy.
+ The methods for doing this are:
+ <ul>
+ <li><a href="#insertUpdate">insertUpdate</a>
+ <li><a href="#removeUpdate">removeUpdate</a>
+ <li><a href="#changedUpdate">changedUpdate</a>
+ </ul>
+ <p>
+</dl>
+ *
+ * @author Timothy Prinzing
+ */
+public abstract class View implements SwingConstants {
+
+ /**
+ * Creates a new <code>View</code> object.
+ *
+ * @param elem the <code>Element</code> to represent
+ */
+ public View(Element elem) {
+ this.elem = elem;
+ }
+
+ /**
+ * Returns the parent of the view.
+ *
+ * @return the parent, or <code>null</code> if none exists
+ */
+ public View getParent() {
+ return parent;
+ }
+
+ /**
+ * Returns a boolean that indicates whether
+ * the view is visible or not. By default
+ * all views are visible.
+ *
+ * @return always returns true
+ */
+ public boolean isVisible() {
+ return true;
+ }
+
+
+ /**
+ * Determines the preferred span for this view along an
+ * axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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 View#getPreferredSpan
+ */
+ public abstract float getPreferredSpan(int axis);
+
+ /**
+ * Determines the minimum span for this view along an
+ * axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @return the minimum span the view can be rendered into
+ * @see View#getPreferredSpan
+ */
+ public float getMinimumSpan(int axis) {
+ int w = getResizeWeight(axis);
+ if (w == 0) {
+ // can't resize
+ return getPreferredSpan(axis);
+ }
+ return 0;
+ }
+
+ /**
+ * Determines the maximum span for this view along an
+ * axis.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @return the maximum span the view can be rendered into
+ * @see View#getPreferredSpan
+ */
+ public float getMaximumSpan(int axis) {
+ int w = getResizeWeight(axis);
+ if (w == 0) {
+ // can't resize
+ return getPreferredSpan(axis);
+ }
+ return Integer.MAX_VALUE;
+ }
+
+ /**
+ * Child views can call this on the parent to indicate that
+ * the preference has changed and should be reconsidered
+ * for layout. By default this just propagates upward to
+ * the next parent. The root view will call
+ * <code>revalidate</code> on the associated text component.
+ *
+ * @param child the child view
+ * @param width true if the width preference has changed
+ * @param height true if the height preference has changed
+ * @see javax.swing.JComponent#revalidate
+ */
+ public void preferenceChanged(View child, boolean width, boolean height) {
+ View parent = getParent();
+ if (parent != null) {
+ parent.preferenceChanged(this, width, height);
+ }
+ }
+
+ /**
+ * Determines the desired alignment for this view along an
+ * axis. The desired alignment is returned. This should be
+ * a value >= 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.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @return the value 0.5
+ */
+ public float getAlignment(int axis) {
+ return 0.5f;
+ }
+
+ /**
+ * Renders using the given rendering surface and area on that
+ * surface. The view may need to do layout and create child
+ * views to enable itself to render into the given allocation.
+ *
+ * @param g the rendering surface to use
+ * @param allocation the allocated region to render into
+ */
+ public abstract void paint(Graphics g, Shape allocation);
+
+ /**
+ * 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 also
+ * the last method called, since it is called to indicate
+ * the view has been removed from the hierarchy as
+ * well. When this method is called to set the parent to
+ * null, this method does the same for each of its children,
+ * propogating the notification that they have been
+ * disconnected from the view tree. If this is
+ * reimplemented, <code>super.setParent()</code> should
+ * be called.
+ *
+ * @param parent the new parent, or <code>null</code> if the view is
+ * being removed from a parent
+ */
+ public void setParent(View parent) {
+ // if the parent is null then propogate down the view tree
+ if (parent == null) {
+ for (int i = 0; i < getViewCount(); i++) {
+ if (getView(i).getParent() == this) {
+ // in FlowView.java view might be referenced
+ // from two super-views as a child. see logicalView
+ getView(i).setParent(null);
+ }
+ }
+ }
+ this.parent = parent;
+ }
+
+ /**
+ * Returns the number of views in this view. Since
+ * the default is to not be a composite view this
+ * returns 0.
+ *
+ * @return the number of views >= 0
+ * @see View#getViewCount
+ */
+ public int getViewCount() {
+ return 0;
+ }
+
+ /**
+ * Gets the <i>n</i>th child view. Since there are no
+ * children by default, this returns <code>null</code>.
+ *
+ * @param n the number of the view to get, >= 0 && < getViewCount()
+ * @return the view
+ */
+ public View getView(int n) {
+ return null;
+ }
+
+
+ /**
+ * Removes all of the children. This is a convenience
+ * call to <code>replace</code>.
+ *
+ * @since 1.3
+ */
+ public void removeAll() {
+ replace(0, getViewCount(), null);
+ }
+
+ /**
+ * Removes one of the children at the given position.
+ * This is a convenience call to <code>replace</code>.
+ * @since 1.3
+ */
+ public void remove(int i) {
+ replace(i, 1, null);
+ }
+
+ /**
+ * Inserts a single child view. This is a convenience
+ * call to <code>replace</code>.
+ *
+ * @param offs the offset of the view to insert before >= 0
+ * @param v the view
+ * @see #replace
+ * @since 1.3
+ */
+ public void insert(int offs, View v) {
+ View[] one = new View[1];
+ one[0] = v;
+ replace(offs, 0, one);
+ }
+
+ /**
+ * Appends a single child view. This is a convenience
+ * call to <code>replace</code>.
+ *
+ * @param v the view
+ * @see #replace
+ * @since 1.3
+ */
+ public void append(View v) {
+ View[] one = new View[1];
+ one[0] = v;
+ replace(getViewCount(), 0, one);
+ }
+
+ /**
+ * Replaces child views. If there are no views to remove
+ * this acts as an insert. If there are no views to
+ * add this acts as a remove. Views being removed will
+ * have the parent set to <code>null</code>, and the internal reference
+ * to them removed so that they can be garbage collected.
+ * This is implemented to do nothing, because by default
+ * a view has no children.
+ *
+ * @param offset the starting index into the child views to insert
+ * the new views. This should be a value >= 0 and <= getViewCount
+ * @param length the number of existing child views to remove
+ * This should be a value >= 0 and <= (getViewCount() - offset).
+ * @param views the child views to add. This value can be
+ * <code>null</code> to indicate no children are being added
+ * (useful to remove).
+ * @since 1.3
+ */
+ public void replace(int offset, int length, View[] views) {
+ }
+
+ /**
+ * Returns the child view index representing the given position in
+ * the model. By default a view has no children so this is implemented
+ * to return -1 to indicate there is no valid child index for any
+ * position.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ * @since 1.3
+ */
+ public int getViewIndex(int pos, Position.Bias b) {
+ return -1;
+ }
+
+ /**
+ * Fetches the allocation for the given child view.
+ * This enables finding out where various views
+ * are located, without assuming how the views store
+ * their location. This returns <code>null</code> since the
+ * default is to not have any child views.
+ *
+ * @param index the index of the child, >= 0 && <
+ * <code>getViewCount()</code>
+ * @param a the allocation to this view
+ * @return the allocation to the child
+ */
+ public Shape getChildAllocation(int index, Shape a) {
+ return null;
+ }
+
+ /**
+ * Provides a way to determine the next visually represented model
+ * location at which one might place a caret.
+ * Some views may not be visible,
+ * they might not be in the same order found in the model, or they just
+ * might not allow access to some of the locations in the model.
+ *
+ * @param pos the position to convert >= 0
+ * @param a the allocated region in which to render
+ * @param direction the direction from the current position that can
+ * be thought of as the arrow keys typically found on a keyboard.
+ * This will be one of the following values:
+ * <ul>
+ * <li>SwingConstants.WEST
+ * <li>SwingConstants.EAST
+ * <li>SwingConstants.NORTH
+ * <li>SwingConstants.SOUTH
+ * </ul>
+ * @return the location within the model that best represents the next
+ * location visual position
+ * @exception BadLocationException
+ * @exception IllegalArgumentException if <code>direction</code>
+ * doesn't have one of the legal values above
+ */
+ public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a,
+ int direction, Position.Bias[] biasRet)
+ throws BadLocationException {
+
+ biasRet[0] = Position.Bias.Forward;
+ switch (direction) {
+ case NORTH:
+ case SOUTH:
+ {
+ if (pos == -1) {
+ pos = (direction == NORTH) ? Math.max(0, getEndOffset() - 1) :
+ getStartOffset();
+ break;
+ }
+ JTextComponent target = (JTextComponent) getContainer();
+ Caret c = (target != null) ? target.getCaret() : null;
+ // YECK! Ideally, the x location from the magic caret position
+ // would be passed in.
+ Point mcp;
+ if (c != null) {
+ mcp = c.getMagicCaretPosition();
+ }
+ else {
+ mcp = null;
+ }
+ int x;
+ if (mcp == null) {
+ Rectangle loc = target.modelToView(pos);
+ x = (loc == null) ? 0 : loc.x;
+ }
+ else {
+ x = mcp.x;
+ }
+ if (direction == NORTH) {
+ pos = Utilities.getPositionAbove(target, pos, x);
+ }
+ else {
+ pos = Utilities.getPositionBelow(target, pos, x);
+ }
+ }
+ break;
+ case WEST:
+ if(pos == -1) {
+ pos = Math.max(0, getEndOffset() - 1);
+ }
+ else {
+ pos = Math.max(0, pos - 1);
+ }
+ break;
+ case EAST:
+ if(pos == -1) {
+ pos = getStartOffset();
+ }
+ else {
+ pos = Math.min(pos + 1, getDocument().getLength());
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Bad direction: " + direction);
+ }
+ return pos;
+ }
+
+ /**
+ * Provides a mapping, for a given character,
+ * from the document model coordinate space
+ * to the view coordinate space.
+ *
+ * @param pos the position of the desired character (>=0)
+ * @param a the area of the view, which encompasses the requested character
+ * @param b the bias toward the previous character or the
+ * next character represented by the offset, in case the
+ * position is a boundary of two views; <code>b</code> will have one
+ * of these values:
+ * <ul>
+ * <li> <code>Position.Bias.Forward</code>
+ * <li> <code>Position.Bias.Backward</code>
+ * </ul>
+ * @return the bounding box, in view coordinate space,
+ * of the character at the specified position
+ * @exception BadLocationException if the specified position does
+ * not represent a valid location in the associated document
+ * @exception IllegalArgumentException if <code>b</code> is not one of the
+ * legal <code>Position.Bias</code> values listed above
+ * @see View#viewToModel
+ */
+ public abstract Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException;
+
+ /**
+ * Provides a mapping, for a given region,
+ * from the document model coordinate space
+ * to the view coordinate space. The specified region is
+ * created as a union of the first and last character positions.
+ *
+ * @param p0 the position of the first character (>=0)
+ * @param b0 the bias of the first character position,
+ * toward the previous character or the
+ * next character represented by the offset, in case the
+ * position is a boundary of two views; <code>b0</code> will have one
+ * of these values:
+ * <ul>
+ * <li> <code>Position.Bias.Forward</code>
+ * <li> <code>Position.Bias.Backward</code>
+ * </ul>
+ * @param p1 the position of the last character (>=0)
+ * @param b1 the bias for the second character position, defined
+ * one of the legal values shown above
+ * @param a the area of the view, which encompasses the requested region
+ * @return the bounding box which is a union of the region specified
+ * by the first and last character positions
+ * @exception BadLocationException if the given position does
+ * not represent a valid location in the associated document
+ * @exception IllegalArgumentException if <code>b0</code> or
+ * <code>b1</code> are not one of the
+ * legal <code>Position.Bias</code> values listed above
+ * @see View#viewToModel
+ */
+ public Shape modelToView(int p0, Position.Bias b0, int p1, Position.Bias b1, Shape a) throws BadLocationException {
+ Shape s0 = modelToView(p0, a, b0);
+ Shape s1;
+ if (p1 == getEndOffset()) {
+ try {
+ s1 = modelToView(p1, a, b1);
+ } catch (BadLocationException ble) {
+ s1 = null;
+ }
+ if (s1 == null) {
+ // Assume extends left to right.
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a :
+ a.getBounds();
+ s1 = new Rectangle(alloc.x + alloc.width - 1, alloc.y,
+ 1, alloc.height);
+ }
+ }
+ else {
+ s1 = modelToView(p1, a, b1);
+ }
+ Rectangle r0 = s0.getBounds();
+ Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle) s1 :
+ s1.getBounds();
+ if (r0.y != r1.y) {
+ // If it spans lines, force it to be the width of the view.
+ Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a :
+ a.getBounds();
+ r0.x = alloc.x;
+ r0.width = alloc.width;
+ }
+ r0.add(r1);
+ return r0;
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model. The <code>biasReturn</code>
+ * argument will be filled in to indicate that the point given is
+ * closer to the next character in the model or the previous
+ * character in the model.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param a the allocated region in which to render
+ * @return the location within the model that best represents the
+ * given point in the view >= 0. The <code>biasReturn</code>
+ * argument will be
+ * filled in to indicate that the point given is closer to the next
+ * character in the model or the previous character in the model.
+ */
+ public abstract int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn);
+
+ /**
+ * Gives notification that something was inserted into
+ * the document in a location that this view is responsible for.
+ * To reduce the burden to subclasses, this functionality is
+ * spread out into the following calls that subclasses can
+ * reimplement:
+ * <ol>
+ * <li><a href="#updateChildren">updateChildren</a> is called
+ * if there were any changes to the element this view is
+ * responsible for. If this view has child views that are
+ * represent the child elements, then this method should do
+ * whatever is necessary to make sure the child views correctly
+ * represent the model.
+ * <li><a href="#forwardUpdate">forwardUpdate</a> is called
+ * to forward the DocumentEvent to the appropriate child views.
+ * <li><a href="#updateLayout">updateLayout</a> is called to
+ * give the view a chance to either repair its layout, to reschedule
+ * layout, or do nothing.
+ * </ol>
+ *
+ * @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) {
+ if (getViewCount() > 0) {
+ Element elem = getElement();
+ DocumentEvent.ElementChange ec = e.getChange(elem);
+ if (ec != null) {
+ if (! updateChildren(ec, e, f)) {
+ // don't consider the element changes they
+ // are for a view further down.
+ ec = null;
+ }
+ }
+ forwardUpdate(ec, e, a, f);
+ updateLayout(ec, e, a);
+ }
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that this view is responsible for.
+ * To reduce the burden to subclasses, this functionality is
+ * spread out into the following calls that subclasses can
+ * reimplement:
+ * <ol>
+ * <li><a href="#updateChildren">updateChildren</a> is called
+ * if there were any changes to the element this view is
+ * responsible for. If this view has child views that are
+ * represent the child elements, then this method should do
+ * whatever is necessary to make sure the child views correctly
+ * represent the model.
+ * <li><a href="#forwardUpdate">forwardUpdate</a> is called
+ * to forward the DocumentEvent to the appropriate child views.
+ * <li><a href="#updateLayout">updateLayout</a> is called to
+ * give the view a chance to either repair its layout, to reschedule
+ * layout, or do nothing.
+ * </ol>
+ *
+ * @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) {
+ if (getViewCount() > 0) {
+ Element elem = getElement();
+ DocumentEvent.ElementChange ec = e.getChange(elem);
+ if (ec != null) {
+ if (! updateChildren(ec, e, f)) {
+ // don't consider the element changes they
+ // are for a view further down.
+ ec = null;
+ }
+ }
+ forwardUpdate(ec, e, a, f);
+ updateLayout(ec, e, a);
+ }
+ }
+
+ /**
+ * Gives notification from the document that attributes were changed
+ * in a location that this view is responsible for.
+ * To reduce the burden to subclasses, this functionality is
+ * spread out into the following calls that subclasses can
+ * reimplement:
+ * <ol>
+ * <li><a href="#updateChildren">updateChildren</a> is called
+ * if there were any changes to the element this view is
+ * responsible for. If this view has child views that are
+ * represent the child elements, then this method should do
+ * whatever is necessary to make sure the child views correctly
+ * represent the model.
+ * <li><a href="#forwardUpdate">forwardUpdate</a> is called
+ * to forward the DocumentEvent to the appropriate child views.
+ * <li><a href="#updateLayout">updateLayout</a> is called to
+ * give the view a chance to either repair its layout, to reschedule
+ * layout, or do nothing.
+ * </ol>
+ *
+ * @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) {
+ if (getViewCount() > 0) {
+ Element elem = getElement();
+ DocumentEvent.ElementChange ec = e.getChange(elem);
+ if (ec != null) {
+ if (! updateChildren(ec, e, f)) {
+ // don't consider the element changes they
+ // are for a view further down.
+ ec = null;
+ }
+ }
+ forwardUpdate(ec, e, a, f);
+ updateLayout(ec, e, a);
+ }
+ }
+
+ /**
+ * Fetches the model associated with the view.
+ *
+ * @return the view model, <code>null</code> if none
+ * @see View#getDocument
+ */
+ public Document getDocument() {
+ return elem.getDocument();
+ }
+
+ /**
+ * Fetches the portion of the model for which this view is
+ * responsible.
+ *
+ * @return the starting offset into the model >= 0
+ * @see View#getStartOffset
+ */
+ public int getStartOffset() {
+ return elem.getStartOffset();
+ }
+
+ /**
+ * Fetches the portion of the model for which this view is
+ * responsible.
+ *
+ * @return the ending offset into the model >= 0
+ * @see View#getEndOffset
+ */
+ public int getEndOffset() {
+ return elem.getEndOffset();
+ }
+
+ /**
+ * Fetches the structural portion of the subject that this
+ * view is mapped to. The view may not be responsible for the
+ * entire portion of the element.
+ *
+ * @return the subject
+ * @see View#getElement
+ */
+ public Element getElement() {
+ return elem;
+ }
+
+ /**
+ * Fetch a <code>Graphics</code> for rendering.
+ * This can be used to determine
+ * font characteristics, and will be different for a print view
+ * than a component view.
+ *
+ * @return a <code>Graphics</code> object for rendering
+ * @since 1.3
+ */
+ public Graphics getGraphics() {
+ // PENDING(prinz) this is a temporary implementation
+ Component c = getContainer();
+ return c.getGraphics();
+ }
+
+ /**
+ * Fetches the attributes to use when rendering. By default
+ * this simply returns the attributes of the associated element.
+ * This method should be used rather than using the element
+ * directly to obtain access to the attributes to allow
+ * view-specific attributes to be mixed in or to allow the
+ * view to have view-specific conversion of attributes by
+ * subclasses.
+ * Each view should document what attributes it recognizes
+ * for the purpose of rendering or layout, and should always
+ * access them through the <code>AttributeSet</code> returned
+ * by this method.
+ */
+ public AttributeSet getAttributes() {
+ return elem.getAttributes();
+ }
+
+ /**
+ * Tries to break this view on the given axis. This is
+ * called by views that try to do formatting of their
+ * children. For example, a view of a paragraph will
+ * typically try to place its children into row and
+ * views representing chunks of text can sometimes be
+ * broken down into smaller pieces.
+ * <p>
+ * This is implemented to return the view itself, which
+ * represents the default behavior on not being
+ * breakable. If the view does support breaking, the
+ * starting offset of the view returned should be the
+ * given offset, and the end offset should be less than
+ * or equal to the end offset of the view being broken.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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, if the view can be broken. If the view
+ * doesn't support breaking behavior, the view itself is
+ * returned.
+ * @see ParagraphView
+ */
+ public View breakView(int axis, int offset, float pos, float len) {
+ return this;
+ }
+
+ /**
+ * Creates a view that represents a portion of the element.
+ * This is potentially useful during formatting operations
+ * for taking measurements of fragments of the view. If
+ * the view doesn't support fragmenting (the default), it
+ * should return itself.
+ *
+ * @param p0 the starting offset >= 0. This should be a value
+ * greater or equal to the element starting offset and
+ * less than the element ending offset.
+ * @param p1 the ending offset > p0. This should be a value
+ * less than or equal to the elements end offset and
+ * greater than the elements starting offset.
+ * @return the view fragment, or itself if the view doesn't
+ * support breaking into fragments
+ * @see LabelView
+ */
+ public View createFragment(int p0, int p1) {
+ return this;
+ }
+
+ /**
+ * Determines how attractive a break opportunity in
+ * this view is. This can be used for determining which
+ * view is the most attractive to call <code>breakView</code>
+ * 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 <code>BadBreakWeight</code>
+ * should not be considered for a break. A value greater
+ * than or equal to <code>ForcedBreakWeight</code> should
+ * be broken.
+ * <p>
+ * This is implemented to provide the default behavior
+ * of returning <code>BadBreakWeight</code> 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 <code>LabelView</code>.
+ * An example of a view that uses break weight is
+ * <code>ParagraphView</code>.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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 <em>pos</em>
+ * where a potential break is desired >= 0
+ * @return the weight, which should be a value between
+ * ForcedBreakWeight and BadBreakWeight
+ * @see LabelView
+ * @see ParagraphView
+ * @see #BadBreakWeight
+ * @see #GoodBreakWeight
+ * @see #ExcellentBreakWeight
+ * @see #ForcedBreakWeight
+ */
+ public int getBreakWeight(int axis, float pos, float len) {
+ if (len > getPreferredSpan(axis)) {
+ return GoodBreakWeight;
+ }
+ return BadBreakWeight;
+ }
+
+ /**
+ * Determines the resizability of the view along the
+ * given axis. A value of 0 or less is not resizable.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @return the weight
+ */
+ public int getResizeWeight(int axis) {
+ return 0;
+ }
+
+ /**
+ * Sets the size of the view. This should cause
+ * layout of the view along the given axis, if it
+ * has any layout duties.
+ *
+ * @param width the width >= 0
+ * @param height the height >= 0
+ */
+ public void setSize(float width, float height) {
+ }
+
+ /**
+ * Fetches the container hosting the view. This is useful for
+ * things like scheduling a repaint, finding out the host
+ * components font, etc. The default implementation
+ * of this is to forward the query to the parent view.
+ *
+ * @return the container, <code>null</code> if none
+ */
+ public Container getContainer() {
+ View v = getParent();
+ return (v != null) ? v.getContainer() : null;
+ }
+
+ /**
+ * Fetches the <code>ViewFactory</code> implementation that is feeding
+ * the view hierarchy. Normally the views are given this
+ * as an argument to updates from the model when they
+ * are most likely to need the factory, but this
+ * method serves to provide it at other times.
+ *
+ * @return the factory, <code>null</code> if none
+ */
+ public ViewFactory getViewFactory() {
+ View v = getParent();
+ return (v != null) ? v.getViewFactory() : null;
+ }
+
+ /**
+ * Returns the tooltip text at the specified location. The default
+ * implementation returns the value from the child View identified by
+ * the passed in location.
+ *
+ * @since 1.4
+ * @see JTextComponent#getToolTipText
+ */
+ public String getToolTipText(float x, float y, Shape allocation) {
+ int viewIndex = getViewIndex(x, y, allocation);
+ if (viewIndex >= 0) {
+ allocation = getChildAllocation(viewIndex, allocation);
+ Rectangle rect = (allocation instanceof Rectangle) ?
+ (Rectangle)allocation : allocation.getBounds();
+ if (rect.contains(x, y)) {
+ return getView(viewIndex).getToolTipText(x, y, allocation);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the child view index representing the given position in
+ * the view. This iterates over all the children returning the
+ * first with a bounds that contains <code>x</code>, <code>y</code>.
+ *
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param allocation current allocation of the View.
+ * @return index of the view representing the given location, or
+ * -1 if no view represents that position
+ * @since 1.4
+ */
+ public int getViewIndex(float x, float y, Shape allocation) {
+ for (int counter = getViewCount() - 1; counter >= 0; counter--) {
+ Shape childAllocation = getChildAllocation(counter, allocation);
+
+ if (childAllocation != null) {
+ Rectangle rect = (childAllocation instanceof Rectangle) ?
+ (Rectangle)childAllocation : childAllocation.getBounds();
+
+ if (rect.contains(x, y)) {
+ return counter;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Updates the child views in response to receiving notification
+ * that the model changed, and there is change record for the
+ * element this view is responsible for. This is implemented
+ * to assume the child views are directly responsible for the
+ * child elements of the element this view represents. The
+ * <code>ViewFactory</code> is used to create child views for each element
+ * specified as added in the <code>ElementChange</code>, starting at the
+ * index specified in the given <code>ElementChange</code>. The number of
+ * child views representing the removed elements specified are
+ * removed.
+ *
+ * @param ec the change information for the element this view
+ * is responsible for. This should not be <code>null</code> if
+ * this method gets called
+ * @param e the change information from the associated document
+ * @param f the factory to use to build child views
+ * @return whether or not the child views represent the
+ * child elements of the element this view is responsible
+ * for. Some views create children that represent a portion
+ * of the element they are responsible for, and should return
+ * false. This information is used to determine if views
+ * in the range of the added elements should be forwarded to
+ * or not
+ * @see #insertUpdate
+ * @see #removeUpdate
+ * @see #changedUpdate
+ * @since 1.3
+ */
+ protected boolean updateChildren(DocumentEvent.ElementChange ec,
+ DocumentEvent e, ViewFactory f) {
+ Element[] removedElems = ec.getChildrenRemoved();
+ Element[] addedElems = ec.getChildrenAdded();
+ View[] added = null;
+ if (addedElems != null) {
+ added = new View[addedElems.length];
+ for (int i = 0; i < addedElems.length; i++) {
+ added[i] = f.create(addedElems[i]);
+ }
+ }
+ int nremoved = 0;
+ int index = ec.getIndex();
+ if (removedElems != null) {
+ nremoved = removedElems.length;
+ }
+ replace(index, nremoved, added);
+ return true;
+ }
+
+ /**
+ * Forwards the given <code>DocumentEvent</code> to the child views
+ * that need to be notified of the change to the model.
+ * If there were changes to the element this view is
+ * responsible for, that should be considered when
+ * forwarding (i.e. new child views should not get
+ * notified).
+ *
+ * @param ec changes to the element this view is responsible
+ * for (may be <code>null</code> if there were no changes).
+ * @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 #insertUpdate
+ * @see #removeUpdate
+ * @see #changedUpdate
+ * @since 1.3
+ */
+ protected void forwardUpdate(DocumentEvent.ElementChange ec,
+ DocumentEvent e, Shape a, ViewFactory f) {
+ Element elem = getElement();
+ int pos = e.getOffset();
+ int index0 = getViewIndex(pos, Position.Bias.Forward);
+ if (index0 == -1 && e.getType() == DocumentEvent.EventType.REMOVE &&
+ pos >= 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 = getViewCount() - 1;
+ }
+ int index1 = index0;
+ View v = (index0 >= 0) ? getView(index0) : null;
+ if (v != null) {
+ if ((v.getStartOffset() == pos) && (pos > 0)) {
+ // If v is at a boundary, forward the event to the previous
+ // view too.
+ index0 = Math.max(index0 - 1, 0);
+ }
+ }
+ if (e.getType() != DocumentEvent.EventType.REMOVE) {
+ index1 = getViewIndex(pos + e.getLength(), Position.Bias.Forward);
+ if (index1 < 0) {
+ index1 = getViewCount() - 1;
+ }
+ }
+ int hole0 = index1 + 1;
+ int hole1 = hole0;
+ Element[] addedElems = (ec != null) ? ec.getChildrenAdded() : null;
+ if ((addedElems != null) && (addedElems.length > 0)) {
+ hole0 = ec.getIndex();
+ hole1 = hole0 + addedElems.length - 1;
+ }
+
+ // forward to any view not in the forwarding hole
+ // formed by added elements (i.e. they will be updated
+ // by initialization.
+ index0 = Math.max(index0, 0);
+ for (int i = index0; i <= index1; i++) {
+ if (! ((i >= hole0) && (i <= hole1))) {
+ v = getView(i);
+ if (v != null) {
+ Shape childAlloc = getChildAllocation(i, a);
+ forwardUpdateToView(v, e, childAlloc, f);
+ }
+ }
+ }
+ }
+
+ /**
+ * Forwards the <code>DocumentEvent</code> to the give child view. This
+ * simply messages the view with a call to <code>insertUpdate</code>,
+ * <code>removeUpdate</code>, or <code>changedUpdate</code> depending
+ * upon the type of the event. This is called by
+ * <a href="#forwardUpdate">forwardUpdate</a> to forward
+ * the event to children that need it.
+ *
+ * @param v the child view to forward the event to
+ * @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 #forwardUpdate
+ * @since 1.3
+ */
+ protected void forwardUpdateToView(View v, DocumentEvent e,
+ Shape a, ViewFactory f) {
+ DocumentEvent.EventType type = e.getType();
+ if (type == DocumentEvent.EventType.INSERT) {
+ v.insertUpdate(e, a, f);
+ } else if (type == DocumentEvent.EventType.REMOVE) {
+ v.removeUpdate(e, a, f);
+ } else {
+ v.changedUpdate(e, a, f);
+ }
+ }
+
+ /**
+ * Updates the layout in response to receiving notification of
+ * change from the model. This is implemented to call
+ * <code>preferenceChanged</code> to reschedule a new layout
+ * if the <code>ElementChange</code> record is not <code>null</code>.
+ *
+ * @param ec changes to the element this view is responsible
+ * for (may be <code>null</code> if there were no changes)
+ * @param e the change information from the associated document
+ * @param a the current allocation of the view
+ * @see #insertUpdate
+ * @see #removeUpdate
+ * @see #changedUpdate
+ * @since 1.3
+ */
+ protected void updateLayout(DocumentEvent.ElementChange ec,
+ DocumentEvent e, Shape a) {
+ if ((ec != null) && (a != null)) {
+ // should damage more intelligently
+ preferenceChanged(null, true, true);
+ Container host = getContainer();
+ if (host != null) {
+ host.repaint();
+ }
+ }
+ }
+
+ /**
+ * The weight to indicate a view is a bad break
+ * opportunity for the purpose of formatting. This
+ * value indicates that no attempt should be made to
+ * break the view into fragments as the view has
+ * not been written to support fragmenting.
+ *
+ * @see #getBreakWeight
+ * @see #GoodBreakWeight
+ * @see #ExcellentBreakWeight
+ * @see #ForcedBreakWeight
+ */
+ public static final int BadBreakWeight = 0;
+
+ /**
+ * The weight to indicate a view supports breaking,
+ * but better opportunities probably exist.
+ *
+ * @see #getBreakWeight
+ * @see #BadBreakWeight
+ * @see #ExcellentBreakWeight
+ * @see #ForcedBreakWeight
+ */
+ public static final int GoodBreakWeight = 1000;
+
+ /**
+ * The weight to indicate a view supports breaking,
+ * and this represents a very attractive place to
+ * break.
+ *
+ * @see #getBreakWeight
+ * @see #BadBreakWeight
+ * @see #GoodBreakWeight
+ * @see #ForcedBreakWeight
+ */
+ public static final int ExcellentBreakWeight = 2000;
+
+ /**
+ * The weight to indicate a view supports breaking,
+ * and must be broken to be represented properly
+ * when placed in a view that formats its children
+ * by breaking them.
+ *
+ * @see #getBreakWeight
+ * @see #BadBreakWeight
+ * @see #GoodBreakWeight
+ * @see #ExcellentBreakWeight
+ */
+ public static final int ForcedBreakWeight = 3000;
+
+ /**
+ * Axis for format/break operations.
+ */
+ public static final int X_AXIS = HORIZONTAL;
+
+ /**
+ * Axis for format/break operations.
+ */
+ public static final int Y_AXIS = VERTICAL;
+
+ /**
+ * Provides a mapping from the document model coordinate space
+ * to the coordinate space of the view mapped to it. This is
+ * implemented to default the bias to <code>Position.Bias.Forward</code>
+ * which was previously implied.
+ *
+ * @param pos the position to convert >= 0
+ * @param a the allocated region in which to render
+ * @return the bounding box of the given position is returned
+ * @exception BadLocationException if the given position does
+ * not represent a valid location in the associated document
+ * @see View#modelToView
+ * @deprecated
+ */
+ @Deprecated
+ public Shape modelToView(int pos, Shape a) throws BadLocationException {
+ return modelToView(pos, a, Position.Bias.Forward);
+ }
+
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param x the X coordinate >= 0
+ * @param y the Y coordinate >= 0
+ * @param a the allocated region in which to render
+ * @return the location within the model that best represents the
+ * given point in the view >= 0
+ * @see View#viewToModel
+ * @deprecated
+ */
+ @Deprecated
+ public int viewToModel(float x, float y, Shape a) {
+ sharedBiasReturn[0] = Position.Bias.Forward;
+ return viewToModel(x, y, a, sharedBiasReturn);
+ }
+
+ // static argument available for viewToModel calls since only
+ // one thread at a time may call this method.
+ static final Position.Bias[] sharedBiasReturn = new Position.Bias[1];
+
+ private View parent;
+ private Element elem;
+
+};
diff --git a/src/share/classes/javax/swing/text/ViewFactory.java b/src/share/classes/javax/swing/text/ViewFactory.java
new file mode 100644
index 000000000..4b89dbf5a
--- /dev/null
+++ b/src/share/classes/javax/swing/text/ViewFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 1997-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;
+
+import java.awt.Container;
+
+/**
+ * A factory to create a view of some portion of document subject.
+ * This is intended to enable customization of how views get
+ * mapped over a document model.
+ *
+ * @author Timothy Prinzing
+ */
+public interface ViewFactory {
+
+ /**
+ * Creates a view from the given structural element of a
+ * document.
+ *
+ * @param elem the piece of the document to build a view of
+ * @return the view
+ * @see View
+ */
+ public View create(Element elem);
+
+}
diff --git a/src/share/classes/javax/swing/text/WhitespaceBasedBreakIterator.java b/src/share/classes/javax/swing/text/WhitespaceBasedBreakIterator.java
new file mode 100644
index 000000000..571cdeb7b
--- /dev/null
+++ b/src/share/classes/javax/swing/text/WhitespaceBasedBreakIterator.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 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;
+
+import java.text.BreakIterator;
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
+import java.util.Arrays;
+
+/**
+ * A simple whitespace-based BreakIterator implementation.
+ *
+ * @author Sergey Groznyh
+ */
+class WhitespaceBasedBreakIterator extends BreakIterator {
+ private char[] text = new char[0];
+ private int[] breaks = new int[] { 0 } ;
+ private int pos = 0;
+
+ /**
+ * Calculate break positions eagerly parallel to reading text.
+ */
+ public void setText(CharacterIterator ci) {
+ int begin = ci.getBeginIndex();
+ text = new char[ci.getEndIndex() - begin];
+ int[] breaks0 = new int[text.length + 1];
+ int brIx = 0;
+ breaks0[brIx++] = begin;
+
+ int charIx = 0;
+ boolean inWs = false;
+ for (char c = ci.first(); c != CharacterIterator.DONE; c = ci.next()) {
+ text[charIx] = c;
+ boolean ws = Character.isWhitespace(c);
+ if (inWs && !ws) {
+ breaks0[brIx++] = charIx + begin;
+ }
+ inWs = ws;
+ charIx++;
+ }
+ if (text.length > 0) {
+ breaks0[brIx++] = text.length + begin;
+ }
+ System.arraycopy(breaks0, 0, breaks = new int[brIx], 0, brIx);
+ }
+
+ public CharacterIterator getText() {
+ return new StringCharacterIterator(new String(text));
+ }
+
+ public int first() {
+ return breaks[pos = 0];
+ }
+
+ public int last() {
+ return breaks[pos = breaks.length - 1];
+ }
+
+ public int current() {
+ return breaks[pos];
+ }
+
+ public int next() {
+ return (pos == breaks.length - 1 ? DONE : breaks[++pos]);
+ }
+
+ public int previous() {
+ return (pos == 0 ? DONE : breaks[--pos]);
+ }
+
+ public int next(int n) {
+ return checkhit(pos + n);
+ }
+
+ public int following(int n) {
+ return adjacent(n, 1);
+ }
+
+ public int preceding(int n) {
+ return adjacent(n, -1);
+ }
+
+ private int checkhit(int hit) {
+ if ((hit < 0) || (hit >= breaks.length)) {
+ return DONE;
+ } else {
+ return breaks[pos = hit];
+ }
+ }
+
+ private int adjacent(int n, int bias) {
+ int hit = Arrays.binarySearch(breaks, n);
+ int offset = (hit < 0 ? (bias < 0 ? -1 : -2) : 0);
+ return checkhit(Math.abs(hit) + bias + offset);
+ }
+}
diff --git a/src/share/classes/javax/swing/text/WrappedPlainView.java b/src/share/classes/javax/swing/text/WrappedPlainView.java
new file mode 100644
index 000000000..fb7aede73
--- /dev/null
+++ b/src/share/classes/javax/swing/text/WrappedPlainView.java
@@ -0,0 +1,877 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import java.util.Properties;
+import java.awt.*;
+import java.lang.ref.SoftReference;
+import javax.swing.event.*;
+
+/**
+ * View of plain text (text with only one font and color)
+ * that does line-wrapping. This view expects that its
+ * associated element has child elements that represent
+ * the lines it should be wrapping. It is implemented
+ * as a vertical box that contains logical line views.
+ * The logical line views are nested classes that render
+ * the logical line as multiple physical line if the logical
+ * line is too wide to fit within the allocation. The
+ * line views draw upon the outer class for its state
+ * to reduce their memory requirements.
+ * <p>
+ * The line views do all of their rendering through the
+ * <code>drawLine</code> method which in turn does all of
+ * its rendering through the <code>drawSelectedText</code>
+ * and <code>drawUnselectedText</code> methods. This
+ * enables subclasses to easily specialize the rendering
+ * without concern for the layout aspects.
+ *
+ * @author Timothy Prinzing
+ * @see View
+ */
+public class WrappedPlainView extends BoxView implements TabExpander {
+
+ /**
+ * Creates a new WrappedPlainView. Lines will be wrapped
+ * on character boundaries.
+ *
+ * @param elem the element underlying the view
+ */
+ public WrappedPlainView(Element elem) {
+ this(elem, false);
+ }
+
+ /**
+ * Creates a new WrappedPlainView. Lines can be wrapped on
+ * either character or word boundaries depending upon the
+ * setting of the wordWrap parameter.
+ *
+ * @param elem the element underlying the view
+ * @param wordWrap should lines be wrapped on word boundaries?
+ */
+ public WrappedPlainView(Element elem, boolean wordWrap) {
+ super(elem, Y_AXIS);
+ this.wordWrap = wordWrap;
+ }
+
+ /**
+ * Returns the tab size set for the document, defaulting to 8.
+ *
+ * @return the tab size
+ */
+ protected int getTabSize() {
+ Integer i = (Integer) getDocument().getProperty(PlainDocument.tabSizeAttribute);
+ int size = (i != null) ? i.intValue() : 8;
+ return size;
+ }
+
+ /**
+ * Renders a line of text, suppressing whitespace at the end
+ * and expanding any tabs. This is implemented to make calls
+ * to the methods <code>drawUnselectedText</code> and
+ * <code>drawSelectedText</code> so that the way selected and
+ * unselected text are rendered can be customized.
+ *
+ * @param p0 the starting document location to use >= 0
+ * @param p1 the ending document location to use >= p1
+ * @param g the graphics context
+ * @param x the starting X position >= 0
+ * @param y the starting Y position >= 0
+ * @see #drawUnselectedText
+ * @see #drawSelectedText
+ */
+ protected void drawLine(int p0, int p1, Graphics g, int x, int y) {
+ Element lineMap = getElement();
+ Element line = lineMap.getElement(lineMap.getElementIndex(p0));
+ Element elem;
+
+ try {
+ if (line.isLeaf()) {
+ drawText(line, p0, p1, g, x, y);
+ } else {
+ // this line contains the composed text.
+ int idx = line.getElementIndex(p0);
+ int lastIdx = line.getElementIndex(p1);
+ for(; idx <= lastIdx; idx++) {
+ elem = line.getElement(idx);
+ int start = Math.max(elem.getStartOffset(), p0);
+ int end = Math.min(elem.getEndOffset(), p1);
+ x = drawText(elem, start, end, g, x, y);
+ }
+ }
+ } catch (BadLocationException e) {
+ throw new StateInvariantError("Can't render: " + p0 + "," + p1);
+ }
+ }
+
+ private int drawText(Element elem, int p0, int p1, Graphics g, int x, int y) throws BadLocationException {
+ p1 = Math.min(getDocument().getLength(), p1);
+ AttributeSet attr = elem.getAttributes();
+
+ if (Utilities.isComposedTextAttributeDefined(attr)) {
+ g.setColor(unselected);
+ x = Utilities.drawComposedText(this, attr, g, x, y,
+ p0-elem.getStartOffset(),
+ p1-elem.getStartOffset());
+ } else {
+ if (sel0 == sel1 || selected == unselected) {
+ // no selection, or it is invisible
+ x = drawUnselectedText(g, x, y, p0, p1);
+ } else if ((p0 >= sel0 && p0 <= sel1) && (p1 >= sel0 && p1 <= sel1)) {
+ x = drawSelectedText(g, x, y, p0, p1);
+ } else if (sel0 >= p0 && sel0 <= p1) {
+ if (sel1 >= p0 && sel1 <= p1) {
+ x = drawUnselectedText(g, x, y, p0, sel0);
+ x = drawSelectedText(g, x, y, sel0, sel1);
+ x = drawUnselectedText(g, x, y, sel1, p1);
+ } else {
+ x = drawUnselectedText(g, x, y, p0, sel0);
+ x = drawSelectedText(g, x, y, sel0, p1);
+ }
+ } else if (sel1 >= p0 && sel1 <= p1) {
+ x = drawSelectedText(g, x, y, p0, sel1);
+ x = drawUnselectedText(g, x, y, sel1, p1);
+ } else {
+ x = drawUnselectedText(g, x, y, p0, p1);
+ }
+ }
+
+ return x;
+ }
+
+ /**
+ * Renders the given range in the model as normal unselected
+ * text.
+ *
+ * @param g the graphics context
+ * @param x the starting X coordinate >= 0
+ * @param y the starting Y coordinate >= 0
+ * @param p0 the beginning position in the model >= 0
+ * @param p1 the ending position in the model >= p0
+ * @return the X location of the end of the range >= 0
+ * @exception BadLocationException if the range is invalid
+ */
+ protected int drawUnselectedText(Graphics g, int x, int y,
+ int p0, int p1) throws BadLocationException {
+ g.setColor(unselected);
+ Document doc = getDocument();
+ Segment segment = SegmentCache.getSharedSegment();
+ doc.getText(p0, p1 - p0, segment);
+ int ret = Utilities.drawTabbedText(this, segment, x, y, g, this, p0);
+ SegmentCache.releaseSharedSegment(segment);
+ return ret;
+ }
+
+ /**
+ * Renders the given range in the model as selected text. This
+ * is implemented to render the text in the color specified in
+ * the hosting component. It assumes the highlighter will render
+ * the selected background.
+ *
+ * @param g the graphics context
+ * @param x the starting X coordinate >= 0
+ * @param y the starting Y coordinate >= 0
+ * @param p0 the beginning position in the model >= 0
+ * @param p1 the ending position in the model >= p0
+ * @return the location of the end of the range.
+ * @exception BadLocationException if the range is invalid
+ */
+ protected int drawSelectedText(Graphics g, int x,
+ int y, int p0, int p1) throws BadLocationException {
+ g.setColor(selected);
+ Document doc = getDocument();
+ Segment segment = SegmentCache.getSharedSegment();
+ doc.getText(p0, p1 - p0, segment);
+ int ret = Utilities.drawTabbedText(this, segment, x, y, g, this, p0);
+ SegmentCache.releaseSharedSegment(segment);
+ return ret;
+ }
+
+ /**
+ * Gives access to a buffer that can be used to fetch
+ * text from the associated document.
+ *
+ * @return the buffer
+ */
+ protected final Segment getLineBuffer() {
+ if (lineBuffer == null) {
+ lineBuffer = new Segment();
+ }
+ return lineBuffer;
+ }
+
+ /**
+ * This is called by the nested wrapped line
+ * views to determine the break location. This can
+ * be reimplemented to alter the breaking behavior.
+ * It will either break at word or character boundaries
+ * depending upon the break argument given at
+ * construction.
+ */
+ protected int calculateBreakPosition(int p0, int p1) {
+ int p;
+ Segment segment = SegmentCache.getSharedSegment();
+ loadText(segment, p0, p1);
+ int currentWidth = getWidth();
+ if (currentWidth == Integer.MAX_VALUE) {
+ currentWidth = (int) getDefaultSpan(View.X_AXIS);
+ }
+ if (wordWrap) {
+ p = p0 + Utilities.getBreakLocation(segment, metrics,
+ tabBase, tabBase + currentWidth,
+ this, p0);
+ } else {
+ p = p0 + Utilities.getTabbedTextOffset(segment, metrics,
+ tabBase, tabBase + currentWidth,
+ this, p0, false);
+ }
+ SegmentCache.releaseSharedSegment(segment);
+ return p;
+ }
+
+ /**
+ * Loads all of the children to initialize the view.
+ * This is called by the <code>setParent</code> method.
+ * Subclasses can reimplement this to initialize their
+ * child views in a different manner. The default
+ * implementation creates a child view for each
+ * child element.
+ *
+ * @param f the view factory
+ */
+ protected void loadChildren(ViewFactory f) {
+ Element e = getElement();
+ int n = e.getElementCount();
+ if (n > 0) {
+ View[] added = new View[n];
+ for (int i = 0; i < n; i++) {
+ added[i] = new WrappedLine(e.getElement(i));
+ }
+ replace(0, 0, added);
+ }
+ }
+
+ /**
+ * Update the child views in response to a
+ * document event.
+ */
+ void updateChildren(DocumentEvent e, Shape a) {
+ Element elem = getElement();
+ DocumentEvent.ElementChange ec = e.getChange(elem);
+ if (ec != null) {
+ // the structure of this element changed.
+ Element[] removedElems = ec.getChildrenRemoved();
+ Element[] addedElems = ec.getChildrenAdded();
+ View[] added = new View[addedElems.length];
+ for (int i = 0; i < addedElems.length; i++) {
+ added[i] = new WrappedLine(addedElems[i]);
+ }
+ replace(ec.getIndex(), removedElems.length, added);
+
+ // should damge a little more intelligently.
+ if (a != null) {
+ preferenceChanged(null, true, true);
+ getContainer().repaint();
+ }
+ }
+
+ // update font metrics which may be used by the child views
+ updateMetrics();
+ }
+
+ /**
+ * Load the text buffer with the given range
+ * of text. This is used by the fragments
+ * broken off of this view as well as this
+ * view itself.
+ */
+ final void loadText(Segment segment, int p0, int p1) {
+ try {
+ Document doc = getDocument();
+ doc.getText(p0, p1 - p0, segment);
+ } catch (BadLocationException bl) {
+ throw new StateInvariantError("Can't get line text");
+ }
+ }
+
+ final void updateMetrics() {
+ Component host = getContainer();
+ Font f = host.getFont();
+ metrics = host.getFontMetrics(f);
+ tabSize = getTabSize() * metrics.charWidth('m');
+ }
+
+ /**
+ * Return reasonable default values for the view dimensions. The standard
+ * text terminal size 80x24 is pretty suitable for the wrapped plain view.
+ */
+ private float getDefaultSpan(int axis) {
+ switch (axis) {
+ case View.X_AXIS:
+ return 80 * metrics.getWidths()['M'];
+ case View.Y_AXIS:
+ return 24 * metrics.getHeight();
+ default:
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ }
+
+ // --- TabExpander methods ------------------------------------------
+
+ /**
+ * Returns the next tab stop position after a given reference position.
+ * This implementation does not support things like centering so it
+ * ignores the tabOffset argument.
+ *
+ * @param x the current position >= 0
+ * @param tabOffset the position within the text stream
+ * that the tab occurred at >= 0.
+ * @return the tab stop, measured in points >= 0
+ */
+ public float nextTabStop(float x, int tabOffset) {
+ if (tabSize == 0)
+ return x;
+ int ntabs = ((int) x - tabBase) / tabSize;
+ return tabBase + ((ntabs + 1) * tabSize);
+ }
+
+
+ // --- View methods -------------------------------------
+
+ /**
+ * Renders using the given rendering surface and area
+ * on that surface. This is implemented to stash the
+ * selection positions, selection colors, and font
+ * metrics for the nested lines to use.
+ *
+ * @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) {
+ Rectangle alloc = (Rectangle) a;
+ tabBase = alloc.x;
+ JTextComponent host = (JTextComponent) getContainer();
+ sel0 = host.getSelectionStart();
+ sel1 = host.getSelectionEnd();
+ unselected = (host.isEnabled()) ?
+ host.getForeground() : host.getDisabledTextColor();
+ Caret c = host.getCaret();
+ selected = c.isSelectionVisible() && host.getHighlighter() != null ?
+ host.getSelectedTextColor() : unselected;
+ g.setFont(host.getFont());
+
+ // superclass paints the children
+ super.paint(g, a);
+ }
+
+ /**
+ * Sets the size of the view. This should cause
+ * layout of the view along the given axis, if it
+ * has any layout duties.
+ *
+ * @param width the width >= 0
+ * @param height the height >= 0
+ */
+ public void setSize(float width, float height) {
+ updateMetrics();
+ if ((int) width != getWidth()) {
+ // invalidate the view itself since the childrens
+ // desired widths will be based upon this views width.
+ preferenceChanged(null, true, true);
+ widthChanging = true;
+ }
+ super.setSize(width, height);
+ widthChanging = false;
+ }
+
+ /**
+ * Determines the preferred span for this view along an
+ * axis. This is implemented to provide the superclass
+ * behavior after first making sure that the current font
+ * metrics are cached (for the nested lines which use
+ * the metrics to determine the height of the potentially
+ * wrapped lines).
+ *
+ * @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 View#getPreferredSpan
+ */
+ public float getPreferredSpan(int axis) {
+ updateMetrics();
+ return super.getPreferredSpan(axis);
+ }
+
+ /**
+ * Determines the minimum span for this view along an
+ * axis. This is implemented to provide the superclass
+ * behavior after first making sure that the current font
+ * metrics are cached (for the nested lines which use
+ * the metrics to determine the height of the potentially
+ * wrapped lines).
+ *
+ * @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 View#getMinimumSpan
+ */
+ public float getMinimumSpan(int axis) {
+ updateMetrics();
+ return super.getMinimumSpan(axis);
+ }
+
+ /**
+ * Determines the maximum span for this view along an
+ * axis. This is implemented to provide the superclass
+ * behavior after first making sure that the current font
+ * metrics are cached (for the nested lines which use
+ * the metrics to determine the height of the potentially
+ * wrapped lines).
+ *
+ * @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 View#getMaximumSpan
+ */
+ public float getMaximumSpan(int axis) {
+ updateMetrics();
+ return super.getMaximumSpan(axis);
+ }
+
+ /**
+ * Gives notification that something was inserted into the
+ * document in a location that this view is responsible for.
+ * This is implemented to simply update the children.
+ *
+ * @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) {
+ updateChildren(e, a);
+
+ Rectangle alloc = ((a != null) && isAllocationValid()) ?
+ getInsideAllocation(a) : null;
+ int pos = e.getOffset();
+ View v = getViewAtPosition(pos, alloc);
+ if (v != null) {
+ v.insertUpdate(e, alloc, f);
+ }
+ }
+
+ /**
+ * Gives notification that something was removed from the
+ * document in a location that this view is responsible for.
+ * This is implemented to simply update the children.
+ *
+ * @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) {
+ updateChildren(e, a);
+
+ Rectangle alloc = ((a != null) && isAllocationValid()) ?
+ getInsideAllocation(a) : null;
+ int pos = e.getOffset();
+ View v = getViewAtPosition(pos, alloc);
+ if (v != null) {
+ v.removeUpdate(e, alloc, 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) {
+ updateChildren(e, a);
+ }
+
+ // --- variables -------------------------------------------
+
+ FontMetrics metrics;
+ Segment lineBuffer;
+ boolean widthChanging;
+ int tabBase;
+ int tabSize;
+ boolean wordWrap;
+
+ int sel0;
+ int sel1;
+ Color unselected;
+ Color selected;
+
+
+ /**
+ * Simple view of a line that wraps if it doesn't
+ * fit withing the horizontal space allocated.
+ * This class tries to be lightweight by carrying little
+ * state of it's own and sharing the state of the outer class
+ * with it's sibblings.
+ */
+ class WrappedLine extends View {
+
+ WrappedLine(Element elem) {
+ super(elem);
+ lineCount = -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.
+ * @see View#getPreferredSpan
+ */
+ public float getPreferredSpan(int axis) {
+ switch (axis) {
+ case View.X_AXIS:
+ float width = getWidth();
+ if (width == Integer.MAX_VALUE) {
+ // We have been initially set to MAX_VALUE, but we don't
+ // want this as our preferred.
+ width = getDefaultSpan(axis);
+ }
+ return width;
+ case View.Y_AXIS:
+ if (getDocument().getLength() > 0) {
+ if ((lineCount < 0) || widthChanging) {
+ breakLines(getStartOffset());
+ }
+ return lineCount * metrics.getHeight();
+ } else {
+ return getDefaultSpan(axis);
+ }
+ default:
+ throw new IllegalArgumentException("Invalid axis: " + axis);
+ }
+ }
+
+ /**
+ * Renders using the given rendering surface and area on that
+ * surface. The view may need to do layout and create child
+ * views to enable itself to render into the given allocation.
+ *
+ * @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) {
+ Rectangle alloc = (Rectangle) a;
+ int y = alloc.y + metrics.getAscent();
+ int x = alloc.x;
+
+ JTextComponent host = (JTextComponent)getContainer();
+ Highlighter h = host.getHighlighter();
+ LayeredHighlighter dh = (h instanceof LayeredHighlighter) ?
+ (LayeredHighlighter)h : null;
+
+ int start = getStartOffset();
+ int end = getEndOffset();
+ int p0 = start;
+ int[] lineEnds = getLineEnds();
+ for (int i = 0; i < lineCount; i++) {
+ int p1 = (lineEnds == null) ? end :
+ start + lineEnds[i];
+ if (dh != null) {
+ int hOffset = (p1 == end)
+ ? (p1 - 1)
+ : p1;
+ dh.paintLayeredHighlights(g, p0, hOffset, a, host, this);
+ }
+ drawLine(p0, p1, g, x, y);
+
+ p0 = p1;
+ y += metrics.getHeight();
+ }
+ }
+
+ /**
+ * 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 is returned
+ * @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 {
+ Rectangle alloc = a.getBounds();
+ alloc.height = metrics.getHeight();
+ alloc.width = 1;
+
+ int p0 = getStartOffset();
+ if (pos < p0 || pos > getEndOffset()) {
+ throw new BadLocationException("Position out of range", pos);
+ }
+
+ int testP = (b == Position.Bias.Forward) ? pos :
+ Math.max(p0, pos - 1);
+ int line = 0;
+ int[] lineEnds = getLineEnds();
+ if (lineEnds != null) {
+ line = findLine(testP - p0);
+ if (line > 0) {
+ p0 += lineEnds[line - 1];
+ }
+ alloc.y += alloc.height * line;
+ }
+
+ if (pos > p0) {
+ Segment segment = SegmentCache.getSharedSegment();
+ loadText(segment, p0, pos);
+ alloc.x += Utilities.getTabbedTextWidth(segment, metrics,
+ alloc.x, WrappedPlainView.this, p0);
+ SegmentCache.releaseSharedSegment(segment);
+ }
+ return alloc;
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model.
+ *
+ * @param fx the X coordinate
+ * @param fy the Y coordinate
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents the
+ * given point in the view
+ * @see View#viewToModel
+ */
+ public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
+ // PENDING(prinz) implement bias properly
+ bias[0] = Position.Bias.Forward;
+
+ Rectangle alloc = (Rectangle) a;
+ int x = (int) fx;
+ int y = (int) fy;
+ if (y < alloc.y) {
+ // above the area covered by this icon, so the the position
+ // is assumed to be the start of the coverage for this view.
+ return getStartOffset();
+ } else if (y > alloc.y + alloc.height) {
+ // below the area covered by this icon, so the the position
+ // is assumed to be the end of the coverage for this view.
+ return getEndOffset() - 1;
+ } else {
+ // positioned within the coverage of this view vertically,
+ // so we figure out which line the point corresponds to.
+ // if the line is greater than the number of lines contained, then
+ // simply use the last line as it represents the last possible place
+ // we can position to.
+ alloc.height = metrics.getHeight();
+ int line = (alloc.height > 0 ?
+ (y - alloc.y) / alloc.height : lineCount - 1);
+ if (line >= lineCount) {
+ return getEndOffset() - 1;
+ } else {
+ int p0 = getStartOffset();
+ int p1;
+ if (lineCount == 1) {
+ p1 = getEndOffset();
+ } else {
+ int[] lineEnds = getLineEnds();
+ p1 = p0 + lineEnds[line];
+ if (line > 0) {
+ p0 += lineEnds[line - 1];
+ }
+ }
+
+ if (x < alloc.x) {
+ // point is to the left of the line
+ return p0;
+ } else if (x > alloc.x + alloc.width) {
+ // point is to the right of the line
+ return p1 - 1;
+ } else {
+ // Determine the offset into the text
+ Segment segment = SegmentCache.getSharedSegment();
+ loadText(segment, p0, p1);
+ int n = Utilities.getTabbedTextOffset(segment, metrics,
+ alloc.x, x,
+ WrappedPlainView.this, p0);
+ SegmentCache.releaseSharedSegment(segment);
+ return Math.min(p0 + n, p1 - 1);
+ }
+ }
+ }
+ }
+
+ public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) {
+ update(e, a);
+ }
+
+ public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) {
+ update(e, a);
+ }
+
+ private void update(DocumentEvent ev, Shape a) {
+ int oldCount = lineCount;
+ breakLines(ev.getOffset());
+ if (oldCount != lineCount) {
+ WrappedPlainView.this.preferenceChanged(this, false, true);
+ // have to repaint any views after the receiver.
+ getContainer().repaint();
+ } else if (a != null) {
+ Component c = getContainer();
+ Rectangle alloc = (Rectangle) a;
+ c.repaint(alloc.x, alloc.y, alloc.width, alloc.height);
+ }
+ }
+
+ /**
+ * Returns line cache. If the cache was GC'ed, recreates it.
+ * If there's no cache, returns null
+ */
+ final int[] getLineEnds() {
+ if (lineCache == null) {
+ return null;
+ } else {
+ int[] lineEnds = lineCache.get();
+ if (lineEnds == null) {
+ // Cache was GC'ed, so rebuild it
+ return breakLines(getStartOffset());
+ } else {
+ return lineEnds;
+ }
+ }
+ }
+
+ /**
+ * Creates line cache if text breaks into more than one physical line.
+ * @param startPos position to start breaking from
+ * @return the cache created, ot null if text breaks into one line
+ */
+ final int[] breakLines(int startPos) {
+ int[] lineEnds = (lineCache == null) ? null : lineCache.get();
+ int[] oldLineEnds = lineEnds;
+ int start = getStartOffset();
+ int lineIndex = 0;
+ if (lineEnds != null) {
+ lineIndex = findLine(startPos - start);
+ if (lineIndex > 0) {
+ lineIndex--;
+ }
+ }
+
+ int p0 = (lineIndex == 0) ? start : start + lineEnds[lineIndex - 1];
+ int p1 = getEndOffset();
+ while (p0 < p1) {
+ int p = calculateBreakPosition(p0, p1);
+ p0 = (p == p0) ? ++p : p; // 4410243
+
+ if (lineIndex == 0 && p0 >= p1) {
+ // do not use cache if there's only one line
+ lineCache = null;
+ lineEnds = null;
+ lineIndex = 1;
+ break;
+ } else if (lineEnds == null || lineIndex >= lineEnds.length) {
+ // we have 2+ lines, and the cache is not big enough
+ // we try to estimate total number of lines
+ double growFactor = ((double)(p1 - start) / (p0 - start));
+ int newSize = (int)Math.ceil((lineIndex + 1) * growFactor);
+ newSize = Math.max(newSize, lineIndex + 2);
+ int[] tmp = new int[newSize];
+ if (lineEnds != null) {
+ System.arraycopy(lineEnds, 0, tmp, 0, lineIndex);
+ }
+ lineEnds = tmp;
+ }
+ lineEnds[lineIndex++] = p0 - start;
+ }
+
+ lineCount = lineIndex;
+ if (lineCount > 1) {
+ // check if the cache is too big
+ int maxCapacity = lineCount + lineCount / 3;
+ if (lineEnds.length > maxCapacity) {
+ int[] tmp = new int[maxCapacity];
+ System.arraycopy(lineEnds, 0, tmp, 0, lineCount);
+ lineEnds = tmp;
+ }
+ }
+
+ if (lineEnds != null && lineEnds != oldLineEnds) {
+ lineCache = new SoftReference<int[]>(lineEnds);
+ }
+ return lineEnds;
+ }
+
+ /**
+ * Binary search in the cache for line containing specified offset
+ * (which is relative to the beginning of the view). This method
+ * assumes that cache exists.
+ */
+ private int findLine(int offset) {
+ int[] lineEnds = lineCache.get();
+ if (offset < lineEnds[0]) {
+ return 0;
+ } else if (offset > lineEnds[lineCount - 1]) {
+ return lineCount;
+ } else {
+ return findLine(lineEnds, offset, 0, lineCount - 1);
+ }
+ }
+
+ private int findLine(int[] array, int offset, int min, int max) {
+ if (max - min <= 1) {
+ return max;
+ } else {
+ int mid = (max + min) / 2;
+ return (offset < array[mid]) ?
+ findLine(array, offset, min, mid) :
+ findLine(array, offset, mid, max);
+ }
+ }
+
+ int lineCount;
+ SoftReference<int[]> lineCache = null;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/ZoneView.java b/src/share/classes/javax/swing/text/ZoneView.java
new file mode 100644
index 000000000..0dda695fb
--- /dev/null
+++ b/src/share/classes/javax/swing/text/ZoneView.java
@@ -0,0 +1,651 @@
+/*
+ * 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;
+
+import java.util.Vector;
+import java.awt.*;
+import javax.swing.event.*;
+
+/**
+ * ZoneView is a View implementation that creates zones for which
+ * the child views are not created or stored until they are needed
+ * for display or model/view translations. This enables a substantial
+ * reduction in memory consumption for situations where the model
+ * being represented is very large, by building view objects only for
+ * the region being actively viewed/edited. The size of the children
+ * can be estimated in some way, or calculated asynchronously with
+ * only the result being saved.
+ * <p>
+ * ZoneView extends BoxView to provide a box that implements
+ * zones for its children. The zones are special View implementations
+ * (the children of an instance of this class) that represent only a
+ * portion of the model that an instance of ZoneView is responsible
+ * for. The zones don't create child views until an attempt is made
+ * to display them. A box shaped view is well suited to this because:
+ * <ul>
+ * <li>
+ * Boxes are a heavily used view, and having a box that
+ * provides this behavior gives substantial opportunity
+ * to plug the behavior into a view hierarchy from the
+ * view factory.
+ * <li>
+ * Boxes are tiled in one direction, so it is easy to
+ * divide them into zones in a reliable way.
+ * <li>
+ * Boxes typically have a simple relationship to the model (i.e. they
+ * create child views that directly represent the child elements).
+ * <li>
+ * Boxes are easier to estimate the size of than some other shapes.
+ * </ul>
+ * <p>
+ * The default behavior is controled by two properties, maxZoneSize
+ * and maxZonesLoaded. Setting maxZoneSize to Integer.MAX_VALUE would
+ * have the effect of causing only one zone to be created. This would
+ * effectively turn the view into an implementation of the decorator
+ * pattern. Setting maxZonesLoaded to a value of Integer.MAX_VALUE would
+ * cause zones to never be unloaded. For simplicity, zones are created on
+ * boundaries represented by the child elements of the element the view is
+ * responsible for. The zones can be any View implementation, but the
+ * default implementation is based upon AsyncBoxView which supports fairly
+ * large zones efficiently.
+ *
+ * @author Timothy Prinzing
+ * @see View
+ * @since 1.3
+ */
+public class ZoneView extends BoxView {
+
+ int maxZoneSize = 8 * 1024;
+ int maxZonesLoaded = 3;
+ Vector loadedZones;
+
+ /**
+ * Constructs a ZoneView.
+ *
+ * @param elem the element this view is responsible for
+ * @param axis either View.X_AXIS or View.Y_AXIS
+ */
+ public ZoneView(Element elem, int axis) {
+ super(elem, axis);
+ loadedZones = new Vector();
+ }
+
+ /**
+ * Get the current maximum zone size.
+ */
+ public int getMaximumZoneSize() {
+ return maxZoneSize;
+ }
+
+ /**
+ * Set the desired maximum zone size. A
+ * zone may get larger than this size if
+ * a single child view is larger than this
+ * size since zones are formed on child view
+ * boundaries.
+ *
+ * @param size the number of characters the zone
+ * may represent before attempting to break
+ * the zone into a smaller size.
+ */
+ public void setMaximumZoneSize(int size) {
+ maxZoneSize = size;
+ }
+
+ /**
+ * Get the current setting of the number of zones
+ * allowed to be loaded at the same time.
+ */
+ public int getMaxZonesLoaded() {
+ return maxZonesLoaded;
+ }
+
+ /**
+ * Sets the current setting of the number of zones
+ * allowed to be loaded at the same time. This will throw an
+ * <code>IllegalArgumentException</code> if <code>mzl</code> is less
+ * than 1.
+ *
+ * @param mzl the desired maximum number of zones
+ * to be actively loaded, must be greater than 0
+ * @exception IllegalArgumentException if <code>mzl</code> is < 1
+ */
+ public void setMaxZonesLoaded(int mzl) {
+ if (mzl < 1) {
+ throw new IllegalArgumentException("ZoneView.setMaxZonesLoaded must be greater than 0.");
+ }
+ maxZonesLoaded = mzl;
+ unloadOldZones();
+ }
+
+ /**
+ * Called by a zone when it gets loaded. This happens when
+ * an attempt is made to display or perform a model/view
+ * translation on a zone that was in an unloaded state.
+ * This is imlemented to check if the maximum number of
+ * zones was reached and to unload the oldest zone if so.
+ *
+ * @param zone the child view that was just loaded.
+ */
+ protected void zoneWasLoaded(View zone) {
+ //System.out.println("loading: " + zone.getStartOffset() + "," + zone.getEndOffset());
+ loadedZones.addElement(zone);
+ unloadOldZones();
+ }
+
+ void unloadOldZones() {
+ while (loadedZones.size() > getMaxZonesLoaded()) {
+ View zone = (View) loadedZones.elementAt(0);
+ loadedZones.removeElementAt(0);
+ unloadZone(zone);
+ }
+ }
+
+ /**
+ * Unload a zone (Convert the zone to its memory saving state).
+ * The zones are expected to represent a subset of the
+ * child elements of the element this view is responsible for.
+ * Therefore, the default implementation is to simple remove
+ * all the children.
+ *
+ * @param zone the child view desired to be set to an
+ * unloaded state.
+ */
+ protected void unloadZone(View zone) {
+ //System.out.println("unloading: " + zone.getStartOffset() + "," + zone.getEndOffset());
+ zone.removeAll();
+ }
+
+ /**
+ * Determine if a zone is in the loaded state.
+ * The zones are expected to represent a subset of the
+ * child elements of the element this view is responsible for.
+ * Therefore, the default implementation is to return
+ * true if the view has children.
+ */
+ protected boolean isZoneLoaded(View zone) {
+ return (zone.getViewCount() > 0);
+ }
+
+ /**
+ * Create a view to represent a zone for the given
+ * range within the model (which should be within
+ * the range of this objects responsibility). This
+ * is called by the zone management logic to create
+ * new zones. Subclasses can provide a different
+ * implementation for a zone by changing this method.
+ *
+ * @param p0 the start of the desired zone. This should
+ * be >= getStartOffset() and < getEndOffset(). This
+ * value should also be < p1.
+ * @param p1 the end of the desired zone. This should
+ * be > getStartOffset() and <= getEndOffset(). This
+ * value should also be > p0.
+ */
+ protected View createZone(int p0, int p1) {
+ Document doc = getDocument();
+ View zone = null;
+ try {
+ zone = new Zone(getElement(),
+ doc.createPosition(p0),
+ doc.createPosition(p1));
+ } catch (BadLocationException ble) {
+ // this should puke in some way.
+ throw new StateInvariantError(ble.getMessage());
+ }
+ return zone;
+ }
+
+ /**
+ * Loads all of the children to initialize the view.
+ * This is called by the <code>setParent</code> method.
+ * This is reimplemented to not load any children directly
+ * (as they are created by the zones). This method creates
+ * the initial set of zones. Zones don't actually get
+ * populated however until an attempt is made to display
+ * them or to do model/view coordinate translation.
+ *
+ * @param f the view factory
+ */
+ protected void loadChildren(ViewFactory f) {
+ // build the first zone.
+ Document doc = getDocument();
+ int offs0 = getStartOffset();
+ int offs1 = getEndOffset();
+ append(createZone(offs0, offs1));
+ handleInsert(offs0, offs1 - offs0);
+ }
+
+ /**
+ * Returns the child view index representing the given position in
+ * the model.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ */
+ protected int getViewIndexAtPosition(int pos) {
+ // PENDING(prinz) this could be done as a binary
+ // search, and probably should be.
+ int n = getViewCount();
+ if (pos == getEndOffset()) {
+ return n - 1;
+ }
+ for(int i = 0; i < n; i++) {
+ View v = getView(i);
+ if(pos >= v.getStartOffset() &&
+ pos < v.getEndOffset()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ void handleInsert(int pos, int length) {
+ int index = getViewIndex(pos, Position.Bias.Forward);
+ View v = getView(index);
+ int offs0 = v.getStartOffset();
+ int offs1 = v.getEndOffset();
+ if ((offs1 - offs0) > maxZoneSize) {
+ splitZone(index, offs0, offs1);
+ }
+ }
+
+ void handleRemove(int pos, int length) {
+ // IMPLEMENT
+ }
+
+ /**
+ * Break up the zone at the given index into pieces
+ * of an acceptable size.
+ */
+ void splitZone(int index, int offs0, int offs1) {
+ // divide the old zone into a new set of bins
+ Element elem = getElement();
+ Document doc = elem.getDocument();
+ Vector zones = new Vector();
+ int offs = offs0;
+ do {
+ offs0 = offs;
+ offs = Math.min(getDesiredZoneEnd(offs0), offs1);
+ zones.addElement(createZone(offs0, offs));
+ } while (offs < offs1);
+ View oldZone = getView(index);
+ View[] newZones = new View[zones.size()];
+ zones.copyInto(newZones);
+ replace(index, 1, newZones);
+ }
+
+ /**
+ * Returns the zone position to use for the
+ * end of a zone that starts at the given
+ * position. By default this returns something
+ * close to half the max zone size.
+ */
+ int getDesiredZoneEnd(int pos) {
+ Element elem = getElement();
+ int index = elem.getElementIndex(pos + (maxZoneSize / 2));
+ Element child = elem.getElement(index);
+ int offs0 = child.getStartOffset();
+ int offs1 = child.getEndOffset();
+ if ((offs1 - pos) > maxZoneSize) {
+ if (offs0 > pos) {
+ return offs0;
+ }
+ }
+ return offs1;
+ }
+
+ // ---- View methods ----------------------------------------------------
+
+ /**
+ * The superclass behavior will try to update the child views
+ * which is not desired in this case, since the children are
+ * zones and not directly effected by the changes to the
+ * associated element. This is reimplemented to do nothing
+ * and return false.
+ */
+ protected boolean updateChildren(DocumentEvent.ElementChange ec,
+ DocumentEvent e, ViewFactory f) {
+ return false;
+ }
+
+ /**
+ * Gives notification that something was inserted into the document
+ * in a location that this view is responsible for. This is largely
+ * delegated to the superclass, but is reimplemented to update the
+ * relevant zone (i.e. determine if a zone needs to be split into a
+ * set of 2 or more zones).
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ handleInsert(changes.getOffset(), changes.getLength());
+ super.insertUpdate(changes, a, f);
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that this view is responsible for. This is largely
+ * delegated to the superclass, but is reimplemented to update the
+ * relevant zones (i.e. determine if zones need to be removed or
+ * joined with another zone).
+ *
+ * @param changes 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 changes, Shape a, ViewFactory f) {
+ handleRemove(changes.getOffset(), changes.getLength());
+ super.removeUpdate(changes, a, f);
+ }
+
+ /**
+ * Internally created view that has the purpose of holding
+ * the views that represent the children of the ZoneView
+ * that have been arranged in a zone.
+ */
+ class Zone extends AsyncBoxView {
+
+ private Position start;
+ private Position end;
+
+ public Zone(Element elem, Position start, Position end) {
+ super(elem, ZoneView.this.getAxis());
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Creates the child views and populates the
+ * zone with them. This is done by translating
+ * the positions to child element index locations
+ * and building views to those elements. If the
+ * zone is already loaded, this does nothing.
+ */
+ public void load() {
+ if (! isLoaded()) {
+ setEstimatedMajorSpan(true);
+ Element e = getElement();
+ ViewFactory f = getViewFactory();
+ int index0 = e.getElementIndex(getStartOffset());
+ int index1 = e.getElementIndex(getEndOffset());
+ View[] added = new View[index1 - index0 + 1];
+ for (int i = index0; i <= index1; i++) {
+ added[i - index0] = f.create(e.getElement(i));
+ }
+ replace(0, 0, added);
+
+ zoneWasLoaded(this);
+ }
+ }
+
+ /**
+ * Removes the child views and returns to a
+ * state of unloaded.
+ */
+ public void unload() {
+ setEstimatedMajorSpan(true);
+ removeAll();
+ }
+
+ /**
+ * Determines if the zone is in the loaded state
+ * or not.
+ */
+ public boolean isLoaded() {
+ return (getViewCount() != 0);
+ }
+
+ /**
+ * This method is reimplemented to not build the children
+ * since the children are created when the zone is loaded
+ * rather then when it is placed in the view hierarchy.
+ * The major span is estimated at this point by building
+ * the first child (but not storing it), and calling
+ * setEstimatedMajorSpan(true) followed by setSpan for
+ * the major axis with the estimated span.
+ */
+ protected void loadChildren(ViewFactory f) {
+ // mark the major span as estimated
+ setEstimatedMajorSpan(true);
+
+ // estimate the span
+ Element elem = getElement();
+ int index0 = elem.getElementIndex(getStartOffset());
+ int index1 = elem.getElementIndex(getEndOffset());
+ int nChildren = index1 - index0;
+
+ // replace this with something real
+ //setSpan(getMajorAxis(), nChildren * 10);
+
+ View first = f.create(elem.getElement(index0));
+ first.setParent(this);
+ float w = first.getPreferredSpan(X_AXIS);
+ float h = first.getPreferredSpan(Y_AXIS);
+ if (getMajorAxis() == X_AXIS) {
+ w *= nChildren;
+ } else {
+ h += nChildren;
+ }
+
+ setSize(w, h);
+ }
+
+ /**
+ * Publish the changes in preferences upward to the parent
+ * view.
+ * <p>
+ * This is reimplemented to stop the superclass behavior
+ * if the zone has not yet been loaded. If the zone is
+ * unloaded for example, the last seen major span is the
+ * best estimate and a calculated span for no children
+ * is undesirable.
+ */
+ protected void flushRequirementChanges() {
+ if (isLoaded()) {
+ super.flushRequirementChanges();
+ }
+ }
+
+ /**
+ * Returns the child view index representing the given position in
+ * the model. Since the zone contains a cluster of the overall
+ * set of child elements, we can determine the index fairly
+ * quickly from the model by subtracting the index of the
+ * start offset from the index of the position given.
+ *
+ * @param pos the position >= 0
+ * @return index of the view representing the given position, or
+ * -1 if no view represents that position
+ * @since 1.3
+ */
+ public int getViewIndex(int pos, Position.Bias b) {
+ boolean isBackward = (b == Position.Bias.Backward);
+ pos = (isBackward) ? Math.max(0, pos - 1) : pos;
+ Element elem = getElement();
+ int index1 = elem.getElementIndex(pos);
+ int index0 = elem.getElementIndex(getStartOffset());
+ return index1 - index0;
+ }
+
+ protected boolean updateChildren(DocumentEvent.ElementChange ec,
+ DocumentEvent e, ViewFactory f) {
+ // the structure of this element changed.
+ Element[] removedElems = ec.getChildrenRemoved();
+ Element[] addedElems = ec.getChildrenAdded();
+ Element elem = getElement();
+ int index0 = elem.getElementIndex(getStartOffset());
+ int index1 = elem.getElementIndex(getEndOffset()-1);
+ int index = ec.getIndex();
+ if ((index >= index0) && (index <= index1)) {
+ // The change is in this zone
+ int replaceIndex = index - index0;
+ int nadd = Math.min(index1 - index0 + 1, addedElems.length);
+ int nremove = Math.min(index1 - index0 + 1, removedElems.length);
+ View[] added = new View[nadd];
+ for (int i = 0; i < nadd; i++) {
+ added[i] = f.create(addedElems[i]);
+ }
+ replace(replaceIndex, nremove, added);
+ }
+ return true;
+ }
+
+ // --- View methods ----------------------------------
+
+ /**
+ * Fetches the attributes to use when rendering. This view
+ * isn't directly responsible for an element so it returns
+ * the outer classes attributes.
+ */
+ public AttributeSet getAttributes() {
+ return ZoneView.this.getAttributes();
+ }
+
+ /**
+ * Renders using the given rendering surface and area on that
+ * surface. This is implemented to load the zone if its not
+ * already loaded, and then perform the superclass behavior.
+ *
+ * @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) {
+ load();
+ super.paint(g, a);
+ }
+
+ /**
+ * Provides a mapping from the view coordinate space to the logical
+ * coordinate space of the model. This is implemented to first
+ * make sure the zone is loaded before providing the superclass
+ * behavior.
+ *
+ * @param x x coordinate of the view location to convert >= 0
+ * @param y y coordinate of the view location to convert >= 0
+ * @param a the allocated region to render into
+ * @return the location within the model that best represents the
+ * given point in the view >= 0
+ * @see View#viewToModel
+ */
+ public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
+ load();
+ return super.viewToModel(x, y, a, bias);
+ }
+
+ /**
+ * Provides a mapping from the document model coordinate space
+ * to the coordinate space of the view mapped to it. This is
+ * implemented to provide the superclass behavior after first
+ * making sure the zone is loaded (The zone must be loaded to
+ * make this calculation).
+ *
+ * @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 {
+ load();
+ return super.modelToView(pos, a, b);
+ }
+
+ /**
+ * Start of the zones range.
+ *
+ * @see View#getStartOffset
+ */
+ public int getStartOffset() {
+ return start.getOffset();
+ }
+
+ /**
+ * End of the zones range.
+ */
+ public int getEndOffset() {
+ return end.getOffset();
+ }
+
+ /**
+ * Gives notification that something was inserted into
+ * the document in a location that this view is responsible for.
+ * If the zone has been loaded, the superclass behavior is
+ * invoked, otherwise this does nothing.
+ *
+ * @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) {
+ if (isLoaded()) {
+ super.insertUpdate(e, a, f);
+ }
+ }
+
+ /**
+ * Gives notification that something was removed from the document
+ * in a location that this view is responsible for.
+ * If the zone has been loaded, the superclass behavior is
+ * invoked, otherwise this does nothing.
+ *
+ * @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) {
+ if (isLoaded()) {
+ super.removeUpdate(e, a, f);
+ }
+ }
+
+ /**
+ * Gives notification from the document that attributes were changed
+ * in a location that this view is responsible for.
+ * If the zone has been loaded, the superclass behavior is
+ * invoked, otherwise this does nothing.
+ *
+ * @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 changedUpdate(DocumentEvent e, Shape a, ViewFactory f) {
+ if (isLoaded()) {
+ super.changedUpdate(e, a, f);
+ }
+ }
+
+ }
+}
diff --git a/src/share/classes/javax/swing/text/doc-files/Document-coord.gif b/src/share/classes/javax/swing/text/doc-files/Document-coord.gif
new file mode 100644
index 000000000..116d99844
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/Document-coord.gif
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/Document-insert.gif b/src/share/classes/javax/swing/text/doc-files/Document-insert.gif
new file mode 100644
index 000000000..7e943ce77
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/Document-insert.gif
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/Document-notification.gif b/src/share/classes/javax/swing/text/doc-files/Document-notification.gif
new file mode 100644
index 000000000..02579f36c
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/Document-notification.gif
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/Document-remove.gif b/src/share/classes/javax/swing/text/doc-files/Document-remove.gif
new file mode 100644
index 000000000..69cb1f53c
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/Document-remove.gif
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/Document-structure.gif b/src/share/classes/javax/swing/text/doc-files/Document-structure.gif
new file mode 100644
index 000000000..47e78e263
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/Document-structure.gif
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/OpenBookIcon.gif b/src/share/classes/javax/swing/text/doc-files/OpenBookIcon.gif
new file mode 100644
index 000000000..86384f773
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/OpenBookIcon.gif
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/View-flexibility.jpg b/src/share/classes/javax/swing/text/doc-files/View-flexibility.jpg
new file mode 100644
index 000000000..599ca4622
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/View-flexibility.jpg
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/View-layout.jpg b/src/share/classes/javax/swing/text/doc-files/View-layout.jpg
new file mode 100644
index 000000000..bcbec154d
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/View-layout.jpg
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/editor.gif b/src/share/classes/javax/swing/text/doc-files/editor.gif
new file mode 100644
index 000000000..d588df80f
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/editor.gif
Binary files differ
diff --git a/src/share/classes/javax/swing/text/doc-files/paragraph.gif b/src/share/classes/javax/swing/text/doc-files/paragraph.gif
new file mode 100644
index 000000000..6f56046b3
--- /dev/null
+++ b/src/share/classes/javax/swing/text/doc-files/paragraph.gif
Binary files differ
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 <code>View</code> 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
+ * <code>unlock</code>.
+ */
+ private Object lock() {
+ Document document = getDocument();
+
+ if (document instanceof AbstractDocument) {
+ ((AbstractDocument)document).readLock();
+ return document;
+ }
+ return null;
+ }
+
+ /**
+ * Releases a lock previously obtained via <code>lock</code>.
+ */
+ private void unlock(Object key) {
+ if (key != null) {
+ ((AbstractDocument)key).readUnlock();
+ }
+ }
+
+ /**
+ * 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
+ * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text").
+ *
+ * @return the localized description of the object; <code>null</code>
+ * 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).
+ * <p>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().
+ * <p>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
+ * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text").
+ *
+ * @return the localized description of the object; <code>null</code>
+ * 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).
+ * <p>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 <code>Segment</code> 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 <code>index</code>.
+ * If <code>direction</code> 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 <code>Segment</code> containing the paragraph text
+ * at <code>index</code>, or null if <code>index</code> 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 <code>index</code> representing either
+ * the paragraph or sentence as identified by <code>part</code>, 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
+ * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text").
+ *
+ * @return the localized description of the object; <code>null</code>
+ * 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).
+ * <p>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
+ * <code>JEditorPane</code> instead (e.g. "plain/text", "html/text").
+ *
+ * @return the localized description of the object; <code>null</code>
+ * 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).
+ * <p>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
+ * <code>validateIfNecessary</code>, 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 <code>element</code> 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 <code>readLock</code>. If this is overriden
+ * it MUST invoke supers implementation first!
+ */
+ protected void validate() {
+ isValid = true;
+ loadChildren(getElement());
+ }
+
+ /**
+ * Recreates the direct children of <code>info</code>.
+ */
+ 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 <code>ElementInfo</code> 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
+ * <code>child</code> 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 <code>index</code>, or null
+ * if <code>index</code> 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 <code>isValid</code> before
+ * using one. This will reload the children and invoke
+ * <code>validate</code> 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
+ * <code>update</code> on the <code>RootInfo</code> 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 &lt;BR&gt; tag. In other words, forces a line break.
+ *
+ * @author Sunita Mani
+ */
+class BRView extends InlineView {
+
+ /**
+ * Creates a new view that represents a &lt;BR&gt; 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.
+ * <p>
+ * 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 <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @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 <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @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 <code>View.X_AXIS</code>
+ * or <code>View.Y_AXIS</code>
+ * @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 <code>want</code> to fit in the minimum size specified
+ * by <code>min</code>.
+ */
+ 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
+ * <a href="http://www.w3.org/TR/REC-CSS1">CSS attributes</a>
+ * 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.
+ * <p>The following describes the CSS properties that are suppored by the
+ * rendering engine:
+ * <ul><li>font-family
+ * <li>font-style
+ * <li>font-size (supports relative units)
+ * <li>font-weight
+ * <li>font
+ * <li>color
+ * <li>background-color (with the exception of transparent)
+ * <li>background-image
+ * <li>background-repeat
+ * <li>background-position
+ * <li>background
+ * <li>background-repeat
+ * <li>text-decoration (with the exception of blink and overline)
+ * <li>vertical-align (only sup and super)
+ * <li>text-align (justify is treated as center)
+ * <li>margin-top
+ * <li>margin-right
+ * <li>margin-bottom
+ * <li>margin-left
+ * <li>margin
+ * <li>padding-top
+ * <li>padding-right
+ * <li>padding-bottom
+ * <li>padding-left
+ * <li>border-style (only supports inset, outset and none)
+ * <li>list-style-type
+ * <li>list-style-position
+ * </ul>
+ * The following are modeled, but currently not rendered.
+ * <ul><li>font-variant
+ * <li>background-attachment (background always treated as scroll)
+ * <li>word-spacing
+ * <li>letter-spacing
+ * <li>text-indent
+ * <li>text-transform
+ * <li>line-height
+ * <li>border-top-width (this is used to indicate if a border should be used)
+ * <li>border-right-width
+ * <li>border-bottom-width
+ * <li>border-left-width
+ * <li>border-width
+ * <li>border-top
+ * <li>border-right
+ * <li>border-bottom
+ * <li>border-left
+ * <li>border
+ * <li>width
+ * <li>height
+ * <li>float
+ * <li>clear
+ * <li>display
+ * <li>white-space
+ * <li>list-style
+ * </ul>
+ * <p><b>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 <dir>, <menu>, <ul> 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. <code>sz</code> is a CSS value, and is
+ * not necessarily the point size. Use getPointSize to determine the
+ * point size corresponding to <code>sz</code>.
+ */
+ 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 <code>key</code> with value
+ * <code>value</code> placing the result in <code>att</code>.
+ */
+ 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 <code>value</code> which is
+ * a CSS value of the CSS attribute named <code>key</code>. The receiver
+ * should not modify <code>value</code>, and the first <code>count</code>
+ * 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.
+ * <code>key</code> 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.
+ * <code>sc</code> 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 <code>key</code>.
+ *
+ * @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 <code>a</code> with
+ * key <code>key</code>.
+ */
+ 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 <em>well known</em>
+ * 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 <code>CSS.Attribute</code> object.
+ * This will return <code>null</code> 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 <code>CSS.Attribute</code> object,
+ * or <code>null</code> 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 <code>CSS.Value</code> object.
+ * This will return <code>null</code> 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 <code>CSS.Value</code> object,
+ * or <code>null</code> 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,
+ * <code>base</code> 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 <code>rgb(r, g, b)</code> 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 <code>string</code> starting
+ * at <code>index[0]</code>. 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 <code>value</code>
+ * 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.
+ * <p>
+ * 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).
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>StyleConstants</code> attribute value to
+ * a CSS attribute value. If there is no conversion,
+ * returns <code>null</code>. By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @param value the value of a <code>StyleConstants</code>
+ * attribute to be converted
+ * @return the CSS value that represents the
+ * <code>StyleConstants</code> value
+ */
+ Object fromStyleConstants(StyleConstants key, Object value) {
+ return null;
+ }
+
+ /**
+ * Converts a CSS attribute value to a
+ * <code>StyleConstants</code>
+ * value. If there is no conversion, returns
+ * <code>null</code>.
+ * By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @param v the view containing <code>AttributeSet</code>
+ * @return the <code>StyleConstants</code> 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 <code>StyleConstants</code> attribute value to
+ * a CSS attribute value. If there is no conversion
+ * returns <code>null</code>.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @param value the value of a <code>StyleConstants</code>
+ * attribute to be converted
+ * @return the CSS value that represents the
+ * <code>StyleConstants</code> 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
+ * <code>StyleConstants</code> value.
+ * If there is no conversion, returns <code>null</code>.
+ * By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @return the <code>StyleConstants</code> 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 <basefont>'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 <code>StyleConstants</code> attribute value to
+ * a CSS attribute value. If there is no conversion
+ * returns <code>null</code>. By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @param value the value of a <code>StyleConstants</code>
+ * attribute to be converted
+ * @return the CSS value that represents the
+ * <code>StyleConstants</code> 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 <code>StyleConstants</code>
+ * value. If there is no conversion, returns <code>null</code>.
+ * By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @return the <code>StyleConstants</code> 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 <code>StyleConstants</code> attribute value to
+ * a CSS attribute value. If there is no conversion
+ * returns <code>null</code>. By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @param value the value of a <code>StyleConstants</code>
+ * attribute to be converted
+ * @return the CSS value that represents the
+ * <code>StyleConstants</code> value
+ */
+ Object fromStyleConstants(StyleConstants key, Object value) {
+ return parseCssValue(value.toString());
+ }
+
+ /**
+ * Converts a CSS attribute value to a <code>StyleConstants</code>
+ * value. If there is no conversion, returns <code>null</code>.
+ * By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @return the <code>StyleConstants</code> 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 <code>StyleConstants</code> attribute value to
+ * a CSS attribute value. If there is no conversion
+ * returns <code>null</code>. By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @param value the value of a <code>StyleConstants</code>
+ * attribute to be converted
+ * @return the CSS value that represents the
+ * <code>StyleConstants</code> 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 <code>StyleConstants</code>
+ * value. If there is no conversion, returns <code>null</code>.
+ * By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @return the <code>StyleConstants</code> 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 <code>StyleConstants</code> attribute value to
+ * a CSS attribute value. If there is no conversion
+ * returns <code>null</code>. By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @param value the value of a <code>StyleConstants</code>
+ * attribute to be converted
+ * @return the CSS value that represents the
+ * <code>StyleConstants</code> 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 <code>StyleConstants</code>
+ * value. If there is no conversion, returns <code>null</code>.
+ * By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @return the <code>StyleConstants</code> 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 <code>currentValue</code>.
+ */
+ 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 <code>StyleConstants</code> attribute value to
+ * a CSS attribute value. If there is no conversion,
+ * returns <code>null</code>. By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @param value the value of a <code>StyleConstants</code>
+ * attribute to be converted
+ * @return the CSS value that represents the
+ * <code>StyleConstants</code> 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 <code>StyleConstants</code>
+ * value. If there is no conversion, returns <code>null</code>.
+ * By default, there is no conversion.
+ *
+ * @param key the <code>StyleConstants</code> attribute
+ * @return the <code>StyleConstants</code> 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 <code>value</code>, placing the
+ * result in <code>attr</code>.
+ */
+ static void parseShorthandFont(CSS css, String value,
+ MutableAttributeSet attr) {
+ // font is of the form:
+ // [ <font-style> || <font-variant> || <font-weight> ]? <font-size>
+ // [ / <line-height> ]? <font-family>
+ 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 <code>value</code>, placing the
+ * result in <code>attr</code>.
+ */
+ 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
+ * <code>value</code>, placing the result in <code>attr</code>.
+ * <code>names</code> 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<Value, BorderPainter> borderPainters =
+ new HashMap<Value, BorderPainter>();
+
+ /* 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:
+ * <ul>
+ * <li>Import statement: <code>handleImport</code>
+ * <li>Selectors <code>handleSelector</code>. 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'.
+ * <li>When a rule starts, <code>startRule</code>
+ * <li>Properties in the rule via the <code>handleProperty</code>. 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'.
+ * <li>Values in the rule via the <code>handleValue</code>, this is notified
+ * for the total value.
+ * <li>When a rule ends, <code>endRule</code>
+ * </ul>
+ * This will parse much more than CSS 1, and loosely implements the
+ * recommendation for <i>Forward-compatible parsing</i> in section
+ * 7.1 of the CSS spec found at:
+ * <a href=http://www.w3.org/TR/REC-CSS1>http://www.w3.org/TR/REC-CSS1</a>.
+ * If an error results in parsing, a RuntimeException will be thrown.
+ * <p>
+ * 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 <code>extraChar</code> 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 <code>stopChar</code>, 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 <code>stopChar</code> 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.
+ * <UL>
+ * <LI><code>GET</code> corresponds to the GET form method</LI>
+ * <LI><code>POST</code> corresponds to the POST from method</LI>
+ * </UL>
+ * @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
+ * <code>Method.GET</code> or <code>Method.POST</code>.
+ */
+ 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, &lt;input&gt;, &lt;textarea&gt;,
+ * and &lt;select&gt;. 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
+ * <code>HTMLDocument.HTMLReader.FormAction</code>. If there are
+ * multiple views mapped over the document, they will share the
+ * embedded component models.
+ * <p>
+ * The following table shows what components get built
+ * by this view.
+ * <table summary="shows what components get built by this view">
+ * <tr>
+ * <th>Element Type</th>
+ * <th>Component built</th>
+ * </tr>
+ * <tr>
+ * <td>input, type button</td>
+ * <td>JButton</td>
+ * </tr>
+ * <tr>
+ * <td>input, type checkbox</td>
+ * <td>JCheckBox</td>
+ * </tr>
+ * <tr>
+ * <td>input, type image</td>
+ * <td>JButton</td>
+ * </tr>
+ * <tr>
+ * <td>input, type password</td>
+ * <td>JPasswordField</td>
+ * </tr>
+ * <tr>
+ * <td>input, type radio</td>
+ * <td>JRadioButton</td>
+ * </tr>
+ * <tr>
+ * <td>input, type reset</td>
+ * <td>JButton</td>
+ * </tr>
+ * <tr>
+ * <td>input, type submit</td>
+ * <td>JButton</td>
+ * </tr>
+ * <tr>
+ * <td>input, type text</td>
+ * <td>JTextField</td>
+ * </tr>
+ * <tr>
+ * <td>select, size &gt; 1 or multiple attribute defined</td>
+ * <td>JList in a JScrollPane</td>
+ * </tr>
+ * <tr>
+ * <td>select, size unspecified or 1</td>
+ * <td>JComboBox</td>
+ * </tr>
+ * <tr>
+ * <td>textarea</td>
+ * <td>JTextArea in a JScrollPane</td>
+ * </tr>
+ * <tr>
+ * <td>input, type file</td>
+ * <td>JTextField</td>
+ * </tr>
+ * </table>
+ *
+ * @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 &lt;INPUT&gt; element based on the
+ * value of the "type" attribute.
+ *
+ * @param set of attributes associated with the &lt;INPUT&gt; 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 <PostDataProperty>.<Target frame name>). 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 &lt;INPUT&gt; 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 <code>FORM</code>.
+ */
+ 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 &lt;INPUT&gt; 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 &lt;TEXTAREA&gt; 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 <code>elem</code> 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
+ * &lt;INPUT&gt; 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
+ * &lt;FRAMESET&gt; 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
+ * &lt;FRAME&gt; 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 <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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 <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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 &lt;hr&gt; 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 <em>pos</em>
+ * 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 <code>HTMLDocument</code>. 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 <code>Tag</code> with the specified <code>id</code>,
+ * and with <code>causesBreak</code> and <code>isBlock</code>
+ * set to <code>false</code>.
+ *
+ * @param id the id of the new tag
+ */
+ protected Tag(String id) {
+ this(id, false, false);
+ }
+
+ /**
+ * Creates a new <code>Tag</code> with the specified <code>id</code>;
+ * <code>causesBreak</code> and <code>isBlock</code> are defined
+ * by the user.
+ *
+ * @param id the id of the new tag
+ * @param causesBreak <code>true</code> if this tag
+ * causes a break to the flow of data
+ * @param isBlock <code>true</code> 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 <code>true</code> if this tag is a block
+ * tag, which is a tag used to add structure to a
+ * document.
+ *
+ * @return <code>true</code> if this tag is a block
+ * tag, otherwise returns <code>false</code>
+ */
+ public boolean isBlock() {
+ return blockTag;
+ }
+
+ /**
+ * Returns <code>true</code> if this tag causes a
+ * line break to the flow of data, otherwise returns
+ * <code>false</code>.
+ *
+ * @return <code>true</code> if this tag causes a
+ * line break to the flow of data, otherwise returns
+ * <code>false</code>
+ */
+ public boolean breaksFlow() {
+ return breakTag;
+ }
+
+ /**
+ * Returns <code>true</code> if this tag is pre-formatted,
+ * which is true if the tag is either <code>PRE</code> or
+ * <code>TEXTAREA</code>.
+ *
+ * @return <code>true</code> if this tag is pre-formatted,
+ * otherwise returns <code>false</code>
+ */
+ public boolean isPreformatted() {
+ return (this == PRE || this == TEXTAREA);
+ }
+
+ /**
+ * Returns the string representation of the
+ * tag.
+ *
+ * @return the <code>String</code> representation of the tag
+ */
+ public String toString() {
+ return name;
+ }
+
+ /**
+ * Returns <code>true</code> if this tag is considered to be a paragraph
+ * in the internal HTML model. <code>false</code> - otherwise.
+ *
+ * @return <code>true</code> if this tag is considered to be a paragraph
+ * in the internal HTML model. <code>false</code> - 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>UnknownTag</code> with the specified
+ * <code>id</code>.
+ * @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 <code>true</code> if and only if the argument is not
+ * <code>null</code> and is an <code>UnknownTag</code> object
+ * with the same name.
+ *
+ * @param obj the object to compare this tag with
+ * @return <code>true</code> if the objects are equal;
+ * <code>false</code> 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 <code>Attribute</code> with the specified
+ * <code>id</code>.
+ *
+ * @param id the id of the new <code>Attribute</code>
+ */
+ 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
+ * <code>null</code> will be returned.
+ *
+ * @param tagName the <code>String</code> name requested
+ * @return a tag constant corresponding to the <code>tagName</code>,
+ * or <code>null</code> if not found
+ */
+ public static Tag getTag(String tagName) {
+
+ Object t = tagHashtable.get(tagName);
+ return (t == null ? null : (Tag)t);
+ }
+
+ /**
+ * Returns the HTML <code>Tag</code> associated with the
+ * <code>StyleConstants</code> key <code>sc</code>.
+ * If no matching <code>Tag</code> is found, returns
+ * <code>null</code>.
+ *
+ * @param sc the <code>StyleConstants</code> key
+ * @return tag which corresponds to <code>sc</code>, or
+ * <code>null</code> 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 <code>null</code> will be returned.
+ *
+ * @param attName the <code>String</code> requested
+ * @return the <code>Attribute</code> corresponding to <code>attName</code>
+ */
+ 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 <code>HTMLDocument.HTMLReader</code>, which implements the
+ * <code>HTMLEditorKit.ParserCallback</code> protocol that the parser
+ * expects. To change the structure one can subclass
+ * <code>HTMLReader</code>, and reimplement the method {@link
+ * #getReader(int)} to return the new reader implementation. The
+ * documentation for <code>HTMLReader</code> 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).
+ *
+ * <p>The document models only HTML, and makes no attempt to store
+ * view attributes in it. The elements are identified by the
+ * <code>StyleContext.NameAttribute</code> attribute, which should
+ * always have a value of type <code>HTML.Tag</code> that identifies
+ * the kind of element. Some of the elements (such as comments) are
+ * synthesized. The <code>HTMLFactory</code> uses this attribute to
+ * determine what kind of view to build.</p>
+ *
+ * <p>This document supports incremental loading. The
+ * <code>TokenThreshold</code> 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 <code>EditorKit</code> so
+ * that subclasses can disable it.</p>
+ *
+ * <p>The <code>Base</code> property determines the URL against which
+ * relative URLs are resolved. By default, this will be the
+ * <code>Document.StreamDescriptionProperty</code> if the value of the
+ * property is a URL. If a &lt;BASE&gt; 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.</p>
+ *
+ * <p>The default content storage mechanism for this document is a gap
+ * buffer (<code>GapContent</code>). Alternatives can be supplied by
+ * using the constructor that takes a <code>Content</code>
+ * implementation.</p>
+ *
+ * <h2>Modifying HTMLDocument</h2>
+ *
+ * <p>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.</p>
+ *
+ * <ul>
+ * <li>{@link #setInnerHTML(Element, String)}</li>
+ * <li>{@link #setOuterHTML(Element, String)}</li>
+ * <li>{@link #insertBeforeStart(Element, String)}</li>
+ * <li>{@link #insertAfterStart(Element, String)}</li>
+ * <li>{@link #insertBeforeEnd(Element, String)}</li>
+ * <li>{@link #insertAfterEnd(Element, String)}</li>
+ * </ul>
+ *
+ * <p>The following examples illustrate using these methods. Each
+ * example assumes the HTML document is initialized in the following
+ * way:</p>
+ *
+ * <pre>
+ * JEditorPane p = new JEditorPane();
+ * p.setContentType("text/html");
+ * p.setText("..."); // Document text is provided below.
+ * HTMLDocument d = (HTMLDocument) p.getDocument();
+ * </pre>
+ *
+ * <p>With the following HTML content:</p>
+ *
+ * <pre>
+ * &lt;html>
+ * &lt;head>
+ * &lt;title>An example HTMLDocument&lt;/title>
+ * &lt;style type="text/css">
+ * div { background-color: silver; }
+ * ul { color: red; }
+ * &lt;/style>
+ * &lt;/head>
+ * &lt;body>
+ * &lt;div id="BOX">
+ * &lt;p>Paragraph 1&lt;/p>
+ * &lt;p>Paragraph 2&lt;/p>
+ * &lt;/div>
+ * &lt;/body>
+ * &lt;/html>
+ * </pre>
+ *
+ * <p>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, <code>d.getElement(d.getDefaultRootElement(),
+ * StyleConstants.NameAttribute, HTML.Tag.P)</code> returns the first
+ * paragraph element.</p>
+ *
+ * <p>A convenient shortcut for locating elements is the method {@link
+ * #getElement(String)}; returns an element whose <code>ID</code>
+ * attribute matches the specified value. For example,
+ * <code>d.getElement("BOX")</code> returns the <code>DIV</code>
+ * element.</p>
+ *
+ * <p>The {@link #getIterator(HTML.Tag t)} method can also be used for
+ * finding all occurrences of the specified HTML tag in the
+ * document.</p>
+ *
+ * <h3>Inserting elements</h3>
+ *
+ * <p>Elements can be inserted before or after the existing children
+ * of any non-leaf element by using the methods
+ * <code>insertAfterStart</code> and <code>insertBeforeEnd</code>.
+ * For example, if <code>e</code> is the <code>DIV</code> element,
+ * <code>d.insertAfterStart(e, "&lt;ul>&lt;li>List
+ * Item&lt;/li>&lt;/ul>")</code> inserts the list before the first
+ * paragraph, and <code>d.insertBeforeEnd(e, "&lt;ul>&lt;li>List
+ * Item&lt;/li>&lt;/ul>")</code> inserts the list after the last
+ * paragraph. The <code>DIV</code> block becomes the parent of the
+ * newly inserted elements.</p>
+ *
+ * <p>Sibling elements can be inserted before or after any element by
+ * using the methods <code>insertBeforeStart</code> and
+ * <code>insertAfterEnd</code>. For example, if <code>e</code> is the
+ * <code>DIV</code> element, <code>d.insertBeforeStart(e,
+ * "&lt;ul>&lt;li>List Item&lt;/li>&lt;/ul>")</code> inserts the list
+ * before the <code>DIV</code> element, and <code>d.insertAfterEnd(e,
+ * "&lt;ul>&lt;li>List Item&lt;/li>&lt;/ul>")</code> inserts the list
+ * after the <code>DIV</code> element. The newly inserted elements
+ * become siblings of the <code>DIV</code> element.</p>
+ *
+ * <h3>Replacing elements</h3>
+ *
+ * <p>Elements and all their descendants can be replaced by using the
+ * methods <code>setInnerHTML</code> and <code>setOuterHTML</code>.
+ * For example, if <code>e</code> is the <code>DIV</code> element,
+ * <code>d.setInnerHTML(e, "&lt;ul>&lt;li>List
+ * Item&lt;/li>&lt;/ul>")</code> replaces all children paragraphs with
+ * the list, and <code>d.setOuterHTML(e, "&lt;ul>&lt;li>List
+ * Item&lt;/li>&lt;/ul>")</code> replaces the <code>DIV</code> element
+ * itself. In latter case the parent of the list is the
+ * <code>BODY</code> element.
+ *
+ * <h3>Summary</h3>
+ *
+ * <p>The following table shows the example document and the results
+ * of various methods described above.</p>
+ *
+ * <table border=1 cellspacing=0>
+ * <tr>
+ * <th>Example</th>
+ * <th><code>insertAfterStart</code></th>
+ * <th><code>insertBeforeEnd</code></th>
+ * <th><code>insertBeforeStart</code></th>
+ * <th><code>insertAfterEnd</code></th>
+ * <th><code>setInnerHTML</code></th>
+ * <th><code>setOuterHTML</code></th>
+ * </tr>
+ * <tr valign="top">
+ * <td nowrap="nowrap">
+ * <div style="background-color: silver;">
+ * <p>Paragraph 1</p>
+ * <p>Paragraph 2</p>
+ * </div>
+ * </td>
+ * <!--insertAfterStart-->
+ * <td nowrap="nowrap">
+ * <div style="background-color: silver;">
+ * <ul style="color: red;">
+ * <li>List Item</li>
+ * </ul>
+ * <p>Paragraph 1</p>
+ * <p>Paragraph 2</p>
+ * </div>
+ * </td>
+ * <!--insertBeforeEnd-->
+ * <td nowrap="nowrap">
+ * <div style="background-color: silver;">
+ * <p>Paragraph 1</p>
+ * <p>Paragraph 2</p>
+ * <ul style="color: red;">
+ * <li>List Item</li>
+ * </ul>
+ * </div>
+ * </td>
+ * <!--insertBeforeStart-->
+ * <td nowrap="nowrap">
+ * <ul style="color: red;">
+ * <li>List Item</li>
+ * </ul>
+ * <div style="background-color: silver;">
+ * <p>Paragraph 1</p>
+ * <p>Paragraph 2</p>
+ * </div>
+ * </td>
+ * <!--insertAfterEnd-->
+ * <td nowrap="nowrap">
+ * <div style="background-color: silver;">
+ * <p>Paragraph 1</p>
+ * <p>Paragraph 2</p>
+ * </div>
+ * <ul style="color: red;">
+ * <li>List Item</li>
+ * </ul>
+ * </td>
+ * <!--setInnerHTML-->
+ * <td nowrap="nowrap">
+ * <div style="background-color: silver;">
+ * <ul style="color: red;">
+ * <li>List Item</li>
+ * </ul>
+ * </div>
+ * </td>
+ * <!--setOuterHTML-->
+ * <td nowrap="nowrap">
+ * <ul style="color: red;">
+ * <li>List Item</li>
+ * </ul>
+ * </td>
+ * </tr>
+ * </table>
+ *
+ * <p><strong>Warning:</strong> 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 JavaBeans<sup><font
+ * size="-2">TM</font></sup> has been added to the
+ * <code>java.beans</code> package. Please see {@link
+ * java.beans.XMLEncoder}.</p>
+ *
+ * @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 <code>StyleSheet</code>. This is a convenience
+ * method for the constructor
+ * <code>HTMLDocument(Content, StyleSheet)</code>.
+ */
+ 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
+ * <code>HTMLDocument(Content, StyleSheet)</code>.
+ *
+ * @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
+ * <code>HTMLDocument.HTMLReader</code>.
+ * 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
+ * <code>HTMLDocument.HTMLReader</code>.
+ * 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.)
+ * <p>This is a convenience method for
+ * <code>getReader(int, int, int, HTML.Tag, TRUE)</code>.
+ *
+ * @param popDepth the number of <code>ElementSpec.EndTagTypes</code>
+ * to generate before inserting
+ * @param pushDepth the number of <code>ElementSpec.StartTagTypes</code>
+ * with a direction of <code>ElementSpec.JoinNextDirection</code>
+ * 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 <code>ElementSpec.EndTagTypes</code>
+ * to generate before inserting
+ * @param pushDepth the number of <code>ElementSpec.StartTagTypes</code>
+ * with a direction of <code>ElementSpec.JoinNextDirection</code>
+ * 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.
+ * <p>This also sets the base of the <code>StyleSheet</code>
+ * to be <code>u</code> 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.
+ * <p>
+ * This method is thread safe, although most Swing methods
+ * are not. Please see
+ * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How
+ * to Use Threads</A> 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 <code>StyleSheet</code> with the document-specific display
+ * rules (CSS) that were specified in the HTML document itself.
+ *
+ * @return the <code>StyleSheet</code>
+ */
+ 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 <code>HTML.Tag</code>
+ * @return the <code>Iterator</code> 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
+ * <code>HTMLDocument.RunElement</code>.
+ *
+ * @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
+ * <code>HTMLDocument.BlockElement</code>.
+ *
+ * @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
+ * <code>Integer.MAX_VALUE</code>.
+ *
+ * @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 <code>HyperlinkEvents</code> that
+ * are generated by documents in an HTML frame.
+ * The <code>HyperlinkEvent</code> type, as the parameter suggests,
+ * is <code>HTMLFrameHyperlinkEvent</code>.
+ * In addition to the typical information contained in a
+ * <code>HyperlinkEvent</code>,
+ * 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:
+ * <ul>
+ * <li> _self
+ * <li> _parent
+ * <li> _top
+ * <li> a named frame
+ * </ul>
+ *
+ * If target is _self, the action is to change the value of the
+ * <code>HTML.Attribute.SRC</code> attribute and fires a
+ * <code>ChangedUpdate</code> event.
+ *<p>
+ * If the target is _parent, then it deletes the parent element,
+ * which is a &lt;FRAMESET&gt; element, and inserts a new &lt;FRAME&gt;
+ * element, and sets its <code>HTML.Attribute.SRC</code> attribute
+ * to have a value equal to the destination URL and fire a
+ * <code>RemovedUpdate</code> and <code>InsertUpdate</code>.
+ *<p>
+ * If the target is _top, this method does nothing. In the implementation
+ * of the view for a frame, namely the <code>FrameView</code>,
+ * 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.
+ *<p>
+ * If the target is a named frame, then the element hierarchy is searched
+ * for an element with a name equal to the target, its
+ * <code>HTML.Attribute.SRC</code> attribute is updated and a
+ * <code>ChangedUpdate</code> 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 <code>frameName</code>.
+ *
+ * @param frameName
+ * @return the element whose NAME attribute has a value of
+ * <code>frameName</code>; returns <code>null</code>
+ * 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 <code>StyleConstants.NameAttribute</code> 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 = "<frame";
+ if (url != null) {
+ html += " src=\"" + url + "\"";
+ }
+ html += ">";
+ installParserIfNecessary();
+ setOuterHTML(element, html);
+ } catch (BadLocationException e1) {
+ // Should handle this better
+ } catch (IOException ioe) {
+ // Should handle this better
+ }
+ }
+
+
+ /**
+ * Updates the Frame elements <code>HTML.Attribute.SRC attribute</code>
+ * and fires a <code>ChangedUpdate</code> 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 <code>Map</code> 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 <code>Map</code> 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 <code>Map</code>
+ * @return the <code>Map</code> or <code>null</code> if it can't
+ * be found, or if <code>name</code> is <code>null</code>
+ */
+ 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 <code>Enumeration</code> of the possible Maps.
+ * @return the enumerated list of maps, or <code>null</code>
+ * if the maps are not an instance of <code>Hashtable</code>
+ */
+ 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 <code>setInnerHTML</code>,
+ * and <code>setOuterHTML</code>.
+ * <p>
+ * <code>HTMLEditorKit.createDefaultDocument</code> will set the parser
+ * for you. If you create an <code>HTMLDocument</code> 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.
+ *
+ * <p>This will be seen as at least two events, n inserts followed by
+ * a remove.</p>
+ *
+ * <p>Consider the following structure (the <code>elem</code>
+ * parameter is <b>in bold</b>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * / \
+ * &lt;p> &lt;p>
+ * </pre>
+ *
+ * <p>Invoking <code>setInnerHTML(elem, "&lt;ul>&lt;li>")</code>
+ * results in the following structure (new elements are <font
+ * color="red">in red</font>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * \
+ * <font color="red">&lt;ul></font>
+ * \
+ * <font color="red">&lt;li></font>
+ * </pre>
+ *
+ * <p>Parameter <code>elem</code> must not be a leaf element,
+ * otherwise an <code>IllegalArgumentException</code> is thrown.
+ * If either <code>elem</code> or <code>htmlText</code> parameter
+ * is <code>null</code>, no changes are made to the document.</p>
+ *
+ * <p>For this to work correcty, the document must have an
+ * <code>HTMLEditorKit.Parser</code> set. This will be the case
+ * if the document was created from an HTMLEditorKit via the
+ * <code>createDefaultDocument</code> method.</p>
+ *
+ * @param elem the branch element whose children will be replaced
+ * @param htmlText the string to be parsed and assigned to <code>elem</code>
+ * @throws IllegalArgumentException if <code>elem</code> is a leaf
+ * @throws IllegalStateException if an <code>HTMLEditorKit.Parser</code>
+ * 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.
+ *
+ * <p>This will be seen as at least two events, n inserts followed by
+ * a remove.</p>
+ *
+ * <p>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 &lt;img&gt; this would create
+ * two elements, one for the image, ane one for the newline.</p>
+ *
+ * <p>If you try to replace the element at length you will most
+ * likely end up with two elements, eg
+ * <code>setOuterHTML(getCharacterElement (getLength()),
+ * "blah")</code> will result in two leaf elements at the end, one
+ * representing 'blah', and the other representing the end
+ * element.</p>
+ *
+ * <p>Consider the following structure (the <code>elem</code>
+ * parameter is <b>in bold</b>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * / \
+ * &lt;p> &lt;p>
+ * </pre>
+ *
+ * <p>Invoking <code>setOuterHTML(elem, "&lt;ul>&lt;li>")</code>
+ * results in the following structure (new elements are <font
+ * color="red">in red</font>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <font color="red">&lt;ul></font>
+ * \
+ * <font color="red">&lt;li></font>
+ * </pre>
+ *
+ * <p>If either <code>elem</code> or <code>htmlText</code>
+ * parameter is <code>null</code>, no changes are made to the
+ * document.</p>
+ *
+ * <p>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
+ * <code>createDefaultDocument</code> method.</p>
+ *
+ * @param elem the element to replace
+ * @param htmlText the string to be parsed and inserted in place of <code>elem</code>
+ * @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.
+ *
+ * <p>Consider the following structure (the <code>elem</code>
+ * parameter is <b>in bold</b>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * / \
+ * &lt;p> &lt;p>
+ * </pre>
+ *
+ * <p>Invoking <code>insertAfterStart(elem,
+ * "&lt;ul>&lt;li>")</code> results in the following structure
+ * (new elements are <font color="red">in red</font>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * / | \
+ * <font color="red">&lt;ul></font> &lt;p> &lt;p>
+ * /
+ * <font color="red">&lt;li></font>
+ * </pre>
+ *
+ * <p>Unlike the <code>insertBeforeStart</code> method, new
+ * elements become <em>children</em> of the specified element,
+ * not siblings.</p>
+ *
+ * <p>Parameter <code>elem</code> must not be a leaf element,
+ * otherwise an <code>IllegalArgumentException</code> is thrown.
+ * If either <code>elem</code> or <code>htmlText</code> parameter
+ * is <code>null</code>, no changes are made to the document.</p>
+ *
+ * <p>For this to work correcty, the document must have an
+ * <code>HTMLEditorKit.Parser</code> set. This will be the case
+ * if the document was created from an HTMLEditorKit via the
+ * <code>createDefaultDocument</code> method.</p>
+ *
+ * @param elem the branch element to be the root for the new text
+ * @param htmlText the string to be parsed and assigned to <code>elem</code>
+ * @throws IllegalArgumentException if <code>elem</code> 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.
+ *
+ * <p> If <code>elem</code>'s children are leaves, and the
+ * character at a <code>elem.getEndOffset() - 1</code> is a newline,
+ * this will insert before the newline so that there isn't text after
+ * the newline.</p>
+ *
+ * <p>Consider the following structure (the <code>elem</code>
+ * parameter is <b>in bold</b>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * / \
+ * &lt;p> &lt;p>
+ * </pre>
+ *
+ * <p>Invoking <code>insertBeforeEnd(elem, "&lt;ul>&lt;li>")</code>
+ * results in the following structure (new elements are <font
+ * color="red">in red</font>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * / | \
+ * &lt;p> &lt;p> <font color="red">&lt;ul></font>
+ * \
+ * <font color="red">&lt;li></font>
+ * </pre>
+ *
+ * <p>Unlike the <code>insertAfterEnd</code> method, new elements
+ * become <em>children</em> of the specified element, not
+ * siblings.</p>
+ *
+ * <p>Parameter <code>elem</code> must not be a leaf element,
+ * otherwise an <code>IllegalArgumentException</code> is thrown.
+ * If either <code>elem</code> or <code>htmlText</code> parameter
+ * is <code>null</code>, no changes are made to the document.</p>
+ *
+ * <p>For this to work correcty, the document must have an
+ * <code>HTMLEditorKit.Parser</code> set. This will be the case
+ * if the document was created from an HTMLEditorKit via the
+ * <code>createDefaultDocument</code> method.</p>
+ *
+ * @param elem the element to be the root for the new text
+ * @param htmlText the string to be parsed and assigned to <code>elem</code>
+ * @throws IllegalArgumentException if <code>elem</code> 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.
+ *
+ * <p>Consider the following structure (the <code>elem</code>
+ * parameter is <b>in bold</b>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * / \
+ * &lt;p> &lt;p>
+ * </pre>
+ *
+ * <p>Invoking <code>insertBeforeStart(elem,
+ * "&lt;ul>&lt;li>")</code> results in the following structure
+ * (new elements are <font color="red">in red</font>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * / \
+ * <font color="red">&lt;ul></font> <b>&lt;div></b>
+ * / / \
+ * <font color="red">&lt;li></font> &lt;p> &lt;p>
+ * </pre>
+ *
+ * <p>Unlike the <code>insertAfterStart</code> method, new
+ * elements become <em>siblings</em> of the specified element, not
+ * children.</p>
+ *
+ * <p>If either <code>elem</code> or <code>htmlText</code>
+ * parameter is <code>null</code>, no changes are made to the
+ * document.</p>
+ *
+ * <p>For this to work correcty, the document must have an
+ * <code>HTMLEditorKit.Parser</code> set. This will be the case
+ * if the document was created from an HTMLEditorKit via the
+ * <code>createDefaultDocument</code> method.</p>
+ *
+ * @param elem the element the content is inserted before
+ * @param htmlText the string to be parsed and inserted before <code>elem</code>
+ * @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.
+ *
+ * <p>Consider the following structure (the <code>elem</code>
+ * parameter is <b>in bold</b>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * |
+ * <b>&lt;div></b>
+ * / \
+ * &lt;p> &lt;p>
+ * </pre>
+ *
+ * <p>Invoking <code>insertAfterEnd(elem, "&lt;ul>&lt;li>")</code>
+ * results in the following structure (new elements are <font
+ * color="red">in red</font>).</p>
+ *
+ * <pre>
+ * &lt;body>
+ * / \
+ * <b>&lt;div></b> <font color="red">&lt;ul></font>
+ * / \ \
+ * &lt;p> &lt;p> <font color="red">&lt;li></font>
+ * </pre>
+ *
+ * <p>Unlike the <code>insertBeforeEnd</code> method, new elements
+ * become <em>siblings</em> of the specified element, not
+ * children.</p>
+ *
+ * <p>If either <code>elem</code> or <code>htmlText</code>
+ * parameter is <code>null</code>, no changes are made to the
+ * document.</p>
+ *
+ * <p>For this to work correcty, the document must have an
+ * <code>HTMLEditorKit.Parser</code> set. This will be the case
+ * if the document was created from an HTMLEditorKit via the
+ * <code>createDefaultDocument</code> method.</p>
+ *
+ * @param elem the element the content is inserted after
+ * @param htmlText the string to be parsed and inserted after <code>elem</code>
+ * @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 <code>Attribute</code>.
+ * If the element can't be found, <code>null</code> is returned.
+ * Note that this method works on an <code>Attribute</code>,
+ * <i>not</i> a character tag. In the following HTML snippet:
+ * <code>&lt;a id="HelloThere"&gt;</code> the attribute is
+ * 'id' and the character tag is 'a'.
+ * This is a convenience method for
+ * <code>getElement(RootElement, HTML.Attribute.id, id)</code>.
+ * This is not thread-safe.
+ *
+ * @param id the string representing the desired <code>Attribute</code>
+ * @return the element with the specified <code>Attribute</code>
+ * or <code>null</code> if it can't be found,
+ * or <code>null</code> if <code>id</code> is <code>null</code>
+ * @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 <code>e</code> that contains the
+ * attribute, <code>attribute</code> with value <code>value</code>, or
+ * <code>null</code> if one isn't found. This is not thread-safe.
+ *
+ * @param e the root element where the search begins
+ * @param attribute the desired <code>Attribute</code>
+ * @param value the values for the specified <code>Attribute</code>
+ * @return the element with the specified <code>Attribute</code>
+ * and the specified <code>value</code>, or <code>null</code>
+ * 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 <code>e</code> that contains the
+ * attribute, <code>attribute</code> with value <code>value</code>, or
+ * <code>null</code> if one isn't found. This is not thread-safe.
+ * <p>
+ * If <code>searchLeafAttributes</code> is true, and <code>e</code> is
+ * a leaf, any attributes that are instances of <code>HTML.Tag</code>
+ * with a value that is an <code>AttributeSet</code> will also be checked.
+ *
+ * @param e the root element where the search begins
+ * @param attribute the desired <code>Attribute</code>
+ * @param value the values for the specified <code>Attribute</code>
+ * @return the element with the specified <code>Attribute</code>
+ * and the specified <code>value</code>, or <code>null</code>
+ * 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 <code>HTMLEditorKit.Parser</code> set.
+ * If <code>getParser</code> returns <code>null</code>, 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.
+ * <code>parent</code> is used to identify the location to insert the
+ * <code>html</code>. If <code>parent</code> 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 <code>e</code>. This
+ * will do the necessary cleanup to ensure the element representing the
+ * end character is correctly created.
+ * <p>This is not a general purpose method, it assumes that <code>e</code>
+ * will still have at least one child after the remove, and it assumes
+ * the character at <code>e.getStartOffset() - 1</code> 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 <code>e</code> when one of the
+ * elements to remove is representing the end character.
+ * <p>Since the Content will not allow a removal to the end character
+ * this will do a remove from <code>start - 1</code> to <code>end</code>.
+ * The end Element(s) will be removed, and the element representing
+ * <code>start - 1</code> to <code>start</code> will be recreated. This
+ * Element has to be recreated as after the content removal its offsets
+ * become <code>start - 1</code> to <code>start - 1</code>.
+ */
+ 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 <code>removeElementsAtEnd</code>, it removes
+ * <code>count</code> elements starting at <code>start</code> from
+ * <code>e</code>. If <code>remove</code> is true text of length
+ * <code>start - 1</code> to <code>end - 1</code> is removed. If
+ * <code>create</code> 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 <code>AttributeSet</code> for this tag, or
+ * <code>null</code> 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 <code>AttributeSet</code> for this tag,
+ * or <code>null</code> 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 <code>HTML.Tag</code> 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 <code>null</code>.
+ * @return true if current position is not <code>null</code>,
+ * 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 <code>endOffset</code>.
+ */
+ 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.
+ * <p>
+ * The reader can be configured by registering actions
+ * (of type <code>HTMLDocument.HTMLReader.TagAction</code>)
+ * 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:
+ * <dl>
+ * <dt>Block
+ * <dd>Build the structure like it's specified in the stream.
+ * This produces elements that contain other elements.
+ * <dt>Paragraph
+ * <dd>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.
+ * <dt>Character
+ * <dd>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.
+ * <dt>Special
+ * <dd>Produce an embedded graphical element.
+ * <dt>Form
+ * <dd>Produce an element that is like the embedded graphical
+ * element, except that it also has a component model associated
+ * with it.
+ * <dt>Hidden
+ * <dd>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.
+ *
+ * </dl>
+ * <p>
+ * Currently, &lt;APPLET&gt;, &lt;PARAM&gt;, &lt;MAP&gt;, &lt;AREA&gt;, &lt;LINK&gt;,
+ * &lt;SCRIPT&gt; and &lt;STYLE&gt; are unsupported.
+ *
+ * <p>
+ * The assignment of the actions described is shown in the
+ * following table for the tags defined in <code>HTML.Tag</code>.<P>
+ * <table border=1 summary="HTML tags and assigned actions">
+ * <tr><th>Tag</th><th>Action</th></tr>
+ * <tr><td><code>HTML.Tag.A</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.ADDRESS</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.APPLET</code> <td>HiddenAction
+ * <tr><td><code>HTML.Tag.AREA</code> <td>AreaAction
+ * <tr><td><code>HTML.Tag.B</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.BASE</code> <td>BaseAction
+ * <tr><td><code>HTML.Tag.BASEFONT</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.BIG</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.BLOCKQUOTE</code><td>BlockAction
+ * <tr><td><code>HTML.Tag.BODY</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.BR</code> <td>SpecialAction
+ * <tr><td><code>HTML.Tag.CAPTION</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.CENTER</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.CITE</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.CODE</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.DD</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.DFN</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.DIR</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.DIV</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.DL</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.DT</code> <td>ParagraphAction
+ * <tr><td><code>HTML.Tag.EM</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.FONT</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.FORM</code> <td>As of 1.4 a BlockAction
+ * <tr><td><code>HTML.Tag.FRAME</code> <td>SpecialAction
+ * <tr><td><code>HTML.Tag.FRAMESET</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.H1</code> <td>ParagraphAction
+ * <tr><td><code>HTML.Tag.H2</code> <td>ParagraphAction
+ * <tr><td><code>HTML.Tag.H3</code> <td>ParagraphAction
+ * <tr><td><code>HTML.Tag.H4</code> <td>ParagraphAction
+ * <tr><td><code>HTML.Tag.H5</code> <td>ParagraphAction
+ * <tr><td><code>HTML.Tag.H6</code> <td>ParagraphAction
+ * <tr><td><code>HTML.Tag.HEAD</code> <td>HeadAction
+ * <tr><td><code>HTML.Tag.HR</code> <td>SpecialAction
+ * <tr><td><code>HTML.Tag.HTML</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.I</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.IMG</code> <td>SpecialAction
+ * <tr><td><code>HTML.Tag.INPUT</code> <td>FormAction
+ * <tr><td><code>HTML.Tag.ISINDEX</code> <td>IsndexAction
+ * <tr><td><code>HTML.Tag.KBD</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.LI</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.LINK</code> <td>LinkAction
+ * <tr><td><code>HTML.Tag.MAP</code> <td>MapAction
+ * <tr><td><code>HTML.Tag.MENU</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.META</code> <td>MetaAction
+ * <tr><td><code>HTML.Tag.NOFRAMES</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.OBJECT</code> <td>SpecialAction
+ * <tr><td><code>HTML.Tag.OL</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.OPTION</code> <td>FormAction
+ * <tr><td><code>HTML.Tag.P</code> <td>ParagraphAction
+ * <tr><td><code>HTML.Tag.PARAM</code> <td>HiddenAction
+ * <tr><td><code>HTML.Tag.PRE</code> <td>PreAction
+ * <tr><td><code>HTML.Tag.SAMP</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.SCRIPT</code> <td>HiddenAction
+ * <tr><td><code>HTML.Tag.SELECT</code> <td>FormAction
+ * <tr><td><code>HTML.Tag.SMALL</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.STRIKE</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.S</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.STRONG</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.STYLE</code> <td>StyleAction
+ * <tr><td><code>HTML.Tag.SUB</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.SUP</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.TABLE</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.TD</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.TEXTAREA</code> <td>FormAction
+ * <tr><td><code>HTML.Tag.TH</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.TITLE</code> <td>TitleAction
+ * <tr><td><code>HTML.Tag.TR</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.TT</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.U</code> <td>CharacterAction
+ * <tr><td><code>HTML.Tag.UL</code> <td>BlockAction
+ * <tr><td><code>HTML.Tag.VAR</code> <td>CharacterAction
+ * </table>
+ * <p>
+ * Once &lt;/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, <code>insertTag</code> is
+ * non-<code>null</code>, and <code>offset</code> 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 <code>inParagraph</code> flag.
+ * It is left in <code>false</code> 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 <code>ElementSpecs</code>
+ * 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 <code>offset</code>
+ * until a parent with name, <code>name</code> has been
+ * found. -1 indicates no matching parent with
+ * <code>name</code>.
+ */
+ 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 <code>comment</code> 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
+ * <code>flush</code>. <code>eol</code> will be one of \n, \r
+ * or \r\n, which ever is encountered the most in parsing the
+ * stream.
+ *
+ * @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 &lt;INPUT&gt;, &lt;TEXTAREA&gt;, &lt;SELECT&gt;,
+ * and &lt;OPTION&gt; tags. The element created by
+ * this action is expected to have the attribute
+ * <code>StyleConstants.ModelAttribute</code> 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.
+ * <table summary="model assignments for the various types of form elements">
+ * <tr>
+ * <th>Element Type
+ * <th>Model Type
+ * <tr>
+ * <td>input, type button
+ * <td>{@link DefaultButtonModel}
+ * <tr>
+ * <td>input, type checkbox
+ * <td>{@link javax.swing.JToggleButton.ToggleButtonModel}
+ * <tr>
+ * <td>input, type image
+ * <td>{@link DefaultButtonModel}
+ * <tr>
+ * <td>input, type password
+ * <td>{@link PlainDocument}
+ * <tr>
+ * <td>input, type radio
+ * <td>{@link javax.swing.JToggleButton.ToggleButtonModel}
+ * <tr>
+ * <td>input, type reset
+ * <td>{@link DefaultButtonModel}
+ * <tr>
+ * <td>input, type submit
+ * <td>{@link DefaultButtonModel}
+ * <tr>
+ * <td>input, type text or type is null.
+ * <td>{@link PlainDocument}
+ * <tr>
+ * <td>select
+ * <td>{@link DefaultComboBoxModel} or an {@link DefaultListModel}, with an item type of Option
+ * <tr>
+ * <td>textarea
+ * <td>{@link PlainDocument}
+ * </table>
+ *
+ */
+ 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 &lt;SELECT&gt; tag is being processed, this
+ * model will be a reference to the model being filled
+ * with the &lt;OPTION&gt; elements (which produce
+ * objects of type <code>Option</code>.
+ */
+ 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 <code>insertTag</code>
+ * 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 <code>rules</code>.
+ */
+ void addCSSRules(String rules) {
+ StyleSheet ss = getStyleSheet();
+ ss.addRule(rules);
+ }
+
+ /**
+ * Adds the CSS stylesheet at <code>href</code> 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 <code>t</code>. 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 <html>.
+ */
+ private boolean receivedEndHTML;
+ /** Number of times <code>flushBuffer</code> 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:
+ * <p>LINK: in which case it is followed by an AttributeSet
+ * <p>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<ElementSpec> parseBuffer = new Vector(); // Vector<ElementSpec>
+ 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 <em>block</em> 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 &lt;applet&gt; tag is not supported, but some support is provided
+ * for the &lt;object&gt; tag.
+ * <p>
+ * There are several goals of the HTML EditorKit provided, that have
+ * an effect upon the way that HTML is modeled. These
+ * have influenced its design in a substantial way.
+ * <dl>
+ * <p>
+ * <dt>
+ * Support editing
+ * <dd>
+ * It might seem fairly obvious that a plug-in for JEditorPane
+ * should provide editing support, but that fact has several
+ * design considerations. There are a substantial number of HTML
+ * documents that don't properly conform to an HTML specification.
+ * These must be normalized somewhat into a correct form if one
+ * is to edit them. Additionally, users don't like to be presented
+ * with an excessive amount of structure editing, so using traditional
+ * text editing gestures is preferred over using the HTML structure
+ * exactly as defined in the HTML document.
+ * <p>
+ * The modeling of HTML is provided by the class <code>HTMLDocument</code>.
+ * Its documention describes the details of how the HTML is modeled.
+ * The editing support leverages heavily off of the text package.
+ * <p>
+ * <dt>
+ * Extendable/Scalable
+ * <dd>
+ * To maximize the usefulness of this kit, a great deal of effort
+ * has gone into making it extendable. These are some of the
+ * features.
+ * <ol>
+ * <li>
+ * The parser is 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.
+ * <li>
+ * The parser drives the ParserCallback, which is provided by
+ * HTMLDocument. To change the callback, subclass HTMLDocument
+ * and reimplement the createDefaultDocument method to return
+ * document that produces a different reader. The reader controls
+ * how the document is structured. Although the Document provides
+ * HTML support by default, there is nothing preventing support of
+ * non-HTML tags that result in alternative element structures.
+ * <li>
+ * The default view of the models are provided as a hierarchy of
+ * View implementations, so one can easily customize how a particular
+ * element is displayed or add capabilities for new kinds of elements
+ * by providing new View implementations. The default set of views
+ * are provided by the <code>HTMLFactory</code> class. This can
+ * be easily changed by subclassing or replacing the HTMLFactory
+ * and reimplementing the getViewFactory method to return the alternative
+ * factory.
+ * <li>
+ * The View implementations work primarily off of CSS attributes,
+ * which are kept in the views. This makes it possible to have
+ * multiple views mapped over the same model that appear substantially
+ * different. This can be especially useful for printing. For
+ * most HTML attributes, the HTML attributes are converted to CSS
+ * attributes for display. This helps make the View implementations
+ * more general purpose
+ * </ol>
+ * <p>
+ * <dt>
+ * Asynchronous Loading
+ * <dd>
+ * Larger documents involve a lot of parsing and take some time
+ * to load. By default, this kit produces documents that will be
+ * loaded asynchronously if loaded using <code>JEditorPane.setPage</code>.
+ * This is controlled by a property on the document. The method
+ * <a href="#createDefaultDocument">createDefaultDocument</a> can
+ * be overriden to change this. The batching of work is done
+ * by the <code>HTMLDocument.HTMLReader</code> class. The actual
+ * work is done by the <code>DefaultStyledDocument</code> and
+ * <code>AbstractDocument</code> classes in the text package.
+ * <p>
+ * <dt>
+ * Customization from current LAF
+ * <dd>
+ * HTML provides a well known set of features without exactly
+ * specifying the display characteristics. Swing has a theme
+ * mechanism for its look-and-feel implementations. It is desirable
+ * for the look-and-feel to feed display characteristics into the
+ * HTML views. An user with poor vision for example would want
+ * high contrast and larger than typical fonts.
+ * <p>
+ * The support for this is provided by the <code>StyleSheet</code>
+ * class. The presentation of the HTML can be heavily influenced
+ * by the setting of the StyleSheet property on the EditorKit.
+ * <p>
+ * <dt>
+ * Not lossy
+ * <dd>
+ * An EditorKit has the ability to be read and save documents.
+ * It is generally the most pleasing to users if there is no loss
+ * of data between the two operation. The policy of the HTMLEditorKit
+ * will be to store things not recognized or not necessarily visible
+ * so they can be subsequently written out. The model of the HTML document
+ * should therefore contain all information discovered while reading the
+ * document. This is constrained in some ways by the need to support
+ * editing (i.e. incorrect documents sometimes must be normalized).
+ * The guiding principle is that information shouldn't be lost, but
+ * some might be synthesized to produce a more correct model or it might
+ * be rearranged.
+ * </dl>
+ *
+ * @author Timothy Prinzing
+ */
+public class HTMLEditorKit extends StyledEditorKit implements Accessible {
+
+ private JEditorPane theEditor;
+
+ /**
+ * Constructs an HTMLEditorKit, creates a StyleContext,
+ * and loads the style sheet.
+ */
+ public HTMLEditorKit() {
+
+ }
+
+ /**
+ * Get the MIME type of the data that this
+ * kit represents support for. This kit supports
+ * the type <code>text/html</code>.
+ *
+ * @return the type
+ */
+ public String getContentType() {
+ return "text/html";
+ }
+
+ /**
+ * Fetch a factory that is suitable for producing
+ * views of any models that are produced by this
+ * kit.
+ *
+ * @return the factory
+ */
+ public ViewFactory getViewFactory() {
+ return defaultFactory;
+ }
+
+ /**
+ * Create an uninitialized text storage model
+ * that is appropriate for this type of editor.
+ *
+ * @return the model
+ */
+ public Document createDefaultDocument() {
+ StyleSheet styles = getStyleSheet();
+ StyleSheet ss = new StyleSheet();
+
+ ss.addStyleSheet(styles);
+
+ HTMLDocument doc = new HTMLDocument(ss);
+ doc.setParser(getParser());
+ doc.setAsynchronousLoadPriority(4);
+ doc.setTokenThreshold(100);
+ return doc;
+ }
+
+ /**
+ * Try to get an HTML parser from the document. If no parser is set for
+ * the document, return the editor kit's default parser. It is an error
+ * if no parser could be obtained from the editor kit.
+ */
+ private Parser ensureParser(HTMLDocument doc) throws IOException {
+ Parser p = doc.getParser();
+ if (p == null) {
+ p = getParser();
+ }
+ if (p == null) {
+ throw new IOException("Can't load parser");
+ }
+ return p;
+ }
+
+ /**
+ * Inserts content from the given stream. If <code>doc</code> is
+ * an instance of HTMLDocument, this will read
+ * HTML 3.2 text. Inserting HTML into a non-empty document must be inside
+ * the body Element, if you do not insert into the body an exception will
+ * be thrown. When inserting into a non-empty document all tags outside
+ * of the body (head, title) will be dropped.
+ *
+ * @param in the stream to read from
+ * @param doc the destination for the insertion
+ * @param pos the location in the document to place the
+ * content
+ * @exception IOException on any I/O error
+ * @exception BadLocationException if pos represents an invalid
+ * location within the document
+ * @exception RuntimeException (will eventually be a BadLocationException)
+ * if pos is invalid
+ */
+ public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException {
+
+ if (doc instanceof HTMLDocument) {
+ HTMLDocument hdoc = (HTMLDocument) doc;
+ if (pos > doc.getLength()) {
+ throw new BadLocationException("Invalid location", pos);
+ }
+
+ Parser p = ensureParser(hdoc);
+ ParserCallback receiver = hdoc.getReader(pos);
+ Boolean ignoreCharset = (Boolean)doc.getProperty("IgnoreCharsetDirective");
+ p.parse(in, receiver, (ignoreCharset == null) ? false : ignoreCharset.booleanValue());
+ receiver.flush();
+ } else {
+ super.read(in, doc, pos);
+ }
+ }
+
+ /**
+ * Inserts HTML into an existing document.
+ *
+ * @param doc the document to insert into
+ * @param offset the offset to insert HTML at
+ * @param popDepth the number of ElementSpec.EndTagTypes to generate before
+ * inserting
+ * @param 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 <code>element</code>s AttributeSet into
+ * <code>set</code>. This does not copy component, icon, or element
+ * names attributes. Subclasses may wish to refine what is and what
+ * isn't copied here. But be sure to first remove all the attributes that
+ * are in <code>set</code>.<p>
+ * This is called anytime the caret moves over a different location.
+ *
+ */
+ protected void createInputAttributes(Element element,
+ MutableAttributeSet set) {
+ set.removeAttributes(set);
+ set.addAttributes(element.getAttributes());
+ set.removeAttribute(StyleConstants.ComposedTextAttribute);
+
+ Object o = set.getAttribute(StyleConstants.NameAttribute);
+ if (o instanceof HTML.Tag) {
+ HTML.Tag tag = (HTML.Tag)o;
+ // PENDING: we need a better way to express what shouldn't be
+ // copied when editing...
+ if(tag == HTML.Tag.IMG) {
+ // Remove the related image attributes, src, width, height
+ set.removeAttribute(HTML.Attribute.SRC);
+ set.removeAttribute(HTML.Attribute.HEIGHT);
+ set.removeAttribute(HTML.Attribute.WIDTH);
+ set.addAttribute(StyleConstants.NameAttribute,
+ HTML.Tag.CONTENT);
+ }
+ else if (tag == HTML.Tag.HR || tag == HTML.Tag.BR) {
+ // Don't copy HRs or BRs either.
+ set.addAttribute(StyleConstants.NameAttribute,
+ HTML.Tag.CONTENT);
+ }
+ else if (tag == HTML.Tag.COMMENT) {
+ // Don't copy COMMENTs either
+ set.addAttribute(StyleConstants.NameAttribute,
+ HTML.Tag.CONTENT);
+ set.removeAttribute(HTML.Attribute.COMMENT);
+ }
+ else if (tag == HTML.Tag.INPUT) {
+ // or INPUT either
+ set.addAttribute(StyleConstants.NameAttribute,
+ HTML.Tag.CONTENT);
+ set.removeAttribute(HTML.Tag.INPUT);
+ }
+ else if (tag instanceof HTML.UnknownTag) {
+ // Don't copy unknowns either:(
+ set.addAttribute(StyleConstants.NameAttribute,
+ HTML.Tag.CONTENT);
+ set.removeAttribute(HTML.Attribute.ENDTAG);
+ }
+ }
+ }
+
+ /**
+ * Gets the input attributes used for the styled
+ * editing actions.
+ *
+ * @return the attribute set
+ */
+ public MutableAttributeSet getInputAttributes() {
+ if (input == null) {
+ input = getStyleSheet().addStyle(null, null);
+ }
+ return input;
+ }
+
+ /**
+ * Sets the default cursor.
+ *
+ * @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 <code>FormSubmitEvent</code> is fired.
+ *
+ * @return true if html form submission is processed automatically,
+ * false otherwise.
+ *
+ * @see #setAutoFormSubmission
+ * @since 1.5
+ */
+ public boolean isAutoFormSubmission() {
+ return isAutoFormSubmission;
+ }
+
+ /**
+ * Specifies if an html form submission is processed
+ * automatically or only <code>FormSubmitEvent</code> is fired.
+ * By default it is set to true.
+ *
+ * @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 <code>e</code> contains
+ * the location <code>x</code>, <code>y</code>. <code>offset</code>
+ * 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.<p>This is implemented
+ * to forward to the method with the same name, but with the following
+ * args both == -1.
+ *
+ * @param pos the position
+ * @param editor the editor pane
+ */
+ protected void activateLink(int pos, JEditorPane editor) {
+ activateLink(pos, editor, null);
+ }
+
+ /**
+ * Calls linkActivated on the associated JEditorPane
+ * if the given position represents a link. If this was the result
+ * of a mouse click, <code>x</code> and
+ * <code>y</code> will give the location of the mouse, otherwise
+ * they will be < 0.
+ *
+ * @param pos the position
+ * @param html the editor pane
+ */
+ void activateLink(int pos, JEditorPane html, MouseEvent mouseEvent) {
+ Document doc = html.getDocument();
+ if (doc instanceof HTMLDocument) {
+ HTMLDocument hdoc = (HTMLDocument) doc;
+ Element e = hdoc.getCharacterElement(pos);
+ AttributeSet a = e.getAttributes();
+ AttributeSet anchor = (AttributeSet)a.getAttribute(HTML.Tag.A);
+ HyperlinkEvent linkEvent = null;
+ String description;
+ int x = -1;
+ int y = -1;
+
+ if (mouseEvent != null) {
+ x = mouseEvent.getX();
+ y = mouseEvent.getY();
+ }
+
+ if (anchor == null) {
+ href = getMapHREF(html, hdoc, e, a, pos, x, y);
+ }
+ else {
+ href = (String)anchor.getAttribute(HTML.Attribute.HREF);
+ }
+
+ if (href != null) {
+ linkEvent = createHyperlinkEvent(html, hdoc, href, anchor,
+ e, mouseEvent);
+ }
+ if (linkEvent != null) {
+ html.fireHyperlinkUpdate(linkEvent);
+ }
+ }
+ }
+
+ /**
+ * Creates and returns a new instance of HyperlinkEvent. If
+ * <code>hdoc</code> is a frame document a HTMLFrameHyperlinkEvent
+ * will be created.
+ */
+ HyperlinkEvent createHyperlinkEvent(JEditorPane html,
+ HTMLDocument hdoc, String href,
+ AttributeSet anchor,
+ Element element,
+ MouseEvent mouseEvent) {
+ URL u;
+ try {
+ URL base = hdoc.getBase();
+ u = new URL(base, href);
+ // Following is a workaround for 1.2, in which
+ // new URL("file://...", "#...") causes the filename to
+ // be lost.
+ if (href != null && "file".equals(u.getProtocol()) &&
+ href.startsWith("#")) {
+ String baseFile = base.getFile();
+ String newFile = u.getFile();
+ if (baseFile != null && newFile != null &&
+ !newFile.startsWith(baseFile)) {
+ u = new URL(base, baseFile + href);
+ }
+ }
+ } catch (MalformedURLException m) {
+ u = null;
+ }
+ HyperlinkEvent linkEvent = 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
+ * <code>flush</code> method will be the last method
+ * called, to give the receiver a chance to flush any
+ * pending data into the document.
+ * <p>Refer to DocumentParser, the default parser used, for further
+ * information on the contents of the AttributeSets, the positions, and
+ * other info.
+ *
+ * @see javax.swing.text.html.parser.DocumentParser
+ */
+ public static class ParserCallback {
+ /**
+ * This is passed as an attribute in the attributeset to indicate
+ * the element is implied eg, the string '&lt;&gt;foo&lt;\t&gt;'
+ * 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
+ * <code>flush</code>. <code>eol</code> will be one of \n, \r
+ * or \r\n, which ever is encountered the most in parsing the
+ * stream.
+ *
+ * @since 1.3
+ */
+ public void handleEndOfLineString(String eol) {
+ }
+ }
+
+ /**
+ * A factory to build views for HTML. The following
+ * table describes what this factory will build by
+ * default.
+ *
+ * <table summary="Describes the tag and view created by this factory by default">
+ * <tr>
+ * <th align=left>Tag<th align=left>View created
+ * </tr><tr>
+ * <td>HTML.Tag.CONTENT<td>InlineView
+ * </tr><tr>
+ * <td>HTML.Tag.IMPLIED<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.P<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.H1<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.H2<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.H3<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.H4<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.H5<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.H6<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.DT<td>javax.swing.text.html.ParagraphView
+ * </tr><tr>
+ * <td>HTML.Tag.MENU<td>ListView
+ * </tr><tr>
+ * <td>HTML.Tag.DIR<td>ListView
+ * </tr><tr>
+ * <td>HTML.Tag.UL<td>ListView
+ * </tr><tr>
+ * <td>HTML.Tag.OL<td>ListView
+ * </tr><tr>
+ * <td>HTML.Tag.LI<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.DL<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.DD<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.BODY<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.HTML<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.CENTER<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.DIV<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.BLOCKQUOTE<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.PRE<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.BLOCKQUOTE<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.PRE<td>BlockView
+ * </tr><tr>
+ * <td>HTML.Tag.IMG<td>ImageView
+ * </tr><tr>
+ * <td>HTML.Tag.HR<td>HRuleView
+ * </tr><tr>
+ * <td>HTML.Tag.BR<td>BRView
+ * </tr><tr>
+ * <td>HTML.Tag.TABLE<td>javax.swing.text.html.TableView
+ * </tr><tr>
+ * <td>HTML.Tag.INPUT<td>FormView
+ * </tr><tr>
+ * <td>HTML.Tag.SELECT<td>FormView
+ * </tr><tr>
+ * <td>HTML.Tag.TEXTAREA<td>FormView
+ * </tr><tr>
+ * <td>HTML.Tag.OBJECT<td>ObjectView
+ * </tr><tr>
+ * <td>HTML.Tag.FRAMESET<td>FrameSetView
+ * </tr><tr>
+ * <td>HTML.Tag.FRAME<td>FrameView
+ * </tr>
+ * </table>
+ */
+ public static class HTMLFactory implements ViewFactory {
+
+ /**
+ * Creates a view from an element.
+ *
+ * @param elem the element
+ * @return the view
+ */
+ public View create(Element elem) {
+ AttributeSet attrs = elem.getAttributes();
+ Object elementName =
+ attrs.getAttribute(AbstractDocument.ElementNameAttribute);
+ Object o = (elementName != null) ?
+ null : attrs.getAttribute(StyleConstants.NameAttribute);
+ if (o instanceof HTML.Tag) {
+ HTML.Tag kind = (HTML.Tag) o;
+ if (kind == HTML.Tag.CONTENT) {
+ return new InlineView(elem);
+ } else if (kind == HTML.Tag.IMPLIED) {
+ String ws = (String) elem.getAttributes().getAttribute(
+ CSS.Attribute.WHITE_SPACE);
+ if ((ws != null) && ws.equals("pre")) {
+ return new LineView(elem);
+ }
+ return new javax.swing.text.html.ParagraphView(elem);
+ } else if ((kind == HTML.Tag.P) ||
+ (kind == HTML.Tag.H1) ||
+ (kind == HTML.Tag.H2) ||
+ (kind == HTML.Tag.H3) ||
+ (kind == HTML.Tag.H4) ||
+ (kind == HTML.Tag.H5) ||
+ (kind == HTML.Tag.H6) ||
+ (kind == HTML.Tag.DT)) {
+ // paragraph
+ return new javax.swing.text.html.ParagraphView(elem);
+ } else if ((kind == HTML.Tag.MENU) ||
+ (kind == HTML.Tag.DIR) ||
+ (kind == HTML.Tag.UL) ||
+ (kind == HTML.Tag.OL)) {
+ return new ListView(elem);
+ } else if (kind == HTML.Tag.BODY) {
+ return new BodyBlockView(elem);
+ } else if (kind == HTML.Tag.HTML) {
+ return new BlockView(elem, View.Y_AXIS);
+ } else if ((kind == HTML.Tag.LI) ||
+ (kind == HTML.Tag.CENTER) ||
+ (kind == HTML.Tag.DL) ||
+ (kind == HTML.Tag.DD) ||
+ (kind == HTML.Tag.DIV) ||
+ (kind == HTML.Tag.BLOCKQUOTE) ||
+ (kind == HTML.Tag.PRE) ||
+ (kind == HTML.Tag.FORM)) {
+ // vertical box
+ return new BlockView(elem, View.Y_AXIS);
+ } else if (kind == HTML.Tag.NOFRAMES) {
+ return new NoFramesView(elem, View.Y_AXIS);
+ } else if (kind==HTML.Tag.IMG) {
+ return new ImageView(elem);
+ } else if (kind == HTML.Tag.ISINDEX) {
+ return new IsindexView(elem);
+ } else if (kind == HTML.Tag.HR) {
+ return new HRuleView(elem);
+ } else if (kind == HTML.Tag.BR) {
+ return new BRView(elem);
+ } else if (kind == HTML.Tag.TABLE) {
+ return new javax.swing.text.html.TableView(elem);
+ } else if ((kind == HTML.Tag.INPUT) ||
+ (kind == HTML.Tag.SELECT) ||
+ (kind == HTML.Tag.TEXTAREA)) {
+ return new FormView(elem);
+ } else if (kind == HTML.Tag.OBJECT) {
+ return new ObjectView(elem);
+ } else if (kind == HTML.Tag.FRAMESET) {
+ if (elem.getAttributes().isDefined(HTML.Attribute.ROWS)) {
+ return new FrameSetView(elem, View.Y_AXIS);
+ } else if (elem.getAttributes().isDefined(HTML.Attribute.COLS)) {
+ return new FrameSetView(elem, View.X_AXIS);
+ }
+ throw new RuntimeException("Can't build a" + kind + ", " + elem + ":" +
+ "no ROWS or COLS defined.");
+ } else if (kind == HTML.Tag.FRAME) {
+ return new FrameView(elem);
+ } else if (kind instanceof HTML.UnknownTag) {
+ return new HiddenTagView(elem);
+ } else if (kind == HTML.Tag.COMMENT) {
+ return new CommentView(elem);
+ } else if (kind == HTML.Tag.HEAD) {
+ // Make the head never visible, and never load its
+ // children. For Cursor positioning,
+ // getNextVisualPositionFrom is overriden to always return
+ // the end offset of the element.
+ return new BlockView(elem, View.X_AXIS) {
+ public float getPreferredSpan(int axis) {
+ return 0;
+ }
+ public float getMinimumSpan(int axis) {
+ return 0;
+ }
+ public float getMaximumSpan(int axis) {
+ return 0;
+ }
+ protected void loadChildren(ViewFactory f) {
+ }
+ public Shape modelToView(int pos, Shape a,
+ Position.Bias b) throws BadLocationException {
+ return a;
+ }
+ public int getNextVisualPositionFrom(int pos,
+ Position.Bias b, Shape a,
+ int direction, Position.Bias[] biasRet) {
+ return getElement().getEndOffset();
+ }
+ };
+ } else if ((kind == HTML.Tag.TITLE) ||
+ (kind == HTML.Tag.META) ||
+ (kind == HTML.Tag.LINK) ||
+ (kind == HTML.Tag.STYLE) ||
+ (kind == HTML.Tag.SCRIPT) ||
+ (kind == HTML.Tag.AREA) ||
+ (kind == HTML.Tag.MAP) ||
+ (kind == HTML.Tag.PARAM) ||
+ (kind == HTML.Tag.APPLET)) {
+ return new HiddenTagView(elem);
+ }
+ }
+ // If we get here, it's either an element we don't know about
+ // or something from StyledDocument that doesn't have a mapping to HTML.
+ String nm = (elementName != null) ? (String)elementName :
+ elem.getName();
+ if (nm != null) {
+ if (nm.equals(AbstractDocument.ContentElementName)) {
+ return new LabelView(elem);
+ } else if (nm.equals(AbstractDocument.ParagraphElementName)) {
+ return new ParagraphView(elem);
+ } else if (nm.equals(AbstractDocument.SectionElementName)) {
+ return new BoxView(elem, View.Y_AXIS);
+ } else if (nm.equals(StyleConstants.ComponentElementName)) {
+ return new ComponentView(elem);
+ } else if (nm.equals(StyleConstants.IconElementName)) {
+ return new IconView(elem);
+ }
+ }
+
+ // default to text display
+ return new LabelView(elem);
+ }
+
+ static class BodyBlockView extends BlockView implements ComponentListener {
+ public BodyBlockView(Element elem) {
+ super(elem,View.Y_AXIS);
+ }
+ // reimplement major axis requirements to indicate that the
+ // block is flexible for the body element... so that it can
+ // be stretched to fill the background properly.
+ protected SizeRequirements calculateMajorAxisRequirements(int axis, SizeRequirements r) {
+ r = super.calculateMajorAxisRequirements(axis, r);
+ r.maximum = Integer.MAX_VALUE;
+ return r;
+ }
+
+ protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
+ Container container = getContainer();
+ Container parentContainer;
+ if (container != null
+ && (container instanceof javax.swing.JEditorPane)
+ && (parentContainer = container.getParent()) != null
+ && (parentContainer instanceof javax.swing.JViewport)) {
+ JViewport viewPort = (JViewport)parentContainer;
+ 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 = "<table border=1><tr><td></td></tr></table>";
+
+ /** HTML used when inserting unordered lists. */
+ private static final String INSERT_UL_HTML = "<ul><li></li></ul>";
+
+ /** HTML used when inserting ordered lists. */
+ private static final String INSERT_OL_HTML = "<ol><li></li></ol>";
+
+ /** HTML used when inserting hr. */
+ private static final String INSERT_HR_HTML = "<hr>";
+
+ /** HTML used when inserting pre. */
+ private static final String INSERT_PRE_HTML = "<pre></pre>";
+
+ private static final NavigateLinkAction nextLinkAction =
+ new NavigateLinkAction("next-link-action");
+
+ private static final NavigateLinkAction previousLinkAction =
+ new NavigateLinkAction("previous-link-action");
+
+ private static final ActivateLinkAction activateLinkAction =
+ new ActivateLinkAction("activate-link-action");
+
+ private static final Action[] defaultActions = {
+ new InsertHTMLTextAction("InsertTable", INSERT_TABLE_HTML,
+ HTML.Tag.BODY, HTML.Tag.TABLE),
+ new InsertHTMLTextAction("InsertTableRow", INSERT_TABLE_HTML,
+ HTML.Tag.TABLE, HTML.Tag.TR,
+ HTML.Tag.BODY, HTML.Tag.TABLE),
+ new InsertHTMLTextAction("InsertTableDataCell", INSERT_TABLE_HTML,
+ HTML.Tag.TR, HTML.Tag.TD,
+ HTML.Tag.BODY, HTML.Tag.TABLE),
+ new InsertHTMLTextAction("InsertUnorderedList", INSERT_UL_HTML,
+ HTML.Tag.BODY, HTML.Tag.UL),
+ new InsertHTMLTextAction("InsertUnorderedListItem", INSERT_UL_HTML,
+ HTML.Tag.UL, HTML.Tag.LI,
+ HTML.Tag.BODY, HTML.Tag.UL),
+ new InsertHTMLTextAction("InsertOrderedList", INSERT_OL_HTML,
+ HTML.Tag.BODY, HTML.Tag.OL),
+ new InsertHTMLTextAction("InsertOrderedListItem", INSERT_OL_HTML,
+ HTML.Tag.OL, HTML.Tag.LI,
+ HTML.Tag.BODY, HTML.Tag.OL),
+ new InsertHRAction(),
+ new InsertHTMLTextAction("InsertPre", INSERT_PRE_HTML,
+ HTML.Tag.BODY, HTML.Tag.PRE),
+ nextLinkAction, previousLinkAction, activateLinkAction,
+
+ new BeginAction(beginAction, false),
+ new BeginAction(selectionBeginAction, true)
+ };
+
+ // link navigation support
+ private boolean foundLink = false;
+ private int prevHypertextOffset = -1;
+ private Object linkNavigationTag;
+
+
+ /**
+ * An abstract Action providing some convenience methods that may
+ * be useful in inserting HTML into an existing document.
+ * <p>NOTE: None of the convenience methods obtain a lock on the
+ * document. If you have another thread modifying the text these
+ * methods may have inconsistent behavior, or return the wrong thing.
+ */
+ public static abstract class HTMLTextAction extends StyledTextAction {
+ public HTMLTextAction(String name) {
+ super(name);
+ }
+
+ /**
+ * @return HTMLDocument of <code>e</code>.
+ */
+ protected HTMLDocument getHTMLDocument(JEditorPane e) {
+ Document d = e.getDocument();
+ if (d instanceof HTMLDocument) {
+ return (HTMLDocument) d;
+ }
+ throw new IllegalArgumentException("document must be HTMLDocument");
+ }
+
+ /**
+ * @return HTMLEditorKit for <code>e</code>.
+ */
+ protected HTMLEditorKit getHTMLEditorKit(JEditorPane e) {
+ EditorKit k = e.getEditorKit();
+ if (k instanceof HTMLEditorKit) {
+ return (HTMLEditorKit) k;
+ }
+ throw new IllegalArgumentException("EditorKit must be HTMLEditorKit");
+ }
+
+ /**
+ * Returns an array of the Elements that contain <code>offset</code>.
+ * The first elements corresponds to the root.
+ */
+ protected Element[] getElementsAt(HTMLDocument doc, int offset) {
+ return getElementsAt(doc.getDefaultRootElement(), offset, 0);
+ }
+
+ /**
+ * Recursive method used by getElementsAt.
+ */
+ private Element[] getElementsAt(Element parent, int offset,
+ int depth) {
+ if (parent.isLeaf()) {
+ Element[] retValue = new Element[depth + 1];
+ retValue[depth] = parent;
+ return retValue;
+ }
+ Element[] retValue = getElementsAt(parent.getElement
+ (parent.getElementIndex(offset)), offset, depth + 1);
+ retValue[depth] = parent;
+ return retValue;
+ }
+
+ /**
+ * Returns number of elements, starting at the deepest leaf, needed
+ * to get to an element representing <code>tag</code>. This will
+ * return -1 if no elements is found representing <code>tag</code>,
+ * or 0 if the parent of the leaf at <code>offset</code> represents
+ * <code>tag</code>.
+ */
+ protected int elementCountToTag(HTMLDocument doc, int offset,
+ HTML.Tag tag) {
+ int depth = -1;
+ Element e = doc.getCharacterElement(offset);
+ while (e != null && e.getAttributes().getAttribute
+ (StyleConstants.NameAttribute) != tag) {
+ e = e.getParentElement();
+ depth++;
+ }
+ if (e == null) {
+ return -1;
+ }
+ return depth;
+ }
+
+ /**
+ * Returns the deepest element at <code>offset</code> matching
+ * <code>tag</code>.
+ */
+ protected Element findElementMatchingTag(HTMLDocument doc, int offset,
+ HTML.Tag tag) {
+ Element e = doc.getDefaultRootElement();
+ Element lastMatch = null;
+ while (e != null) {
+ if (e.getAttributes().getAttribute
+ (StyleConstants.NameAttribute) == tag) {
+ lastMatch = e;
+ }
+ e = e.getElement(e.getElementIndex(offset));
+ }
+ return lastMatch;
+ }
+ }
+
+
+ /**
+ * InsertHTMLTextAction can be used to insert an arbitrary string of HTML
+ * into an existing HTML document. At least two HTML.Tags need to be
+ * supplied. The first Tag, parentTag, identifies the parent in
+ * the document to add the elements to. The second tag, addTag,
+ * identifies the first tag that should be added to the document as
+ * seen in the HTML string. One important thing to remember, is that
+ * the parser is going to generate all the appropriate tags, even if
+ * they aren't in the HTML string passed in.<p>
+ * For example, lets say you wanted to create an action to insert
+ * a table into the body. The parentTag would be HTML.Tag.BODY,
+ * addTag would be HTML.Tag.TABLE, and the string could be something
+ * like &lt;table&gt;&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;.
+ * <p>There is also an option to supply an alternate parentTag and
+ * addTag. These will be checked for if there is no parentTag at
+ * offset.
+ */
+ 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 <code>tag</code> at
+ * <code>offset</code>, this will invoke either insertAtBoundary
+ * or <code>insertHTML</code>. This returns true if there is
+ * a match, and one of the inserts is invoked.
+ */
+ /*protected*/
+ boolean insertIntoTag(JEditorPane editor, HTMLDocument doc,
+ int offset, HTML.Tag tag, HTML.Tag addTag) {
+ Element e = findElementMatchingTag(doc, offset, tag);
+ if (e != null && e.getStartOffset() == offset) {
+ insertAtBoundary(editor, doc, offset, e, html,
+ tag, addTag);
+ return true;
+ }
+ else if (offset > 0) {
+ int depth = elementCountToTag(doc, offset - 1, tag);
+ if (depth != -1) {
+ insertHTML(editor, doc, offset, html, depth, 0, addTag);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called after an insertion to adjust the selection.
+ */
+ /* protected */
+ void adjustSelection(JEditorPane pane, HTMLDocument doc,
+ int startOffset, int oldLength) {
+ int newLength = doc.getLength();
+ if (newLength != oldLength && startOffset < newLength) {
+ if (startOffset > 0) {
+ String text;
+ try {
+ text = doc.getText(startOffset - 1, 1);
+ } catch (BadLocationException ble) {
+ text = null;
+ }
+ if (text != null && text.length() > 0 &&
+ text.charAt(0) == '\n') {
+ pane.select(startOffset, startOffset);
+ }
+ else {
+ pane.select(startOffset + 1, startOffset + 1);
+ }
+ }
+ else {
+ pane.select(1, 1);
+ }
+ }
+ }
+
+ /**
+ * Inserts the HTML into the document.
+ *
+ * @param ae the event
+ */
+ public void actionPerformed(ActionEvent ae) {
+ JEditorPane editor = getEditor(ae);
+ if (editor != null) {
+ HTMLDocument doc = getHTMLDocument(editor);
+ int offset = editor.getSelectionStart();
+ int length = doc.getLength();
+ boolean inserted;
+ // Try first choice
+ if (!insertIntoTag(editor, doc, offset, parentTag, addTag) &&
+ alternateParentTag != null) {
+ // Then alternate.
+ inserted = insertIntoTag(editor, doc, offset,
+ alternateParentTag,
+ alternateAddTag);
+ }
+ else {
+ inserted = true;
+ }
+ if (adjustSelection && inserted) {
+ adjustSelection(editor, doc, offset, length);
+ }
+ }
+ }
+
+ /** HTML to insert. */
+ protected String html;
+ /** Tag to check for in the document. */
+ protected HTML.Tag parentTag;
+ /** Tag in HTML to start adding tags from. */
+ protected HTML.Tag addTag;
+ /** Alternate Tag to check for in the document if parentTag is
+ * not found. */
+ protected HTML.Tag alternateParentTag;
+ /** Alternate tag in HTML to start adding tags from if parentTag
+ * is not found and alternateParentTag is found. */
+ protected HTML.Tag alternateAddTag;
+ /** True indicates the selection should be adjusted after an insert. */
+ boolean adjustSelection;
+ }
+
+
+ /**
+ * InsertHRAction is special, at actionPerformed time it will determine
+ * the parent HTML.Tag based on the paragraph element at the selection
+ * start.
+ */
+ static class InsertHRAction extends InsertHTMLTextAction {
+ InsertHRAction() {
+ super("InsertHR", "<hr>", null, HTML.Tag.IMPLIED, null, null,
+ false);
+ }
+
+ /**
+ * Inserts the HTML into the document.
+ *
+ * @param ae the event
+ */
+ public void actionPerformed(ActionEvent ae) {
+ JEditorPane editor = getEditor(ae);
+ if (editor != null) {
+ HTMLDocument doc = getHTMLDocument(editor);
+ int offset = editor.getSelectionStart();
+ Element paragraph = doc.getParagraphElement(offset);
+ if (paragraph.getParentElement() != null) {
+ parentTag = (HTML.Tag)paragraph.getParentElement().
+ getAttributes().getAttribute
+ (StyleConstants.NameAttribute);
+ super.actionPerformed(ae);
+ }
+ }
+ }
+
+ }
+
+ /*
+ * Returns the object in an AttributeSet matching a key
+ */
+ 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
+ * <code>unlock</code>.
+ */
+ private Object lock(JEditorPane editor) {
+ Document document = editor.getDocument();
+
+ if (document instanceof AbstractDocument) {
+ ((AbstractDocument)document).readLock();
+ return document;
+ }
+ return null;
+ }
+
+ /*
+ * Releases a lock previously obtained via <code>lock</code>.
+ */
+ private void unlock(Object key) {
+ if (key != null) {
+ ((AbstractDocument)key).readUnlock();
+ }
+ }
+
+ /*
+ * The operation to perform when this action is triggered.
+ */
+ public void actionPerformed(ActionEvent e) {
+
+ JTextComponent c = getTextComponent(e);
+ if (c.isEditable() || !(c instanceof JEditorPane)) {
+ return;
+ }
+ JEditorPane editor = (JEditorPane)c;
+
+ Document d = editor.getDocument();
+ if (d == null || !(d instanceof HTMLDocument)) {
+ return;
+ }
+ HTMLDocument doc = (HTMLDocument)d;
+
+ ElementIterator ei = new ElementIterator(doc);
+ int currentOffset = editor.getCaretPosition();
+
+ // invoke the next link or object action
+ String urlString = null;
+ String objString = null;
+ Element currentElement = 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
+ * &lt;b&gt;, &lt;i&gt;, &lt;font&gt;, and &lt;a&gt;.
+ */
+ 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 &lt;) 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("<head>");
+ writeLineSeparator();
+ incrIndent();
+ writeStyles(((HTMLDocument)getDocument()).getStyleSheet());
+ decrIndent();
+ writeLineSeparator();
+ indentSmart();
+ write("</head>");
+ 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("<base href=\"" + document.getBase() + "\">");
+ 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("<!--");
+ if (string != null) {
+ write(string);
+ }
+ 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("<map");
+ if (name != null) {
+ write(" name=\"");
+ write(name);
+ 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("<area");
+ writeAttributes(areas[counter]);
+ write("></area>");
+ writeLineSeparator();
+ }
+ }
+ decrIndent();
+ indentSmart();
+ write("</map>");
+ 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. <code>outputStyle</code> 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("<style type=\"text/css\">");
+ incrIndent();
+ writeLineSeparator();
+ indentSmart();
+ write("<!--");
+ incrIndent();
+ writeLineSeparator();
+ }
+
+ void writeStyleEndTag() throws IOException {
+ decrIndent();
+ indentSmart();
+ write("-->");
+ writeLineSeparator();
+ decrIndent();
+ indentSmart();
+ write("</style>");
+ 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.<p>
+ * This will put the converted values into <code>to</code>, 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 &lt;font&gt; 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
+ * &lt; to &amp;lt;. <code>super.output</code> 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("&lt;");
+ break;
+ case '>':
+ if (counter > last) {
+ super.output(chars, last, counter - last);
+ }
+ last = counter + 1;
+ output("&gt;");
+ break;
+ case '&':
+ if (counter > last) {
+ super.output(chars, last, counter - last);
+ }
+ last = counter + 1;
+ output("&amp;");
+ break;
+ case '"':
+ if (counter > last) {
+ super.output(chars, last, counter - last);
+ }
+ last = counter + 1;
+ output("&quot;");
+ 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 <code>output</code> after converting
+ * <code>string</code> 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.
+ * <p>If this is invoked on the event dispatching thread, this
+ * directly invokes <code>_setTextFromModel</code>, otherwise
+ * <code>SwingUtilities.invokeLater</code> is used to schedule execution
+ * of <code>_setTextFromModel</code>.
+ */
+ 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.
+ * <p>If this is invoked on the event dispatching thread, this
+ * directly invokes <code>_updateModelFromText</code>, otherwise
+ * <code>SwingUtilities.invokeLater</code> is used to schedule execution
+ * of <code>_updateModelFromText</code>.
+ */
+ 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 &lt;IMG&gt; 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
+ * <code>ALT</code> attribute will be rendered.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>sMissingImageIcon</code> and
+ // <code>sPendingImageIcon</code>
+ 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
+ * <code>HTML.Attribute.ALT</code>.
+ */
+ 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 <code>newValue</code> 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
+ * <code>ALT</code> attribute. This is overriden to return
+ * <code>getAltText</code>.
+ *
+ * @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 <code>loadImage</code> or
+ * <code>updateImageSize</code> 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 <code>getImageURL</code>. This should
+ * only be invoked from <code>refreshImage</code>.
+ */
+ 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 <code>refreshImage</code>.
+ */
+ 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 <code>preferenceChanged</code> 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 <dfn>inline element</dfn> 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 <code>null</code>, 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 <code>null</code>, 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 <code>breakView</code>
+ * 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 <code>BadBreakWeight</code>
+ * should not be considered for a break. A value greater
+ * than or equal to <code>ForcedBreakWeight</code> should
+ * be broken.
+ * <p>
+ * This is implemented to provide the default behavior
+ * of returning <code>BadBreakWeight</code> 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 <code>LabelView</code>.
+ * An example of a view that uses break weight is
+ * <code>ParagraphView</code>.
+ *
+ * @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 <em>pos</em>
+ * 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.
+ * <p>Behavior of this method is unspecified in case <code>axis</code>
+ * is neither <code>View.X_AXIS</code> nor <code>View.Y_AXIS</code>, and
+ * in case <code>offset</code>, <code>pos</code>, or <code>len</code>
+ * is null.
+ *
+ * @param axis may be either <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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 &lt;ISINDEX&lt; 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 <code>JPanel</code>,
+ * that contains the PROMPT to the left and <code>JTextField</code>
+ * 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.<p>
+ * 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 <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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
+ * <code>preferenceChanged</code> 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
+ * <code>getTabbedSpan</code> 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.
+ * <code>LabelView</code> is an example of a view that delegates
+ * its tab expansion needs upward to the paragraph.
+ * <p>
+ * This is implemented to try and locate a <code>TabSet</code>
+ * 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,
+ * <code>x</code>, <code>y</code>. <code>width</code>, <code>height</code>
+ * 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
+ * <code>stringCoords</code>. 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 <code>x</code>, <code>y</code>
+ * falls inside the region defined in the receiver.
+ * <code>width</code>, <code>height</code> 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:
+ * <pre>
+ * &lt;html&gt;
+ * &lt;head&gt;
+ * &lt;style&gt;
+ * &lt;!-- list of named styles
+ * p.normal {
+ * font-family: SansSerif;
+ * margin-height: 0;
+ * font-size: 14
+ * }
+ * --&gt;
+ * &lt;/style&gt;
+ * &lt;/head&gt;
+ * &lt;body&gt;
+ * &lt;p style=normal&gt;
+ * <b>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 &lt;span&gt; tag.
+ * The syntax is similar to inline styles.</b>
+ * &lt;/p&gt;
+ * &lt;/body&gt;
+ * &lt;/html&gt;
+ * </pre>
+ *
+ * @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
+ * &lt;span&gt; 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("<html>");
+ writeHeader();
+ writeBody();
+ writeEndTag("</html>");
+ }
+
+
+ /**
+ * 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 &lt;head&gt; and &lt;style&gt;
+ * tags, and then invokes writeStyles() to write
+ * out all the named styles as the content of the
+ * &lt;style&gt; 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("<head>");
+ writeStartTag("<style>");
+ writeStartTag("<!--");
+ writeStyles();
+ writeEndTag("-->");
+ writeEndTag("</style>");
+ writeEndTag("</head>");
+ }
+
+
+
+ /**
+ * Writes out all the named styles as the
+ * content of the &lt;style&gt; 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("<body>");
+
+ 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("</body>");
+ }
+
+
+ /**
+ * Emits an end tag for a &lt;p&gt;
+ * 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("</p>");
+ }
+
+
+ /**
+ * 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
+ * &lt;p&gt; 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("<p class=" + mapStyleName(((StyleContext.NamedStyle)resolveAttr).getName()) + ">");
+ } else {
+ writeStartTag("<p>");
+ }
+ }
+
+
+ /**
+ * 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 &lt;img&gt; 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 &lt;b&gt;, italic &lt;i&gt;, and &lt;u&gt; 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 &lt;u&gt;, &lt;i&gt;, and &lt;b&gt; 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("<u>");
+ }
+ if ((mask & ITALIC) != 0) {
+ write("<i>");
+ }
+ if ((mask & BOLD) != 0) {
+ write("<b>");
+ }
+ }
+ }
+
+ /**
+ * Writes out end tags for &lt;u&gt;, &lt;i&gt;, and &lt;b&gt; 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("</b>");
+ }
+ if ((mask & ITALIC) != 0) {
+ write("</i>");
+ }
+ if ((mask & UNDERLINE) != 0) {
+ write("</u>");
+ }
+ }
+ }
+
+
+ /**
+ * 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 &lt;span&gt; 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 &lt;font&gt; tag.
+ */
+ protected boolean inFontTag() {
+ return (fontAttributes != null);
+ }
+
+ /**
+ * This is no longer used, instead &lt;span&gt; will be written out.
+ * <p>
+ * Writes out an end tag for the &lt;font&gt; tag.
+ *
+ * @exception IOException on any I/O error
+ */
+ protected void endFontTag() throws IOException {
+ write(NEWLINE);
+ writeEndTag("</font>");
+ fontAttributes = null;
+ }
+
+
+ /**
+ * This is no longer used, instead &lt;span&gt; will be written out.
+ * <p>
+ * Writes out a start tag for the &lt;font&gt; 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("<font style=\"" + style + "\">");
+ if (callIndent) {
+ indent();
+ }
+ }
+
+ /**
+ * Writes out a start tag for the &lt;font&gt; 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("<span style=\"" + style + "\">");
+ if (callIndent) {
+ indent();
+ }
+ }
+
+ /**
+ * Writes out an end tag for the &lt;span&gt; tag.
+ *
+ * @exception IOException on any I/O error
+ */
+ private void endSpanTag() throws IOException {
+ write(NEWLINE);
+ writeEndTag("</span>");
+ fontAttributes = null;
+ }
+
+ /**
+ * Adds the style named <code>style</code> 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 <code>style</code>.
+ */
+ 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 <code>AttributeSet</code> that can multiplex
+ * across a set of <code>AttributeSet</code>s.
+ *
+ */
+class MuxingAttributeSet implements AttributeSet, Serializable {
+ /**
+ * Creates a <code>MuxingAttributeSet</code> with the passed in
+ * attributes.
+ */
+ public MuxingAttributeSet(AttributeSet[] attrs) {
+ this.attrs = attrs;
+ }
+
+ /**
+ * Creates an empty <code>MuxingAttributeSet</code>. This is intended for
+ * use by subclasses only, and it is also intended that subclasses will
+ * set the constituent <code>AttributeSet</code>s before invoking any
+ * of the <code>AttributeSet</code> methods.
+ */
+ protected MuxingAttributeSet() {
+ }
+
+ /**
+ * Directly sets the <code>AttributeSet</code>s that comprise this
+ * <code>MuxingAttributeSet</code>.
+ */
+ protected synchronized void setAttributes(AttributeSet[] attrs) {
+ this.attrs = attrs;
+ }
+
+ /**
+ * Returns the <code>AttributeSet</code>s multiplexing too. When the
+ * <code>AttributeSet</code>s need to be referenced, this should be called.
+ */
+ protected synchronized AttributeSet[] getAttributes() {
+ return attrs;
+ }
+
+ /**
+ * Inserts <code>as</code> at <code>index</code>. This assumes
+ * the value of <code>index</code> 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 <code>index</code>. This assumes
+ * the value of <code>index</code> 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 <code>AttributeSet</code>s that make up the resulting
+ * <code>AttributeSet</code>.
+ */
+ 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 <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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 <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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 &lt;object&gt; elements.
+ * <p>
+ * This view will try to load the class specified by the
+ * <code>classid</code> 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, <code>Class.forName</code> is used.
+ * <p>
+ * If the class can successfully be loaded, an attempt will
+ * be made to create an instance of it by calling
+ * <code>Class.newInstance</code>. An attempt will be made
+ * to narrow the instance to type <code>java.awt.Component</code>
+ * to display the object.
+ * <p>
+ * This view can also manage a set of parameters with limitations.
+ * The parameters to the &lt;object&gt; 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.
+ * <p>
+ * A simple example HTML invocation is:
+ * <pre>
+ * &lt;object classid="javax.swing.JLabel"&gt;
+ * &lt;param name="text" value="sample text"&gt;
+ * &lt;/object&gt;
+ * </pre>
+ *
+ * @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,
+ * <code>Class.forName</code> 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 &lt;param&gt; elements in the corresponding
+ * &lt;object&gt; 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
+ * &lt;option&gt; elements. This is the object
+ * installed as items of the DefaultComboBoxModel
+ * used to represent the &lt;select&gt; element.
+ * <p>
+ * <strong>Warning:</strong>
+ * 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 JavaBeans<sup><font size="-2">TM</font></sup>
+ * has been added to the <code>java.beans</code> 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 <code>value</code> 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 <code>ListSelectionListener</code>s added
+ * to this OptionListModel with addListSelectionListener().
+ *
+ * @return all of the <code>ListSelectionListener</code>s 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.
+ * <code>listenerLists</code> are not duplicated.
+ *
+ * @return a clone of the receiver
+ * @exception CloneNotSupportedException if the receiver does not
+ * both (a) implement the <code>Cloneable</code> interface
+ * and (b) define a <code>clone</code> 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * This is implemented
+ * to forward to the superclass as well as call the
+ * <a href="#setPropertiesFromAttributes">setPropertiesFromAttributes</a>
+ * 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.
+ *
+ * <p>If size requirements are explicitly specified for the paragraph,
+ * use that requirements. Otherwise, use the requirements of the
+ * superclass {@link javax.swing.text.ParagraphView}.</p>
+ *
+ * <p>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.</p>
+ *
+ * @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 <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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 <code>View.X_AXIS</code> or
+ * <code>View.Y_AXIS</code>
+ * @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
+ * <code>toString</code> method is required
+ * to return a string representation of CSS value.
+ * <p>
+ * The primary entry point for HTML View implementations
+ * to get their attributes is the
+ * <a href="#getViewAttributes">getViewAttributes</a>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <code><pre>
+ * &nbsp;
+ * &nbsp; import java.util.*;
+ * &nbsp; import javax.swing.text.*;
+ * &nbsp; import javax.swing.text.html.*;
+ * &nbsp;
+ * &nbsp; public class ShowStyles {
+ * &nbsp;
+ * &nbsp; public static void main(String[] args) {
+ * &nbsp; HTMLEditorKit kit = new HTMLEditorKit();
+ * &nbsp; HTMLDocument doc = (HTMLDocument) kit.createDefaultDocument();
+ * &nbsp; StyleSheet styles = doc.getStyleSheet();
+ * &nbsp;
+ * &nbsp; Enumeration rules = styles.getStyleNames();
+ * &nbsp; while (rules.hasMoreElements()) {
+ * &nbsp; String name = (String) rules.nextElement();
+ * &nbsp; Style rule = styles.getStyle(name);
+ * &nbsp; System.out.println(rule.toString());
+ * &nbsp; }
+ * &nbsp; System.exit(0);
+ * &nbsp; }
+ * &nbsp; }
+ * &nbsp;
+ * </pre></code>
+ * <p>
+ * The semantics for when a CSS style should overide visual attributes
+ * defined by an element are not well defined. For example, the html
+ * <code>&lt;body bgcolor=red&gt;</code> makes the body have a red
+ * background. But if the html file also contains the CSS rule
+ * <code>body { background: blue }</code> 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.
+ * <p>
+ * 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 <code>A:link { color: red }</code>,
+ * and the <code>important</code> modifier.
+ * <p>
+ * <font color="red">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.</font>
+ *
+ * @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 <code>selector</code> is a space separated
+ * String of the element names. For example, <code>selector</code>
+ * might be 'html body tr td''<p>
+ * 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 &lt;style&gt; 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 <code>ss</code> to those of
+ * the receiver. <code>ss's</code> 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 <code>ss</code> 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 <code>url</code>. 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 <code>base</code>.
+ *
+ * @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
+ * <code>attr</code>.
+ */
+ 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 <code>ss</code>.
+ */
+ 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 <code>ss</code>.
+ * <code>index</code> 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 <code>selector</code>. 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 <code>selector</code>. 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 <code>mapping</code> to <code>elements</code>. It is added
+ * such that <code>elements</code> 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 <code>parentMapping</code> to <code>styles</code>, and
+ * recursively calls this method if <code>parentMapping</code> has
+ * any child mappings for any of the Elements in <code>elements</code>.
+ */
+ 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
+ * <code>selector</code>.
+ */
+ 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 <code>selector</code>.
+ *
+ * @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 <code>elements</code>
+ */
+ 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
+ * <code>selector</code>. It is assumed that each simple selector
+ * in <code>selector</code> 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 <code>sb</code> 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.
+ * <p>
+ * 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
+ * <dir>, <menu>, <ul>, <ol>
+ * 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 <dir>, <menu>, <ul> 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 <code>parent</code>.
+ */
+ 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
+ * <code>childIndex</code> with. The retValue will usually be
+ * <code>childIndex</code> + 1, unless <code>parentView</code>
+ * 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.
+ * <code>style</code> 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
+ * <code>style</code> 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 <code>s</code> as one of the Attributesets to look up
+ * attributes in.
+ */
+ synchronized void insertExtendedStyleAt(Style attr, int index) {
+ insertAttributeSetAt(attr, extendedIndex + index);
+ }
+
+ /**
+ * Adds <code>s</code> 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 <code>index</code> +
+ * <code>extendedIndex</code>.
+ */
+ synchronized void removeExtendedStyleAt(int index) {
+ removeAttributeSetAt(extendedIndex + index);
+ }
+
+ /**
+ * Returns true if the receiver matches <code>selector</code>, 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 <code>SelectorMapping</code>s,
+ * so that it behaves like a tree.
+ * <p>
+ * 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
+ * <code>selector</code>. If a child mapping does not exist for
+ *<code>selector</code>, and <code>create</code> 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 <code>SelectorMapping</code> with the specified
+ * <code>specificity</code>.
+ */
+ protected SelectorMapping createChildSelectorMapping(int specificity) {
+ return new SelectorMapping(specificity);
+ }
+
+ /**
+ * Returns the specificity for the child selector
+ * <code>selector</code>.
+ */
+ 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.<p>
+ * 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
+ * <code>importStyleSheet</code> if a
+ * <code>MalformedURLException</code> 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).
+ * <p>
+ * 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).
+ * <p>
+ * 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.
+ * <p>
+ * This is implemented to call the
+ * <a href="#layoutColumns">layoutColumns</a> 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.
+ * <p>
+ * 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.
+ * <p>
+ * This is implemented
+ * to forward to the superclass as well as call the
+ * <a href="#setPropertiesFromAttributes">setPropertiesFromAttributes</a>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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).
+ * <p>
+ * 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 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+<!--
+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.
+-->
+
+</head>
+<body bgcolor="white">
+
+Provides the class <code>HTMLEditorKit</code> and supporting classes
+for creating HTML text editors.
+
+<p>
+<strong>Note:</strong>
+Most of the Swing API is <em>not</em> thread safe.
+For details, see
+<a
+href="http://java.sun.com/docs/books/tutorial/uiswing/overview/threads.html"
+target="_top">Threads and Swing</a>,
+a section in
+<em><a href="http://java.sun.com/docs/books/tutorial/"
+target="_top">The Java Tutorial</a></em>.
+
+<h2>Package Specification</h2>
+
+
+<ul>
+ <li><a href="http://www.w3.org/TR/REC-html32.html" target="_top">
+ HTML 3.2 Reference Specification</a> -
+ The HTML specification on which HTMLEditorKit is based.
+</ul>
+
+@since 1.2
+@serial exclude
+
+</body>
+</html>
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.
+ * <p>
+ * 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.<p>
+ * 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<Element> 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.
+ * <p>
+ * Each time a token is reduced a new state is created.
+ * <p>
+ * 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<Element> elements = new Vector<Element>();
+ public Hashtable<String,Element> elementHash
+ = new Hashtable<String,Element>();
+ public Hashtable<Object,Entity> entityHash
+ = new Hashtable<Object,Entity>();
+ 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 <code>String</code> 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 <code>Entity</code> corresponding to the
+ * <code>name</code> <code>String</code>
+ */
+ public Entity getEntity(String name) {
+ return (Entity)entityHash.get(name);
+ }
+
+ /**
+ * Gets a character entity.
+ * @return the <code>Entity</code> corresponding to the
+ * <code>ch</code> character
+ */
+ public Entity getEntity(int ch) {
+ return (Entity)entityHash.get(new Integer(ch));
+ }
+
+ /**
+ * Returns <code>true</code> if the element is part of the DTD,
+ * otherwise returns <code>false</code>.
+ *
+ * @param name the requested <code>String</code>
+ * @return <code>true</code> if <code>name</code> exists as
+ * part of the DTD, otherwise returns <code>false</code>
+ */
+ 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 <code>String</code>
+ * @return the <code>Element</code> corresponding to
+ * <code>name</code>, 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 <code>Element</code> corresponding to
+ * <code>index</code>
+ */
+ public Element getElement(int index) {
+ return (Element)elements.elementAt(index);
+ }
+
+ /**
+ * Defines an entity. If the <code>Entity</code> specified
+ * by <code>name</code>, <code>type</code>, and <code>data</code>
+ * exists, it is returned; otherwise a new <code>Entity</code>
+ * is created and is returned.
+ *
+ * @param name the name of the <code>Entity</code> as a <code>String</code>
+ * @param type the type of the <code>Entity</code>
+ * @param data the <code>Entity</code>'s data
+ * @return the <code>Entity</code> requested or a new <code>Entity</code>
+ * 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 <code>Element</code> which matches the
+ * specified parameters. If one doesn't exist, a new
+ * one is created and returned.
+ *
+ * @param name the name of the <code>Element</code>
+ * @param type the type of the <code>Element</code>
+ * @param omitStart <code>true</code> if start should be omitted
+ * @param omitEnd <code>true</code> if end should be omitted
+ * @param content the <code>ContentModel</code>
+ * @param atts the <code>AttributeList</code> specifying the
+ * <code>Element</code>
+ * @return the <code>Element</code> 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 <code>Element</code>
+ * @param atts the <code>AttributeList</code> specifying the
+ * <code>Element</code>
+ */
+ public void defineAttributes(String name, AttributeList atts) {
+ Element e = getElement(name);
+ e.atts = atts;
+ }
+
+ /**
+ * Creates and returns a character <code>Entity</code>.
+ * @param name the entity's name
+ * @return the new character <code>Entity</code>
+ */
+ public Entity defEntity(String name, int type, int ch) {
+ char data[] = {(char)ch};
+ return defineEntity(name, type, data);
+ }
+
+ /**
+ * Creates and returns an <code>Entity</code>.
+ * @param name the entity's name
+ * @return the new <code>Entity</code>
+ */
+ 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 <code>Element</code>.
+ * @param name the element's name
+ * @return the new <code>Element</code>
+ */
+ 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 <code>AttributeList</code>.
+ * @param name the attribute list's name
+ * @return the new <code>AttributeList</code>
+ */
+ 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 <code>ContentModel</code>
+ */
+ 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 <code>name</code>. 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 <code>name</code>
+ */
+ 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 <code>DataInputStream</code> 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.
+ * <p>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 (&lt;p&gt;blah) only has a p tag defined. The callback
+ * will see the following methods:
+ * <ol><li><i>handleStartTag(html, ...)</i></li>
+ * <li><i>handleStartTag(head, ...)</i></li>
+ * <li><i>handleEndTag(head)</i></li>
+ * <li><i>handleStartTag(body, ...)</i></li>
+ * <li>handleStartTag(p, ...)</i></li>
+ * <li>handleText(...)</li>
+ * <li><i>handleEndTag(p)</i></li>
+ * <li><i>handleEndTag(body)</i></li>
+ * <li><i>handleEndTag(html)</i></li>
+ * </ol>
+ * The items in <i>italic</i> 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
+ * <code>Boolean.TRUE</code> for the key
+ * <code>HTMLEditorKit.ParserCallback.IMPLIED</code>.
+ * <p>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 &lt;p foo=bar class=neat&gt; 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'.
+ * <p>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.
+ * <p>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.
+ * <p>
+ * For attributes that do not have a value, eg in the html
+ * string <code>&lt;foo blah&gt;</code> the attribute <code>blah</code>
+ * does not have a value, there are two possible values that will be
+ * placed in the AttributeSet's value:
+ * <ul>
+ * <li>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 <code>HTML.NULL_ATTRIBUTE_VALUE</code>.
+ * <li>If the DTD contains an explicit value, as in:
+ * <code>&lt;!ATTLIST OPTION selected (selected) #IMPLIED&gt;</code>
+ * this value from the dtd (in this case selected) will be used.
+ * </ul>
+ * <p>
+ * 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 <code>String</code>
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the type of the entity.
+ * @return the type of the entity
+ */
+ public int getType() {
+ return type & 0xFFFF;
+ }
+
+ /**
+ * Returns <code>true</code> if it is a parameter entity.
+ * @return <code>true</code> if it is a parameter entity
+ */
+ public boolean isParameter() {
+ return (type & PARAMETER) != 0;
+ }
+
+ /**
+ * Returns <code>true</code> if it is a general entity.
+ * @return <code>true</code> if it is a general entity
+ */
+ public boolean isGeneral() {
+ return (type & GENERAL) != 0;
+ }
+
+ /**
+ * Returns the <code>data</code>.
+ * @return the <code>data</code>
+ */
+ public char getData()[] {
+ return data;
+ }
+
+ /**
+ * Returns the data as a <code>String</code>.
+ * @return the data as a <code>String</code>
+ */
+ 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 <code>nm</code> 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 &amp;nbsp to force the space to
+ * be displayed):
+ * <p>
+ * '&lt;b>blah&nbsp;&lt;i>&nbsp;&lt;strike>&nbsp;foo' which can be treated as:
+ * '&lt;b>blah&nbsp;&lt;i>&lt;strike>foo'
+ * <p>as well as:
+ * '&lt;p>&lt;a href="xx">&nbsp;&lt;em>Using&lt;/em>&lt;/a>&lt;/p>'
+ * which appears to be treated as:
+ * '&lt;p>&lt;a href="xx">&lt;em>Using&lt;/em>&lt;/a>&lt;/p>'
+ * <p>
+ * If <code>strict</code> is false, when a tag that breaks flow,
+ * (<code>TagElement.breaksFlows</code>) 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 <html>, <head> and <body>. 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.
+ * <p>
+ * The problematic scenarios are:
+ * '&lt;b>blah &lt;i> &lt;strike> foo' which can be treated as:
+ * '&lt;b>blah &lt;i>&lt;strike>foo'
+ * as well as:
+ * '&lt;p>&lt;a href="xx"> &lt;em>Using&lt;/em>&lt;/a>&lt;/p>'
+ * which appears to be treated as:
+ * '&lt;p>&lt;a href="xx">&lt;em>Using&lt;/em>&lt;/a>&lt;/p>'
+ * <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, // &#130;
+ 402, // &#131;
+ 8222, // &#132;
+ 8230, // &#133;
+ 8224, // &#134;
+ 8225, // &#135;
+ 710, // &#136;
+ 8240, // &#137;
+ 352, // &#138;
+ 8249, // &#139;
+ 338, // &#140;
+ 141, // &#141;
+ 142, // &#142;
+ 143, // &#143;
+ 144, // &#144;
+ 8216, // &#145;
+ 8217, // &#146;
+ 8220, // &#147;
+ 8221, // &#148;
+ 8226, // &#149;
+ 8211, // &#150;
+ 8212, // &#151;
+ 732, // &#152;
+ 8482, // &#153;
+ 353, // &#154;
+ 8250, // &#155;
+ 339, // &#156;
+ 157, // &#157;
+ 158, // &#158;
+ 376 // &#159;
+ };
+
+ 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 <td>, <th> (these we handle in
+ legalElementContext()) and #pcdata. We also ignore the
+ <font> tag in the context of <ul> and <ol> We additonally
+ ignore the <meta> and the <style> tag if the body tag has
+ been seen. **/
+ if ((elemName.equals("html") && seenHtml) ||
+ (elemName.equals("head") && seenHead) ||
+ (elemName.equals("body") && seenBody)) {
+ return true;
+ }
+ if (elemName.equals("dt") || elemName.equals("dd")) {
+ TagStack s = stack;
+ while (s != null && !s.elem.getName().equals("dl")) {
+ s = s.next;
+ }
+ if (s == null) {
+ return true;
+ }
+ }
+
+ if (((stackElement.equals("table")) &&
+ (!elemName.equals("#pcdata")) && (!elemName.equals("input"))) ||
+ ((elemName.equals("font")) &&
+ (stackElement.equals("ul") || stackElement.equals("ol"))) ||
+ (elemName.equals("meta") && stack != null) ||
+ (elemName.equals("style") && seenBody) ||
+ (stackElement.equals("table") && elemName.equals("a"))) {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Marks the first time a tag has been seen in a document
+ */
+
+ protected void markFirstTime(Element elem) {
+ String elemName = elem.getName();
+ if (elemName.equals("html")) {
+ seenHtml = true;
+ } else if (elemName.equals("head")) {
+ seenHead = true;
+ } else if (elemName.equals("body")) {
+ if (buf.length == 1) {
+ // Refer to note in definition of buf for details on this.
+ char[] newBuf = new char[256];
+
+ newBuf[0] = buf[0];
+ buf = newBuf;
+ }
+ seenBody = true;
+ }
+ }
+
+ /**
+ * Create a legal content for an element.
+ */
+ boolean legalElementContext(Element elem) throws ChangedCharSetException {
+
+ // System.out.println("-- legalContext -- " + elem);
+
+ // Deal with the empty stack
+ if (stack == null) {
+ // System.out.println("-- stack is empty");
+ if (elem != dtd.html) {
+ // System.out.println("-- pushing html");
+ startTag(makeTag(dtd.html, true));
+ return legalElementContext(elem);
+ }
+ return true;
+ }
+
+ // Is it allowed in the current context
+ if (stack.advance(elem)) {
+ // System.out.println("-- legal context");
+ markFirstTime(elem);
+ return true;
+ }
+ boolean insertTag = false;
+
+ // The use of all error recovery strategies are contingent
+ // on the value of the strict property.
+ //
+ // These are commonly occuring errors. if insertTag is true,
+ // then we want to adopt an error recovery strategy that
+ // involves attempting to insert an additional tag to
+ // legalize the context. The two errors addressed here
+ // are:
+ // 1) when a <td> or <th> is seen soon after a <table> tag.
+ // In this case we insert a <tr>.
+ // 2) when any other tag apart from a <tr> is seen
+ // in the context of a <tr>. In this case we would
+ // like to add a <td>. If a <tr> is seen within a
+ // <tr> context, then we will close out the current
+ // <tr>.
+ //
+ // This insertion strategy is handled later in the method.
+ // The reason for checking this now, is that in other cases
+ // we would like to apply other error recovery strategies for example
+ // ignoring tags.
+ //
+ // In certain cases it is better to ignore a tag than try to
+ // fix the situation. So the first test is to see if this
+ // is what we need to do.
+ //
+ String stackElemName = stack.elem.getName();
+ String elemName = elem.getName();
+
+
+ if (!strict &&
+ ((stackElemName.equals("table") && elemName.equals("td")) ||
+ (stackElemName.equals("table") && elemName.equals("th")) ||
+ (stackElemName.equals("tr") && !elemName.equals("tr")))){
+ insertTag = true;
+ }
+
+
+ if (!strict && !insertTag && (stack.elem.getName() != elem.getName() ||
+ elem.getName().equals("body"))) {
+ if (skipTag = ignoreElement(elem)) {
+ error("tag.ignore", elem.getName());
+ return skipTag;
+ }
+ }
+
+ // Check for anything after the start of the table besides tr, td, th
+ // or caption, and if those aren't there, insert the <tr> and call
+ // legalElementContext again.
+ if (!strict && stackElemName.equals("table") &&
+ !elemName.equals("tr") && !elemName.equals("td") &&
+ !elemName.equals("th") && !elemName.equals("caption")) {
+ Element e = dtd.getElement("tr");
+ TagElement t = makeTag(e, true);
+ legalTagContext(t);
+ startTag(t);
+ error("start.missing", elem.getName());
+ return legalElementContext(elem);
+ }
+
+ // They try to find a legal context by checking if the current
+ // tag is valid in an enclosing context. If so
+ // close out the tags by outputing end tags and then
+ // insert the curent tag. If the tags that are
+ // being closed out do not have an optional end tag
+ // specification in the DTD then an html error is
+ // reported.
+ //
+ if (!insertTag && stack.terminate() && (!strict || stack.elem.omitEnd())) {
+ for (TagStack s = stack.next ; s != null ; s = s.next) {
+ if (s.advance(elem)) {
+ while (stack != s) {
+ endTag(true);
+ }
+ return true;
+ }
+ if (!s.terminate() || (strict && !s.elem.omitEnd())) {
+ break;
+ }
+ }
+ }
+
+ // Check if we know what tag is expected next.
+ // If so insert the tag. Report an error if the
+ // tag does not have its start tag spec in the DTD as optional.
+ //
+ Element next = stack.first();
+ if (next != null && (!strict || next.omitStart()) &&
+ !(next==dtd.head && elem==dtd.pcdata) ) {
+ // System.out.println("-- omitting start tag: " + next);
+ TagElement t = makeTag(next, true);
+ legalTagContext(t);
+ startTag(t);
+ if (!next.omitStart()) {
+ error("start.missing", elem.getName());
+ }
+ return legalElementContext(elem);
+ }
+
+
+ // Traverse the list of expected elements and determine if adding
+ // any of these elements would make for a legal context.
+ //
+
+ if (!strict) {
+ ContentModel content = stack.contentModel();
+ Vector elemVec = new Vector();
+ if (content != null) {
+ content.getElements(elemVec);
+ for (Enumeration v = elemVec.elements(); v.hasMoreElements();) {
+ Element e = (Element)v.nextElement();
+
+ // Ensure that this element has not been included as
+ // part of the exclusions in the DTD.
+ //
+ if (stack.excluded(e.getIndex())) {
+ continue;
+ }
+
+ boolean reqAtts = false;
+
+ for (AttributeList a = e.getAttributes(); a != null ; a = a.next) {
+ if (a.modifier == REQUIRED) {
+ reqAtts = true;
+ break;
+ }
+ }
+ // Ensure that no tag that has required attributes
+ // gets inserted.
+ //
+ if (reqAtts) {
+ continue;
+ }
+
+ ContentModel m = e.getContent();
+ if (m != null && m.first(elem)) {
+ // System.out.println("-- adding a legal tag: " + e);
+ TagElement t = makeTag(e, true);
+ legalTagContext(t);
+ startTag(t);
+ error("start.missing", e.getName());
+ return legalElementContext(elem);
+ }
+ }
+ }
+ }
+
+ // Check if the stack can be terminated. If so add the appropriate
+ // end tag. Report an error if the tag being ended does not have its
+ // end tag spec in the DTD as optional.
+ //
+ if (stack.terminate() && (stack.elem != dtd.body) && (!strict || stack.elem.omitEnd())) {
+ // System.out.println("-- omitting end tag: " + stack.elem);
+ if (!stack.elem.omitEnd()) {
+ error("end.missing", elem.getName());
+ }
+
+ endTag(true);
+ return legalElementContext(elem);
+ }
+
+ // At this point we know that something is screwed up.
+ return false;
+ }
+
+ /**
+ * Create a legal context for a tag.
+ */
+ void legalTagContext(TagElement tag) throws ChangedCharSetException {
+ if (legalElementContext(tag.getElement())) {
+ markFirstTime(tag.getElement());
+ return;
+ }
+
+ // Avoid putting a block tag in a flow tag.
+ if (tag.breaksFlow() && (stack != null) && !stack.tag.breaksFlow()) {
+ endTag(true);
+ legalTagContext(tag);
+ return;
+ }
+
+ // Avoid putting something wierd in the head of the document.
+ for (TagStack s = stack ; s != null ; s = s.next) {
+ if (s.tag.getElement() == dtd.head) {
+ while (stack != s) {
+ endTag(true);
+ }
+ endTag(true);
+ legalTagContext(tag);
+ return;
+ }
+ }
+
+ // Everything failed
+ error("tag.unexpected", tag.getElement().getName());
+ }
+
+ /**
+ * Error context. Something went wrong, make sure we are in
+ * the document's body context
+ */
+ void errorContext() throws ChangedCharSetException {
+ for (; (stack != null) && (stack.tag.getElement() != dtd.body) ; stack = stack.next) {
+ handleEndTag(stack.tag);
+ }
+ if (stack == null) {
+ legalElementContext(dtd.body);
+ startTag(makeTag(dtd.body, true));
+ }
+ }
+
+ /**
+ * Add a char to the string buffer.
+ */
+ void addString(int c) {
+ if (strpos == str.length) {
+ char newstr[] = new char[str.length + 128];
+ System.arraycopy(str, 0, newstr, 0, str.length);
+ str = newstr;
+ }
+ str[strpos++] = (char)c;
+ }
+
+ /**
+ * Get the string that's been accumulated.
+ */
+ String getString(int pos) {
+ char newStr[] = new char[strpos - pos];
+ System.arraycopy(str, pos, newStr, 0, strpos - pos);
+ strpos = pos;
+ return new String(newStr);
+ }
+
+ char[] getChars(int pos) {
+ char newStr[] = new char[strpos - pos];
+ System.arraycopy(str, pos, newStr, 0, strpos - pos);
+ strpos = pos;
+ return newStr;
+ }
+
+ char[] getChars(int pos, int endPos) {
+ char newStr[] = new char[endPos - pos];
+ System.arraycopy(str, pos, newStr, 0, endPos - pos);
+ // REMIND: it's not clear whether this version should set strpos or not
+ // strpos = pos;
+ return newStr;
+ }
+
+ void resetStrBuffer() {
+ strpos = 0;
+ }
+
+ int strIndexOf(char target) {
+ for (int i = 0; i < strpos; i++) {
+ if (str[i] == target) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Skip space.
+ * [5] 297:5
+ */
+ void skipSpace() throws IOException {
+ while (true) {
+ switch (ch) {
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ break;
+
+ case '\r':
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ break;
+ case ' ':
+ case '\t':
+ ch = readCh();
+ break;
+
+ default:
+ return;
+ }
+ }
+ }
+
+ /**
+ * Parse identifier. Uppercase characters are folded
+ * to lowercase when lower is true. Returns falsed if
+ * no identifier is found. [55] 346:17
+ */
+ boolean parseIdentifier(boolean lower) throws IOException {
+ switch (ch) {
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
+ case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
+ case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
+ case 'Y': case 'Z':
+ if (lower) {
+ ch = 'a' + (ch - 'A');
+ }
+
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
+ case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
+ case 's': case 't': case 'u': case 'v': case 'w': case 'x':
+ case 'y': case 'z':
+ break;
+
+ default:
+ return false;
+ }
+
+ while (true) {
+ addString(ch);
+
+ switch (ch = readCh()) {
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
+ case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
+ case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
+ case 'Y': case 'Z':
+ if (lower) {
+ ch = 'a' + (ch - 'A');
+ }
+
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
+ case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
+ case 's': case 't': case 'u': case 'v': case 'w': case 'x':
+ case 'y': case 'z':
+
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+
+ case '.': case '-':
+
+ case '_': // not officially allowed
+ break;
+
+ default:
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Parse an entity reference. [59] 350:17
+ */
+ private char[] parseEntityReference() throws IOException {
+ int pos = strpos;
+
+ if ((ch = readCh()) == '#') {
+ int n = 0;
+ ch = readCh();
+ if ((ch >= '0') && (ch <= '9') ||
+ ch == 'x' || ch == 'X') {
+
+ if ((ch >= '0') && (ch <= '9')) {
+ // parse decimal reference
+ while ((ch >= '0') && (ch <= '9')) {
+ n = (n * 10) + ch - '0';
+ ch = readCh();
+ }
+ } else {
+ // parse hexadecimal reference
+ ch = readCh();
+ char lch = (char) Character.toLowerCase(ch);
+ while ((lch >= '0') && (lch <= '9') ||
+ (lch >= 'a') && (lch <= 'f')) {
+ if (lch >= '0' && lch <= '9') {
+ n = (n * 16) + lch - '0';
+ } else {
+ n = (n * 16) + lch - 'a' + 10;
+ }
+ ch = readCh();
+ lch = (char) Character.toLowerCase(ch);
+ }
+ }
+ switch (ch) {
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ break;
+
+ case '\r':
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ break;
+
+ case ';':
+ ch = readCh();
+ break;
+ }
+ char data[] = {mapNumericReference((char) n)};
+ return data;
+ }
+ addString('#');
+ if (!parseIdentifier(false)) {
+ error("ident.expected");
+ strpos = pos;
+ char data[] = {'&', '#'};
+ return data;
+ }
+ } else if (!parseIdentifier(false)) {
+ char data[] = {'&'};
+ return data;
+ }
+ switch (ch) {
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ break;
+
+ case '\r':
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ break;
+
+ case ';':
+ ch = readCh();
+ break;
+ }
+
+ String nm = getString(pos);
+ Entity ent = dtd.getEntity(nm);
+
+ // entities are case sensitive - however if strict
+ // is false then we will try to make a match by
+ // converting the string to all lowercase.
+ //
+ if (!strict && (ent == null)) {
+ ent = dtd.getEntity(nm.toLowerCase());
+ }
+ if ((ent == null) || !ent.isGeneral()) {
+
+ if (nm.length() == 0) {
+ error("invalid.entref", nm);
+ return new char[0];
+ }
+ /* given that there is not a match restore the entity reference */
+ String str = "&" + nm + ";";
+
+ char b[] = new char[str.length()];
+ str.getChars(0, b.length, b, 0);
+ return b;
+ }
+ return ent.getData();
+ }
+
+ /**
+ * Converts numeric character reference to Unicode character.
+ *
+ * Normally the code in a reference should be always converted
+ * to the Unicode character with the same code, but due to
+ * wide usage of Cp1252 charset most browsers map numeric references
+ * in the range 130-159 (which are control chars in Unicode set)
+ * to displayable characters with other codes.
+ *
+ * @param c the code of numeric character reference.
+ * @return the character corresponding to the reference code.
+ */
+ private char mapNumericReference(char c) {
+ if (c < 130 || c > 159) {
+ return c;
+ }
+ return cp1252Map[c - 130];
+ }
+
+ /**
+ * Parse a comment. [92] 391:7
+ */
+ void parseComment() throws IOException {
+
+ while (true) {
+ int c = ch;
+ switch (c) {
+ case '-':
+ /** Presuming that the start string of a comment "<!--" has
+ already been parsed, the '-' character is valid only as
+ part of a comment termination and further more it must
+ be present in even numbers. Hence if strict is true, we
+ presume the comment has been terminated and return.
+ However if strict is false, then there is no even number
+ requirement and this character can appear anywhere in the
+ comment. The parser reads on until it sees the following
+ pattern: "-->" or "--!>".
+ **/
+ if (!strict && (strpos != 0) && (str[strpos - 1] == '-')) {
+ if ((ch = readCh()) == '>') {
+ return;
+ }
+ if (ch == '!') {
+ if ((ch = readCh()) == '>') {
+ return;
+ } else {
+ /* to account for extra read()'s that happened */
+ addString('-');
+ addString('!');
+ continue;
+ }
+ }
+ break;
+ }
+
+ if ((ch = readCh()) == '-') {
+ ch = readCh();
+ if (strict || ch == '>') {
+ return;
+ }
+ if (ch == '!') {
+ if ((ch = readCh()) == '>') {
+ return;
+ } else {
+ /* to account for extra read()'s that happened */
+ addString('-');
+ addString('!');
+ continue;
+ }
+ }
+ /* to account for the extra read() */
+ addString('-');
+ }
+ break;
+
+ case -1:
+ handleEOFInComment();
+ return;
+
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ break;
+
+ case '>':
+ ch = readCh();
+ break;
+
+ case '\r':
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ c = '\n';
+ break;
+ default:
+ ch = readCh();
+ break;
+ }
+
+ addString(c);
+ }
+ }
+
+ /**
+ * Parse literal content. [46] 343:1 and [47] 344:1
+ */
+ void parseLiteral(boolean replace) throws IOException {
+ while (true) {
+ int c = ch;
+ switch (c) {
+ case -1:
+ error("eof.literal", stack.elem.getName());
+ endTag(true);
+ return;
+
+ case '>':
+ ch = readCh();
+ int i = textpos - (stack.elem.name.length() + 2), j = 0;
+
+ // match end tag
+ if ((i >= 0) && (text[i++] == '<') && (text[i] == '/')) {
+ while ((++i < textpos) &&
+ (Character.toLowerCase(text[i]) == stack.elem.name.charAt(j++)));
+ if (i == textpos) {
+ textpos -= (stack.elem.name.length() + 2);
+ if ((textpos > 0) && (text[textpos-1] == '\n')) {
+ textpos--;
+ }
+ endTag(false);
+ return;
+ }
+ }
+ break;
+
+ case '&':
+ char data[] = parseEntityReference();
+ if (textpos + data.length > text.length) {
+ char newtext[] = new char[Math.max(textpos + data.length + 128, text.length * 2)];
+ System.arraycopy(text, 0, newtext, 0, text.length);
+ text = newtext;
+ }
+ System.arraycopy(data, 0, text, textpos, data.length);
+ textpos += data.length;
+ continue;
+
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ break;
+
+ case '\r':
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ c = '\n';
+ break;
+ default:
+ ch = readCh();
+ break;
+ }
+
+ // output character
+ if (textpos == text.length) {
+ char newtext[] = new char[text.length + 128];
+ System.arraycopy(text, 0, newtext, 0, text.length);
+ text = newtext;
+ }
+ text[textpos++] = (char)c;
+ }
+ }
+
+ /**
+ * Parse attribute value. [33] 331:1
+ */
+ String parseAttributeValue(boolean lower) throws IOException {
+ int delim = -1;
+
+ // Check for a delimiter
+ switch(ch) {
+ case '\'':
+ case '"':
+ delim = ch;
+ ch = readCh();
+ break;
+ }
+
+ // Parse the rest of the value
+ while (true) {
+ int c = ch;
+
+ switch (c) {
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ if (delim < 0) {
+ return getString(0);
+ }
+ break;
+
+ case '\r':
+ ln++;
+
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ if (delim < 0) {
+ return getString(0);
+ }
+ break;
+
+ case '\t':
+ if (delim < 0)
+ c = ' ';
+ case ' ':
+ ch = readCh();
+ if (delim < 0) {
+ return getString(0);
+ }
+ break;
+
+ case '>':
+ case '<':
+ if (delim < 0) {
+ return getString(0);
+ }
+ ch = readCh();
+ break;
+
+ case '\'':
+ case '"':
+ ch = readCh();
+ if (c == delim) {
+ return getString(0);
+ } else if (delim == -1) {
+ error("attvalerr");
+ if (strict || ch == ' ') {
+ return getString(0);
+ } else {
+ continue;
+ }
+ }
+ break;
+
+ case '=':
+ if (delim < 0) {
+ /* In SGML a construct like <img src=/cgi-bin/foo?x=1>
+ is considered invalid since an = sign can only be contained
+ in an attributes value if the string is quoted.
+ */
+ error("attvalerr");
+ /* If strict is true then we return with the string we have thus far.
+ Otherwise we accept the = sign as part of the attribute's value and
+ process the rest of the img tag. */
+ if (strict) {
+ return getString(0);
+ }
+ }
+ ch = readCh();
+ break;
+
+ case '&':
+ if (strict && delim < 0) {
+ ch = readCh();
+ break;
+ }
+
+ char data[] = parseEntityReference();
+ for (int i = 0 ; i < data.length ; i++) {
+ c = data[i];
+ addString((lower && (c >= 'A') && (c <= 'Z')) ? 'a' + c - 'A' : c);
+ }
+ continue;
+
+ case -1:
+ return getString(0);
+
+ default:
+ if (lower && (c >= 'A') && (c <= 'Z')) {
+ c = 'a' + c - 'A';
+ }
+ ch = readCh();
+ break;
+ }
+ addString(c);
+ }
+ }
+
+
+ /**
+ * Parse attribute specification List. [31] 327:17
+ */
+ void parseAttributeSpecificationList(Element elem) throws IOException {
+
+ while (true) {
+ skipSpace();
+
+ switch (ch) {
+ case '/':
+ case '>':
+ case '<':
+ case -1:
+ return;
+
+ case '-':
+ if ((ch = readCh()) == '-') {
+ ch = readCh();
+ parseComment();
+ strpos = 0;
+ } else {
+ error("invalid.tagchar", "-", elem.getName());
+ ch = readCh();
+ }
+ continue;
+ }
+
+ AttributeList att = null;
+ String attname = null;
+ String attvalue = null;
+
+ if (parseIdentifier(true)) {
+ attname = getString(0);
+ skipSpace();
+ if (ch == '=') {
+ ch = readCh();
+ skipSpace();
+ att = elem.getAttribute(attname);
+// Bug ID 4102750
+// Load the NAME of an Attribute Case Sensitive
+// The case of the NAME must be intact
+// MG 021898
+ attvalue = parseAttributeValue((att != null) && (att.type != CDATA) && (att.type != NOTATION) && (att.type != NAME));
+// attvalue = parseAttributeValue((att != null) && (att.type != CDATA) && (att.type != NOTATION));
+ } else {
+ attvalue = attname;
+ att = elem.getAttributeByValue(attvalue);
+ if (att == null) {
+ att = elem.getAttribute(attname);
+ if (att != null) {
+ attvalue = att.getValue();
+ }
+ else {
+ // Make it null so that NULL_ATTRIBUTE_VALUE is
+ // used
+ attvalue = null;
+ }
+ }
+ }
+ } else if (!strict && ch == ',') { // allows for comma separated attribute-value pairs
+ ch = readCh();
+ continue;
+ } else if (!strict && ch == '"') { // allows for quoted attributes
+ ch = readCh();
+ skipSpace();
+ if (parseIdentifier(true)) {
+ attname = getString(0);
+ if (ch == '"') {
+ ch = readCh();
+ }
+ skipSpace();
+ if (ch == '=') {
+ ch = readCh();
+ skipSpace();
+ att = elem.getAttribute(attname);
+ attvalue = parseAttributeValue((att != null) &&
+ (att.type != CDATA) &&
+ (att.type != NOTATION));
+ } else {
+ attvalue = attname;
+ att = elem.getAttributeByValue(attvalue);
+ if (att == null) {
+ att = elem.getAttribute(attname);
+ if (att != null) {
+ attvalue = att.getValue();
+ }
+ }
+ }
+ } else {
+ char str[] = {(char)ch};
+ error("invalid.tagchar", new String(str), elem.getName());
+ ch = readCh();
+ continue;
+ }
+ } else if (!strict && (attributes.isEmpty()) && (ch == '=')) {
+ ch = readCh();
+ skipSpace();
+ attname = elem.getName();
+ att = elem.getAttribute(attname);
+ attvalue = parseAttributeValue((att != null) &&
+ (att.type != CDATA) &&
+ (att.type != NOTATION));
+ } else if (!strict && (ch == '=')) {
+ ch = readCh();
+ skipSpace();
+ attvalue = parseAttributeValue(true);
+ error("attvalerr");
+ return;
+ } else {
+ char str[] = {(char)ch};
+ error("invalid.tagchar", new String(str), elem.getName());
+ if (!strict) {
+ ch = readCh();
+ continue;
+ } else {
+ return;
+ }
+ }
+
+ if (att != null) {
+ attname = att.getName();
+ } else {
+ error("invalid.tagatt", attname, elem.getName());
+ }
+
+ // Check out the value
+ if (attributes.isDefined(attname)) {
+ error("multi.tagatt", attname, elem.getName());
+ }
+ if (attvalue == null) {
+ attvalue = ((att != null) && (att.value != null)) ? att.value :
+ HTML.NULL_ATTRIBUTE_VALUE;
+ } else if ((att != null) && (att.values != null) && !att.values.contains(attvalue)) {
+ error("invalid.tagattval", attname, elem.getName());
+ }
+ HTML.Attribute attkey = HTML.getAttributeKey(attname);
+ if (attkey == null) {
+ attributes.addAttribute(attname, attvalue);
+ } else {
+ attributes.addAttribute(attkey, attvalue);
+ }
+ }
+ }
+
+ /**
+ * Parses th Document Declaration Type markup declaration.
+ * Currently ignores it.
+ */
+ public String parseDTDMarkup() throws IOException {
+
+ StringBuffer strBuff = new StringBuffer();
+ ch = readCh();
+ while(true) {
+ switch (ch) {
+ case '>':
+ ch = readCh();
+ return strBuff.toString();
+ case -1:
+ error("invalid.markup");
+ return strBuff.toString();
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ break;
+ case '"':
+ ch = readCh();
+ break;
+ case '\r':
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ break;
+ default:
+ strBuff.append((char)(ch & 0xFF));
+ ch = readCh();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parse markup declarations.
+ * Currently only handles the Document Type Declaration markup.
+ * Returns true if it is a markup declaration false otherwise.
+ */
+ protected boolean parseMarkupDeclarations(StringBuffer strBuff) throws IOException {
+
+ /* Currently handles only the DOCTYPE */
+ if ((strBuff.length() == "DOCTYPE".length()) &&
+ (strBuff.toString().toUpperCase().equals("DOCTYPE"))) {
+ parseDTDMarkup();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Parse an invalid tag.
+ */
+ void parseInvalidTag() throws IOException {
+ // ignore all data upto the close bracket '>'
+ while (true) {
+ skipSpace();
+ switch (ch) {
+ case '>':
+ case -1:
+ ch = readCh();
+ return;
+ case '<':
+ return;
+ default:
+ ch = readCh();
+
+ }
+ }
+ }
+
+ /**
+ * Parse a start or end tag.
+ */
+ void parseTag() throws IOException {
+ Element elem = null;
+ boolean net = false;
+ boolean warned = false;
+ boolean unknown = false;
+
+ switch (ch = readCh()) {
+ case '!':
+ switch (ch = readCh()) {
+ case '-':
+ // Parse comment. [92] 391:7
+ while (true) {
+ if (ch == '-') {
+ if (!strict || ((ch = readCh()) == '-')) {
+ ch = readCh();
+ if (!strict && ch == '-') {
+ ch = readCh();
+ }
+ // send over any text you might see
+ // before parsing and sending the
+ // comment
+ if (textpos != 0) {
+ char newtext[] = new char[textpos];
+ System.arraycopy(text, 0, newtext, 0, textpos);
+ handleText(newtext);
+ lastBlockStartPos = currentBlockStartPos;
+ textpos = 0;
+ }
+ parseComment();
+ last = makeTag(dtd.getElement("comment"), true);
+ handleComment(getChars(0));
+ continue;
+ } else if (!warned) {
+ warned = true;
+ error("invalid.commentchar", "-");
+ }
+ }
+ skipSpace();
+ switch (ch) {
+ case '-':
+ continue;
+ case '>':
+ ch = readCh();
+ case -1:
+ return;
+ default:
+ ch = readCh();
+ if (!warned) {
+ warned = true;
+ error("invalid.commentchar",
+ String.valueOf((char)ch));
+ }
+ break;
+ }
+ }
+
+ default:
+ // deal with marked sections
+ StringBuffer strBuff = new StringBuffer();
+ while (true) {
+ strBuff.append((char)ch);
+ if (parseMarkupDeclarations(strBuff)) {
+ return;
+ }
+ switch(ch) {
+ case '>':
+ ch = readCh();
+ case -1:
+ error("invalid.markup");
+ return;
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ break;
+ case '\r':
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ break;
+
+ default:
+ ch = readCh();
+ break;
+ }
+ }
+ }
+
+ case '/':
+ // parse end tag [19] 317:4
+ switch (ch = readCh()) {
+ case '>':
+ ch = readCh();
+ case '<':
+ // empty end tag. either </> or </<
+ if (recent == null) {
+ error("invalid.shortend");
+ return;
+ }
+ elem = recent;
+ break;
+
+ default:
+ if (!parseIdentifier(true)) {
+ error("expected.endtagname");
+ return;
+ }
+ skipSpace();
+ switch (ch) {
+ case '>':
+ ch = readCh();
+ case '<':
+ break;
+
+ default:
+ error("expected", "'>'");
+ while ((ch != -1) && (ch != '\n') && (ch != '>')) {
+ ch = readCh();
+ }
+ if (ch == '>') {
+ ch = readCh();
+ }
+ break;
+ }
+ String elemStr = getString(0);
+ if (!dtd.elementExists(elemStr)) {
+ error("end.unrecognized", elemStr);
+ // Ignore RE before end tag
+ if ((textpos > 0) && (text[textpos-1] == '\n')) {
+ textpos--;
+ }
+ elem = dtd.getElement("unknown");
+ elem.name = elemStr;
+ unknown = true;
+ } else {
+ elem = dtd.getElement(elemStr);
+ }
+ break;
+ }
+
+
+ // If the stack is null, we're seeing end tags without any begin
+ // tags. Ignore them.
+
+ if (stack == null) {
+ error("end.extra.tag", elem.getName());
+ return;
+ }
+
+ // Ignore RE before end tag
+ if ((textpos > 0) && (text[textpos-1] == '\n')) {
+ // In a pre tag, if there are blank lines
+ // we do not want to remove the newline
+ // before the end tag. Hence this code.
+ //
+ if (stack.pre) {
+ if ((textpos > 1) && (text[textpos-2] != '\n')) {
+ textpos--;
+ }
+ } else {
+ textpos--;
+ }
+ }
+
+ // If the end tag is a form, since we did not put it
+ // on the tag stack, there is no corresponding start
+ // start tag to find. Hence do not touch the tag stack.
+ //
+
+ /*
+ if (!strict && elem.getName().equals("form")) {
+ if (lastFormSent != null) {
+ handleEndTag(lastFormSent);
+ return;
+ } else {
+ // do nothing.
+ return;
+ }
+ }
+ */
+
+ if (unknown) {
+ // we will not see a corresponding start tag
+ // on the the stack. If we are seeing an
+ // end tag, lets send this on as an empty
+ // tag with the end tag attribute set to
+ // true.
+ TagElement t = makeTag(elem);
+ handleText(t);
+ attributes.addAttribute(HTML.Attribute.ENDTAG, "true");
+ handleEmptyTag(makeTag(elem));
+ unknown = false;
+ return;
+ }
+
+ // find the corresponding start tag
+
+ // A commonly occuring error appears to be the insertion
+ // of extra end tags in a table. The intent here is ignore
+ // such extra end tags.
+ //
+ if (!strict) {
+ String stackElem = stack.elem.getName();
+
+ if (stackElem.equals("table")) {
+ // If it isnt a valid end tag ignore it and return
+ //
+ if (!elem.getName().equals(stackElem)) {
+ error("tag.ignore", elem.getName());
+ return;
+ }
+ }
+
+
+
+ if (stackElem.equals("tr") ||
+ stackElem.equals("td")) {
+ if ((!elem.getName().equals("table")) &&
+ (!elem.getName().equals(stackElem))) {
+ error("tag.ignore", elem.getName());
+ return;
+ }
+ }
+ }
+ TagStack sp = stack;
+
+ while ((sp != null) && (elem != sp.elem)) {
+ sp = sp.next;
+ }
+ if (sp == null) {
+ error("unmatched.endtag", elem.getName());
+ return;
+ }
+
+ // People put font ending tags in the darndest places.
+ // Don't close other contexts based on them being between
+ // a font tag and the corresponding end tag. Instead,
+ // ignore the end tag like it doesn't exist and allow the end
+ // of the document to close us out.
+ String elemName = elem.getName();
+ if (stack != sp &&
+ (elemName.equals("font") ||
+ elemName.equals("center"))) {
+
+ // Since closing out a center tag can have real wierd
+ // effects on the formatting, make sure that tags
+ // for which omitting an end tag is legimitate
+ // get closed out.
+ //
+ if (elemName.equals("center")) {
+ while(stack.elem.omitEnd() && stack != sp) {
+ endTag(true);
+ }
+ if (stack.elem == elem) {
+ endTag(false);
+ }
+ }
+ return;
+ }
+ // People do the same thing with center tags. In this
+ // case we would like to close off the center tag but
+ // not necessarily all enclosing tags.
+
+
+
+ // end tags
+ while (stack != sp) {
+ endTag(true);
+ }
+
+ endTag(false);
+ return;
+
+ case -1:
+ error("eof");
+ return;
+ }
+
+ // start tag [14] 314:1
+ if (!parseIdentifier(true)) {
+ elem = recent;
+ if ((ch != '>') || (elem == null)) {
+ error("expected.tagname");
+ return;
+ }
+ } else {
+ String elemStr = getString(0);
+
+ if (elemStr.equals("image")) {
+ elemStr = new String("img");
+ }
+
+ /* determine if this element is part of the dtd. */
+
+ if (!dtd.elementExists(elemStr)) {
+ // parseInvalidTag();
+ error("tag.unrecognized ", elemStr);
+ elem = dtd.getElement("unknown");
+ elem.name = elemStr;
+ unknown = true;
+ } else {
+ elem = dtd.getElement(elemStr);
+ }
+ }
+
+ // Parse attributes
+ parseAttributeSpecificationList(elem);
+
+ switch (ch) {
+ case '/':
+ net = true;
+ case '>':
+ ch = readCh();
+ if (ch == '>' && net) {
+ ch = readCh();
+ }
+ case '<':
+ break;
+
+ default:
+ error("expected", "'>'");
+ break;
+ }
+
+ if (!strict) {
+ if (elem.getName().equals("script")) {
+ error("javascript.unsupported");
+ }
+ }
+
+ // ignore RE after start tag
+ //
+ if (!elem.isEmpty()) {
+ if (ch == '\n') {
+ ln++;
+ lfCount++;
+ ch = readCh();
+ } else if (ch == '\r') {
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ }
+ }
+
+ // ensure a legal context for the tag
+ TagElement tag = makeTag(elem, false);
+
+
+ /** In dealing with forms, we have decided to treat
+ them as legal in any context. Also, even though
+ they do have a start and an end tag, we will
+ not put this tag on the stack. This is to deal
+ several pages in the web oasis that choose to
+ start and end forms in any possible location. **/
+
+ /*
+ if (!strict && elem.getName().equals("form")) {
+ if (lastFormSent == null) {
+ lastFormSent = tag;
+ } else {
+ handleEndTag(lastFormSent);
+ lastFormSent = tag;
+ }
+ } else {
+ */
+ // Smlly, if a tag is unknown, we will apply
+ // no legalTagContext logic to it.
+ //
+ if (!unknown) {
+ legalTagContext(tag);
+
+ // If skip tag is true, this implies that
+ // the tag was illegal and that the error
+ // recovery strategy adopted is to ignore
+ // the tag.
+ if (!strict && skipTag) {
+ skipTag = false;
+ return;
+ }
+ }
+ /*
+ }
+ */
+
+ startTag(tag);
+
+ if (!elem.isEmpty()) {
+ switch (elem.getType()) {
+ case CDATA:
+ parseLiteral(false);
+ break;
+ case RCDATA:
+ parseLiteral(true);
+ break;
+ default:
+ if (stack != null) {
+ stack.net = net;
+ }
+ break;
+ }
+ }
+ }
+
+ private static final String START_COMMENT = "<!--";
+ private static final String END_COMMENT = "-->";
+ private static final char[] SCRIPT_END_TAG = "</script>".toCharArray();
+ private static final char[] SCRIPT_END_TAG_UPPER_CASE =
+ "</SCRIPT>".toCharArray();
+
+ void parseScript() throws IOException {
+ char[] charsToAdd = new char[SCRIPT_END_TAG.length];
+
+ /* Here, ch should be the first character after <script> */
+ while (true) {
+ int i = 0;
+ while (i < SCRIPT_END_TAG.length
+ && (SCRIPT_END_TAG[i] == ch
+ || SCRIPT_END_TAG_UPPER_CASE[i] == ch)) {
+ charsToAdd[i] = (char) ch;
+ ch = readCh();
+ i++;
+ }
+ if (i == SCRIPT_END_TAG.length) {
+
+ /* '</script>' tag detected */
+ /* Here, ch == '>' */
+ ch = readCh();
+ /* Here, ch == the first character after </script> */
+ return;
+ } else {
+
+ /* To account for extra read()'s that happened */
+ for (int j = 0; j < i; j++) {
+ addString(charsToAdd[j]);
+ }
+
+ switch (ch) {
+ case -1:
+ error("eof.script");
+ return;
+ case '\n':
+ ln++;
+ ch = readCh();
+ lfCount++;
+ addString('\n');
+ break;
+ case '\r':
+ ln++;
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ } else {
+ crCount++;
+ }
+ addString('\n');
+ break;
+ default:
+ addString(ch);
+ ch = readCh();
+ break;
+ } // switch
+ }
+ } // while
+ }
+
+ /**
+ * Parse Content. [24] 320:1
+ */
+ void parseContent() throws IOException {
+ Thread curThread = Thread.currentThread();
+
+ for (;;) {
+ if (curThread.isInterrupted()) {
+ curThread.interrupt(); // resignal the interrupt
+ break;
+ }
+
+ int c = ch;
+ currentBlockStartPos = currentPosition;
+
+ if (recent == dtd.script) { // means: if after starting <script> tag
+
+ /* Here, ch has to be the first character after <script> */
+ parseScript();
+ last = makeTag(dtd.getElement("comment"), true);
+
+ /* Remove leading and trailing HTML comment declarations */
+ String str = new String(getChars(0)).trim();
+ int minLength = START_COMMENT.length() + END_COMMENT.length();
+ if (str.startsWith(START_COMMENT) && str.endsWith(END_COMMENT)
+ && str.length() >= (minLength)) {
+ str = str.substring(START_COMMENT.length(),
+ str.length() - END_COMMENT.length());
+ }
+
+ /* Handle resulting chars as comment */
+ handleComment(str.toCharArray());
+ endTag(false);
+ lastBlockStartPos = currentPosition;
+ } else {
+ switch (c) {
+ case '<':
+ parseTag();
+ lastBlockStartPos = currentPosition;
+ continue;
+
+ case '/':
+ ch = readCh();
+ if ((stack != null) && stack.net) {
+ // null end tag.
+ endTag(false);
+ continue;
+ }
+ break;
+
+ case -1:
+ return;
+
+ case '&':
+ if (textpos == 0) {
+ if (!legalElementContext(dtd.pcdata)) {
+ error("unexpected.pcdata");
+ }
+ if (last.breaksFlow()) {
+ space = false;
+ }
+ }
+ char data[] = parseEntityReference();
+ if (textpos + data.length + 1 > text.length) {
+ char newtext[] = new char[Math.max(textpos + data.length + 128, text.length * 2)];
+ System.arraycopy(text, 0, newtext, 0, text.length);
+ text = newtext;
+ }
+ if (space) {
+ space = false;
+ text[textpos++] = ' ';
+ }
+ System.arraycopy(data, 0, text, textpos, data.length);
+ textpos += data.length;
+ ignoreSpace = false;
+ continue;
+
+ case '\n':
+ ln++;
+ lfCount++;
+ ch = readCh();
+ if ((stack != null) && stack.pre) {
+ break;
+ }
+ if (textpos == 0) {
+ lastBlockStartPos = currentPosition;
+ }
+ if (!ignoreSpace) {
+ space = true;
+ }
+ continue;
+
+ case '\r':
+ ln++;
+ c = '\n';
+ if ((ch = readCh()) == '\n') {
+ ch = readCh();
+ crlfCount++;
+ }
+ else {
+ crCount++;
+ }
+ if ((stack != null) && stack.pre) {
+ break;
+ }
+ if (textpos == 0) {
+ lastBlockStartPos = currentPosition;
+ }
+ if (!ignoreSpace) {
+ space = true;
+ }
+ continue;
+
+
+ case '\t':
+ case ' ':
+ ch = readCh();
+ if ((stack != null) && stack.pre) {
+ break;
+ }
+ if (textpos == 0) {
+ lastBlockStartPos = currentPosition;
+ }
+ if (!ignoreSpace) {
+ space = true;
+ }
+ continue;
+
+ default:
+ if (textpos == 0) {
+ if (!legalElementContext(dtd.pcdata)) {
+ error("unexpected.pcdata");
+ }
+ if (last.breaksFlow()) {
+ space = false;
+ }
+ }
+ ch = readCh();
+ break;
+ }
+ }
+
+ // enlarge buffer if needed
+ if (textpos + 2 > text.length) {
+ char newtext[] = new char[text.length + 128];
+ System.arraycopy(text, 0, newtext, 0, text.length);
+ text = newtext;
+ }
+
+ // output pending space
+ if (space) {
+ if (textpos == 0) {
+ lastBlockStartPos--;
+ }
+ text[textpos++] = ' ';
+ space = false;
+ }
+ text[textpos++] = (char)c;
+ ignoreSpace = false;
+ }
+ }
+
+ /**
+ * Returns the end of line string. This will return the end of line
+ * string that has been encountered the most, one of \r, \n or \r\n.
+ */
+ String getEndOfLineString() {
+ if (crlfCount >= crCount) {
+ if (lfCount >= crlfCount) {
+ return "\n";
+ }
+ else {
+ return "\r\n";
+ }
+ }
+ else {
+ if (crCount > lfCount) {
+ return "\r";
+ }
+ else {
+ return "\n";
+ }
+ }
+ }
+
+ /**
+ * Parse an HTML stream, given a DTD.
+ */
+ public synchronized void parse(Reader in) throws IOException {
+ this.in = in;
+
+ this.ln = 1;
+
+ seenHtml = false;
+ seenHead = false;
+ seenBody = false;
+
+ crCount = lfCount = crlfCount = 0;
+
+ try {
+ ch = readCh();
+ text = new char[1024];
+ str = new char[128];
+
+ parseContent();
+ // NOTE: interruption may have occurred. Control flows out
+ // of here normally.
+ while (stack != null) {
+ endTag(true);
+ }
+ in.close();
+ } catch (IOException e) {
+ errorContext();
+ error("ioexception");
+ throw e;
+ } catch (Exception e) {
+ errorContext();
+ error("exception", e.getClass().getName(), e.getMessage());
+ e.printStackTrace();
+ } catch (ThreadDeath e) {
+ errorContext();
+ error("terminated");
+ e.printStackTrace();
+ throw e;
+ } finally {
+ for (; stack != null ; stack = stack.next) {
+ handleEndTag(stack.tag);
+ }
+
+ text = null;
+ str = null;
+ }
+
+ }
+
+
+ /*
+ * Input cache. This is much faster than calling down to a synchronized
+ * method of BufferedReader for each byte. Measurements done 5/30/97
+ * show that there's no point in having a bigger buffer: Increasing
+ * the buffer to 8192 had no measurable impact for a program discarding
+ * one character at a time (reading from an http URL to a local machine).
+ * NOTE: If the current encoding is bogus, and we read too much
+ * (past the content-type) we may suffer a MalformedInputException. For
+ * this reason the initial size is 1 and when the body is encountered the
+ * size is adjusted to 256.
+ */
+ private char buf[] = new char[1];
+ private int pos;
+ private int len;
+ /*
+ tracks position relative to the beginning of the
+ document.
+ */
+ private int currentPosition;
+
+
+ private final int readCh() throws IOException {
+
+ if (pos >= len) {
+
+ // This loop allows us to ignore interrupts if the flag
+ // says so
+ for (;;) {
+ try {
+ len = in.read(buf);
+ break;
+ } catch (InterruptedIOException ex) {
+ throw ex;
+ }
+ }
+
+ if (len <= 0) {
+ return -1; // eof
+ }
+ pos = 0;
+ }
+ ++currentPosition;
+
+ return buf[pos++];
+ }
+
+
+ protected int getCurrentPos() {
+ return currentPosition;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/html/parser/ParserDelegator.java b/src/share/classes/javax/swing/text/html/parser/ParserDelegator.java
new file mode 100644
index 000000000..df48ca2ce
--- /dev/null
+++ b/src/share/classes/javax/swing/text/html/parser/ParserDelegator.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 1998-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.parser;
+
+import javax.swing.text.html.HTMLEditorKit;
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.DataInputStream;
+import java.io.ObjectInputStream;
+import java.io.Reader;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+/**
+ * Responsible for starting up a new DocumentParser
+ * each time its parse method is invoked. Stores a
+ * reference to the dtd.
+ *
+ * @author Sunita Mani
+ */
+
+public class ParserDelegator extends HTMLEditorKit.Parser implements Serializable {
+
+ private static DTD dtd = null;
+
+ protected static synchronized void setDefaultDTD() {
+ if (dtd == null) {
+ DTD _dtd = null;
+ // (PENDING) Hate having to hard code!
+ String nm = "html32";
+ try {
+ _dtd = DTD.getDTD(nm);
+ } catch (IOException e) {
+ // (PENDING) UGLY!
+ System.out.println("Throw an exception: could not get default dtd: " + nm);
+ }
+ dtd = createDTD(_dtd, nm);
+ }
+ }
+
+ protected static DTD createDTD(DTD dtd, String name) {
+
+ InputStream in = null;
+ boolean debug = true;
+ try {
+ String path = name + ".bdtd";
+ in = getResourceAsStream(path);
+ if (in != null) {
+ dtd.read(new DataInputStream(new BufferedInputStream(in)));
+ dtd.putDTDHash(name, dtd);
+ }
+ } catch (Exception e) {
+ System.out.println(e);
+ }
+ return dtd;
+ }
+
+
+ public ParserDelegator() {
+ if (dtd == null) {
+ setDefaultDTD();
+ }
+ }
+
+ public void parse(Reader r, HTMLEditorKit.ParserCallback cb, boolean ignoreCharSet) throws IOException {
+ new DocumentParser(dtd).parse(r, cb, ignoreCharSet);
+ }
+
+ /**
+ * Fetch a resource relative to the ParserDelegator classfile.
+ * If this is called on 1.2 the loading will occur under the
+ * protection of a doPrivileged call to allow the ParserDelegator
+ * to function when used in an applet.
+ *
+ * @param name the name of the resource, relative to the
+ * ParserDelegator class.
+ * @returns 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 ParserDelegator.class.getResourceAsStream(name);
+ }
+ }
+
+ private void readObject(ObjectInputStream s)
+ throws ClassNotFoundException, IOException {
+ s.defaultReadObject();
+ if (dtd == null) {
+ setDefaultDTD();
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/html/parser/ResourceLoader.java b/src/share/classes/javax/swing/text/html/parser/ResourceLoader.java
new file mode 100644
index 000000000..02e4fe31c
--- /dev/null
+++ b/src/share/classes/javax/swing/text/html/parser/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.parser;
+
+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 ParserDelegator.getResourceAsStream.
+ *
+ * @author Timothy Prinzing
+ */
+class ResourceLoader implements java.security.PrivilegedAction {
+
+ ResourceLoader(String name) {
+ this.name = name;
+ }
+
+ public Object run() {
+ Object o = ParserDelegator.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/parser/TagElement.java b/src/share/classes/javax/swing/text/html/parser/TagElement.java
new file mode 100644
index 000000000..0545010e1
--- /dev/null
+++ b/src/share/classes/javax/swing/text/html/parser/TagElement.java
@@ -0,0 +1,74 @@
+/*
+ * 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 javax.swing.text.html.HTML;
+/**
+ * A generic HTML TagElement class. The methods define how white
+ * space is interpreted around the tag.
+ *
+ * @author Sunita Mani
+ */
+
+public class TagElement {
+
+ Element elem;
+ HTML.Tag htmlTag;
+ boolean insertedByErrorRecovery;
+
+ public TagElement ( Element elem ) {
+ this(elem, false);
+ }
+
+ public TagElement (Element elem, boolean fictional) {
+ this.elem = elem;
+ htmlTag = HTML.getTag(elem.getName());
+ if (htmlTag == null) {
+ htmlTag = new HTML.UnknownTag(elem.getName());
+ }
+ insertedByErrorRecovery = fictional;
+ }
+
+ public boolean breaksFlow() {
+ return htmlTag.breaksFlow();
+ }
+
+ public boolean isPreformatted() {
+ return htmlTag.isPreformatted();
+ }
+
+ public Element getElement() {
+ return elem;
+ }
+
+ public HTML.Tag getHTMLTag() {
+ return htmlTag;
+ }
+
+ public boolean fictional() {
+ return insertedByErrorRecovery;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/html/parser/TagStack.java b/src/share/classes/javax/swing/text/html/parser/TagStack.java
new file mode 100644
index 000000000..5fcb4d388
--- /dev/null
+++ b/src/share/classes/javax/swing/text/html/parser/TagStack.java
@@ -0,0 +1,220 @@
+/*
+ * 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.BitSet;
+import java.util.Vector;
+import java.io.*;
+
+
+/**
+ * A stack of tags. Used while parsing an HTML document.
+ * It, together with the ContentModelStates, defines the
+ * complete state of the parser while reading a document.
+ * When a start tag is encountered an element is pushed onto
+ * the stack, when an end tag is enountered an element is popped
+ * of the stack.
+ *
+ * @see Parser
+ * @see DTD
+ * @see ContentModelState
+ * @author Arthur van Hoff
+ */
+final
+class TagStack implements DTDConstants {
+ TagElement tag;
+ Element elem;
+ ContentModelState state;
+ TagStack next;
+ BitSet inclusions;
+ BitSet exclusions;
+ boolean net;
+ boolean pre;
+
+ /**
+ * Construct a stack element.
+ */
+ TagStack(TagElement tag, TagStack next) {
+ this.tag = tag;
+ this.elem = tag.getElement();
+ this.next = next;
+
+ Element elem = tag.getElement();
+ if (elem.getContent() != null) {
+ this.state = new ContentModelState(elem.getContent());
+ }
+
+ if (next != null) {
+ inclusions = next.inclusions;
+ exclusions = next.exclusions;
+ pre = next.pre;
+ }
+ if (tag.isPreformatted()) {
+ pre = true;
+ }
+
+ if (elem.inclusions != null) {
+ if (inclusions != null) {
+ inclusions = (BitSet)inclusions.clone();
+ inclusions.or(elem.inclusions);
+ } else {
+ inclusions = elem.inclusions;
+ }
+ }
+ if (elem.exclusions != null) {
+ if (exclusions != null) {
+ exclusions = (BitSet)exclusions.clone();
+ exclusions.or(elem.exclusions);
+ } else {
+ exclusions = elem.exclusions;
+ }
+ }
+ }
+
+ /**
+ * Return the element that must come next in the
+ * input stream.
+ */
+ public Element first() {
+ return (state != null) ? state.first() : null;
+ }
+
+ /**
+ * Return the ContentModel that must be satisfied by
+ * what comes next in the input stream.
+ */
+ public ContentModel contentModel() {
+ if (state == null) {
+ return null;
+ } else {
+ return state.getModel();
+ }
+ }
+
+ /**
+ * Return true if the element that is contained at
+ * the index specified by the parameter is part of
+ * the exclusions specified in the DTD for the element
+ * currently on the TagStack.
+ */
+ boolean excluded(int elemIndex) {
+ return (exclusions != null) && exclusions.get(elem.getIndex());
+ }
+
+ /**
+ * Update the Vector elemVec with all the elements that
+ * are part of the inclusions listed in DTD for the element
+ * currently on the TagStack.
+ */
+ boolean included(Vector elemVec, DTD dtd) {
+
+ for (int i = 0 ; i < inclusions.size(); i++) {
+ if (inclusions.get(i)) {
+ elemVec.addElement(dtd.getElement(i));
+ System.out.println("Element add thru' inclusions: " + dtd.getElement(i).getName());
+ }
+ }
+ return (!elemVec.isEmpty());
+ }
+
+
+ /**
+ * Advance the state by reducing the given element.
+ * Returns false if the element is not legal and the
+ * state is not advanced.
+ */
+ boolean advance(Element elem) {
+ if ((exclusions != null) && exclusions.get(elem.getIndex())) {
+ return false;
+ }
+ if (state != null) {
+ ContentModelState newState = state.advance(elem);
+ if (newState != null) {
+ state = newState;
+ return true;
+ }
+ } else if (this.elem.getType() == ANY) {
+ return true;
+ }
+ return (inclusions != null) && inclusions.get(elem.getIndex());
+ }
+
+ /**
+ * Return true if the current state can be terminated.
+ */
+ boolean terminate() {
+ return (state == null) || state.terminate();
+ }
+
+ /**
+ * Convert to a string.
+ */
+ public String toString() {
+ return (next == null) ?
+ "<" + tag.getElement().getName() + ">" :
+ next + " <" + tag.getElement().getName() + ">";
+ }
+}
+
+class NPrintWriter extends PrintWriter {
+
+ private int numLines = 5;
+ private int numPrinted = 0;
+
+ public NPrintWriter (int numberOfLines) {
+ super(System.out);
+ numLines = numberOfLines;
+ }
+
+ public void println(char[] array) {
+ if (numPrinted >= numLines) {
+ return;
+ }
+
+ char[] partialArray = null;
+
+ for (int i = 0; i < array.length; i++) {
+ if (array[i] == '\n') {
+ numPrinted++;
+ }
+
+ if (numPrinted == numLines) {
+ System.arraycopy(array, 0, partialArray, 0, i);
+ }
+ }
+
+ if (partialArray != null) {
+ super.print(partialArray);
+ }
+
+ if (numPrinted == numLines) {
+ return;
+ }
+
+ super.println(array);
+ numPrinted++;
+ }
+}
diff --git a/src/share/classes/javax/swing/text/html/parser/html32.bdtd b/src/share/classes/javax/swing/text/html/parser/html32.bdtd
new file mode 100644
index 000000000..a48d51ce8
--- /dev/null
+++ b/src/share/classes/javax/swing/text/html/parser/html32.bdtd
Binary files differ
diff --git a/src/share/classes/javax/swing/text/html/parser/package.html b/src/share/classes/javax/swing/text/html/parser/package.html
new file mode 100644
index 000000000..0121ae6b2
--- /dev/null
+++ b/src/share/classes/javax/swing/text/html/parser/package.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+<!--
+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.
+-->
+
+</head>
+<body bgcolor="white">
+
+Provides the default HTML parser, along with support classes.
+As the stream is parsed,
+the parser notifies a delegate,
+which must implement
+the <code>HTMLEditorKit.ParserCallback</code> interface.
+
+<p>
+<strong>Note:</strong>
+Most of the Swing API is <em>not</em> thread safe.
+For details, see
+<a
+href="http://java.sun.com/docs/books/tutorial/uiswing/overview/threads.html"
+target="_top">Threads and Swing</a>,
+a section in
+<em><a href="http://java.sun.com/docs/books/tutorial/"
+target="_top">The Java Tutorial</a></em>.
+
+@see javax.swing.text.html.HTMLEditorKit.ParserCallback
+@since 1.2
+@serial exclude
+
+</body>
+</html>
diff --git a/src/share/classes/javax/swing/text/package.html b/src/share/classes/javax/swing/text/package.html
new file mode 100644
index 000000000..a254853a6
--- /dev/null
+++ b/src/share/classes/javax/swing/text/package.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+<!--
+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.
+-->
+
+</head>
+<body bgcolor="white">
+
+Provides classes and interfaces that deal with editable
+and noneditable text components. Examples of text components are text
+fields and text areas, of which password fields and document editors
+are special instantiations. Features that are supported by this
+package include selection/highlighting, editing, style,
+and key mapping.
+
+<p>
+<strong>Note:</strong>
+Most of the Swing API is <em>not</em> thread safe.
+For details, see
+<a
+href="http://java.sun.com/docs/books/tutorial/uiswing/overview/threads.html"
+target="_top">Threads and Swing</a>,
+a section in
+<em><a href="http://java.sun.com/docs/books/tutorial/"
+target="_top">The Java Tutorial</a></em>.
+
+<h2>Related Documentation</h2>
+
+For overviews, tutorials, examples, guides, and tool documentation, please see:
+<ul>
+ <li><a href="http://java.sun.com/docs/books/tutorial/uiswing/components/text.html" target="_top">Using Text Components</a>,
+ a section in <em>The Java Tutorial</em>
+</ul>
+
+@since 1.2
+@serial exclude
+
+</body>
+</html>
diff --git a/src/share/classes/javax/swing/text/rtf/AbstractFilter.java b/src/share/classes/javax/swing/text/rtf/AbstractFilter.java
new file mode 100644
index 000000000..d1233ecd1
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/AbstractFilter.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 1997-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.rtf;
+
+import java.io.*;
+import java.lang.*;
+
+/**
+ * A generic superclass for streams which read and parse text
+ * consisting of runs of characters interspersed with occasional
+ * ``specials'' (formatting characters).
+ *
+ * <p> Most of the functionality
+ * of this class would be redundant except that the
+ * <code>ByteToChar</code> converters
+ * are suddenly private API. Presumably this class will disappear
+ * when the API is made public again. (sigh) That will also let us handle
+ * multibyte character sets...
+ *
+ * <P> A subclass should override at least <code>write(char)</code>
+ * and <code>writeSpecial(int)</code>. For efficiency's sake it's a
+ * good idea to override <code>write(String)</code> as well. The subclass'
+ * initializer may also install appropriate translation and specials tables.
+ *
+ * @see OutputStream
+ */
+abstract class AbstractFilter extends OutputStream
+{
+ /** A table mapping bytes to characters */
+ protected char translationTable[];
+ /** A table indicating which byte values should be interpreted as
+ * characters and which should be treated as formatting codes */
+ protected boolean specialsTable[];
+
+ /** A translation table which does ISO Latin-1 (trivial) */
+ static final char latin1TranslationTable[];
+ /** A specials table which indicates that no characters are special */
+ static final boolean noSpecialsTable[];
+ /** A specials table which indicates that all characters are special */
+ static final boolean allSpecialsTable[];
+
+ static {
+ int i;
+
+ noSpecialsTable = new boolean[256];
+ for (i = 0; i < 256; i++)
+ noSpecialsTable[i] = false;
+
+ allSpecialsTable = new boolean[256];
+ for (i = 0; i < 256; i++)
+ allSpecialsTable[i] = true;
+
+ latin1TranslationTable = new char[256];
+ for (i = 0; i < 256; i++)
+ latin1TranslationTable[i] = (char)i;
+ }
+
+ /**
+ * A convenience method that reads text from a FileInputStream
+ * and writes it to the receiver.
+ * The format in which the file
+ * is read is determined by the concrete subclass of
+ * AbstractFilter to which this method is sent.
+ * <p>This method does not close the receiver after reaching EOF on
+ * the input stream.
+ * The user must call <code>close()</code> to ensure that all
+ * data are processed.
+ *
+ * @param in An InputStream providing text.
+ */
+ public void readFromStream(InputStream in)
+ throws IOException
+ {
+ byte buf[];
+ int count;
+
+ buf = new byte[16384];
+
+ while(true) {
+ count = in.read(buf);
+ if (count < 0)
+ break;
+
+ this.write(buf, 0, count);
+ }
+ }
+
+ public void readFromReader(Reader in)
+ throws IOException
+ {
+ char buf[];
+ int count;
+
+ buf = new char[2048];
+
+ while(true) {
+ count = in.read(buf);
+ if (count < 0)
+ break;
+ for (int i = 0; i < count; i++) {
+ this.write(buf[i]);
+ }
+ }
+ }
+
+ public AbstractFilter()
+ {
+ translationTable = latin1TranslationTable;
+ specialsTable = noSpecialsTable;
+ }
+
+ /**
+ * Implements the abstract method of OutputStream, of which this class
+ * is a subclass.
+ */
+ public void write(int b)
+ throws IOException
+ {
+ if (b < 0)
+ b += 256;
+ if (specialsTable[b])
+ writeSpecial(b);
+ else {
+ char ch = translationTable[b];
+ if (ch != (char)0)
+ write(ch);
+ }
+ }
+
+ /**
+ * Implements the buffer-at-a-time write method for greater
+ * efficiency.
+ *
+ * <p> <strong>PENDING:</strong> Does <code>write(byte[])</code>
+ * call <code>write(byte[], int, int)</code> or is it the other way
+ * around?
+ */
+ public void write(byte[] buf, int off, int len)
+ throws IOException
+ {
+ StringBuffer accumulator = null;
+ while (len > 0) {
+ short b = (short)buf[off];
+
+ // stupid signed bytes
+ if (b < 0)
+ b += 256;
+
+ if (specialsTable[b]) {
+ if (accumulator != null) {
+ write(accumulator.toString());
+ accumulator = null;
+ }
+ writeSpecial(b);
+ } else {
+ char ch = translationTable[b];
+ if (ch != (char)0) {
+ if (accumulator == null)
+ accumulator = new StringBuffer();
+ accumulator.append(ch);
+ }
+ }
+
+ len --;
+ off ++;
+ }
+
+ if (accumulator != null)
+ write(accumulator.toString());
+ }
+
+ /**
+ * Hopefully, all subclasses will override this method to accept strings
+ * of text, but if they don't, AbstractFilter's implementation
+ * will spoon-feed them via <code>write(char)</code>.
+ *
+ * @param s The string of non-special characters written to the
+ * OutputStream.
+ */
+ public void write(String s)
+ throws IOException
+ {
+ int index, length;
+
+ length = s.length();
+ for(index = 0; index < length; index ++) {
+ write(s.charAt(index));
+ }
+ }
+
+ /**
+ * Subclasses must provide an implementation of this method which
+ * accepts a single (non-special) character.
+ *
+ * @param ch The character written to the OutputStream.
+ */
+ protected abstract void write(char ch) throws IOException;
+
+ /**
+ * Subclasses must provide an implementation of this method which
+ * accepts a single special byte. No translation is performed
+ * on specials.
+ *
+ * @param b The byte written to the OutputStream.
+ */
+ protected abstract void writeSpecial(int b) throws IOException;
+}
diff --git a/src/share/classes/javax/swing/text/rtf/Constants.java b/src/share/classes/javax/swing/text/rtf/Constants.java
new file mode 100644
index 000000000..b014cbac7
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/Constants.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 1997-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.rtf;
+
+/**
+ Class to hold dictionary keys used by the RTF reader/writer.
+ These should be moved into StyleConstants.
+*/
+class Constants
+{
+ /** An array of TabStops */
+ static final String Tabs = "tabs";
+
+ /** The name of the character set the original RTF file was in */
+ static final String RTFCharacterSet = "rtfCharacterSet";
+
+ /** Indicates the domain of a Style */
+ static final String StyleType = "style:type";
+
+ /** Value for StyleType indicating a section style */
+ static final String STSection = "section";
+ /** Value for StyleType indicating a paragraph style */
+ static final String STParagraph = "paragraph";
+ /** Value for StyleType indicating a character style */
+ static final String STCharacter = "character";
+
+ /** The style of the text following this style */
+ static final String StyleNext = "style:nextStyle";
+
+ /** Whether the style is additive */
+ static final String StyleAdditive = "style:additive";
+
+ /** Whether the style is hidden from the user */
+ static final String StyleHidden = "style:hidden";
+
+ /* Miscellaneous character attributes */
+ static final String Caps = "caps";
+ static final String Deleted = "deleted";
+ static final String Outline = "outl";
+ static final String SmallCaps = "scaps";
+ static final String Shadow = "shad";
+ static final String Strikethrough = "strike";
+ static final String Hidden = "v";
+
+ /* Miscellaneous document attributes */
+ static final String PaperWidth = "paperw";
+ static final String PaperHeight = "paperh";
+ static final String MarginLeft = "margl";
+ static final String MarginRight = "margr";
+ static final String MarginTop = "margt";
+ static final String MarginBottom = "margb";
+ static final String GutterWidth = "gutter";
+
+ /* This is both a document and a paragraph attribute */
+ static final String WidowControl = "widowctrl";
+}
diff --git a/src/share/classes/javax/swing/text/rtf/MockAttributeSet.java b/src/share/classes/javax/swing/text/rtf/MockAttributeSet.java
new file mode 100644
index 000000000..e7b6f5f91
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/MockAttributeSet.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 1997-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.rtf;
+
+import java.util.Dictionary;
+import java.util.Enumeration;
+import javax.swing.text.AttributeSet;
+import javax.swing.text.MutableAttributeSet;
+
+
+/* This AttributeSet is made entirely out of tofu and Ritz Crackers
+ and yet has a remarkably attribute-set-like interface! */
+class MockAttributeSet
+ implements AttributeSet, MutableAttributeSet
+{
+ public Dictionary backing;
+
+ public boolean isEmpty()
+ {
+ return backing.isEmpty();
+ }
+
+ public int getAttributeCount()
+ {
+ return backing.size();
+ }
+
+ public boolean isDefined(Object name)
+ {
+ return ( backing.get(name) ) != null;
+ }
+
+ public boolean isEqual(AttributeSet attr)
+ {
+ throw new InternalError("MockAttributeSet: charade revealed!");
+ }
+
+ public AttributeSet copyAttributes()
+ {
+ throw new InternalError("MockAttributeSet: charade revealed!");
+ }
+
+ public Object getAttribute(Object name)
+ {
+ return backing.get(name);
+ }
+
+ public void addAttribute(Object name, Object value)
+ {
+ backing.put(name, value);
+ }
+
+ public void addAttributes(AttributeSet attr)
+ {
+ Enumeration as = attr.getAttributeNames();
+ while(as.hasMoreElements()) {
+ Object el = as.nextElement();
+ backing.put(el, attr.getAttribute(el));
+ }
+ }
+
+ public void removeAttribute(Object name)
+ {
+ backing.remove(name);
+ }
+
+ public void removeAttributes(AttributeSet attr)
+ {
+ throw new InternalError("MockAttributeSet: charade revealed!");
+ }
+
+ public void removeAttributes(Enumeration<?> en)
+ {
+ throw new InternalError("MockAttributeSet: charade revealed!");
+ }
+
+ public void setResolveParent(AttributeSet pp)
+ {
+ throw new InternalError("MockAttributeSet: charade revealed!");
+ }
+
+
+ public Enumeration getAttributeNames()
+ {
+ return backing.keys();
+ }
+
+ public boolean containsAttribute(Object name, Object value)
+ {
+ throw new InternalError("MockAttributeSet: charade revealed!");
+ }
+
+ public boolean containsAttributes(AttributeSet attr)
+ {
+ throw new InternalError("MockAttributeSet: charade revealed!");
+ }
+
+ public AttributeSet getResolveParent()
+ {
+ throw new InternalError("MockAttributeSet: charade revealed!");
+ }
+}
diff --git a/src/share/classes/javax/swing/text/rtf/RTFAttribute.java b/src/share/classes/javax/swing/text/rtf/RTFAttribute.java
new file mode 100644
index 000000000..c6bfd60bb
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/RTFAttribute.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 1997-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.rtf;
+
+import javax.swing.text.AttributeSet;
+import javax.swing.text.MutableAttributeSet;
+import java.io.IOException;
+
+/**
+ * This interface describes a class which defines a 1-1 mapping between
+ * an RTF keyword and a SwingText attribute.
+ */
+interface RTFAttribute
+{
+ static final int D_CHARACTER = 0;
+ static final int D_PARAGRAPH = 1;
+ static final int D_SECTION = 2;
+ static final int D_DOCUMENT = 3;
+ static final int D_META = 4;
+
+ /* These next three should really be public variables,
+ but you can't declare public variables in an interface... */
+ /* int domain; */
+ public int domain();
+ /* String swingName; */
+ public Object swingName();
+ /* String rtfName; */
+ public String rtfName();
+
+ public boolean set(MutableAttributeSet target);
+ public boolean set(MutableAttributeSet target, int parameter);
+
+ public boolean setDefault(MutableAttributeSet target);
+
+ /* TODO: This method is poorly thought out */
+ public boolean write(AttributeSet source,
+ RTFGenerator target,
+ boolean force)
+ throws IOException;
+
+ public boolean writeValue(Object value,
+ RTFGenerator target,
+ boolean force)
+ throws IOException;
+}
diff --git a/src/share/classes/javax/swing/text/rtf/RTFAttributes.java b/src/share/classes/javax/swing/text/rtf/RTFAttributes.java
new file mode 100644
index 000000000..2bf54b31b
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/RTFAttributes.java
@@ -0,0 +1,422 @@
+/*
+ * 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.rtf;
+
+import javax.swing.text.StyleConstants;
+import javax.swing.text.AttributeSet;
+import javax.swing.text.MutableAttributeSet;
+import javax.swing.text.TabStop;
+import java.util.*;
+import java.io.IOException;
+
+class RTFAttributes
+{
+ static RTFAttribute attributes[];
+
+ static {
+ Vector a = new Vector();
+ int CHR = RTFAttribute.D_CHARACTER;
+ int PGF = RTFAttribute.D_PARAGRAPH;
+ int SEC = RTFAttribute.D_SECTION;
+ int DOC = RTFAttribute.D_DOCUMENT;
+ int PST = RTFAttribute.D_META;
+ Boolean True = Boolean.valueOf(true);
+ Boolean False = Boolean.valueOf(false);
+
+ a.addElement(new BooleanAttribute(CHR, StyleConstants.Italic, "i"));
+ a.addElement(new BooleanAttribute(CHR, StyleConstants.Bold, "b"));
+ a.addElement(new BooleanAttribute(CHR, StyleConstants.Underline, "ul"));
+ a.addElement(NumericAttribute.NewTwips(PGF, StyleConstants.LeftIndent, "li",
+ 0f, 0));
+ a.addElement(NumericAttribute.NewTwips(PGF, StyleConstants.RightIndent, "ri",
+ 0f, 0));
+ a.addElement(NumericAttribute.NewTwips(PGF, StyleConstants.FirstLineIndent, "fi",
+ 0f, 0));
+
+ a.addElement(new AssertiveAttribute(PGF, StyleConstants.Alignment,
+ "ql", StyleConstants.ALIGN_LEFT));
+ a.addElement(new AssertiveAttribute(PGF, StyleConstants.Alignment,
+ "qr", StyleConstants.ALIGN_RIGHT));
+ a.addElement(new AssertiveAttribute(PGF, StyleConstants.Alignment,
+ "qc", StyleConstants.ALIGN_CENTER));
+ a.addElement(new AssertiveAttribute(PGF, StyleConstants.Alignment,
+ "qj", StyleConstants.ALIGN_JUSTIFIED));
+ a.addElement(NumericAttribute.NewTwips(PGF, StyleConstants.SpaceAbove,
+ "sa", 0));
+ a.addElement(NumericAttribute.NewTwips(PGF, StyleConstants.SpaceBelow,
+ "sb", 0));
+
+ a.addElement(new AssertiveAttribute(PST, RTFReader.TabAlignmentKey,
+ "tqr", TabStop.ALIGN_RIGHT));
+ a.addElement(new AssertiveAttribute(PST, RTFReader.TabAlignmentKey,
+ "tqc", TabStop.ALIGN_CENTER));
+ a.addElement(new AssertiveAttribute(PST, RTFReader.TabAlignmentKey,
+ "tqdec", TabStop.ALIGN_DECIMAL));
+
+
+ a.addElement(new AssertiveAttribute(PST, RTFReader.TabLeaderKey,
+ "tldot", TabStop.LEAD_DOTS));
+ a.addElement(new AssertiveAttribute(PST, RTFReader.TabLeaderKey,
+ "tlhyph", TabStop.LEAD_HYPHENS));
+ a.addElement(new AssertiveAttribute(PST, RTFReader.TabLeaderKey,
+ "tlul", TabStop.LEAD_UNDERLINE));
+ a.addElement(new AssertiveAttribute(PST, RTFReader.TabLeaderKey,
+ "tlth", TabStop.LEAD_THICKLINE));
+ a.addElement(new AssertiveAttribute(PST, RTFReader.TabLeaderKey,
+ "tleq", TabStop.LEAD_EQUALS));
+
+ /* The following aren't actually recognized by Swing */
+ a.addElement(new BooleanAttribute(CHR, Constants.Caps, "caps"));
+ a.addElement(new BooleanAttribute(CHR, Constants.Outline, "outl"));
+ a.addElement(new BooleanAttribute(CHR, Constants.SmallCaps, "scaps"));
+ a.addElement(new BooleanAttribute(CHR, Constants.Shadow, "shad"));
+ a.addElement(new BooleanAttribute(CHR, Constants.Hidden, "v"));
+ a.addElement(new BooleanAttribute(CHR, Constants.Strikethrough,
+ "strike"));
+ a.addElement(new BooleanAttribute(CHR, Constants.Deleted,
+ "deleted"));
+
+
+
+ a.addElement(new AssertiveAttribute(DOC, "saveformat", "defformat", "RTF"));
+ a.addElement(new AssertiveAttribute(DOC, "landscape", "landscape"));
+
+ a.addElement(NumericAttribute.NewTwips(DOC, Constants.PaperWidth,
+ "paperw", 12240));
+ a.addElement(NumericAttribute.NewTwips(DOC, Constants.PaperHeight,
+ "paperh", 15840));
+ a.addElement(NumericAttribute.NewTwips(DOC, Constants.MarginLeft,
+ "margl", 1800));
+ a.addElement(NumericAttribute.NewTwips(DOC, Constants.MarginRight,
+ "margr", 1800));
+ a.addElement(NumericAttribute.NewTwips(DOC, Constants.MarginTop,
+ "margt", 1440));
+ a.addElement(NumericAttribute.NewTwips(DOC, Constants.MarginBottom,
+ "margb", 1440));
+ a.addElement(NumericAttribute.NewTwips(DOC, Constants.GutterWidth,
+ "gutter", 0));
+
+ a.addElement(new AssertiveAttribute(PGF, Constants.WidowControl,
+ "nowidctlpar", False));
+ a.addElement(new AssertiveAttribute(PGF, Constants.WidowControl,
+ "widctlpar", True));
+ a.addElement(new AssertiveAttribute(DOC, Constants.WidowControl,
+ "widowctrl", True));
+
+
+ RTFAttribute[] attrs = new RTFAttribute[a.size()];
+ a.copyInto(attrs);
+ attributes = attrs;
+ }
+
+ static Dictionary attributesByKeyword()
+ {
+ Dictionary d = new Hashtable(attributes.length);
+ int i, m;
+
+ m = attributes.length;
+ for(i = 0; i < m; i++)
+ d.put(attributes[i].rtfName(), attributes[i]);
+
+ return d;
+ }
+
+ /************************************************************************/
+ /************************************************************************/
+
+ static abstract class GenericAttribute
+ {
+ int domain;
+ Object swingName;
+ String rtfName;
+
+ protected GenericAttribute(int d,Object s, String r)
+ {
+ domain = d;
+ swingName = s;
+ rtfName = r;
+ }
+
+ public int domain() { return domain; }
+ public Object swingName() { return swingName; }
+ public String rtfName() { return rtfName; }
+
+ abstract boolean set(MutableAttributeSet target);
+ abstract boolean set(MutableAttributeSet target, int parameter);
+ abstract boolean setDefault(MutableAttributeSet target);
+
+ public boolean write(AttributeSet source,
+ RTFGenerator target,
+ boolean force)
+ throws IOException
+ {
+ return writeValue(source.getAttribute(swingName), target, force);
+ }
+
+ public boolean writeValue(Object value, RTFGenerator target,
+ boolean force)
+ throws IOException
+ {
+ return false;
+ }
+ }
+
+ static class BooleanAttribute
+ extends GenericAttribute
+ implements RTFAttribute
+ {
+ boolean rtfDefault;
+ boolean swingDefault;
+
+ protected static final Boolean True = Boolean.valueOf(true);
+ protected static final Boolean False = Boolean.valueOf(false);
+
+ public BooleanAttribute(int d, Object s,
+ String r, boolean ds, boolean dr)
+ {
+ super(d, s, r);
+ swingDefault = ds;
+ rtfDefault = dr;
+ }
+
+ public BooleanAttribute(int d, Object s, String r)
+ {
+ super(d, s, r);
+
+ swingDefault = false;
+ rtfDefault = false;
+ }
+
+ public boolean set(MutableAttributeSet target)
+ {
+ /* TODO: There's some ambiguity about whether this should
+ *set* or *toggle* the attribute. */
+ target.addAttribute(swingName, True);
+
+ return true; /* true indicates we were successful */
+ }
+
+ public boolean set(MutableAttributeSet target, int parameter)
+ {
+ /* See above note in the case that parameter==1 */
+ Boolean value = ( parameter != 0 ? True : False );
+
+ target.addAttribute(swingName, value);
+
+ return true; /* true indicates we were successful */
+ }
+
+ public boolean setDefault(MutableAttributeSet target)
+ {
+ if (swingDefault != rtfDefault ||
+ ( target.getAttribute(swingName) != null ) )
+ target.addAttribute(swingName, Boolean.valueOf(rtfDefault));
+ return true;
+ }
+
+ public boolean writeValue(Object o_value,
+ RTFGenerator target,
+ boolean force)
+ throws IOException
+ {
+ Boolean val;
+
+ if (o_value == null)
+ val = Boolean.valueOf(swingDefault);
+ else
+ val = (Boolean)o_value;
+
+ if (force || (val.booleanValue() != rtfDefault)) {
+ if (val.booleanValue()) {
+ target.writeControlWord(rtfName);
+ } else {
+ target.writeControlWord(rtfName, 0);
+ }
+ }
+ return true;
+ }
+ }
+
+
+ static class AssertiveAttribute
+ extends GenericAttribute
+ implements RTFAttribute
+ {
+ Object swingValue;
+
+ public AssertiveAttribute(int d, Object s, String r)
+ {
+ super(d, s, r);
+ swingValue = Boolean.valueOf(true);
+ }
+
+ public AssertiveAttribute(int d, Object s, String r, Object v)
+ {
+ super(d, s, r);
+ swingValue = v;
+ }
+
+ public AssertiveAttribute(int d, Object s, String r, int v)
+ {
+ super(d, s, r);
+ swingValue = new Integer(v);
+ }
+
+ public boolean set(MutableAttributeSet target)
+ {
+ if (swingValue == null)
+ target.removeAttribute(swingName);
+ else
+ target.addAttribute(swingName, swingValue);
+
+ return true;
+ }
+
+ public boolean set(MutableAttributeSet target, int parameter)
+ {
+ return false;
+ }
+
+ public boolean setDefault(MutableAttributeSet target)
+ {
+ target.removeAttribute(swingName);
+ return true;
+ }
+
+ public boolean writeValue(Object value,
+ RTFGenerator target,
+ boolean force)
+ throws IOException
+ {
+ if (value == null) {
+ return ! force;
+ }
+
+ if (value.equals(swingValue)) {
+ target.writeControlWord(rtfName);
+ return true;
+ }
+
+ return ! force;
+ }
+ }
+
+
+ static class NumericAttribute
+ extends GenericAttribute
+ implements RTFAttribute
+ {
+ int rtfDefault;
+ Number swingDefault;
+ float scale;
+
+ protected NumericAttribute(int d, Object s, String r)
+ {
+ super(d, s, r);
+ rtfDefault = 0;
+ swingDefault = null;
+ scale = 1f;
+ }
+
+ public NumericAttribute(int d, Object s,
+ String r, int ds, int dr)
+ {
+ this(d, s, r, new Integer(ds), dr, 1f);
+ }
+
+ public NumericAttribute(int d, Object s,
+ String r, Number ds, int dr, float sc)
+ {
+ super(d, s, r);
+ swingDefault = ds;
+ rtfDefault = dr;
+ scale = sc;
+ }
+
+ public static NumericAttribute NewTwips(int d, Object s, String r,
+ float ds, int dr)
+ {
+ return new NumericAttribute(d, s, r, new Float(ds), dr, 20f);
+ }
+
+ public static NumericAttribute NewTwips(int d, Object s, String r,
+ int dr)
+ {
+ return new NumericAttribute(d, s, r, null, dr, 20f);
+ }
+
+ public boolean set(MutableAttributeSet target)
+ {
+ return false;
+ }
+
+ public boolean set(MutableAttributeSet target, int parameter)
+ {
+ Number swingValue;
+
+ if (scale == 1f)
+ swingValue = new Integer(parameter);
+ else
+ swingValue = new Float(parameter / scale);
+ target.addAttribute(swingName, swingValue);
+ return true;
+ }
+
+ public boolean setDefault(MutableAttributeSet target)
+ {
+ Number old = (Number)target.getAttribute(swingName);
+ if (old == null)
+ old = swingDefault;
+ if (old != null && (
+ (scale == 1f && old.intValue() == rtfDefault) ||
+ (Math.round(old.floatValue() * scale) == rtfDefault)
+ ))
+ return true;
+ set(target, rtfDefault);
+ return true;
+ }
+
+ public boolean writeValue(Object o_value,
+ RTFGenerator target,
+ boolean force)
+ throws IOException
+ {
+ Number value = (Number)o_value;
+ if (value == null)
+ value = swingDefault;
+ if (value == null) {
+ /* TODO: What is the proper behavior if the Swing object does
+ not specify a value, and we don't know its default value?
+ Currently we pretend that the RTF default value is
+ equivalent (probably a workable assumption) */
+ return true;
+ }
+ int int_value = Math.round(value.floatValue() * scale);
+ if (force || (int_value != rtfDefault))
+ target.writeControlWord(rtfName, int_value);
+ return true;
+ }
+ }
+}
diff --git a/src/share/classes/javax/swing/text/rtf/RTFEditorKit.java b/src/share/classes/javax/swing/text/rtf/RTFEditorKit.java
new file mode 100644
index 000000000..bd60986b9
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/RTFEditorKit.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 1997-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.rtf;
+
+import java.awt.*;
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import javax.swing.Action;
+import javax.swing.text.*;
+import javax.swing.*;
+
+/**
+ * This is the default implementation of RTF editing
+ * functionality. The RTF support was not written by the
+ * Swing team. In the future we hope to improve the support
+ * provided.
+ *
+ * @author Timothy Prinzing (of this class, not the package!)
+ */
+public class RTFEditorKit extends StyledEditorKit {
+
+ /**
+ * Constructs an RTFEditorKit.
+ */
+ public RTFEditorKit() {
+ super();
+ }
+
+ /**
+ * Get the MIME type of the data that this
+ * kit represents support for. This kit supports
+ * the type <code>text/rtf</code>.
+ *
+ * @return the type
+ */
+ public String getContentType() {
+ return "text/rtf";
+ }
+
+ /**
+ * Insert content from the given stream which is expected
+ * to be in a format appropriate for this kind of content
+ * handler.
+ *
+ * @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.
+ */
+ public void read(InputStream in, Document doc, int pos) throws IOException, BadLocationException {
+
+ if (doc instanceof StyledDocument) {
+ // PENDING(prinz) this needs to be fixed to
+ // insert to the given position.
+ RTFReader rdr = new RTFReader((StyledDocument) doc);
+ rdr.readFromStream(in);
+ rdr.close();
+ } else {
+ // treat as text/plain
+ super.read(in, doc, pos);
+ }
+ }
+
+ /**
+ * 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(OutputStream out, Document doc, int pos, int len)
+ throws IOException, BadLocationException {
+
+ // PENDING(prinz) this needs to be fixed to
+ // use the given document range.
+ RTFGenerator.writeDocument(doc, out);
+ }
+
+ /**
+ * Insert content from the given stream, which will be
+ * treated as plain text.
+ *
+ * @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.
+ */
+ public void read(Reader in, Document doc, int pos)
+ throws IOException, BadLocationException {
+
+ if (doc instanceof StyledDocument) {
+ RTFReader rdr = new RTFReader((StyledDocument) doc);
+ rdr.readFromReader(in);
+ rdr.close();
+ } else {
+ // treat as text/plain
+ super.read(in, doc, pos);
+ }
+ }
+
+ /**
+ * Write content from a document to the given stream
+ * as plain text.
+ *
+ * @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 {
+
+ throw new IOException("RTF is an 8-bit format");
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/rtf/RTFGenerator.java b/src/share/classes/javax/swing/text/rtf/RTFGenerator.java
new file mode 100644
index 000000000..0c7a92f91
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/RTFGenerator.java
@@ -0,0 +1,1009 @@
+/*
+ * 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.rtf;
+
+import java.lang.*;
+import java.util.*;
+import java.awt.Color;
+import java.awt.Font;
+import java.io.OutputStream;
+import java.io.IOException;
+
+import javax.swing.text.*;
+
+/**
+ * Generates an RTF output stream (java.io.OutputStream) from rich text
+ * (handed off through a series of LTTextAcceptor calls). Can be used to
+ * generate RTF from any object which knows how to write to a text acceptor
+ * (e.g., LTAttributedText and LTRTFFilter).
+ *
+ * <p>Note that this is a lossy conversion since RTF's model of
+ * text does not exactly correspond with LightText's.
+ *
+ * @see LTAttributedText
+ * @see LTRTFFilter
+ * @see LTTextAcceptor
+ * @see java.io.OutputStream
+ */
+
+class RTFGenerator extends Object
+{
+ /* These dictionaries map Colors, font names, or Style objects
+ to Integers */
+ Dictionary colorTable;
+ int colorCount;
+ Dictionary fontTable;
+ int fontCount;
+ Dictionary styleTable;
+ int styleCount;
+
+ /* where all the text is going */
+ OutputStream outputStream;
+
+ boolean afterKeyword;
+
+ MutableAttributeSet outputAttributes;
+
+ /* the value of the last \\ucN keyword emitted */
+ int unicodeCount;
+
+ /* for efficiency's sake (ha) */
+ private Segment workingSegment;
+
+ int[] outputConversion;
+
+ /** The default color, used for text without an explicit color
+ * attribute. */
+ static public final Color defaultRTFColor = Color.black;
+
+ static public final float defaultFontSize = 12f;
+
+ static public final String defaultFontFamily = "Helvetica";
+
+ /* constants so we can avoid allocating objects in inner loops */
+ /* these should all be final, but javac seems to be a bit buggy */
+ static protected Integer One, Zero;
+ static protected Boolean False;
+ static protected Float ZeroPointZero;
+ static private Object MagicToken;
+
+ /* An array of character-keyword pairs. This could be done
+ as a dictionary (and lookup would be quicker), but that
+ would require allocating an object for every character
+ written (slow!). */
+ static class CharacterKeywordPair
+ { public char character; public String keyword; };
+ static protected CharacterKeywordPair[] textKeywords;
+
+ static {
+ One = new Integer(1);
+ Zero = new Integer(0);
+ False = Boolean.valueOf(false);
+ MagicToken = new Object();
+ ZeroPointZero = new Float(0);
+
+ Dictionary textKeywordDictionary = RTFReader.textKeywords;
+ Enumeration keys = textKeywordDictionary.keys();
+ Vector tempPairs = new Vector();
+ while(keys.hasMoreElements()) {
+ CharacterKeywordPair pair = new CharacterKeywordPair();
+ pair.keyword = (String)keys.nextElement();
+ pair.character = ((String)textKeywordDictionary.get(pair.keyword)).charAt(0);
+ tempPairs.addElement(pair);
+ }
+ textKeywords = new CharacterKeywordPair[tempPairs.size()];
+ tempPairs.copyInto(textKeywords);
+ }
+
+ static final char[] hexdigits = { '0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+static public void writeDocument(Document d, OutputStream to)
+ throws IOException
+{
+ RTFGenerator gen = new RTFGenerator(to);
+ Element root = d.getDefaultRootElement();
+
+ gen.examineElement(root);
+ gen.writeRTFHeader();
+ gen.writeDocumentProperties(d);
+
+ /* TODO this assumes a particular element structure; is there
+ a way to iterate more generically ? */
+ int max = root.getElementCount();
+ for(int idx = 0; idx < max; idx++)
+ gen.writeParagraphElement(root.getElement(idx));
+
+ gen.writeRTFTrailer();
+}
+
+public RTFGenerator(OutputStream to)
+{
+ colorTable = new Hashtable();
+ colorTable.put(defaultRTFColor, new Integer(0));
+ colorCount = 1;
+
+ fontTable = new Hashtable();
+ fontCount = 0;
+
+ styleTable = new Hashtable();
+ /* TODO: put default style in style table */
+ styleCount = 0;
+
+ workingSegment = new Segment();
+
+ outputStream = to;
+
+ unicodeCount = 1;
+}
+
+public void examineElement(Element el)
+{
+ AttributeSet a = el.getAttributes();
+ String fontName;
+ Object foregroundColor, backgroundColor;
+
+ tallyStyles(a);
+
+ if (a != null) {
+ /* TODO: default color must be color 0! */
+
+ foregroundColor = StyleConstants.getForeground(a);
+ if (foregroundColor != null &&
+ colorTable.get(foregroundColor) == null) {
+ colorTable.put(foregroundColor, new Integer(colorCount));
+ colorCount ++;
+ }
+
+ backgroundColor = a.getAttribute(StyleConstants.Background);
+ if (backgroundColor != null &&
+ colorTable.get(backgroundColor) == null) {
+ colorTable.put(backgroundColor, new Integer(colorCount));
+ colorCount ++;
+ }
+
+ fontName = StyleConstants.getFontFamily(a);
+
+ if (fontName == null)
+ fontName = defaultFontFamily;
+
+ if (fontName != null &&
+ fontTable.get(fontName) == null) {
+ fontTable.put(fontName, new Integer(fontCount));
+ fontCount ++;
+ }
+ }
+
+ int el_count = el.getElementCount();
+ for(int el_idx = 0; el_idx < el_count; el_idx ++) {
+ examineElement(el.getElement(el_idx));
+ }
+}
+
+private void tallyStyles(AttributeSet a) {
+ while (a != null) {
+ if (a instanceof Style) {
+ Integer aNum = (Integer)styleTable.get(a);
+ if (aNum == null) {
+ styleCount = styleCount + 1;
+ aNum = new Integer(styleCount);
+ styleTable.put(a, aNum);
+ }
+ }
+ a = a.getResolveParent();
+ }
+}
+
+private Style findStyle(AttributeSet a)
+{
+ while(a != null) {
+ if (a instanceof Style) {
+ Object aNum = styleTable.get(a);
+ if (aNum != null)
+ return (Style)a;
+ }
+ a = a.getResolveParent();
+ }
+ return null;
+}
+
+private Integer findStyleNumber(AttributeSet a, String domain)
+{
+ while(a != null) {
+ if (a instanceof Style) {
+ Integer aNum = (Integer)styleTable.get(a);
+ if (aNum != null) {
+ if (domain == null ||
+ domain.equals(a.getAttribute(Constants.StyleType)))
+ return aNum;
+ }
+
+ }
+ a = a.getResolveParent();
+ }
+ return null;
+}
+
+static private Object attrDiff(MutableAttributeSet oldAttrs,
+ AttributeSet newAttrs,
+ Object key,
+ Object dfl)
+{
+ Object oldValue, newValue;
+
+ oldValue = oldAttrs.getAttribute(key);
+ newValue = newAttrs.getAttribute(key);
+
+ if (newValue == oldValue)
+ return null;
+ if (newValue == null) {
+ oldAttrs.removeAttribute(key);
+ if (dfl != null && !dfl.equals(oldValue))
+ return dfl;
+ else
+ return null;
+ }
+ if (oldValue == null ||
+ !equalArraysOK(oldValue, newValue)) {
+ oldAttrs.addAttribute(key, newValue);
+ return newValue;
+ }
+ return null;
+}
+
+static private boolean equalArraysOK(Object a, Object b)
+{
+ Object[] aa, bb;
+ if (a == b)
+ return true;
+ if (a == null || b == null)
+ return false;
+ if (a.equals(b))
+ return true;
+ if (!(a.getClass().isArray() && b.getClass().isArray()))
+ return false;
+ aa = (Object[])a;
+ bb = (Object[])b;
+ if (aa.length != bb.length)
+ return false;
+
+ int i;
+ int l = aa.length;
+ for(i = 0; i < l; i++) {
+ if (!equalArraysOK(aa[i], bb[i]))
+ return false;
+ }
+
+ return true;
+}
+
+/* Writes a line break to the output file, for ease in debugging */
+public void writeLineBreak()
+ throws IOException
+{
+ writeRawString("\n");
+ afterKeyword = false;
+}
+
+
+public void writeRTFHeader()
+ throws IOException
+{
+ int index;
+
+ /* TODO: Should the writer attempt to examine the text it's writing
+ and pick a character set which will most compactly represent the
+ document? (currently the writer always uses the ansi character
+ set, which is roughly ISO-8859 Latin-1, and uses Unicode escapes
+ for all other characters. However Unicode is a relatively
+ recent addition to RTF, and not all readers will understand it.) */
+ writeBegingroup();
+ writeControlWord("rtf", 1);
+ writeControlWord("ansi");
+ outputConversion = outputConversionForName("ansi");
+ writeLineBreak();
+
+ /* write font table */
+ String[] sortedFontTable = new String[fontCount];
+ Enumeration fonts = fontTable.keys();
+ String font;
+ while(fonts.hasMoreElements()) {
+ font = (String)fonts.nextElement();
+ Integer num = (Integer)(fontTable.get(font));
+ sortedFontTable[num.intValue()] = font;
+ }
+ writeBegingroup();
+ writeControlWord("fonttbl");
+ for(index = 0; index < fontCount; index ++) {
+ writeControlWord("f", index);
+ writeControlWord("fnil"); /* TODO: supply correct font style */
+ writeText(sortedFontTable[index]);
+ writeText(";");
+ }
+ writeEndgroup();
+ writeLineBreak();
+
+ /* write color table */
+ if (colorCount > 1) {
+ Color[] sortedColorTable = new Color[colorCount];
+ Enumeration colors = colorTable.keys();
+ Color color;
+ while(colors.hasMoreElements()) {
+ color = (Color)colors.nextElement();
+ Integer num = (Integer)(colorTable.get(color));
+ sortedColorTable[num.intValue()] = color;
+ }
+ writeBegingroup();
+ writeControlWord("colortbl");
+ for(index = 0; index < colorCount; index ++) {
+ color = sortedColorTable[index];
+ if (color != null) {
+ writeControlWord("red", color.getRed());
+ writeControlWord("green", color.getGreen());
+ writeControlWord("blue", color.getBlue());
+ }
+ writeRawString(";");
+ }
+ writeEndgroup();
+ writeLineBreak();
+ }
+
+ /* write the style sheet */
+ if (styleCount > 1) {
+ writeBegingroup();
+ writeControlWord("stylesheet");
+ Enumeration styles = styleTable.keys();
+ while(styles.hasMoreElements()) {
+ Style style = (Style)styles.nextElement();
+ int styleNumber = ((Integer)styleTable.get(style)).intValue();
+ writeBegingroup();
+ String styleType = (String)style.getAttribute(Constants.StyleType);
+ if (styleType == null)
+ styleType = Constants.STParagraph;
+ if (styleType.equals(Constants.STCharacter)) {
+ writeControlWord("*");
+ writeControlWord("cs", styleNumber);
+ } else if(styleType.equals(Constants.STSection)) {
+ writeControlWord("*");
+ writeControlWord("ds", styleNumber);
+ } else {
+ writeControlWord("s", styleNumber);
+ }
+
+ AttributeSet basis = style.getResolveParent();
+ MutableAttributeSet goat;
+ if (basis == null) {
+ goat = new SimpleAttributeSet();
+ } else {
+ goat = new SimpleAttributeSet(basis);
+ }
+
+ updateSectionAttributes(goat, style, false);
+ updateParagraphAttributes(goat, style, false);
+ updateCharacterAttributes(goat, style, false);
+
+ basis = style.getResolveParent();
+ if (basis != null && basis instanceof Style) {
+ Integer basedOn = (Integer)styleTable.get(basis);
+ if (basedOn != null) {
+ writeControlWord("sbasedon", basedOn.intValue());
+ }
+ }
+
+ Style nextStyle = (Style)style.getAttribute(Constants.StyleNext);
+ if (nextStyle != null) {
+ Integer nextNum = (Integer)styleTable.get(nextStyle);
+ if (nextNum != null) {
+ writeControlWord("snext", nextNum.intValue());
+ }
+ }
+
+ Boolean hidden = (Boolean)style.getAttribute(Constants.StyleHidden);
+ if (hidden != null && hidden.booleanValue())
+ writeControlWord("shidden");
+
+ Boolean additive = (Boolean)style.getAttribute(Constants.StyleAdditive);
+ if (additive != null && additive.booleanValue())
+ writeControlWord("additive");
+
+
+ writeText(style.getName());
+ writeText(";");
+ writeEndgroup();
+ }
+ writeEndgroup();
+ writeLineBreak();
+ }
+
+ outputAttributes = new SimpleAttributeSet();
+}
+
+void writeDocumentProperties(Document doc)
+ throws IOException
+{
+ /* Write the document properties */
+ int i;
+ boolean wroteSomething = false;
+
+ for(i = 0; i < RTFAttributes.attributes.length; i++) {
+ RTFAttribute attr = RTFAttributes.attributes[i];
+ if (attr.domain() != RTFAttribute.D_DOCUMENT)
+ continue;
+ Object prop = doc.getProperty(attr.swingName());
+ boolean ok = attr.writeValue(prop, this, false);
+ if (ok)
+ wroteSomething = true;
+ }
+
+ if (wroteSomething)
+ writeLineBreak();
+}
+
+public void writeRTFTrailer()
+ throws IOException
+{
+ writeEndgroup();
+ writeLineBreak();
+}
+
+protected void checkNumericControlWord(MutableAttributeSet currentAttributes,
+ AttributeSet newAttributes,
+ Object attrName,
+ String controlWord,
+ float dflt, float scale)
+ throws IOException
+{
+ Object parm;
+
+ if ((parm = attrDiff(currentAttributes, newAttributes,
+ attrName, MagicToken)) != null) {
+ float targ;
+ if (parm == MagicToken)
+ targ = dflt;
+ else
+ targ = ((Number)parm).floatValue();
+ writeControlWord(controlWord, Math.round(targ * scale));
+ }
+}
+
+protected void checkControlWord(MutableAttributeSet currentAttributes,
+ AttributeSet newAttributes,
+ RTFAttribute word)
+ throws IOException
+{
+ Object parm;
+
+ if ((parm = attrDiff(currentAttributes, newAttributes,
+ word.swingName(), MagicToken)) != null) {
+ if (parm == MagicToken)
+ parm = null;
+ word.writeValue(parm, this, true);
+ }
+}
+
+protected void checkControlWords(MutableAttributeSet currentAttributes,
+ AttributeSet newAttributes,
+ RTFAttribute words[],
+ int domain)
+ throws IOException
+{
+ int wordIndex;
+ int wordCount = words.length;
+ for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
+ RTFAttribute attr = words[wordIndex];
+ if (attr.domain() == domain)
+ checkControlWord(currentAttributes, newAttributes, attr);
+ }
+}
+
+void updateSectionAttributes(MutableAttributeSet current,
+ AttributeSet newAttributes,
+ boolean emitStyleChanges)
+ throws IOException
+{
+ if (emitStyleChanges) {
+ Object oldStyle = current.getAttribute("sectionStyle");
+ Object newStyle = findStyleNumber(newAttributes, Constants.STSection);
+ if (oldStyle != newStyle) {
+ if (oldStyle != null) {
+ resetSectionAttributes(current);
+ }
+ if (newStyle != null) {
+ writeControlWord("ds", ((Integer)newStyle).intValue());
+ current.addAttribute("sectionStyle", newStyle);
+ } else {
+ current.removeAttribute("sectionStyle");
+ }
+ }
+ }
+
+ checkControlWords(current, newAttributes,
+ RTFAttributes.attributes, RTFAttribute.D_SECTION);
+}
+
+protected void resetSectionAttributes(MutableAttributeSet currentAttributes)
+ throws IOException
+{
+ writeControlWord("sectd");
+
+ int wordIndex;
+ int wordCount = RTFAttributes.attributes.length;
+ for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
+ RTFAttribute attr = RTFAttributes.attributes[wordIndex];
+ if (attr.domain() == RTFAttribute.D_SECTION)
+ attr.setDefault(currentAttributes);
+ }
+
+ currentAttributes.removeAttribute("sectionStyle");
+}
+
+void updateParagraphAttributes(MutableAttributeSet current,
+ AttributeSet newAttributes,
+ boolean emitStyleChanges)
+ throws IOException
+{
+ Object parm;
+ Object oldStyle, newStyle;
+
+ /* The only way to get rid of tabs or styles is with the \pard keyword,
+ emitted by resetParagraphAttributes(). Ideally we should avoid
+ emitting \pard if the new paragraph's tabs are a superset of the old
+ paragraph's tabs. */
+
+ if (emitStyleChanges) {
+ oldStyle = current.getAttribute("paragraphStyle");
+ newStyle = findStyleNumber(newAttributes, Constants.STParagraph);
+ if (oldStyle != newStyle) {
+ if (oldStyle != null) {
+ resetParagraphAttributes(current);
+ oldStyle = null;
+ }
+ }
+ } else {
+ oldStyle = null;
+ newStyle = null;
+ }
+
+ Object oldTabs = current.getAttribute(Constants.Tabs);
+ Object newTabs = newAttributes.getAttribute(Constants.Tabs);
+ if (oldTabs != newTabs) {
+ if (oldTabs != null) {
+ resetParagraphAttributes(current);
+ oldTabs = null;
+ oldStyle = null;
+ }
+ }
+
+ if (oldStyle != newStyle && newStyle != null) {
+ writeControlWord("s", ((Integer)newStyle).intValue());
+ current.addAttribute("paragraphStyle", newStyle);
+ }
+
+ checkControlWords(current, newAttributes,
+ RTFAttributes.attributes, RTFAttribute.D_PARAGRAPH);
+
+ if (oldTabs != newTabs && newTabs != null) {
+ TabStop tabs[] = (TabStop[])newTabs;
+ int index;
+ for(index = 0; index < tabs.length; index ++) {
+ TabStop tab = tabs[index];
+ switch (tab.getAlignment()) {
+ case TabStop.ALIGN_LEFT:
+ case TabStop.ALIGN_BAR:
+ break;
+ case TabStop.ALIGN_RIGHT:
+ writeControlWord("tqr");
+ break;
+ case TabStop.ALIGN_CENTER:
+ writeControlWord("tqc");
+ break;
+ case TabStop.ALIGN_DECIMAL:
+ writeControlWord("tqdec");
+ break;
+ }
+ switch (tab.getLeader()) {
+ case TabStop.LEAD_NONE:
+ break;
+ case TabStop.LEAD_DOTS:
+ writeControlWord("tldot");
+ break;
+ case TabStop.LEAD_HYPHENS:
+ writeControlWord("tlhyph");
+ break;
+ case TabStop.LEAD_UNDERLINE:
+ writeControlWord("tlul");
+ break;
+ case TabStop.LEAD_THICKLINE:
+ writeControlWord("tlth");
+ break;
+ case TabStop.LEAD_EQUALS:
+ writeControlWord("tleq");
+ break;
+ }
+ int twips = Math.round(20f * tab.getPosition());
+ if (tab.getAlignment() == TabStop.ALIGN_BAR) {
+ writeControlWord("tb", twips);
+ } else {
+ writeControlWord("tx", twips);
+ }
+ }
+ current.addAttribute(Constants.Tabs, tabs);
+ }
+}
+
+public void writeParagraphElement(Element el)
+ throws IOException
+{
+ updateParagraphAttributes(outputAttributes, el.getAttributes(), true);
+
+ int sub_count = el.getElementCount();
+ for(int idx = 0; idx < sub_count; idx ++) {
+ writeTextElement(el.getElement(idx));
+ }
+
+ writeControlWord("par");
+ writeLineBreak(); /* makes the raw file more readable */
+}
+
+/* debugging. TODO: remove.
+private static String tabdump(Object tso)
+{
+ String buf;
+ int i;
+
+ if (tso == null)
+ return "[none]";
+
+ TabStop[] ts = (TabStop[])tso;
+
+ buf = "[";
+ for(i = 0; i < ts.length; i++) {
+ buf = buf + ts[i].toString();
+ if ((i+1) < ts.length)
+ buf = buf + ",";
+ }
+ return buf + "]";
+}
+*/
+
+protected void resetParagraphAttributes(MutableAttributeSet currentAttributes)
+ throws IOException
+{
+ writeControlWord("pard");
+
+ currentAttributes.addAttribute(StyleConstants.Alignment, Zero);
+
+ int wordIndex;
+ int wordCount = RTFAttributes.attributes.length;
+ for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
+ RTFAttribute attr = RTFAttributes.attributes[wordIndex];
+ if (attr.domain() == RTFAttribute.D_PARAGRAPH)
+ attr.setDefault(currentAttributes);
+ }
+
+ currentAttributes.removeAttribute("paragraphStyle");
+ currentAttributes.removeAttribute(Constants.Tabs);
+}
+
+void updateCharacterAttributes(MutableAttributeSet current,
+ AttributeSet newAttributes,
+ boolean updateStyleChanges)
+ throws IOException
+{
+ Object parm;
+
+ if (updateStyleChanges) {
+ Object oldStyle = current.getAttribute("characterStyle");
+ Object newStyle = findStyleNumber(newAttributes,
+ Constants.STCharacter);
+ if (oldStyle != newStyle) {
+ if (oldStyle != null) {
+ resetCharacterAttributes(current);
+ }
+ if (newStyle != null) {
+ writeControlWord("cs", ((Integer)newStyle).intValue());
+ current.addAttribute("characterStyle", newStyle);
+ } else {
+ current.removeAttribute("characterStyle");
+ }
+ }
+ }
+
+ if ((parm = attrDiff(current, newAttributes,
+ StyleConstants.FontFamily, null)) != null) {
+ Number fontNum = (Number)fontTable.get(parm);
+ writeControlWord("f", fontNum.intValue());
+ }
+
+ checkNumericControlWord(current, newAttributes,
+ StyleConstants.FontSize, "fs",
+ defaultFontSize, 2f);
+
+ checkControlWords(current, newAttributes,
+ RTFAttributes.attributes, RTFAttribute.D_CHARACTER);
+
+ checkNumericControlWord(current, newAttributes,
+ StyleConstants.LineSpacing, "sl",
+ 0, 20f); /* TODO: sl wackiness */
+
+ if ((parm = attrDiff(current, newAttributes,
+ StyleConstants.Background, MagicToken)) != null) {
+ int colorNum;
+ if (parm == MagicToken)
+ colorNum = 0;
+ else
+ colorNum = ((Number)colorTable.get(parm)).intValue();
+ writeControlWord("cb", colorNum);
+ }
+
+ if ((parm = attrDiff(current, newAttributes,
+ StyleConstants.Foreground, null)) != null) {
+ int colorNum;
+ if (parm == MagicToken)
+ colorNum = 0;
+ else
+ colorNum = ((Number)colorTable.get(parm)).intValue();
+ writeControlWord("cf", colorNum);
+ }
+}
+
+protected void resetCharacterAttributes(MutableAttributeSet currentAttributes)
+ throws IOException
+{
+ writeControlWord("plain");
+
+ int wordIndex;
+ int wordCount = RTFAttributes.attributes.length;
+ for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
+ RTFAttribute attr = RTFAttributes.attributes[wordIndex];
+ if (attr.domain() == RTFAttribute.D_CHARACTER)
+ attr.setDefault(currentAttributes);
+ }
+
+ StyleConstants.setFontFamily(currentAttributes, defaultFontFamily);
+ currentAttributes.removeAttribute(StyleConstants.FontSize); /* =default */
+ currentAttributes.removeAttribute(StyleConstants.Background);
+ currentAttributes.removeAttribute(StyleConstants.Foreground);
+ currentAttributes.removeAttribute(StyleConstants.LineSpacing);
+ currentAttributes.removeAttribute("characterStyle");
+}
+
+public void writeTextElement(Element el)
+ throws IOException
+{
+ updateCharacterAttributes(outputAttributes, el.getAttributes(), true);
+
+ if (el.isLeaf()) {
+ try {
+ el.getDocument().getText(el.getStartOffset(),
+ el.getEndOffset() - el.getStartOffset(),
+ this.workingSegment);
+ } catch (BadLocationException ble) {
+ /* TODO is this the correct error to raise? */
+ ble.printStackTrace();
+ throw new InternalError(ble.getMessage());
+ }
+ writeText(this.workingSegment);
+ } else {
+ int sub_count = el.getElementCount();
+ for(int idx = 0; idx < sub_count; idx ++)
+ writeTextElement(el.getElement(idx));
+ }
+}
+
+public void writeText(Segment s)
+ throws IOException
+{
+ int pos, end;
+ char[] array;
+
+ pos = s.offset;
+ end = pos + s.count;
+ array = s.array;
+ for( ; pos < end; pos ++)
+ writeCharacter(array[pos]);
+}
+
+public void writeText(String s)
+ throws IOException
+{
+ int pos, end;
+
+ pos = 0;
+ end = s.length();
+ for( ; pos < end; pos ++)
+ writeCharacter(s.charAt(pos));
+}
+
+public void writeRawString(String str)
+ throws IOException
+{
+ int strlen = str.length();
+ for (int offset = 0; offset < strlen; offset ++)
+ outputStream.write((int)str.charAt(offset));
+}
+
+public void writeControlWord(String keyword)
+ throws IOException
+{
+ outputStream.write('\\');
+ writeRawString(keyword);
+ afterKeyword = true;
+}
+
+public void writeControlWord(String keyword, int arg)
+ throws IOException
+{
+ outputStream.write('\\');
+ writeRawString(keyword);
+ writeRawString(String.valueOf(arg)); /* TODO: correct in all cases? */
+ afterKeyword = true;
+}
+
+public void writeBegingroup()
+ throws IOException
+{
+ outputStream.write('{');
+ afterKeyword = false;
+}
+
+public void writeEndgroup()
+ throws IOException
+{
+ outputStream.write('}');
+ afterKeyword = false;
+}
+
+public void writeCharacter(char ch)
+ throws IOException
+{
+ /* Nonbreaking space is in most RTF encodings, but the keyword is
+ preferable; same goes for tabs */
+ if (ch == 0xA0) { /* nonbreaking space */
+ outputStream.write(0x5C); /* backslash */
+ outputStream.write(0x7E); /* tilde */
+ afterKeyword = false; /* non-alpha keywords are self-terminating */
+ return;
+ }
+
+ if (ch == 0x09) { /* horizontal tab */
+ writeControlWord("tab");
+ return;
+ }
+
+ if (ch == 10 || ch == 13) { /* newline / paragraph */
+ /* ignore CRs, we'll write a paragraph element soon enough */
+ return;
+ }
+
+ int b = convertCharacter(outputConversion, ch);
+ if (b == 0) {
+ /* Unicode characters which have corresponding RTF keywords */
+ int i;
+ for(i = 0; i < textKeywords.length; i++) {
+ if (textKeywords[i].character == ch) {
+ writeControlWord(textKeywords[i].keyword);
+ return;
+ }
+ }
+ /* In some cases it would be reasonable to check to see if the
+ glyph being written out is in the Symbol encoding, and if so,
+ to switch to the Symbol font for this character. TODO. */
+ /* Currently all unrepresentable characters are written as
+ Unicode escapes. */
+ String approximation = approximationForUnicode(ch);
+ if (approximation.length() != unicodeCount) {
+ unicodeCount = approximation.length();
+ writeControlWord("uc", unicodeCount);
+ }
+ writeControlWord("u", (int)ch);
+ writeRawString(" ");
+ writeRawString(approximation);
+ afterKeyword = false;
+ return;
+ }
+
+ if (b > 127) {
+ int nybble;
+ outputStream.write('\\');
+ outputStream.write('\'');
+ nybble = ( b & 0xF0 ) >>> 4;
+ outputStream.write(hexdigits[nybble]);
+ nybble = ( b & 0x0F );
+ outputStream.write(hexdigits[nybble]);
+ afterKeyword = false;
+ return;
+ }
+
+ switch (b) {
+ case '}':
+ case '{':
+ case '\\':
+ outputStream.write(0x5C); /* backslash */
+ afterKeyword = false; /* in a keyword, actually ... */
+ /* fall through */
+ default:
+ if (afterKeyword) {
+ outputStream.write(0x20); /* space */
+ afterKeyword = false;
+ }
+ outputStream.write(b);
+ break;
+ }
+}
+
+String approximationForUnicode(char ch)
+{
+ /* TODO: Find reasonable approximations for all Unicode characters
+ in all RTF code pages... heh, heh... */
+ return "?";
+}
+
+/** Takes a translation table (a 256-element array of characters)
+ * and creates an output conversion table for use by
+ * convertCharacter(). */
+ /* Not very efficient at all. Could be changed to sort the table
+ for binary search. TODO. (Even though this is inefficient however,
+ writing RTF is still much faster than reading it.) */
+static int[] outputConversionFromTranslationTable(char[] table)
+{
+ int[] conversion = new int[2 * table.length];
+
+ int index;
+
+ for(index = 0; index < table.length; index ++) {
+ conversion[index * 2] = table[index];
+ conversion[(index * 2) + 1] = index;
+ }
+
+ return conversion;
+}
+
+static int[] outputConversionForName(String name)
+ throws IOException
+{
+ char[] table = (char[])RTFReader.getCharacterSet(name);
+ return outputConversionFromTranslationTable(table);
+}
+
+/** Takes a char and a conversion table (an int[] in the current
+ * implementation, but conversion tables should be treated as an opaque
+ * type) and returns the
+ * corresponding byte value (as an int, since bytes are signed).
+ */
+ /* Not very efficient. TODO. */
+static protected int convertCharacter(int[] conversion, char ch)
+{
+ int index;
+
+ for(index = 0; index < conversion.length; index += 2) {
+ if(conversion[index] == ch)
+ return conversion[index + 1];
+ }
+
+ return 0; /* 0 indicates an unrepresentable character */
+}
+
+}
diff --git a/src/share/classes/javax/swing/text/rtf/RTFParser.java b/src/share/classes/javax/swing/text/rtf/RTFParser.java
new file mode 100644
index 000000000..d12a82d59
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/RTFParser.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright 1997-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.rtf;
+
+import java.io.*;
+import java.lang.*;
+
+/**
+ * <b>RTFParser</b> is a subclass of <b>AbstractFilter</b> which understands basic RTF syntax
+ * and passes a stream of control words, text, and begin/end group
+ * indications to its subclass.
+ *
+ * Normally programmers will only use <b>RTFFilter</b>, a subclass of this class that knows what to
+ * do with the tokens this class parses.
+ *
+ * @see AbstractFilter
+ * @see RTFFilter
+ */
+abstract class RTFParser extends AbstractFilter
+{
+ /** The current RTF group nesting level. */
+ public int level;
+
+ private int state;
+ private StringBuffer currentCharacters;
+ private String pendingKeyword; // where keywords go while we
+ // read their parameters
+ private int pendingCharacter; // for the \'xx construct
+
+ private long binaryBytesLeft; // in a \bin blob?
+ ByteArrayOutputStream binaryBuf;
+ private boolean[] savedSpecials;
+
+ /** A stream to which to write warnings and debugging information
+ * while parsing. This is set to <code>System.out</code> to log
+ * any anomalous information to stdout. */
+ protected PrintStream warnings;
+
+ // value for the 'state' variable
+ private final int S_text = 0; // reading random text
+ private final int S_backslashed = 1; // read a backslash, waiting for next
+ private final int S_token = 2; // reading a multicharacter token
+ private final int S_parameter = 3; // reading a token's parameter
+
+ private final int S_aftertick = 4; // after reading \'
+ private final int S_aftertickc = 5; // after reading \'x
+
+ private final int S_inblob = 6; // in a \bin blob
+
+ /** Implemented by subclasses to interpret a parameter-less RTF keyword.
+ * The keyword is passed without the leading '/' or any delimiting
+ * whitespace. */
+ public abstract boolean handleKeyword(String keyword);
+ /** Implemented by subclasses to interpret a keyword with a parameter.
+ * @param keyword The keyword, as with <code>handleKeyword(String)</code>.
+ * @param parameter The parameter following the keyword. */
+ public abstract boolean handleKeyword(String keyword, int parameter);
+ /** Implemented by subclasses to interpret text from the RTF stream. */
+ public abstract void handleText(String text);
+ public void handleText(char ch)
+ { handleText(String.valueOf(ch)); }
+ /** Implemented by subclasses to handle the contents of the \bin keyword. */
+ public abstract void handleBinaryBlob(byte[] data);
+ /** Implemented by subclasses to react to an increase
+ * in the nesting level. */
+ public abstract void begingroup();
+ /** Implemented by subclasses to react to the end of a group. */
+ public abstract void endgroup();
+
+ // table of non-text characters in rtf
+ static final boolean rtfSpecialsTable[];
+ static {
+ rtfSpecialsTable = (boolean[])noSpecialsTable.clone();
+ rtfSpecialsTable['\n'] = true;
+ rtfSpecialsTable['\r'] = true;
+ rtfSpecialsTable['{'] = true;
+ rtfSpecialsTable['}'] = true;
+ rtfSpecialsTable['\\'] = true;
+ }
+
+ public RTFParser()
+ {
+ currentCharacters = new StringBuffer();
+ state = S_text;
+ pendingKeyword = null;
+ level = 0;
+ //warnings = System.out;
+
+ specialsTable = rtfSpecialsTable;
+ }
+
+ // TODO: Handle wrapup at end of file correctly.
+
+ public void writeSpecial(int b)
+ throws IOException
+ {
+ write((char)b);
+ }
+
+ protected void warning(String s) {
+ if (warnings != null) {
+ warnings.println(s);
+ }
+ }
+
+ public void write(String s)
+ throws IOException
+ {
+ if (state != S_text) {
+ int index = 0;
+ int length = s.length();
+ while(index < length && state != S_text) {
+ write(s.charAt(index));
+ index ++;
+ }
+
+ if(index >= length)
+ return;
+
+ s = s.substring(index);
+ }
+
+ if (currentCharacters.length() > 0)
+ currentCharacters.append(s);
+ else
+ handleText(s);
+ }
+
+ public void write(char ch)
+ throws IOException
+ {
+ boolean ok;
+
+ switch (state)
+ {
+ case S_text:
+ if (ch == '\n' || ch == '\r') {
+ break; // unadorned newlines are ignored
+ } else if (ch == '{') {
+ if (currentCharacters.length() > 0) {
+ handleText(currentCharacters.toString());
+ currentCharacters = new StringBuffer();
+ }
+ level ++;
+ begingroup();
+ } else if(ch == '}') {
+ if (currentCharacters.length() > 0) {
+ handleText(currentCharacters.toString());
+ currentCharacters = new StringBuffer();
+ }
+ if (level == 0)
+ throw new IOException("Too many close-groups in RTF text");
+ endgroup();
+ level --;
+ } else if(ch == '\\') {
+ if (currentCharacters.length() > 0) {
+ handleText(currentCharacters.toString());
+ currentCharacters = new StringBuffer();
+ }
+ state = S_backslashed;
+ } else {
+ currentCharacters.append(ch);
+ }
+ break;
+ case S_backslashed:
+ if (ch == '\'') {
+ state = S_aftertick;
+ break;
+ }
+ if (!Character.isLetter(ch)) {
+ char newstring[] = new char[1];
+ newstring[0] = ch;
+ if (!handleKeyword(new String(newstring))) {
+ warning("Unknown keyword: " + newstring + " (" + (byte)ch + ")");
+ }
+ state = S_text;
+ pendingKeyword = null;
+ /* currentCharacters is already an empty stringBuffer */
+ break;
+ }
+
+ state = S_token;
+ /* FALL THROUGH */
+ case S_token:
+ if (Character.isLetter(ch)) {
+ currentCharacters.append(ch);
+ } else {
+ pendingKeyword = currentCharacters.toString();
+ currentCharacters = new StringBuffer();
+
+ // Parameter following?
+ if (Character.isDigit(ch) || (ch == '-')) {
+ state = S_parameter;
+ currentCharacters.append(ch);
+ } else {
+ ok = handleKeyword(pendingKeyword);
+ if (!ok)
+ warning("Unknown keyword: " + pendingKeyword);
+ pendingKeyword = null;
+ state = S_text;
+
+ // Non-space delimiters get included in the text
+ if (!Character.isWhitespace(ch))
+ write(ch);
+ }
+ }
+ break;
+ case S_parameter:
+ if (Character.isDigit(ch)) {
+ currentCharacters.append(ch);
+ } else {
+ /* TODO: Test correct behavior of \bin keyword */
+ if (pendingKeyword.equals("bin")) { /* magic layer-breaking kwd */
+ long parameter = Long.parseLong(currentCharacters.toString());
+ pendingKeyword = null;
+ state = S_inblob;
+ binaryBytesLeft = parameter;
+ if (binaryBytesLeft > Integer.MAX_VALUE)
+ binaryBuf = new ByteArrayOutputStream(Integer.MAX_VALUE);
+ else
+ binaryBuf = new ByteArrayOutputStream((int)binaryBytesLeft);
+ savedSpecials = specialsTable;
+ specialsTable = allSpecialsTable;
+ break;
+ }
+
+ int parameter = Integer.parseInt(currentCharacters.toString());
+ ok = handleKeyword(pendingKeyword, parameter);
+ if (!ok)
+ warning("Unknown keyword: " + pendingKeyword +
+ " (param " + currentCharacters + ")");
+ pendingKeyword = null;
+ currentCharacters = new StringBuffer();
+ state = S_text;
+
+ // Delimiters here are interpreted as text too
+ if (!Character.isWhitespace(ch))
+ write(ch);
+ }
+ break;
+ case S_aftertick:
+ if (Character.digit(ch, 16) == -1)
+ state = S_text;
+ else {
+ pendingCharacter = Character.digit(ch, 16);
+ state = S_aftertickc;
+ }
+ break;
+ case S_aftertickc:
+ state = S_text;
+ if (Character.digit(ch, 16) != -1)
+ {
+ pendingCharacter = pendingCharacter * 16 + Character.digit(ch, 16);
+ ch = translationTable[pendingCharacter];
+ if (ch != 0)
+ handleText(ch);
+ }
+ break;
+ case S_inblob:
+ binaryBuf.write(ch);
+ binaryBytesLeft --;
+ if (binaryBytesLeft == 0) {
+ state = S_text;
+ specialsTable = savedSpecials;
+ savedSpecials = null;
+ handleBinaryBlob(binaryBuf.toByteArray());
+ binaryBuf = null;
+ }
+ }
+ }
+
+ /** Flushes any buffered but not yet written characters.
+ * Subclasses which override this method should call this
+ * method <em>before</em> flushing
+ * any of their own buffers. */
+ public void flush()
+ throws IOException
+ {
+ super.flush();
+
+ if (state == S_text && currentCharacters.length() > 0) {
+ handleText(currentCharacters.toString());
+ currentCharacters = new StringBuffer();
+ }
+ }
+
+ /** Closes the parser. Currently, this simply does a <code>flush()</code>,
+ * followed by some minimal consistency checks. */
+ public void close()
+ throws IOException
+ {
+ flush();
+
+ if (state != S_text || level > 0) {
+ warning("Truncated RTF file.");
+
+ /* TODO: any sane way to handle termination in a non-S_text state? */
+ /* probably not */
+
+ /* this will cause subclasses to behave more reasonably
+ some of the time */
+ while (level > 0) {
+ endgroup();
+ level --;
+ }
+ }
+
+ super.close();
+ }
+
+}
diff --git a/src/share/classes/javax/swing/text/rtf/RTFReader.java b/src/share/classes/javax/swing/text/rtf/RTFReader.java
new file mode 100644
index 000000000..a75111299
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/RTFReader.java
@@ -0,0 +1,1647 @@
+/*
+ * Copyright 1997-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.rtf;
+
+import java.lang.*;
+import java.util.*;
+import java.io.*;
+import java.awt.Font;
+import java.awt.Color;
+
+import javax.swing.text.*;
+
+/**
+ * Takes a sequence of RTF tokens and text and appends the text
+ * described by the RTF to a <code>StyledDocument</code> (the <em>target</em>).
+ * The RTF is lexed
+ * from the character stream by the <code>RTFParser</code> which is this class's
+ * superclass.
+ *
+ * This class is an indirect subclass of OutputStream. It must be closed
+ * in order to guarantee that all of the text has been sent to
+ * the text acceptor.
+ *
+ * @see RTFParser
+ * @see java.io.OutputStream
+ */
+class RTFReader extends RTFParser
+{
+ /** The object to which the parsed text is sent. */
+ StyledDocument target;
+
+ /** Miscellaneous information about the parser's state. This
+ * dictionary is saved and restored when an RTF group begins
+ * or ends. */
+ Dictionary parserState; /* Current parser state */
+ /** This is the "dst" item from parserState. rtfDestination
+ * is the current rtf destination. It is cached in an instance
+ * variable for speed. */
+ Destination rtfDestination;
+ /** This holds the current document attributes. */
+ MutableAttributeSet documentAttributes;
+
+ /** This Dictionary maps Integer font numbers to String font names. */
+ Dictionary fontTable;
+ /** This array maps color indices to Color objects. */
+ Color[] colorTable;
+ /** This array maps character style numbers to Style objects. */
+ Style[] characterStyles;
+ /** This array maps paragraph style numbers to Style objects. */
+ Style[] paragraphStyles;
+ /** This array maps section style numbers to Style objects. */
+ Style[] sectionStyles;
+
+ /** This is the RTF version number, extracted from the \rtf keyword.
+ * The version information is currently not used. */
+ int rtfversion;
+
+ /** <code>true</code> to indicate that if the next keyword is unknown,
+ * the containing group should be ignored. */
+ boolean ignoreGroupIfUnknownKeyword;
+
+ /** The parameter of the most recently parsed \\ucN keyword,
+ * used for skipping alternative representations after a
+ * Unicode character. */
+ int skippingCharacters;
+
+ static private Dictionary straightforwardAttributes;
+ static {
+ straightforwardAttributes = RTFAttributes.attributesByKeyword();
+ }
+
+ private MockAttributeSet mockery;
+
+ /* this should be final, but there's a bug in javac... */
+ /** textKeywords maps RTF keywords to single-character strings,
+ * for those keywords which simply insert some text. */
+ static Dictionary textKeywords = null;
+ static {
+ textKeywords = new Hashtable();
+ textKeywords.put("\\", "\\");
+ textKeywords.put("{", "{");
+ textKeywords.put("}", "}");
+ textKeywords.put(" ", "\u00A0"); /* not in the spec... */
+ textKeywords.put("~", "\u00A0"); /* nonbreaking space */
+ textKeywords.put("_", "\u2011"); /* nonbreaking hyphen */
+ textKeywords.put("bullet", "\u2022");
+ textKeywords.put("emdash", "\u2014");
+ textKeywords.put("emspace", "\u2003");
+ textKeywords.put("endash", "\u2013");
+ textKeywords.put("enspace", "\u2002");
+ textKeywords.put("ldblquote", "\u201C");
+ textKeywords.put("lquote", "\u2018");
+ textKeywords.put("ltrmark", "\u200E");
+ textKeywords.put("rdblquote", "\u201D");
+ textKeywords.put("rquote", "\u2019");
+ textKeywords.put("rtlmark", "\u200F");
+ textKeywords.put("tab", "\u0009");
+ textKeywords.put("zwj", "\u200D");
+ textKeywords.put("zwnj", "\u200C");
+
+ /* There is no Unicode equivalent to an optional hyphen, as far as
+ I can tell. */
+ textKeywords.put("-", "\u2027"); /* TODO: optional hyphen */
+ }
+
+ /* some entries in parserState */
+ static final String TabAlignmentKey = "tab_alignment";
+ static final String TabLeaderKey = "tab_leader";
+
+ static Dictionary characterSets;
+ static boolean useNeXTForAnsi = false;
+ static {
+ characterSets = new Hashtable();
+ }
+
+/* TODO: per-font font encodings ( \fcharset control word ) ? */
+
+/**
+ * Creates a new RTFReader instance. Text will be sent to
+ * the specified TextAcceptor.
+ *
+ * @param destination The TextAcceptor which is to receive the text.
+ */
+public RTFReader(StyledDocument destination)
+{
+ int i;
+
+ target = destination;
+ parserState = new Hashtable();
+ fontTable = new Hashtable();
+
+ rtfversion = -1;
+
+ mockery = new MockAttributeSet();
+ documentAttributes = new SimpleAttributeSet();
+}
+
+/** Called when the RTFParser encounters a bin keyword in the
+ * RTF stream.
+ *
+ * @see RTFParser
+ */
+public void handleBinaryBlob(byte[] data)
+{
+ if (skippingCharacters > 0) {
+ /* a blob only counts as one character for skipping purposes */
+ skippingCharacters --;
+ return;
+ }
+
+ /* someday, someone will want to do something with blobs */
+}
+
+
+/**
+ * Handles any pure text (containing no control characters) in the input
+ * stream. Called by the superclass. */
+public void handleText(String text)
+{
+ if (skippingCharacters > 0) {
+ if (skippingCharacters >= text.length()) {
+ skippingCharacters -= text.length();
+ return;
+ } else {
+ text = text.substring(skippingCharacters);
+ skippingCharacters = 0;
+ }
+ }
+
+ if (rtfDestination != null) {
+ rtfDestination.handleText(text);
+ return;
+ }
+
+ warning("Text with no destination. oops.");
+}
+
+/** The default color for text which has no specified color. */
+Color defaultColor()
+{
+ return Color.black;
+}
+
+/** Called by the superclass when a new RTF group is begun.
+ * This implementation saves the current <code>parserState</code>, and gives
+ * the current destination a chance to save its own state.
+ * @see RTFParser#begingroup
+ */
+public void begingroup()
+{
+ if (skippingCharacters > 0) {
+ /* TODO this indicates an error in the RTF. Log it? */
+ skippingCharacters = 0;
+ }
+
+ /* we do this little dance to avoid cloning the entire state stack and
+ immediately throwing it away. */
+ Object oldSaveState = parserState.get("_savedState");
+ if (oldSaveState != null)
+ parserState.remove("_savedState");
+ Dictionary saveState = (Dictionary)((Hashtable)parserState).clone();
+ if (oldSaveState != null)
+ saveState.put("_savedState", oldSaveState);
+ parserState.put("_savedState", saveState);
+
+ if (rtfDestination != null)
+ rtfDestination.begingroup();
+}
+
+/** Called by the superclass when the current RTF group is closed.
+ * This restores the parserState saved by <code>begingroup()</code>
+ * as well as invoking the endgroup method of the current
+ * destination.
+ * @see RTFParser#endgroup
+ */
+public void endgroup()
+{
+ if (skippingCharacters > 0) {
+ /* NB this indicates an error in the RTF. Log it? */
+ skippingCharacters = 0;
+ }
+
+ Dictionary restoredState = (Dictionary)parserState.get("_savedState");
+ Destination restoredDestination = (Destination)restoredState.get("dst");
+ if (restoredDestination != rtfDestination) {
+ rtfDestination.close(); /* allow the destination to clean up */
+ rtfDestination = restoredDestination;
+ }
+ Dictionary oldParserState = parserState;
+ parserState = restoredState;
+ if (rtfDestination != null)
+ rtfDestination.endgroup(oldParserState);
+}
+
+protected void setRTFDestination(Destination newDestination)
+{
+ /* Check that setting the destination won't close the
+ current destination (should never happen) */
+ Dictionary previousState = (Dictionary)parserState.get("_savedState");
+ if (previousState != null) {
+ if (rtfDestination != previousState.get("dst")) {
+ warning("Warning, RTF destination overridden, invalid RTF.");
+ rtfDestination.close();
+ }
+ }
+ rtfDestination = newDestination;
+ parserState.put("dst", rtfDestination);
+}
+
+/** Called by the user when there is no more input (<i>i.e.</i>,
+ * at the end of the RTF file.)
+ *
+ * @see OutputStream#close
+ */
+public void close()
+ throws IOException
+{
+ Enumeration docProps = documentAttributes.getAttributeNames();
+ while(docProps.hasMoreElements()) {
+ Object propName = docProps.nextElement();
+ target.putProperty(propName,
+ documentAttributes.getAttribute((String)propName));
+ }
+
+ /* RTFParser should have ensured that all our groups are closed */
+
+ warning("RTF filter done.");
+
+ super.close();
+}
+
+/**
+ * Handles a parameterless RTF keyword. This is called by the superclass
+ * (RTFParser) when a keyword is found in the input stream.
+ *
+ * @returns <code>true</code> if the keyword is recognized and handled;
+ * <code>false</code> otherwise
+ * @see RTFParser#handleKeyword
+ */
+public boolean handleKeyword(String keyword)
+{
+ Object item;
+ boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;
+
+ if (skippingCharacters > 0) {
+ skippingCharacters --;
+ return true;
+ }
+
+ ignoreGroupIfUnknownKeyword = false;
+
+ if ((item = textKeywords.get(keyword)) != null) {
+ handleText((String)item);
+ return true;
+ }
+
+ if (keyword.equals("fonttbl")) {
+ setRTFDestination(new FonttblDestination());
+ return true;
+ }
+
+ if (keyword.equals("colortbl")) {
+ setRTFDestination(new ColortblDestination());
+ return true;
+ }
+
+ if (keyword.equals("stylesheet")) {
+ setRTFDestination(new StylesheetDestination());
+ return true;
+ }
+
+ if (keyword.equals("info")) {
+ setRTFDestination(new InfoDestination());
+ return false;
+ }
+
+ if (keyword.equals("mac")) {
+ setCharacterSet("mac");
+ return true;
+ }
+
+ if (keyword.equals("ansi")) {
+ if (useNeXTForAnsi)
+ setCharacterSet("NeXT");
+ else
+ setCharacterSet("ansi");
+ return true;
+ }
+
+ if (keyword.equals("next")) {
+ setCharacterSet("NeXT");
+ return true;
+ }
+
+ if (keyword.equals("pc")) {
+ setCharacterSet("cpg437"); /* IBM Code Page 437 */
+ return true;
+ }
+
+ if (keyword.equals("pca")) {
+ setCharacterSet("cpg850"); /* IBM Code Page 850 */
+ return true;
+ }
+
+ if (keyword.equals("*")) {
+ ignoreGroupIfUnknownKeyword = true;
+ return true;
+ }
+
+ if (rtfDestination != null) {
+ if(rtfDestination.handleKeyword(keyword))
+ return true;
+ }
+
+ /* this point is reached only if the keyword is unrecognized */
+
+ /* other destinations we don't understand and therefore ignore */
+ if (keyword.equals("aftncn") ||
+ keyword.equals("aftnsep") ||
+ keyword.equals("aftnsepc") ||
+ keyword.equals("annotation") ||
+ keyword.equals("atnauthor") ||
+ keyword.equals("atnicn") ||
+ keyword.equals("atnid") ||
+ keyword.equals("atnref") ||
+ keyword.equals("atntime") ||
+ keyword.equals("atrfend") ||
+ keyword.equals("atrfstart") ||
+ keyword.equals("bkmkend") ||
+ keyword.equals("bkmkstart") ||
+ keyword.equals("datafield") ||
+ keyword.equals("do") ||
+ keyword.equals("dptxbxtext") ||
+ keyword.equals("falt") ||
+ keyword.equals("field") ||
+ keyword.equals("file") ||
+ keyword.equals("filetbl") ||
+ keyword.equals("fname") ||
+ keyword.equals("fontemb") ||
+ keyword.equals("fontfile") ||
+ keyword.equals("footer") ||
+ keyword.equals("footerf") ||
+ keyword.equals("footerl") ||
+ keyword.equals("footerr") ||
+ keyword.equals("footnote") ||
+ keyword.equals("ftncn") ||
+ keyword.equals("ftnsep") ||
+ keyword.equals("ftnsepc") ||
+ keyword.equals("header") ||
+ keyword.equals("headerf") ||
+ keyword.equals("headerl") ||
+ keyword.equals("headerr") ||
+ keyword.equals("keycode") ||
+ keyword.equals("nextfile") ||
+ keyword.equals("object") ||
+ keyword.equals("pict") ||
+ keyword.equals("pn") ||
+ keyword.equals("pnseclvl") ||
+ keyword.equals("pntxtb") ||
+ keyword.equals("pntxta") ||
+ keyword.equals("revtbl") ||
+ keyword.equals("rxe") ||
+ keyword.equals("tc") ||
+ keyword.equals("template") ||
+ keyword.equals("txe") ||
+ keyword.equals("xe")) {
+ ignoreGroupIfUnknownKeywordSave = true;
+ }
+
+ if (ignoreGroupIfUnknownKeywordSave) {
+ setRTFDestination(new DiscardingDestination());
+ }
+
+ return false;
+}
+
+/**
+ * Handles an RTF keyword and its integer parameter.
+ * This is called by the superclass
+ * (RTFParser) when a keyword is found in the input stream.
+ *
+ * @returns <code>true</code> if the keyword is recognized and handled;
+ * <code>false</code> otherwise
+ * @see RTFParser#handleKeyword
+ */
+public boolean handleKeyword(String keyword, int parameter)
+{
+ boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;
+
+ if (skippingCharacters > 0) {
+ skippingCharacters --;
+ return true;
+ }
+
+ ignoreGroupIfUnknownKeyword = false;
+
+ if (keyword.equals("uc")) {
+ /* count of characters to skip after a unicode character */
+ parserState.put("UnicodeSkip", Integer.valueOf(parameter));
+ return true;
+ }
+ if (keyword.equals("u")) {
+ if (parameter < 0)
+ parameter = parameter + 65536;
+ handleText((char)parameter);
+ Number skip = (Number)(parserState.get("UnicodeSkip"));
+ if (skip != null) {
+ skippingCharacters = skip.intValue();
+ } else {
+ skippingCharacters = 1;
+ }
+ return true;
+ }
+
+ if (keyword.equals("rtf")) {
+ rtfversion = parameter;
+ setRTFDestination(new DocumentDestination());
+ return true;
+ }
+
+ if (keyword.startsWith("NeXT") ||
+ keyword.equals("private"))
+ ignoreGroupIfUnknownKeywordSave = true;
+
+ if (rtfDestination != null) {
+ if(rtfDestination.handleKeyword(keyword, parameter))
+ return true;
+ }
+
+ /* this point is reached only if the keyword is unrecognized */
+
+ if (ignoreGroupIfUnknownKeywordSave) {
+ setRTFDestination(new DiscardingDestination());
+ }
+
+ return false;
+}
+
+private void setTargetAttribute(String name, Object value)
+{
+// target.changeAttributes(new LFDictionary(LFArray.arrayWithObject(value), LFArray.arrayWithObject(name)));
+}
+
+/**
+ * setCharacterSet sets the current translation table to correspond with
+ * the named character set. The character set is loaded if necessary.
+ *
+ * @see AbstractFilter
+ */
+public void setCharacterSet(String name)
+{
+ Object set;
+
+ try {
+ set = getCharacterSet(name);
+ } catch (Exception e) {
+ warning("Exception loading RTF character set \"" + name + "\": " + e);
+ set = null;
+ }
+
+ if (set != null) {
+ translationTable = (char[])set;
+ } else {
+ warning("Unknown RTF character set \"" + name + "\"");
+ if (!name.equals("ansi")) {
+ try {
+ translationTable = (char[])getCharacterSet("ansi");
+ } catch (IOException e) {
+ throw new InternalError("RTFReader: Unable to find character set resources (" + e + ")");
+ }
+ }
+ }
+
+ setTargetAttribute(Constants.RTFCharacterSet, name);
+}
+
+/** Adds a character set to the RTFReader's list
+ * of known character sets */
+public static void
+defineCharacterSet(String name, char[] table)
+{
+ if (table.length < 256)
+ throw new IllegalArgumentException("Translation table must have 256 entries.");
+ characterSets.put(name, table);
+}
+
+/** Looks up a named character set. A character set is a 256-entry
+ * array of characters, mapping unsigned byte values to their Unicode
+ * equivalents. The character set is loaded if necessary.
+ *
+ * @returns the character set
+ */
+public static Object
+getCharacterSet(final String name)
+ throws IOException
+{
+ char[] set;
+
+ set = (char [])characterSets.get(name);
+ if (set == null) {
+ InputStream charsetStream;
+ charsetStream = (InputStream)java.security.AccessController.
+ doPrivileged(new java.security.PrivilegedAction() {
+ public Object run() {
+ return RTFReader.class.getResourceAsStream
+ ("charsets/" + name + ".txt");
+ }
+ });
+ set = readCharset(charsetStream);
+ defineCharacterSet(name, set);
+ }
+ return set;
+}
+
+/** Parses a character set from an InputStream. The character set
+ * must contain 256 decimal integers, separated by whitespace, with
+ * no punctuation. B- and C- style comments are allowed.
+ *
+ * @returns the newly read character set
+ */
+static char[] readCharset(InputStream strm)
+ throws IOException
+{
+ char[] values = new char[256];
+ int i;
+ StreamTokenizer in = new StreamTokenizer(new BufferedReader(
+ new InputStreamReader(strm, "ISO-8859-1")));
+
+ in.eolIsSignificant(false);
+ in.commentChar('#');
+ in.slashSlashComments(true);
+ in.slashStarComments(true);
+
+ i = 0;
+ while (i < 256) {
+ int ttype;
+ try {
+ ttype = in.nextToken();
+ } catch (Exception e) {
+ throw new IOException("Unable to read from character set file (" + e + ")");
+ }
+ if (ttype != in.TT_NUMBER) {
+// System.out.println("Bad token: type=" + ttype + " tok=" + in.sval);
+ throw new IOException("Unexpected token in character set file");
+// continue;
+ }
+ values[i] = (char)(in.nval);
+ i++;
+ }
+
+ return values;
+}
+
+static char[] readCharset(java.net.URL href)
+ throws IOException
+{
+ return readCharset(href.openStream());
+}
+
+/** An interface (could be an entirely abstract class) describing
+ * a destination. The RTF reader always has a current destination
+ * which is where text is sent.
+ *
+ * @see RTFReader
+ */
+interface Destination {
+ void handleBinaryBlob(byte[] data);
+ void handleText(String text);
+ boolean handleKeyword(String keyword);
+ boolean handleKeyword(String keyword, int parameter);
+
+ void begingroup();
+ void endgroup(Dictionary oldState);
+
+ void close();
+}
+
+/** This data-sink class is used to implement ignored destinations
+ * (e.g. {\*\blegga blah blah blah} )
+ * It accepts all keywords and text but does nothing with them. */
+class DiscardingDestination implements Destination
+{
+ public void handleBinaryBlob(byte[] data)
+ {
+ /* Discard binary blobs. */
+ }
+
+ public void handleText(String text)
+ {
+ /* Discard text. */
+ }
+
+ public boolean handleKeyword(String text)
+ {
+ /* Accept and discard keywords. */
+ return true;
+ }
+
+ public boolean handleKeyword(String text, int parameter)
+ {
+ /* Accept and discard parameterized keywords. */
+ return true;
+ }
+
+ public void begingroup()
+ {
+ /* Ignore groups --- the RTFReader will keep track of the
+ current group level as necessary */
+ }
+
+ public void endgroup(Dictionary oldState)
+ {
+ /* Ignore groups */
+ }
+
+ public void close()
+ {
+ /* No end-of-destination cleanup needed */
+ }
+}
+
+/** Reads the fonttbl group, inserting fonts into the RTFReader's
+ * fontTable dictionary. */
+class FonttblDestination implements Destination
+{
+ int nextFontNumber;
+ Object fontNumberKey = null;
+ String nextFontFamily;
+
+ public void handleBinaryBlob(byte[] data)
+ { /* Discard binary blobs. */ }
+
+ public void handleText(String text)
+ {
+ int semicolon = text.indexOf(';');
+ String fontName;
+
+ if (semicolon > -1)
+ fontName = text.substring(0, semicolon);
+ else
+ fontName = text;
+
+
+ /* TODO: do something with the font family. */
+
+ if (nextFontNumber == -1
+ && fontNumberKey != null) {
+ //font name might be broken across multiple calls
+ fontName = fontTable.get(fontNumberKey) + fontName;
+ } else {
+ fontNumberKey = Integer.valueOf(nextFontNumber);
+ }
+ fontTable.put(fontNumberKey, fontName);
+
+ nextFontNumber = -1;
+ nextFontFamily = null;
+ return;
+ }
+
+ public boolean handleKeyword(String keyword)
+ {
+ if (keyword.charAt(0) == 'f') {
+ nextFontFamily = keyword.substring(1);
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean handleKeyword(String keyword, int parameter)
+ {
+ if (keyword.equals("f")) {
+ nextFontNumber = parameter;
+ return true;
+ }
+
+ return false;
+ }
+
+ /* Groups are irrelevant. */
+ public void begingroup() {}
+ public void endgroup(Dictionary oldState) {}
+
+ /* currently, the only thing we do when the font table ends is
+ dump its contents to the debugging log. */
+ public void close()
+ {
+ Enumeration nums = fontTable.keys();
+ warning("Done reading font table.");
+ while(nums.hasMoreElements()) {
+ Integer num = (Integer)nums.nextElement();
+ warning("Number " + num + ": " + fontTable.get(num));
+ }
+ }
+}
+
+/** Reads the colortbl group. Upon end-of-group, the RTFReader's
+ * color table is set to an array containing the read colors. */
+class ColortblDestination implements Destination
+{
+ int red, green, blue;
+ Vector proTemTable;
+
+ public ColortblDestination()
+ {
+ red = 0;
+ green = 0;
+ blue = 0;
+ proTemTable = new Vector();
+ }
+
+ public void handleText(String text)
+ {
+ int index = 0;
+
+ for (index = 0; index < text.length(); index ++) {
+ if (text.charAt(index) == ';') {
+ Color newColor;
+ newColor = new Color(red, green, blue);
+ proTemTable.addElement(newColor);
+ }
+ }
+ }
+
+ public void close()
+ {
+ int count = proTemTable.size();
+ warning("Done reading color table, " + count + " entries.");
+ colorTable = new Color[count];
+ proTemTable.copyInto(colorTable);
+ }
+
+ public boolean handleKeyword(String keyword, int parameter)
+ {
+ if (keyword.equals("red"))
+ red = parameter;
+ else if (keyword.equals("green"))
+ green = parameter;
+ else if (keyword.equals("blue"))
+ blue = parameter;
+ else
+ return false;
+
+ return true;
+ }
+
+ /* Colortbls don't understand any parameterless keywords */
+ public boolean handleKeyword(String keyword) { return false; }
+
+ /* Groups are irrelevant. */
+ public void begingroup() {}
+ public void endgroup(Dictionary oldState) {}
+
+ /* Shouldn't see any binary blobs ... */
+ public void handleBinaryBlob(byte[] data) {}
+}
+
+/** Handles the stylesheet keyword. Styles are read and sorted
+ * into the three style arrays in the RTFReader. */
+class StylesheetDestination
+ extends DiscardingDestination
+ implements Destination
+{
+ Dictionary definedStyles;
+
+ public StylesheetDestination()
+ {
+ definedStyles = new Hashtable();
+ }
+
+ public void begingroup()
+ {
+ setRTFDestination(new StyleDefiningDestination());
+ }
+
+ public void close()
+ {
+ Vector chrStyles, pgfStyles, secStyles;
+ chrStyles = new Vector();
+ pgfStyles = new Vector();
+ secStyles = new Vector();
+ Enumeration styles = definedStyles.elements();
+ while(styles.hasMoreElements()) {
+ StyleDefiningDestination style;
+ Style defined;
+ style = (StyleDefiningDestination)styles.nextElement();
+ defined = style.realize();
+ warning("Style "+style.number+" ("+style.styleName+"): "+defined);
+ String stype = (String)defined.getAttribute(Constants.StyleType);
+ Vector toSet;
+ if (stype.equals(Constants.STSection)) {
+ toSet = secStyles;
+ } else if (stype.equals(Constants.STCharacter)) {
+ toSet = chrStyles;
+ } else {
+ toSet = pgfStyles;
+ }
+ if (toSet.size() <= style.number)
+ toSet.setSize(style.number + 1);
+ toSet.setElementAt(defined, style.number);
+ }
+ if (!(chrStyles.isEmpty())) {
+ Style[] styleArray = new Style[chrStyles.size()];
+ chrStyles.copyInto(styleArray);
+ characterStyles = styleArray;
+ }
+ if (!(pgfStyles.isEmpty())) {
+ Style[] styleArray = new Style[pgfStyles.size()];
+ pgfStyles.copyInto(styleArray);
+ paragraphStyles = styleArray;
+ }
+ if (!(secStyles.isEmpty())) {
+ Style[] styleArray = new Style[secStyles.size()];
+ secStyles.copyInto(styleArray);
+ sectionStyles = styleArray;
+ }
+
+/* (old debugging code)
+ int i, m;
+ if (characterStyles != null) {
+ m = characterStyles.length;
+ for(i=0;i<m;i++)
+ warnings.println("chrStyle["+i+"]="+characterStyles[i]);
+ } else warnings.println("No character styles.");
+ if (paragraphStyles != null) {
+ m = paragraphStyles.length;
+ for(i=0;i<m;i++)
+ warnings.println("pgfStyle["+i+"]="+paragraphStyles[i]);
+ } else warnings.println("No paragraph styles.");
+ if (sectionStyles != null) {
+ m = characterStyles.length;
+ for(i=0;i<m;i++)
+ warnings.println("secStyle["+i+"]="+sectionStyles[i]);
+ } else warnings.println("No section styles.");
+*/
+ }
+
+ /** This subclass handles an individual style */
+ class StyleDefiningDestination
+ extends AttributeTrackingDestination
+ implements Destination
+ {
+ final int STYLENUMBER_NONE = 222;
+ boolean additive;
+ boolean characterStyle;
+ boolean sectionStyle;
+ public String styleName;
+ public int number;
+ int basedOn;
+ int nextStyle;
+ boolean hidden;
+
+ Style realizedStyle;
+
+ public StyleDefiningDestination()
+ {
+ additive = false;
+ characterStyle = false;
+ sectionStyle = false;
+ styleName = null;
+ number = 0;
+ basedOn = STYLENUMBER_NONE;
+ nextStyle = STYLENUMBER_NONE;
+ hidden = false;
+ }
+
+ public void handleText(String text)
+ {
+ if (styleName != null)
+ styleName = styleName + text;
+ else
+ styleName = text;
+ }
+
+ public void close() {
+ int semicolon = (styleName == null) ? 0 : styleName.indexOf(';');
+ if (semicolon > 0)
+ styleName = styleName.substring(0, semicolon);
+ definedStyles.put(Integer.valueOf(number), this);
+ super.close();
+ }
+
+ public boolean handleKeyword(String keyword)
+ {
+ if (keyword.equals("additive")) {
+ additive = true;
+ return true;
+ }
+ if (keyword.equals("shidden")) {
+ hidden = true;
+ return true;
+ }
+ return super.handleKeyword(keyword);
+ }
+
+ public boolean handleKeyword(String keyword, int parameter)
+ {
+ if (keyword.equals("s")) {
+ characterStyle = false;
+ sectionStyle = false;
+ number = parameter;
+ } else if (keyword.equals("cs")) {
+ characterStyle = true;
+ sectionStyle = false;
+ number = parameter;
+ } else if (keyword.equals("ds")) {
+ characterStyle = false;
+ sectionStyle = true;
+ number = parameter;
+ } else if (keyword.equals("sbasedon")) {
+ basedOn = parameter;
+ } else if (keyword.equals("snext")) {
+ nextStyle = parameter;
+ } else {
+ return super.handleKeyword(keyword, parameter);
+ }
+ return true;
+ }
+
+ public Style realize()
+ {
+ Style basis = null;
+ Style next = null;
+
+ if (realizedStyle != null)
+ return realizedStyle;
+
+ if (basedOn != STYLENUMBER_NONE) {
+ StyleDefiningDestination styleDest;
+ styleDest = (StyleDefiningDestination)definedStyles.get(Integer.valueOf(basedOn));
+ if (styleDest != null && styleDest != this) {
+ basis = styleDest.realize();
+ }
+ }
+
+ /* NB: Swing StyleContext doesn't allow distinct styles with
+ the same name; RTF apparently does. This may confuse the
+ user. */
+ realizedStyle = target.addStyle(styleName, basis);
+
+ if (characterStyle) {
+ realizedStyle.addAttributes(currentTextAttributes());
+ realizedStyle.addAttribute(Constants.StyleType,
+ Constants.STCharacter);
+ } else if (sectionStyle) {
+ realizedStyle.addAttributes(currentSectionAttributes());
+ realizedStyle.addAttribute(Constants.StyleType,
+ Constants.STSection);
+ } else { /* must be a paragraph style */
+ realizedStyle.addAttributes(currentParagraphAttributes());
+ realizedStyle.addAttribute(Constants.StyleType,
+ Constants.STParagraph);
+ }
+
+ if (nextStyle != STYLENUMBER_NONE) {
+ StyleDefiningDestination styleDest;
+ styleDest = (StyleDefiningDestination)definedStyles.get(Integer.valueOf(nextStyle));
+ if (styleDest != null) {
+ next = styleDest.realize();
+ }
+ }
+
+ if (next != null)
+ realizedStyle.addAttribute(Constants.StyleNext, next);
+ realizedStyle.addAttribute(Constants.StyleAdditive,
+ Boolean.valueOf(additive));
+ realizedStyle.addAttribute(Constants.StyleHidden,
+ Boolean.valueOf(hidden));
+
+ return realizedStyle;
+ }
+ }
+}
+
+/** Handles the info group. Currently no info keywords are recognized
+ * so this is a subclass of DiscardingDestination. */
+class InfoDestination
+ extends DiscardingDestination
+ implements Destination
+{
+}
+
+/** RTFReader.TextHandlingDestination is an abstract RTF destination
+ * which simply tracks the attributes specified by the RTF control words
+ * in internal form and can produce acceptable AttributeSets for the
+ * current character, paragraph, and section attributes. It is up
+ * to the subclasses to determine what is done with the actual text. */
+abstract class AttributeTrackingDestination implements Destination
+{
+ /** This is the "chr" element of parserState, cached for
+ * more efficient use */
+ MutableAttributeSet characterAttributes;
+ /** This is the "pgf" element of parserState, cached for
+ * more efficient use */
+ MutableAttributeSet paragraphAttributes;
+ /** This is the "sec" element of parserState, cached for
+ * more efficient use */
+ MutableAttributeSet sectionAttributes;
+
+ public AttributeTrackingDestination()
+ {
+ characterAttributes = rootCharacterAttributes();
+ parserState.put("chr", characterAttributes);
+ paragraphAttributes = rootParagraphAttributes();
+ parserState.put("pgf", paragraphAttributes);
+ sectionAttributes = rootSectionAttributes();
+ parserState.put("sec", sectionAttributes);
+ }
+
+ abstract public void handleText(String text);
+
+ public void handleBinaryBlob(byte[] data)
+ {
+ /* This should really be in TextHandlingDestination, but
+ * since *nobody* does anything with binary blobs, this
+ * is more convenient. */
+ warning("Unexpected binary data in RTF file.");
+ }
+
+ public void begingroup()
+ {
+ AttributeSet characterParent = currentTextAttributes();
+ AttributeSet paragraphParent = currentParagraphAttributes();
+ AttributeSet sectionParent = currentSectionAttributes();
+
+ /* It would probably be more efficient to use the
+ * resolver property of the attributes set for
+ * implementing rtf groups,
+ * but that's needed for styles. */
+
+ /* update the cached attribute dictionaries */
+ characterAttributes = new SimpleAttributeSet();
+ characterAttributes.addAttributes(characterParent);
+ parserState.put("chr", characterAttributes);
+
+ paragraphAttributes = new SimpleAttributeSet();
+ paragraphAttributes.addAttributes(paragraphParent);
+ parserState.put("pgf", paragraphAttributes);
+
+ sectionAttributes = new SimpleAttributeSet();
+ sectionAttributes.addAttributes(sectionParent);
+ parserState.put("sec", sectionAttributes);
+ }
+
+ public void endgroup(Dictionary oldState)
+ {
+ characterAttributes = (MutableAttributeSet)parserState.get("chr");
+ paragraphAttributes = (MutableAttributeSet)parserState.get("pgf");
+ sectionAttributes = (MutableAttributeSet)parserState.get("sec");
+ }
+
+ public void close()
+ {
+ }
+
+ public boolean handleKeyword(String keyword)
+ {
+ if (keyword.equals("ulnone")) {
+ return handleKeyword("ul", 0);
+ }
+
+ {
+ Object item = straightforwardAttributes.get(keyword);
+ if (item != null) {
+ RTFAttribute attr = (RTFAttribute)item;
+ boolean ok;
+
+ switch(attr.domain()) {
+ case RTFAttribute.D_CHARACTER:
+ ok = attr.set(characterAttributes);
+ break;
+ case RTFAttribute.D_PARAGRAPH:
+ ok = attr.set(paragraphAttributes);
+ break;
+ case RTFAttribute.D_SECTION:
+ ok = attr.set(sectionAttributes);
+ break;
+ case RTFAttribute.D_META:
+ mockery.backing = parserState;
+ ok = attr.set(mockery);
+ mockery.backing = null;
+ break;
+ case RTFAttribute.D_DOCUMENT:
+ ok = attr.set(documentAttributes);
+ break;
+ default:
+ /* should never happen */
+ ok = false;
+ break;
+ }
+ if (ok)
+ return true;
+ }
+ }
+
+
+ if (keyword.equals("plain")) {
+ resetCharacterAttributes();
+ return true;
+ }
+
+ if (keyword.equals("pard")) {
+ resetParagraphAttributes();
+ return true;
+ }
+
+ if (keyword.equals("sectd")) {
+ resetSectionAttributes();
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean handleKeyword(String keyword, int parameter)
+ {
+ boolean booleanParameter = (parameter != 0);
+
+ if (keyword.equals("fc"))
+ keyword = "cf"; /* whatEVER, dude. */
+
+ if (keyword.equals("f")) {
+ parserState.put(keyword, Integer.valueOf(parameter));
+ return true;
+ }
+ if (keyword.equals("cf")) {
+ parserState.put(keyword, Integer.valueOf(parameter));
+ return true;
+ }
+
+ {
+ Object item = straightforwardAttributes.get(keyword);
+ if (item != null) {
+ RTFAttribute attr = (RTFAttribute)item;
+ boolean ok;
+
+ switch(attr.domain()) {
+ case RTFAttribute.D_CHARACTER:
+ ok = attr.set(characterAttributes, parameter);
+ break;
+ case RTFAttribute.D_PARAGRAPH:
+ ok = attr.set(paragraphAttributes, parameter);
+ break;
+ case RTFAttribute.D_SECTION:
+ ok = attr.set(sectionAttributes, parameter);
+ break;
+ case RTFAttribute.D_META:
+ mockery.backing = parserState;
+ ok = attr.set(mockery, parameter);
+ mockery.backing = null;
+ break;
+ case RTFAttribute.D_DOCUMENT:
+ ok = attr.set(documentAttributes, parameter);
+ break;
+ default:
+ /* should never happen */
+ ok = false;
+ break;
+ }
+ if (ok)
+ return true;
+ }
+ }
+
+ if (keyword.equals("fs")) {
+ StyleConstants.setFontSize(characterAttributes, (parameter / 2));
+ return true;
+ }
+
+ /* TODO: superscript/subscript */
+
+ if (keyword.equals("sl")) {
+ if (parameter == 1000) { /* magic value! */
+ characterAttributes.removeAttribute(StyleConstants.LineSpacing);
+ } else {
+ /* TODO: The RTF sl attribute has special meaning if it's
+ negative. Make sure that SwingText has the same special
+ meaning, or find a way to imitate that. When SwingText
+ handles this, also recognize the slmult keyword. */
+ StyleConstants.setLineSpacing(characterAttributes,
+ parameter / 20f);
+ }
+ return true;
+ }
+
+ /* TODO: Other kinds of underlining */
+
+ if (keyword.equals("tx") || keyword.equals("tb")) {
+ float tabPosition = parameter / 20f;
+ int tabAlignment, tabLeader;
+ Number item;
+
+ tabAlignment = TabStop.ALIGN_LEFT;
+ item = (Number)(parserState.get("tab_alignment"));
+ if (item != null)
+ tabAlignment = item.intValue();
+ tabLeader = TabStop.LEAD_NONE;
+ item = (Number)(parserState.get("tab_leader"));
+ if (item != null)
+ tabLeader = item.intValue();
+ if (keyword.equals("tb"))
+ tabAlignment = TabStop.ALIGN_BAR;
+
+ parserState.remove("tab_alignment");
+ parserState.remove("tab_leader");
+
+ TabStop newStop = new TabStop(tabPosition, tabAlignment, tabLeader);
+ Dictionary tabs;
+ Integer stopCount;
+
+ tabs = (Dictionary)parserState.get("_tabs");
+ if (tabs == null) {
+ tabs = new Hashtable();
+ parserState.put("_tabs", tabs);
+ stopCount = Integer.valueOf(1);
+ } else {
+ stopCount = (Integer)tabs.get("stop count");
+ stopCount = Integer.valueOf(1 + stopCount.intValue());
+ }
+ tabs.put(stopCount, newStop);
+ tabs.put("stop count", stopCount);
+ parserState.remove("_tabs_immutable");
+
+ return true;
+ }
+
+ if (keyword.equals("s") &&
+ paragraphStyles != null) {
+ parserState.put("paragraphStyle", paragraphStyles[parameter]);
+ return true;
+ }
+
+ if (keyword.equals("cs") &&
+ characterStyles != null) {
+ parserState.put("characterStyle", characterStyles[parameter]);
+ return true;
+ }
+
+ if (keyword.equals("ds") &&
+ sectionStyles != null) {
+ parserState.put("sectionStyle", sectionStyles[parameter]);
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Returns a new MutableAttributeSet containing the
+ * default character attributes */
+ protected MutableAttributeSet rootCharacterAttributes()
+ {
+ MutableAttributeSet set = new SimpleAttributeSet();
+
+ /* TODO: default font */
+
+ StyleConstants.setItalic(set, false);
+ StyleConstants.setBold(set, false);
+ StyleConstants.setUnderline(set, false);
+ StyleConstants.setForeground(set, defaultColor());
+
+ return set;
+ }
+
+ /** Returns a new MutableAttributeSet containing the
+ * default paragraph attributes */
+ protected MutableAttributeSet rootParagraphAttributes()
+ {
+ MutableAttributeSet set = new SimpleAttributeSet();
+
+ StyleConstants.setLeftIndent(set, 0f);
+ StyleConstants.setRightIndent(set, 0f);
+ StyleConstants.setFirstLineIndent(set, 0f);
+
+ /* TODO: what should this be, really? */
+ set.setResolveParent(target.getStyle(StyleContext.DEFAULT_STYLE));
+
+ return set;
+ }
+
+ /** Returns a new MutableAttributeSet containing the
+ * default section attributes */
+ protected MutableAttributeSet rootSectionAttributes()
+ {
+ MutableAttributeSet set = new SimpleAttributeSet();
+
+ return set;
+ }
+
+ /**
+ * Calculates the current text (character) attributes in a form suitable
+ * for SwingText from the current parser state.
+ *
+ * @returns a new MutableAttributeSet containing the text attributes.
+ */
+ MutableAttributeSet currentTextAttributes()
+ {
+ MutableAttributeSet attributes =
+ new SimpleAttributeSet(characterAttributes);
+ Integer fontnum;
+ Integer stateItem;
+
+ /* figure out the font name */
+ /* TODO: catch exceptions for undefined attributes,
+ bad font indices, etc.? (as it stands, it is the caller's
+ job to clean up after corrupt RTF) */
+ fontnum = (Integer)parserState.get("f");
+ /* note setFontFamily() can not handle a null font */
+ String fontFamily;
+ if (fontnum != null)
+ fontFamily = (String)fontTable.get(fontnum);
+ else
+ fontFamily = null;
+ if (fontFamily != null)
+ StyleConstants.setFontFamily(attributes, fontFamily);
+ else
+ attributes.removeAttribute(StyleConstants.FontFamily);
+
+ if (colorTable != null) {
+ stateItem = (Integer)parserState.get("cf");
+ if (stateItem != null) {
+ Color fg = colorTable[stateItem.intValue()];
+ StyleConstants.setForeground(attributes, fg);
+ } else {
+ /* AttributeSet dies if you set a value to null */
+ attributes.removeAttribute(StyleConstants.Foreground);
+ }
+ }
+
+ if (colorTable != null) {
+ stateItem = (Integer)parserState.get("cb");
+ if (stateItem != null) {
+ Color bg = colorTable[stateItem.intValue()];
+ attributes.addAttribute(StyleConstants.Background,
+ bg);
+ } else {
+ /* AttributeSet dies if you set a value to null */
+ attributes.removeAttribute(StyleConstants.Background);
+ }
+ }
+
+ Style characterStyle = (Style)parserState.get("characterStyle");
+ if (characterStyle != null)
+ attributes.setResolveParent(characterStyle);
+
+ /* Other attributes are maintained directly in "attributes" */
+
+ return attributes;
+ }
+
+ /**
+ * Calculates the current paragraph attributes (with keys
+ * as given in StyleConstants) from the current parser state.
+ *
+ * @returns a newly created MutableAttributeSet.
+ * @see StyleConstants
+ */
+ MutableAttributeSet currentParagraphAttributes()
+ {
+ /* NB if there were a mutableCopy() method we should use it */
+ MutableAttributeSet bld = new SimpleAttributeSet(paragraphAttributes);
+
+ Integer stateItem;
+
+ /*** Tab stops ***/
+ TabStop tabs[];
+
+ tabs = (TabStop[])parserState.get("_tabs_immutable");
+ if (tabs == null) {
+ Dictionary workingTabs = (Dictionary)parserState.get("_tabs");
+ if (workingTabs != null) {
+ int count = ((Integer)workingTabs.get("stop count")).intValue();
+ tabs = new TabStop[count];
+ for (int ix = 1; ix <= count; ix ++)
+ tabs[ix-1] = (TabStop)workingTabs.get(Integer.valueOf(ix));
+ parserState.put("_tabs_immutable", tabs);
+ }
+ }
+ if (tabs != null)
+ bld.addAttribute(Constants.Tabs, tabs);
+
+ Style paragraphStyle = (Style)parserState.get("paragraphStyle");
+ if (paragraphStyle != null)
+ bld.setResolveParent(paragraphStyle);
+
+ return bld;
+ }
+
+ /**
+ * Calculates the current section attributes
+ * from the current parser state.
+ *
+ * @returns a newly created MutableAttributeSet.
+ */
+ public AttributeSet currentSectionAttributes()
+ {
+ MutableAttributeSet attributes = new SimpleAttributeSet(sectionAttributes);
+
+ Style sectionStyle = (Style)parserState.get("sectionStyle");
+ if (sectionStyle != null)
+ attributes.setResolveParent(sectionStyle);
+
+ return attributes;
+ }
+
+ /** Resets the filter's internal notion of the current character
+ * attributes to their default values. Invoked to handle the
+ * \plain keyword. */
+ protected void resetCharacterAttributes()
+ {
+ handleKeyword("f", 0);
+ handleKeyword("cf", 0);
+
+ handleKeyword("fs", 24); /* 12 pt. */
+
+ Enumeration attributes = straightforwardAttributes.elements();
+ while(attributes.hasMoreElements()) {
+ RTFAttribute attr = (RTFAttribute)attributes.nextElement();
+ if (attr.domain() == RTFAttribute.D_CHARACTER)
+ attr.setDefault(characterAttributes);
+ }
+
+ handleKeyword("sl", 1000);
+
+ parserState.remove("characterStyle");
+ }
+
+ /** Resets the filter's internal notion of the current paragraph's
+ * attributes to their default values. Invoked to handle the
+ * \pard keyword. */
+ protected void resetParagraphAttributes()
+ {
+ parserState.remove("_tabs");
+ parserState.remove("_tabs_immutable");
+ parserState.remove("paragraphStyle");
+
+ StyleConstants.setAlignment(paragraphAttributes,
+ StyleConstants.ALIGN_LEFT);
+
+ Enumeration attributes = straightforwardAttributes.elements();
+ while(attributes.hasMoreElements()) {
+ RTFAttribute attr = (RTFAttribute)attributes.nextElement();
+ if (attr.domain() == RTFAttribute.D_PARAGRAPH)
+ attr.setDefault(characterAttributes);
+ }
+ }
+
+ /** Resets the filter's internal notion of the current section's
+ * attributes to their default values. Invoked to handle the
+ * \sectd keyword. */
+ protected void resetSectionAttributes()
+ {
+ Enumeration attributes = straightforwardAttributes.elements();
+ while(attributes.hasMoreElements()) {
+ RTFAttribute attr = (RTFAttribute)attributes.nextElement();
+ if (attr.domain() == RTFAttribute.D_SECTION)
+ attr.setDefault(characterAttributes);
+ }
+
+ parserState.remove("sectionStyle");
+ }
+}
+
+/** RTFReader.TextHandlingDestination provides basic text handling
+ * functionality. Subclasses must implement: <dl>
+ * <dt>deliverText()<dd>to handle a run of text with the same
+ * attributes
+ * <dt>finishParagraph()<dd>to end the current paragraph and
+ * set the paragraph's attributes
+ * <dt>endSection()<dd>to end the current section
+ * </dl>
+ */
+abstract class TextHandlingDestination
+ extends AttributeTrackingDestination
+ implements Destination
+{
+ /** <code>true</code> if the reader has not just finished
+ * a paragraph; false upon startup */
+ boolean inParagraph;
+
+ public TextHandlingDestination()
+ {
+ super();
+ inParagraph = false;
+ }
+
+ public void handleText(String text)
+ {
+ if (! inParagraph)
+ beginParagraph();
+
+ deliverText(text, currentTextAttributes());
+ }
+
+ abstract void deliverText(String text, AttributeSet characterAttributes);
+
+ public void close()
+ {
+ if (inParagraph)
+ endParagraph();
+
+ super.close();
+ }
+
+ public boolean handleKeyword(String keyword)
+ {
+ if (keyword.equals("\r") || keyword.equals("\n")) {
+ keyword = "par";
+ }
+
+ if (keyword.equals("par")) {
+// warnings.println("Ending paragraph.");
+ endParagraph();
+ return true;
+ }
+
+ if (keyword.equals("sect")) {
+// warnings.println("Ending section.");
+ endSection();
+ return true;
+ }
+
+ return super.handleKeyword(keyword);
+ }
+
+ protected void beginParagraph()
+ {
+ inParagraph = true;
+ }
+
+ protected void endParagraph()
+ {
+ AttributeSet pgfAttributes = currentParagraphAttributes();
+ AttributeSet chrAttributes = currentTextAttributes();
+ finishParagraph(pgfAttributes, chrAttributes);
+ inParagraph = false;
+ }
+
+ abstract void finishParagraph(AttributeSet pgfA, AttributeSet chrA);
+
+ abstract void endSection();
+}
+
+/** RTFReader.DocumentDestination is a concrete subclass of
+ * TextHandlingDestination which appends the text to the
+ * StyledDocument given by the <code>target</code> ivar of the
+ * containing RTFReader.
+ */
+class DocumentDestination
+ extends TextHandlingDestination
+ implements Destination
+{
+ public void deliverText(String text, AttributeSet characterAttributes)
+ {
+ try {
+ target.insertString(target.getLength(),
+ text,
+ currentTextAttributes());
+ } catch (BadLocationException ble) {
+ /* This shouldn't be able to happen, of course */
+ /* TODO is InternalError the correct error to throw? */
+ throw new InternalError(ble.getMessage());
+ }
+ }
+
+ public void finishParagraph(AttributeSet pgfAttributes,
+ AttributeSet chrAttributes)
+ {
+ int pgfEndPosition = target.getLength();
+ try {
+ target.insertString(pgfEndPosition, "\n", chrAttributes);
+ target.setParagraphAttributes(pgfEndPosition, 1, pgfAttributes, true);
+ } catch (BadLocationException ble) {
+ /* This shouldn't be able to happen, of course */
+ /* TODO is InternalError the correct error to throw? */
+ throw new InternalError(ble.getMessage());
+ }
+ }
+
+ public void endSection()
+ {
+ /* If we implemented sections, we'd end 'em here */
+ }
+}
+
+}
diff --git a/src/share/classes/javax/swing/text/rtf/charsets/NeXT.txt b/src/share/classes/javax/swing/text/rtf/charsets/NeXT.txt
new file mode 100644
index 000000000..0b763563e
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/charsets/NeXT.txt
@@ -0,0 +1,33 @@
+/* the character set used for the \ansi control word in NeXT-rtf mode */
+0 1 2 3 4 5 6 7
+8 9 0 11 12 0 14 15
+16 17 18 19 20 21 22 23
+24 25 26 27 28 29 30 31
+32 33 34 35 36 37 38 39
+40 41 42 43 44 45 46 47
+48 49 50 51 52 53 54 55
+56 57 58 59 60 61 62 63
+64 65 66 67 68 69 70 71
+72 73 74 75 76 77 78 79
+80 81 82 83 84 85 86 87
+88 89 90 91 0 93 94 95
+96 97 98 99 100 101 102 103
+104 105 106 107 108 109 110 111
+112 113 114 115 116 117 118 119
+120 121 122 0 124 0 126 127
+160 192 193 194 195 196 197 199
+200 201 202 203 204 205 206 207
+208 209 210 211 212 213 214 217
+218 219 220 221 222 181 215 247
+169 161 162 163 8260 165 402 167
+164 8217 8220 171 8249 8250 64257 64258
+174 8211 8224 8225 183 166 182 8226
+8218 8222 8221 187 8230 8240 172 191
+185 715 180 710 732 175 728 729
+168 178 730 184 179 733 731 711
+8212 177 188 189 190 224 225 226
+227 228 229 231 232 233 234 235
+236 198 237 170 238 239 240 241
+321 216 338 186 242 243 244 245
+246 230 249 250 251 305 252 253
+322 248 339 223 254 255 0 0
diff --git a/src/share/classes/javax/swing/text/rtf/charsets/ansi.txt b/src/share/classes/javax/swing/text/rtf/charsets/ansi.txt
new file mode 100644
index 000000000..c32fe6e77
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/charsets/ansi.txt
@@ -0,0 +1,38 @@
+# The character set used to read documents with the \ansi control
+# word. The default "ansi" character set doesn't seem to be defined
+# anywhere; this table is derived from the behavior of MSWord97
+# and some guesswork. For the most part it corresponds to
+# ISO 8859 Latin-1.
+0 1 2 3 4 5 6 7
+8 9 10 11 12 13 14 15
+16 17 18 19 20 21 22 23
+24 25 26 27 28 29 30 31
+32 33 34 35 36 37 38 39
+40 41 42 43 44 45 46 47
+48 49 50 51 52 53 54 55
+56 57 58 59 60 61 62 63
+64 65 66 67 68 69 70 71
+72 73 74 75 76 77 78 79
+80 81 82 83 84 85 86 87
+88 89 90 91 92 93 94 95
+96 97 98 99 100 101 102 103
+104 105 106 107 108 109 110 111
+112 113 114 115 116 117 118 119
+120 121 122 123 124 125 126 127
+
+1026 1027 8218 402 8222 8230 8224 8225
+710 8240 352 8249 346 356 381 377
+1106 0 0 0 0 0 0 0
+0 8482 353 8250 347 357 382 378
+0 161 162 163 164 165 166 167
+168 169 170 171 172 173 174 175
+176 177 178 179 180 181 182 183
+184 185 186 187 188 189 190 191
+192 193 194 195 196 197 198 199
+200 201 202 203 204 205 206 207
+208 209 210 211 212 213 214 215
+216 217 218 219 220 221 222 223
+224 225 226 227 228 229 230 231
+232 233 234 235 236 237 238 239
+240 241 242 243 244 245 246 247
+248 249 250 251 252 253 254 255
diff --git a/src/share/classes/javax/swing/text/rtf/charsets/cpg437.txt b/src/share/classes/javax/swing/text/rtf/charsets/cpg437.txt
new file mode 100644
index 000000000..89b978130
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/charsets/cpg437.txt
@@ -0,0 +1,45 @@
+/* IBM/Microsoft Code Page 437 character set */
+/* Derived from tables on ftp.unicode.org */
+/* Original header:
+#
+# Name: cp437_DOSLatinUS to Unicode table
+# Unicode version: 2.0
+# Table version: 2.00
+# Table format: Format A
+# Date: 04/24/96
+# Authors: Lori Brownell <loribr@microsoft.com>
+# K.D. Chang <a-kchang@microsoft.com>
+# General notes: none
+*/
+0 1 2 3 4 5 6 7
+8 9 10 11 12 13 14 15
+16 17 18 19 20 21 22 23
+24 25 26 27 28 29 30 31
+32 33 34 35 36 37 38 39
+40 41 42 43 44 45 46 47
+48 49 50 51 52 53 54 55
+56 57 58 59 60 61 62 63
+64 65 66 67 68 69 70 71
+72 73 74 75 76 77 78 79
+80 81 82 83 84 85 86 87
+88 89 90 91 92 93 94 95
+96 97 98 99 100 101 102 103
+104 105 106 107 108 109 110 111
+112 113 114 115 116 117 118 119
+120 121 122 123 124 125 126 127
+199 252 233 226 228 224 229 231
+234 235 232 239 238 236 196 197
+201 230 198 244 246 242 251 249
+255 214 220 162 163 165 8359 402
+225 237 243 250 241 209 170 186
+191 8976 172 189 188 161 171 187
+9617 9618 9619 9474 9508 9569 9570 9558
+9557 9571 9553 9559 9565 9564 9563 9488
+9492 9524 9516 9500 9472 9532 9566 9567
+9562 9556 9577 9574 9568 9552 9580 9575
+9576 9572 9573 9561 9560 9554 9555 9579
+9578 9496 9484 9608 9604 9612 9616 9600
+945 223 915 960 931 963 181 964
+934 920 937 948 8734 966 949 8745
+8801 177 8805 8804 8992 8993 247 8776
+176 8729 183 8730 8319 178 9632 160
diff --git a/src/share/classes/javax/swing/text/rtf/charsets/cpg850.txt b/src/share/classes/javax/swing/text/rtf/charsets/cpg850.txt
new file mode 100644
index 000000000..3f42a091a
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/charsets/cpg850.txt
@@ -0,0 +1,44 @@
+/* IBM/Microsoft Code Page 850 character set */
+/* derived form tables on ftp.unicode.org */
+/* Original header:
+# Name: cp850_DOSLatin1 to Unicode table
+# Unicode version: 2.0
+# Table version: 2.00
+# Table format: Format A
+# Date: 04/24/96
+# Authors: Lori Brownell <loribr@microsoft.com>
+# K.D. Chang <a-kchang@microsoft.com>
+# General notes: none
+*/
+0 1 2 3 4 5 6 7
+8 9 10 11 12 13 14 15
+16 17 18 19 20 21 22 23
+24 25 26 27 28 29 30 31
+32 33 34 35 36 37 38 39
+40 41 42 43 44 45 46 47
+48 49 50 51 52 53 54 55
+56 57 58 59 60 61 62 63
+64 65 66 67 68 69 70 71
+72 73 74 75 76 77 78 79
+80 81 82 83 84 85 86 87
+88 89 90 91 92 93 94 95
+96 97 98 99 100 101 102 103
+104 105 106 107 108 109 110 111
+112 113 114 115 116 117 118 119
+120 121 122 123 124 125 126 127
+199 252 233 226 228 224 229 231
+234 235 232 239 238 236 196 197
+201 230 198 244 246 242 251 249
+255 214 220 248 163 216 215 402
+225 237 243 250 241 209 170 186
+191 174 172 189 188 161 171 187
+9617 9618 9619 9474 9508 193 194 192
+169 9571 9553 9559 9565 162 165 9488
+9492 9524 9516 9500 9472 9532 227 195
+9562 9556 9577 9574 9568 9552 9580 164
+240 208 202 203 200 305 205 206
+207 9496 9484 9608 9604 166 204 9600
+211 223 212 210 245 213 181 254
+222 218 219 217 253 221 175 180
+173 177 8215 190 182 167 247 184
+176 168 183 185 179 178 9632 160
diff --git a/src/share/classes/javax/swing/text/rtf/charsets/mac.txt b/src/share/classes/javax/swing/text/rtf/charsets/mac.txt
new file mode 100644
index 000000000..adea5646e
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/charsets/mac.txt
@@ -0,0 +1,43 @@
+/* MS RTF MacRoman character set */
+/* Derived from tables on ftp.unicode.org */
+/* original header: follows: */
+# Name: cp10000_MacRoman to Unicode table
+# Unicode version: 2.0
+# Table version: 2.00
+# Table format: Format A
+# Date: 04/24/96
+# Authors: Lori Brownell <loribr@microsoft.com>
+# K.D. Chang <a-kchang@microsoft.com>
+# General notes: none
+0 1 2 3 4 5 6 7
+8 9 10 11 12 13 14 15
+16 17 18 19 20 21 22 23
+24 25 26 27 28 29 30 31
+32 33 34 35 36 37 38 39
+40 41 42 43 44 45 46 47
+48 49 50 51 52 53 54 55
+56 57 58 59 60 61 62 63
+64 65 66 67 68 69 70 71
+72 73 74 75 76 77 78 79
+80 81 82 83 84 85 86 87
+88 89 90 91 92 93 94 95
+96 97 98 99 100 101 102 103
+104 105 106 107 108 109 110 111
+112 113 114 115 116 117 118 119
+120 121 122 123 124 125 126 127
+196 197 199 201 209 214 220 225
+224 226 228 227 229 231 233 232
+234 235 237 236 238 239 241 243
+242 244 246 245 250 249 251 252
+8224 176 162 163 167 8226 182 223
+174 169 8482 180 168 8800 198 216
+8734 177 8804 8805 165 181 8706 8721
+8719 960 8747 170 186 8486 230 248
+191 161 172 8730 402 8776 8710 171
+187 8230 160 192 195 213 338 339
+8211 8212 8220 8221 8216 8217 247 9674
+255 376 8260 164 8249 8250 64257 64258
+8225 183 8218 8222 8240 194 202 193
+203 200 205 206 207 204 211 212
+0 210 218 219 217 305 710 732
+175 728 729 730 184 733 731 711
diff --git a/src/share/classes/javax/swing/text/rtf/package.html b/src/share/classes/javax/swing/text/rtf/package.html
new file mode 100644
index 000000000..090ff7a13
--- /dev/null
+++ b/src/share/classes/javax/swing/text/rtf/package.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+<!--
+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.
+-->
+
+</head>
+<body bgcolor="white">
+
+Provides a class (<code>RTFEditorKit</code>) for creating Rich-Text-Format
+text editors.
+
+<p>
+
+<strong>Note:</strong>
+Most of the Swing API is <em>not</em> thread safe.
+For details, see
+<a
+href="http://java.sun.com/docs/books/tutorial/uiswing/overview/threads.html"
+target="_top">Threads and Swing</a>,
+a section in
+<em><a href="http://java.sun.com/docs/books/tutorial/"
+target="_top">The Java Tutorial</a></em>.
+
+@since 1.2
+@serial exclude
+
+</body>
+</html>