diff options
author | duke <none@none> | 2007-12-01 00:00:00 +0000 |
---|---|---|
committer | duke <none@none> | 2007-12-01 00:00:00 +0000 |
commit | 59308f67f9b7038cfa2ceb9ee9ba27645b927cb5 (patch) | |
tree | 182810ab2fece13f57a928d026f93e9ede0827f9 /src/share/classes/javax/swing/text |
Initial loadjdk7-b24
Diffstat (limited to 'src/share/classes/javax/swing/text')
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> + * int nleft = doc.getDocumentLength(); + * Segment text = new Segment(); + * int offs = 0; + * text.setPartialReturn(true); + * while (nleft > 0) { + * doc.getText(offs, nleft, text); + * // do something with text + * nleft -= text.count; + * offs += text.count; + * } + * </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> + * readLock(); + * try { + * // do something + * } finally { + * readUnlock(); + * } + * </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 >= 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 >= 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 >= 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 >= 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 >= 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 >= 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> + * + * int nleft = doc.getDocumentLength(); + * Segment text = new Segment(); + * int offs = 0; + * text.setPartialReturn(true); + * while (nleft > 0) { + * doc.getText(offs, nleft, text); + * // do someting with text + * nleft -= text.count; + * offs += text.count; + * } + * + * </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("My text component header"), + * new MessageFormat("Footer. Page - {0}"), 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<Boolean> future = + * new FutureTask<Boolean>( + * new Callable<Boolean>() { + * 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 </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> + * + * TABLE + * ROW + * CELL + * CELL + * ROW + * CELL + * 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 <= the preferred span which in turn + will be <= 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 Binary files differnew file mode 100644 index 000000000..116d99844 --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/Document-coord.gif 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 Binary files differnew file mode 100644 index 000000000..7e943ce77 --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/Document-insert.gif 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 Binary files differnew file mode 100644 index 000000000..02579f36c --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/Document-notification.gif 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 Binary files differnew file mode 100644 index 000000000..69cb1f53c --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/Document-remove.gif 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 Binary files differnew file mode 100644 index 000000000..47e78e263 --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/Document-structure.gif diff --git a/src/share/classes/javax/swing/text/doc-files/OpenBookIcon.gif b/src/share/classes/javax/swing/text/doc-files/OpenBookIcon.gif Binary files differnew file mode 100644 index 000000000..86384f773 --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/OpenBookIcon.gif 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 Binary files differnew file mode 100644 index 000000000..599ca4622 --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/View-flexibility.jpg 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 Binary files differnew file mode 100644 index 000000000..bcbec154d --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/View-layout.jpg diff --git a/src/share/classes/javax/swing/text/doc-files/editor.gif b/src/share/classes/javax/swing/text/doc-files/editor.gif Binary files differnew file mode 100644 index 000000000..d588df80f --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/editor.gif diff --git a/src/share/classes/javax/swing/text/doc-files/paragraph.gif b/src/share/classes/javax/swing/text/doc-files/paragraph.gif Binary files differnew file mode 100644 index 000000000..6f56046b3 --- /dev/null +++ b/src/share/classes/javax/swing/text/doc-files/paragraph.gif 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 <BR> tag. In other words, forces a line break. + * + * @author Sunita Mani + */ +class BRView extends InlineView { + + /** + * Creates a new view that represents a <BR> element. + * + * @param elem the element to create a view for + */ + public BRView(Element elem) { + super(elem); + } + + /** + * Forces a line break. + * + * @return View.ForcedBreakWeight + */ + public int getBreakWeight(int axis, float pos, float len) { + if (axis == X_AXIS) { + return ForcedBreakWeight; + } else { + return super.getBreakWeight(axis, pos, len); + } + } +} diff --git a/src/share/classes/javax/swing/text/html/BlockView.java b/src/share/classes/javax/swing/text/html/BlockView.java new file mode 100644 index 000000000..8e453bddd --- /dev/null +++ b/src/share/classes/javax/swing/text/html/BlockView.java @@ -0,0 +1,443 @@ +/* + * Copyright 1997-2006 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.util.Enumeration; +import java.awt.*; +import javax.swing.SizeRequirements; +import javax.swing.border.*; +import javax.swing.event.DocumentEvent; +import javax.swing.text.*; + +/** + * A view implementation to display a block (as a box) + * with CSS specifications. + * + * @author Timothy Prinzing + */ +public class BlockView extends BoxView { + + /** + * Creates a new view that represents an + * html box. This can be used for a number + * of elements. + * + * @param elem the element to create a view for + * @param axis either View.X_AXIS or View.Y_AXIS + */ + public BlockView(Element elem, int axis) { + super(elem, axis); + } + + /** + * Establishes the parent view for this view. This is + * guaranteed to be called before any other methods if the + * parent view is functioning properly. + * <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, <input>, <textarea>, + * and <select>. The model for the component is stored + * as an attribute of the the element (using StyleConstants.ModelAttribute), + * and is used to build the component of the view. The type + * of the model is assumed to of the type that would be set by + * <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 > 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 <INPUT> element based on the + * value of the "type" attribute. + * + * @param set of attributes associated with the <INPUT> element. + * @param model the value of the StyleConstants.ModelAttribute + * @return the component. + */ + private JComponent createInputComponent(AttributeSet attr, Object model) { + JComponent c = null; + String type = (String) attr.getAttribute(HTML.Attribute.TYPE); + + if (type.equals("submit") || type.equals("reset")) { + String value = (String) + attr.getAttribute(HTML.Attribute.VALUE); + if (value == null) { + if (type.equals("submit")) { + value = UIManager.getString("FormView.submitButtonText"); + } else { + value = UIManager.getString("FormView.resetButtonText"); + } + } + JButton button = new JButton(value); + if (model != null) { + button.setModel((ButtonModel)model); + button.addActionListener(this); + } + c = button; + maxIsPreferred = 3; + } else if (type.equals("image")) { + String srcAtt = (String) attr.getAttribute(HTML.Attribute.SRC); + JButton button; + try { + URL base = ((HTMLDocument)getElement().getDocument()).getBase(); + URL srcURL = new URL(base, srcAtt); + Icon icon = new ImageIcon(srcURL); + button = new JButton(icon); + } catch (MalformedURLException e) { + button = new JButton(srcAtt); + } + if (model != null) { + button.setModel((ButtonModel)model); + button.addMouseListener(new MouseEventListener()); + } + c = button; + maxIsPreferred = 3; + } else if (type.equals("checkbox")) { + c = new JCheckBox(); + if (model != null) { + ((JCheckBox)c).setModel((JToggleButton.ToggleButtonModel) model); + } + maxIsPreferred = 3; + } else if (type.equals("radio")) { + c = new JRadioButton(); + if (model != null) { + ((JRadioButton)c).setModel((JToggleButton.ToggleButtonModel)model); + } + maxIsPreferred = 3; + } else if (type.equals("text")) { + int size = HTML.getIntegerAttributeValue(attr, + HTML.Attribute.SIZE, + -1); + JTextField field; + if (size > 0) { + field = new JTextField(); + field.setColumns(size); + } + else { + field = new JTextField(); + field.setColumns(20); + } + c = field; + if (model != null) { + field.setDocument((Document) model); + } + field.addActionListener(this); + maxIsPreferred = 3; + } else if (type.equals("password")) { + JPasswordField field = new JPasswordField(); + c = field; + if (model != null) { + field.setDocument((Document) model); + } + int size = HTML.getIntegerAttributeValue(attr, + HTML.Attribute.SIZE, + -1); + field.setColumns((size > 0) ? size : 20); + field.addActionListener(this); + maxIsPreferred = 3; + } else if (type.equals("file")) { + JTextField field = new JTextField(); + if (model != null) { + field.setDocument((Document)model); + } + int size = HTML.getIntegerAttributeValue(attr, HTML.Attribute.SIZE, + -1); + field.setColumns((size > 0) ? size : 20); + JButton browseButton = new JButton(UIManager.getString + ("FormView.browseFileButtonText")); + Box box = Box.createHorizontalBox(); + box.add(field); + box.add(Box.createHorizontalStrut(5)); + box.add(browseButton); + browseButton.addActionListener(new BrowseFileAction( + attr, (Document)model)); + c = box; + maxIsPreferred = 3; + } + return c; + } + + + /** + * Determines the maximum span for this view along an + * axis. For certain components, the maximum and preferred span are the + * same. For others this will return the value + * returned by Component.getMaximumSize along the + * axis of interest. + * + * @param axis may be either View.X_AXIS or View.Y_AXIS + * @return the span the view would like to be rendered into >= 0. + * Typically the view is told to render into the span + * that is returned, although there is no guarantee. + * The parent may choose to resize or break the view. + * @exception IllegalArgumentException for an invalid axis + */ + public float getMaximumSpan(int axis) { + switch (axis) { + case View.X_AXIS: + if ((maxIsPreferred & 1) == 1) { + super.getMaximumSpan(axis); + return getPreferredSpan(axis); + } + return super.getMaximumSpan(axis); + case View.Y_AXIS: + if ((maxIsPreferred & 2) == 2) { + super.getMaximumSpan(axis); + return getPreferredSpan(axis); + } + return super.getMaximumSpan(axis); + default: + break; + } + return super.getMaximumSpan(axis); + } + + + /** + * Responsible for processeing the ActionEvent. + * If the element associated with the FormView, + * has a type of "submit", "reset", "text" or "password" + * then the action is processed. In the case of a "submit" + * the form is submitted. In the case of a "reset" + * the form is reset to its original state. + * In the case of "text" or "password", if the + * element is the last one of type "text" or "password", + * the form is submitted. Otherwise, focus is transferred + * to the next component in the form. + * + * @param evt the ActionEvent. + */ + public void actionPerformed(ActionEvent evt) { + Element element = getElement(); + StringBuffer dataBuffer = new StringBuffer(); + HTMLDocument doc = (HTMLDocument)getDocument(); + AttributeSet attr = element.getAttributes(); + + String type = (String) attr.getAttribute(HTML.Attribute.TYPE); + + if (type.equals("submit")) { + getFormData(dataBuffer); + submitData(dataBuffer.toString()); + } else if (type.equals("reset")) { + resetForm(); + } else if (type.equals("text") || type.equals("password")) { + if (isLastTextOrPasswordField()) { + getFormData(dataBuffer); + submitData(dataBuffer.toString()); + } else { + getComponent().transferFocus(); + } + } + } + + + /** + * This method is responsible for submitting the form data. + * A thread is forked to undertake the submission. + */ + protected void submitData(String data) { + Element form = getFormElement(); + AttributeSet attrs = form.getAttributes(); + HTMLDocument doc = (HTMLDocument) form.getDocument(); + URL base = doc.getBase(); + + String target = (String) attrs.getAttribute(HTML.Attribute.TARGET); + if (target == null) { + target = "_self"; + } + + String method = (String) attrs.getAttribute(HTML.Attribute.METHOD); + if (method == null) { + method = "GET"; + } + method = method.toLowerCase(); + boolean isPostMethod = method.equals("post"); + if (isPostMethod) { + storePostData(doc, target, data); + } + + String action = (String) attrs.getAttribute(HTML.Attribute.ACTION); + URL actionURL; + try { + actionURL = (action == null) + ? new URL(base.getProtocol(), base.getHost(), + base.getPort(), base.getFile()) + : new URL(base, action); + if (!isPostMethod) { + String query = data.toString(); + actionURL = new URL(actionURL + "?" + query); + } + } catch (MalformedURLException e) { + actionURL = null; + } + final JEditorPane c = (JEditorPane) getContainer(); + HTMLEditorKit kit = (HTMLEditorKit) c.getEditorKit(); + + FormSubmitEvent formEvent = null; + if (!kit.isAutoFormSubmission() || doc.isFrameDocument()) { + FormSubmitEvent.MethodType methodType = isPostMethod + ? FormSubmitEvent.MethodType.POST + : FormSubmitEvent.MethodType.GET; + formEvent = new FormSubmitEvent( + FormView.this, HyperlinkEvent.EventType.ACTIVATED, + actionURL, form, target, methodType, data); + + } + // setPage() may take significant time so schedule it to run later. + final FormSubmitEvent fse = formEvent; + final URL url = actionURL; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (fse != null) { + c.fireHyperlinkUpdate(fse); + } else { + try { + c.setPage(url); + } catch (IOException e) { + UIManager.getLookAndFeel().provideErrorFeedback(c); + } + } + } + }); + } + + private void storePostData(HTMLDocument doc, String target, String data) { + + /* POST data is stored into the document property named by constant + * PostDataProperty from where it is later retrieved by method + * JEditorPane.getPostData(). If the current document is in a frame, + * the data is initially put into the toplevel (frameset) document + * property (named <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 <INPUT> form + * element of type "image". + * + * @param imageData the mouse click coordinates. + */ + protected void imageSubmit(String imageData) { + + StringBuffer dataBuffer = new StringBuffer(); + Element elem = getElement(); + HTMLDocument hdoc = (HTMLDocument)elem.getDocument(); + getFormData(dataBuffer); + if (dataBuffer.length() > 0) { + dataBuffer.append('&'); + } + dataBuffer.append(imageData); + submitData(dataBuffer.toString()); + return; + } + + /** + * Extracts the value of the name attribute + * associated with the input element of type + * image. If name is defined it is encoded using + * the URLEncoder.encode() method and the + * image data is returned in the following format: + * name + ".x" +"="+ x +"&"+ name +".y"+"="+ y + * otherwise, + * "x="+ x +"&y="+ y + * + * @param point associated with the mouse click. + * @return the image data. + */ + private String getImageData(Point point) { + + String mouseCoords = point.x + ":" + point.y; + int sep = mouseCoords.indexOf(':'); + String x = mouseCoords.substring(0, sep); + String y = mouseCoords.substring(++sep); + String name = (String) getElement().getAttributes().getAttribute(HTML.Attribute.NAME); + + String data; + if (name == null || name.equals("")) { + data = "x="+ x +"&y="+ y; + } else { + name = URLEncoder.encode(name); + data = name + ".x" +"="+ x +"&"+ name +".y"+"="+ y; + } + return data; + } + + + /** + * The following methods provide functionality required to + * iterate over a the elements of the form and in the case + * of a form submission, extract the data from each model + * that is associated with each form element, and in the + * case of reset, reinitialize the each model to its + * initial state. + */ + + + /** + * Returns the Element representing the <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 <INPUT> form + * element. The value of "type" attributes is + * used to determine the type of the model associated + * with the element and then the relevant data is + * extracted. + */ + private String getInputElementData(AttributeSet attr) { + + Object model = attr.getAttribute(StyleConstants.ModelAttribute); + String type = (String) attr.getAttribute(HTML.Attribute.TYPE); + String value = null; + + if (type.equals("text") || type.equals("password")) { + Document doc = (Document)model; + try { + value = doc.getText(0, doc.getLength()); + } catch (BadLocationException e) { + value = null; + } + } else if (type.equals("submit") || type.equals("hidden")) { + value = (String) attr.getAttribute(HTML.Attribute.VALUE); + if (value == null) { + value = ""; + } + } else if (type.equals("radio") || type.equals("checkbox")) { + ButtonModel m = (ButtonModel)model; + if (m.isSelected()) { + value = (String) attr.getAttribute(HTML.Attribute.VALUE); + if (value == null) { + value = "on"; + } + } + } else if (type.equals("file")) { + Document doc = (Document)model; + String path; + + try { + path = doc.getText(0, doc.getLength()); + } catch (BadLocationException e) { + path = null; + } + if (path != null && path.length() > 0) { + value = path; +/* + + try { + Reader reader = new BufferedReader(new FileReader(path)); + StringBuffer buffer = new StringBuffer(); + char[] cBuff = new char[1024]; + int read; + + try { + while ((read = reader.read(cBuff)) != -1) { + buffer.append(cBuff, 0, read); + } + } catch (IOException ioe) { + buffer = null; + } + try { + reader.close(); + } catch (IOException ioe) {} + if (buffer != null) { + value = buffer.toString(); + } + } catch (IOException ioe) {} +*/ + } + } + return value; + } + + /** + * Returns the data associated with the <TEXTAREA> form + * element. This is done by getting the text stored in the + * Document model. + */ + private String getTextAreaData(AttributeSet attr) { + Document doc = (Document)attr.getAttribute(StyleConstants.ModelAttribute); + try { + return doc.getText(0, doc.getLength()); + } catch (BadLocationException e) { + return null; + } + } + + + /** + * Loads the buffer with the data associated with the Select + * form element. Basically, only items that are selected + * and have their name attribute set are added to the buffer. + */ + private void loadSelectData(AttributeSet attr, StringBuffer buffer) { + + String name = (String)attr.getAttribute(HTML.Attribute.NAME); + if (name == null) { + return; + } + Object m = attr.getAttribute(StyleConstants.ModelAttribute); + if (m instanceof OptionListModel) { + OptionListModel model = (OptionListModel)m; + + for (int i = 0; i < model.getSize(); i++) { + if (model.isSelectedIndex(i)) { + Option option = (Option) model.getElementAt(i); + appendBuffer(buffer, name, option.getValue()); + } + } + } else if (m instanceof ComboBoxModel) { + ComboBoxModel model = (ComboBoxModel)m; + Option option = (Option)model.getSelectedItem(); + if (option != null) { + appendBuffer(buffer, name, option.getValue()); + } + } + } + + /** + * Appends name / value pairs into the + * buffer. Both names and values are encoded using the + * URLEncoder.encode() method before being added to the + * buffer. + */ + private void appendBuffer(StringBuffer buffer, String name, String value) { + if (buffer.length() > 0) { + buffer.append('&'); + } + String encodedName = URLEncoder.encode(name); + buffer.append(encodedName); + buffer.append('='); + String encodedValue = URLEncoder.encode(value); + buffer.append(encodedValue); + } + + /** + * Returns true if the Element <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 + * <INPUT> element of type password or text, is the last + * one of either kind, in the form to which it belongs. + */ + boolean isLastTextOrPasswordField() { + Element parent = getFormElement(); + Element elem = getElement(); + + if (parent != null) { + ElementIterator it = new ElementIterator(parent); + Element next; + boolean found = false; + + while ((next = it.next()) != null) { + if (next == elem) { + found = true; + } + else if (found && isControl(next)) { + AttributeSet elemAttr = next.getAttributes(); + + if (HTMLDocument.matchNameAttribute + (elemAttr, HTML.Tag.INPUT)) { + String type = (String)elemAttr.getAttribute + (HTML.Attribute.TYPE); + + if ("text".equals(type) || "password".equals(type)) { + return false; + } + } + } + } + } + return true; + } + + /** + * Resets the form + * to its initial state by reinitializing the models + * associated with each form element to their initial + * values. + * + * param elem the element that triggered the reset + */ + void resetForm() { + Element parent = getFormElement(); + + if (parent != null) { + ElementIterator it = new ElementIterator(parent); + Element next; + + while((next = it.next()) != null) { + if (isControl(next)) { + AttributeSet elemAttr = next.getAttributes(); + Object m = elemAttr.getAttribute(StyleConstants. + ModelAttribute); + if (m instanceof TextAreaDocument) { + TextAreaDocument doc = (TextAreaDocument)m; + doc.reset(); + } else if (m instanceof PlainDocument) { + try { + PlainDocument doc = (PlainDocument)m; + doc.remove(0, doc.getLength()); + if (HTMLDocument.matchNameAttribute + (elemAttr, HTML.Tag.INPUT)) { + String value = (String)elemAttr. + getAttribute(HTML.Attribute.VALUE); + if (value != null) { + doc.insertString(0, value, null); + } + } + } catch (BadLocationException e) { + } + } else if (m instanceof OptionListModel) { + OptionListModel model = (OptionListModel) m; + int size = model.getSize(); + for (int i = 0; i < size; i++) { + model.removeIndexInterval(i, i); + } + BitSet selectionRange = model.getInitialSelection(); + for (int i = 0; i < selectionRange.size(); i++) { + if (selectionRange.get(i)) { + model.addSelectionInterval(i, i); + } + } + } else if (m instanceof OptionComboBoxModel) { + OptionComboBoxModel model = (OptionComboBoxModel) m; + Option option = model.getInitialSelection(); + if (option != null) { + model.setSelectedItem(option); + } + } else if (m instanceof JToggleButton.ToggleButtonModel) { + boolean checked = ((String)elemAttr.getAttribute + (HTML.Attribute.CHECKED) != null); + JToggleButton.ToggleButtonModel model = + (JToggleButton.ToggleButtonModel)m; + model.setSelected(checked); + } + } + } + } + } + + + /** + * BrowseFileAction is used for input type == file. When the user + * clicks the button a JFileChooser is brought up allowing the user + * to select a file in the file system. The resulting path to the selected + * file is set in the text field (actually an instance of Document). + */ + private class BrowseFileAction implements ActionListener { + private AttributeSet attrs; + private Document model; + + BrowseFileAction(AttributeSet attrs, Document model) { + this.attrs = attrs; + this.model = model; + } + + public void actionPerformed(ActionEvent ae) { + // PENDING: When mime support is added to JFileChooser use the + // accept value of attrs. + JFileChooser fc = new JFileChooser(); + fc.setMultiSelectionEnabled(false); + if (fc.showOpenDialog(getContainer()) == + JFileChooser.APPROVE_OPTION) { + File selected = fc.getSelectedFile(); + + if (selected != null) { + try { + if (model.getLength() > 0) { + model.remove(0, model.getLength()); + } + model.insertString(0, selected.getPath(), null); + } catch (BadLocationException ble) {} + } + } + } + } +} diff --git a/src/share/classes/javax/swing/text/html/FrameSetView.java b/src/share/classes/javax/swing/text/html/FrameSetView.java new file mode 100644 index 000000000..b653d32ee --- /dev/null +++ b/src/share/classes/javax/swing/text/html/FrameSetView.java @@ -0,0 +1,322 @@ +/* + * Copyright 1998-2003 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import java.util.*; +import javax.swing.*; +import javax.swing.text.*; +import javax.swing.event.*; + +/** + * Implements a FrameSetView, intended to support the HTML + * <FRAMESET> tag. Supports the ROWS and COLS attributes. + * + * @author Sunita Mani + * + * Credit also to the hotjava browser engineers that + * worked on making the allocation of space algorithms + * conform to the HTML 4.0 standard and also be netscape + * compatible. + * + */ + +class FrameSetView extends javax.swing.text.BoxView { + + String[] children; + int[] percentChildren; + int[] absoluteChildren; + int[] relativeChildren; + int percentTotals; + int absoluteTotals; + int relativeTotals; + + /** + * Constructs a FrameSetView for the given element. + * + * @param elem the element that this view is responsible for + */ + public FrameSetView(Element elem, int axis) { + super(elem, axis); + children = null; + } + + /** + * Parses the ROW or COL attributes and returns + * an array of strings that represent the space + * distribution. + * + */ + private String[] parseRowColSpec(HTML.Attribute key) { + + AttributeSet attributes = getElement().getAttributes(); + String spec = "*"; + if (attributes != null) { + if (attributes.getAttribute(key) != null) { + spec = (String)attributes.getAttribute(key); + } + } + + StringTokenizer tokenizer = new StringTokenizer(spec, ","); + int nTokens = tokenizer.countTokens(); + int n = getViewCount(); + String[] items = new String[Math.max(nTokens, n)]; + int i = 0; + for (; i < nTokens; i++) { + items[i] = tokenizer.nextToken().trim(); + // As per the spec, 100% is the same as * + // hence the mapping. + // + if (items[i].equals("100%")) { + items[i] = "*"; + } + } + // extend spec if we have more children than specified + // in ROWS or COLS attribute + for (; i < items.length; i++) { + items[i] = "*"; + } + return items; + } + + + /** + * Initializes a number of internal state variables + * that store information about space allocation + * for the frames contained within the frameset. + */ + private void init() { + if (getAxis() == View.Y_AXIS) { + children = parseRowColSpec(HTML.Attribute.ROWS); + } else { + children = parseRowColSpec(HTML.Attribute.COLS); + } + percentChildren = new int[children.length]; + relativeChildren = new int[children.length]; + absoluteChildren = new int[children.length]; + + for (int i = 0; i < children.length; i++) { + percentChildren[i] = -1; + relativeChildren[i] = -1; + absoluteChildren[i] = -1; + + if (children[i].endsWith("*")) { + if (children[i].length() > 1) { + relativeChildren[i] = + Integer.parseInt(children[i].substring( + 0, children[i].length()-1)); + relativeTotals += relativeChildren[i]; + } else { + relativeChildren[i] = 1; + relativeTotals += 1; + } + } else if (children[i].indexOf('%') != -1) { + percentChildren[i] = parseDigits(children[i]); + percentTotals += percentChildren[i]; + } else { + absoluteChildren[i] = Integer.parseInt(children[i]); + } + } + if (percentTotals > 100) { + for (int i = 0; i < percentChildren.length; i++) { + if (percentChildren[i] > 0) { + percentChildren[i] = + (percentChildren[i] * 100) / percentTotals; + } + } + percentTotals = 100; + } + } + + /** + * Perform layout for the major axis of the box (i.e. the + * axis that it represents). The results of the layout should + * be placed in the given arrays which represent the allocations + * to the children along the major axis. + * + * @param targetSpan the total span given to the view, which + * whould be used to layout the children + * @param axis the axis being layed out + * @param offsets the offsets from the origin of the view for + * each of the child views; this is a return value and is + * filled in by the implementation of this method + * @param spans the span of each child view; this is a return + * value and is filled in by the implementation of this method + * @return the offset and span for each child view in the + * offsets and spans parameters + */ + protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, + int[] spans) { + if (children == null) { + init(); + } + SizeRequirements.calculateTiledPositions(targetSpan, null, + getChildRequests(targetSpan, + axis), + offsets, spans); + } + + protected SizeRequirements[] getChildRequests(int targetSpan, int axis) { + + int span[] = new int[children.length]; + + spread(targetSpan, span); + int n = getViewCount(); + SizeRequirements[] reqs = new SizeRequirements[n]; + for (int i = 0, sIndex = 0; i < n; i++) { + View v = getView(i); + if ((v instanceof FrameView) || (v instanceof FrameSetView)) { + reqs[i] = new SizeRequirements((int) v.getMinimumSpan(axis), + span[sIndex], + (int) v.getMaximumSpan(axis), + 0.5f); + sIndex++; + } else { + int min = (int) v.getMinimumSpan(axis); + int pref = (int) v.getPreferredSpan(axis); + int max = (int) v.getMaximumSpan(axis); + float a = v.getAlignment(axis); + reqs[i] = new SizeRequirements(min, pref, max, a); + } + } + return reqs; + } + + + /** + * This method is responsible for returning in span[] the + * span for each child view along the major axis. it + * computes this based on the information that extracted + * from the value of the ROW/COL attribute. + */ + private void spread(int targetSpan, int span[]) { + + if (targetSpan == 0) { + return; + } + + int tempSpace = 0; + int remainingSpace = targetSpan; + + // allocate the absolute's first, they have + // precedence + // + for (int i = 0; i < span.length; i++) { + if (absoluteChildren[i] > 0) { + span[i] = absoluteChildren[i]; + remainingSpace -= span[i]; + } + } + + // then deal with percents. + // + tempSpace = remainingSpace; + for (int i = 0; i < span.length; i++) { + if (percentChildren[i] > 0 && tempSpace > 0) { + span[i] = (percentChildren[i] * tempSpace) / 100; + remainingSpace -= span[i]; + } else if (percentChildren[i] > 0 && tempSpace <= 0) { + span[i] = targetSpan / span.length; + remainingSpace -= span[i]; + } + } + + // allocate remainingSpace to relative + if (remainingSpace > 0 && relativeTotals > 0) { + for (int i = 0; i < span.length; i++) { + if (relativeChildren[i] > 0) { + span[i] = (remainingSpace * + relativeChildren[i]) / relativeTotals; + } + } + } else if (remainingSpace > 0) { + // There are no relative columns and the space has been + // under- or overallocated. In this case, turn all the + // percentage and pixel specified columns to percentage + // columns based on the ratio of their pixel count to the + // total "virtual" size. (In the case of percentage columns, + // the pixel count would equal the specified percentage + // of the screen size. + + // This action is in accordance with the HTML + // 4.0 spec (see section 8.3, the end of the discussion of + // the FRAMESET tag). The precedence of percentage and pixel + // specified columns is unclear (spec seems to indicate that + // they share priority, however, unspecified what happens when + // overallocation occurs.) + + // addendum is that we behave similiar to netscape in that specified + // widths have precedance over percentage widths... + + float vTotal = (float)(targetSpan - remainingSpace); + float[] tempPercents = new float[span.length]; + remainingSpace = targetSpan; + for (int i = 0; i < span.length; i++) { + // ok we know what our total space is, and we know how large each + // column should be relative to each other... therefore we can use + // that relative information to deduce their percentages of a whole + // and then scale them appropriately for the correct size + tempPercents[i] = ((float)span[i] / vTotal) * 100.00f; + span[i] = (int) ( ((float)targetSpan * tempPercents[i]) / 100.00f); + remainingSpace -= span[i]; + } + + + // this is for just in case there is something left over.. if there is we just + // add it one pixel at a time to the frames in order.. We shouldn't really ever get + // here and if we do it shouldn't be with more than 1 pixel, maybe two. + int i = 0; + while (remainingSpace != 0) { + if (remainingSpace < 0) { + span[i++]--; + remainingSpace++; + } + else { + span[i++]++; + remainingSpace--; + } + + // just in case there are more pixels than frames...should never happen.. + if (i == span.length)i = 0; + } + } + } + + /* + * Users have been known to type things like "%25" and "25 %". Deal + * with it. + */ + private int parseDigits(String mixedStr) { + int result = 0; + for (int i = 0; i < mixedStr.length(); i++) { + char ch = mixedStr.charAt(i); + if (Character.isDigit(ch)) { + result = (result * 10) + Character.digit(ch, 10); + } + } + return result; + } + +} diff --git a/src/share/classes/javax/swing/text/html/FrameView.java b/src/share/classes/javax/swing/text/html/FrameView.java new file mode 100644 index 000000000..f66a5531c --- /dev/null +++ b/src/share/classes/javax/swing/text/html/FrameView.java @@ -0,0 +1,479 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.awt.*; +import java.util.*; +import java.net.*; +import java.io.*; +import javax.swing.*; +import javax.swing.text.*; +import javax.swing.event.*; + +import sun.swing.text.html.FrameEditorPaneTag; + +/** + * Implements a FrameView, intended to support the HTML + * <FRAME> tag. Supports the frameborder, scrolling, + * marginwidth and marginheight attributes. + * + * @author Sunita Mani + */ + +class FrameView extends ComponentView implements HyperlinkListener { + + + JEditorPane htmlPane; + JScrollPane scroller; + boolean editable; + float width; + float height; + URL src; + /** Set to true when the component has been created. */ + private boolean createdComponent; + + /** + * Creates a new Frame. + * + * @param elem the element to represent. + */ + public FrameView(Element elem) { + super(elem); + } + + protected Component createComponent() { + + Element elem = getElement(); + AttributeSet attributes = elem.getAttributes(); + String srcAtt = (String)attributes.getAttribute(HTML.Attribute.SRC); + + if ((srcAtt != null) && (!srcAtt.equals(""))) { + try { + URL base = ((HTMLDocument)elem.getDocument()).getBase(); + src = new URL(base, srcAtt); + htmlPane = new FrameEditorPane(); + htmlPane.addHyperlinkListener(this); + JEditorPane host = getHostPane(); + boolean isAutoFormSubmission = true; + if (host != null) { + htmlPane.setEditable(host.isEditable()); + String charset = (String) host.getClientProperty("charset"); + if (charset != null) { + htmlPane.putClientProperty("charset", charset); + } + HTMLEditorKit hostKit = (HTMLEditorKit)host.getEditorKit(); + if (hostKit != null) { + isAutoFormSubmission = hostKit.isAutoFormSubmission(); + } + } + htmlPane.setPage(src); + HTMLEditorKit kit = (HTMLEditorKit)htmlPane.getEditorKit(); + if (kit != null) { + kit.setAutoFormSubmission(isAutoFormSubmission); + } + + Document doc = htmlPane.getDocument(); + if (doc instanceof HTMLDocument) { + ((HTMLDocument)doc).setFrameDocumentState(true); + } + setMargin(); + createScrollPane(); + setBorder(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + createdComponent = true; + return scroller; + } + + JEditorPane getHostPane() { + Container c = getContainer(); + while ((c != null) && ! (c instanceof JEditorPane)) { + c = c.getParent(); + } + return (JEditorPane) c; + } + + + /** + * Sets the parent view for the FrameView. + * Also determines if the FrameView should be editable + * or not based on whether the JTextComponent that + * contains it is editable. + * + * @param parent View + */ + public void setParent(View parent) { + if (parent != null) { + JTextComponent t = (JTextComponent)parent.getContainer(); + editable = t.isEditable(); + } + super.setParent(parent); + } + + + /** + * Also determines if the FrameView should be editable + * or not based on whether the JTextComponent that + * contains it is editable. And then proceeds to call + * the superclass to do the paint(). + * + * @param parent View + * @see text.ComponentView#paint + */ + public void paint(Graphics g, Shape allocation) { + + Container host = getContainer(); + if (host != null && htmlPane != null && + htmlPane.isEditable() != ((JTextComponent)host).isEditable()) { + editable = ((JTextComponent)host).isEditable(); + htmlPane.setEditable(editable); + } + super.paint(g, allocation); + } + + + /** + * If the marginwidth or marginheight attributes have been specified, + * then the JEditorPane's margin's are set to the new values. + */ + private void setMargin() { + int margin = 0; + Insets in = htmlPane.getMargin(); + Insets newInsets; + boolean modified = false; + AttributeSet attributes = getElement().getAttributes(); + String marginStr = (String)attributes.getAttribute(HTML.Attribute.MARGINWIDTH); + if ( in != null) { + newInsets = new Insets(in.top, in.left, in.right, in.bottom); + } else { + newInsets = new Insets(0,0,0,0); + } + if (marginStr != null) { + margin = Integer.parseInt(marginStr); + if (margin > 0) { + newInsets.left = margin; + newInsets.right = margin; + modified = true; + } + } + marginStr = (String)attributes.getAttribute(HTML.Attribute.MARGINHEIGHT); + if (marginStr != null) { + margin = Integer.parseInt(marginStr); + if (margin > 0) { + newInsets.top = margin; + newInsets.bottom = margin; + modified = true; + } + } + if (modified) { + htmlPane.setMargin(newInsets); + } + } + + /** + * If the frameborder attribute has been specified, either in the frame, + * or by the frames enclosing frameset, the JScrollPane's setBorder() + * method is invoked to achieve the desired look. + */ + private void setBorder() { + + AttributeSet attributes = getElement().getAttributes(); + String frameBorder = (String)attributes.getAttribute(HTML.Attribute.FRAMEBORDER); + if ((frameBorder != null) && + (frameBorder.equals("no") || frameBorder.equals("0"))) { + // make invisible borders. + scroller.setBorder(null); + } + } + + + /** + * This method creates the JScrollPane. The scrollbar policy is determined by + * the scrolling attribute. If not defined, the default is "auto" which + * maps to the scrollbar's being displayed as needed. + */ + private void createScrollPane() { + AttributeSet attributes = getElement().getAttributes(); + String scrolling = (String)attributes.getAttribute(HTML.Attribute.SCROLLING); + if (scrolling == null) { + scrolling = "auto"; + } + + if (!scrolling.equals("no")) { + if (scrolling.equals("yes")) { + scroller = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + } else { + // scrollbars will be displayed if needed + // + scroller = new JScrollPane(); + } + } else { + scroller = new JScrollPane(JScrollPane.VERTICAL_SCROLLBAR_NEVER, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + + JViewport vp = scroller.getViewport(); + vp.add(htmlPane); + vp.setBackingStoreEnabled(true); + scroller.setMinimumSize(new Dimension(5,5)); + scroller.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); + } + + + /** + * Finds the outermost FrameSetView. It then + * returns that FrameSetView's container. + */ + JEditorPane getOutermostJEditorPane() { + + View parent = getParent(); + FrameSetView frameSetView = null; + while (parent != null) { + if (parent instanceof FrameSetView) { + frameSetView = (FrameSetView)parent; + } + parent = parent.getParent(); + } + if (frameSetView != null) { + return (JEditorPane)frameSetView.getContainer(); + } + return null; + } + + + /** + * Returns true if this frame is contained within + * a nested frameset. + */ + private boolean inNestedFrameSet() { + FrameSetView parent = (FrameSetView)getParent(); + return (parent.getParent() instanceof FrameSetView); + } + + + /** + * Notification of a change relative to a + * hyperlink. This method searches for the outermost + * JEditorPane, and then fires an HTMLFrameHyperlinkEvent + * to that frame. In addition, if the target is _parent, + * and there is not nested framesets then the target is + * reset to _top. If the target is _top, in addition to + * firing the event to the outermost JEditorPane, this + * method also invokes the setPage() method and explicitly + * replaces the current document with the destination url. + * + * @param HyperlinkEvent + */ + public void hyperlinkUpdate(HyperlinkEvent evt) { + + JEditorPane c = getOutermostJEditorPane(); + if (c == null) { + return; + } + + if (!(evt instanceof HTMLFrameHyperlinkEvent)) { + c.fireHyperlinkUpdate(evt); + return; + } + + HTMLFrameHyperlinkEvent e = (HTMLFrameHyperlinkEvent)evt; + + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + String target = e.getTarget(); + String postTarget = target; + + if (target.equals("_parent") && !inNestedFrameSet()){ + target = "_top"; + } + + if (evt instanceof FormSubmitEvent) { + HTMLEditorKit kit = (HTMLEditorKit)c.getEditorKit(); + if (kit != null && kit.isAutoFormSubmission()) { + if (target.equals("_top")) { + try { + movePostData(c, postTarget); + c.setPage(e.getURL()); + } catch (IOException ex) { + // Need a way to handle exceptions + } + } else { + HTMLDocument doc = (HTMLDocument)c.getDocument(); + doc.processHTMLFrameHyperlinkEvent(e); + } + } else { + c.fireHyperlinkUpdate(evt); + } + return; + } + + if (target.equals("_top")) { + try { + c.setPage(e.getURL()); + } catch (IOException ex) { + // Need a way to handle exceptions + // ex.printStackTrace(); + } + } + if (!c.isEditable()) { + c.fireHyperlinkUpdate(new HTMLFrameHyperlinkEvent(c, + e.getEventType(), + e.getURL(), + e.getDescription(), + getElement(), + e.getInputEvent(), + target)); + } + } + } + + /** + * Gives notification from the document that attributes were changed + * in a location that this view is responsible for. Currently this view + * handles changes to its SRC attribute. + * + * @param e the change information from the associated document + * @param a the current allocation of the view + * @param f the factory to use to rebuild if the view has children + * + */ + public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { + + Element elem = getElement(); + AttributeSet attributes = elem.getAttributes(); + + URL oldPage = src; + + String srcAtt = (String)attributes.getAttribute(HTML.Attribute.SRC); + URL base = ((HTMLDocument)elem.getDocument()).getBase(); + try { + if (!createdComponent) { + return; + } + + Object postData = movePostData(htmlPane, null); + src = new URL(base, srcAtt); + if (oldPage.equals(src) && (src.getRef() == null) && (postData == null)) { + return; + } + + htmlPane.setPage(src); + Document newDoc = htmlPane.getDocument(); + if (newDoc instanceof HTMLDocument) { + ((HTMLDocument)newDoc).setFrameDocumentState(true); + } + } catch (MalformedURLException e1) { + // Need a way to handle exceptions + //e1.printStackTrace(); + } catch (IOException e2) { + // Need a way to handle exceptions + //e2.printStackTrace(); + } + } + + /** + * Move POST data from temporary storage into the target document property. + * + * @return the POST data or null if no data found + */ + private Object movePostData(JEditorPane targetPane, String frameName) { + Object postData = null; + JEditorPane p = getOutermostJEditorPane(); + if (p != null) { + if (frameName == null) { + frameName = (String) getElement().getAttributes().getAttribute( + HTML.Attribute.NAME); + } + if (frameName != null) { + String propName = FormView.PostDataProperty + "." + frameName; + Document d = p.getDocument(); + postData = d.getProperty(propName); + if (postData != null) { + targetPane.getDocument().putProperty( + FormView.PostDataProperty, postData); + d.putProperty(propName, null); + } + } + } + + return postData; + } + + /** + * Determines the minimum span for this view along an + * axis. + * + * @param axis may be either <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 <hr> element. + * + * @param elem the element to create a view for + */ + public HRuleView(Element elem) { + super(elem); + setPropertiesFromAttributes(); + } + + /** + * Update any cached values that come from attributes. + */ + protected void setPropertiesFromAttributes() { + StyleSheet sheet = ((HTMLDocument)getDocument()).getStyleSheet(); + AttributeSet eAttr = getElement().getAttributes(); + attr = sheet.getViewAttributes(this); + + alignment = StyleConstants.ALIGN_CENTER; + size = 0; + noshade = null; + widthValue = null; + + if (attr != null) { + // getAlignment() returns ALIGN_LEFT by default, and HR should + // use ALIGN_CENTER by default, so we check if the alignment + // attribute is actually defined + if (attr.getAttribute(StyleConstants.Alignment) != null) { + alignment = StyleConstants.getAlignment(attr); + } + + noshade = (String)eAttr.getAttribute(HTML.Attribute.NOSHADE); + Object value = eAttr.getAttribute(HTML.Attribute.SIZE); + if (value != null && (value instanceof String)) + size = Integer.parseInt((String)value); + value = attr.getAttribute(CSS.Attribute.WIDTH); + if (value != null && (value instanceof CSS.LengthValue)) { + widthValue = (CSS.LengthValue)value; + } + topMargin = getLength(CSS.Attribute.MARGIN_TOP, attr); + bottomMargin = getLength(CSS.Attribute.MARGIN_BOTTOM, attr); + leftMargin = getLength(CSS.Attribute.MARGIN_LEFT, attr); + rightMargin = getLength(CSS.Attribute.MARGIN_RIGHT, attr); + } + else { + topMargin = bottomMargin = leftMargin = rightMargin = 0; + } + size = Math.max(2, size); + } + + // This will be removed and centralized at some point, need to unify this + // and avoid private classes. + private float getLength(CSS.Attribute key, AttributeSet a) { + CSS.LengthValue lv = (CSS.LengthValue) a.getAttribute(key); + float len = (lv != null) ? lv.getValue() : 0; + return len; + } + + // --- View methods --------------------------------------------- + + /** + * Paints the view. + * + * @param g the graphics context + * @param a the allocation region for the view + * @see View#paint + */ + public void paint(Graphics g, Shape a) { + Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : + a.getBounds(); + int x = 0; + int y = alloc.y + SPACE_ABOVE + (int)topMargin; + int width = alloc.width - (int)(leftMargin + rightMargin); + if (widthValue != null) { + width = (int)widthValue.getValue((float)width); + } + int height = alloc.height - (SPACE_ABOVE + SPACE_BELOW + + (int)topMargin + (int)bottomMargin); + if (size > 0) + height = size; + + // Align the rule horizontally. + switch (alignment) { + case StyleConstants.ALIGN_CENTER: + x = alloc.x + (alloc.width / 2) - (width / 2); + break; + case StyleConstants.ALIGN_RIGHT: + x = alloc.x + alloc.width - width - (int)rightMargin; + break; + case StyleConstants.ALIGN_LEFT: + default: + x = alloc.x + (int)leftMargin; + break; + } + + // Paint either a shaded rule or a solid line. + if (noshade != null) { + g.setColor(Color.black); + g.fillRect(x, y, width, height); + } + else { + Color bg = getContainer().getBackground(); + Color bottom, top; + if (bg == null || bg.equals(Color.white)) { + top = Color.darkGray; + bottom = Color.lightGray; + } + else { + top = Color.darkGray; + bottom = Color.white; + } + g.setColor(bottom); + g.drawLine(x + width - 1, y, x + width - 1, y + height - 1); + g.drawLine(x, y + height - 1, x + width - 1, y + height - 1); + g.setColor(top); + g.drawLine(x, y, x + width - 1, y); + g.drawLine(x, y, x, y + height - 1); + } + + } + + + /** + * Calculates the desired shape of the rule... this is + * basically the preferred size of the border. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the desired span + * @see View#getPreferredSpan + */ + public float getPreferredSpan(int axis) { + switch (axis) { + case View.X_AXIS: + return 1; + case View.Y_AXIS: + if (size > 0) { + return size + SPACE_ABOVE + SPACE_BELOW + topMargin + + bottomMargin; + } else { + if (noshade != null) { + return 2 + SPACE_ABOVE + SPACE_BELOW + topMargin + + bottomMargin; + } else { + return SPACE_ABOVE + SPACE_BELOW + topMargin +bottomMargin; + } + } + default: + throw new IllegalArgumentException("Invalid axis: " + axis); + } + } + + /** + * Gets the resize weight for the axis. + * The rule is: rigid vertically and flexible horizontally. + * + * @param axis may be either X_AXIS or Y_AXIS + * @return the weight + */ + public int getResizeWeight(int axis) { + if (axis == View.X_AXIS) { + return 1; + } else if (axis == View.Y_AXIS) { + return 0; + } else { + return 0; + } + } + + /** + * Determines how attractive a break opportunity in + * this view is. This is implemented to request a forced break. + * + * @param axis may be either View.X_AXIS or View.Y_AXIS + * @param pos the potential location of the start of the + * broken view (greater than or equal to zero). + * This may be useful for calculating tab + * positions. + * @param len specifies the relative length from <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 <BASE> tag is encountered, the base + * will become the URL specified by that tag. Because the base URL is + * a property, it can of course be set directly.</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> + * <html> + * <head> + * <title>An example HTMLDocument</title> + * <style type="text/css"> + * div { background-color: silver; } + * ul { color: red; } + * </style> + * </head> + * <body> + * <div id="BOX"> + * <p>Paragraph 1</p> + * <p>Paragraph 2</p> + * </div> + * </body> + * </html> + * </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, "<ul><li>List + * Item</li></ul>")</code> inserts the list before the first + * paragraph, and <code>d.insertBeforeEnd(e, "<ul><li>List + * Item</li></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, + * "<ul><li>List Item</li></ul>")</code> inserts the list + * before the <code>DIV</code> element, and <code>d.insertAfterEnd(e, + * "<ul><li>List Item</li></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, "<ul><li>List + * Item</li></ul>")</code> replaces all children paragraphs with + * the list, and <code>d.setOuterHTML(e, "<ul><li>List + * Item</li></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 <FRAMESET> element, and inserts a new <FRAME> + * 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> + * <body> + * | + * <b><div></b> + * / \ + * <p> <p> + * </pre> + * + * <p>Invoking <code>setInnerHTML(elem, "<ul><li>")</code> + * results in the following structure (new elements are <font + * color="red">in red</font>).</p> + * + * <pre> + * <body> + * | + * <b><div></b> + * \ + * <font color="red"><ul></font> + * \ + * <font color="red"><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 <img> 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> + * <body> + * | + * <b><div></b> + * / \ + * <p> <p> + * </pre> + * + * <p>Invoking <code>setOuterHTML(elem, "<ul><li>")</code> + * results in the following structure (new elements are <font + * color="red">in red</font>).</p> + * + * <pre> + * <body> + * | + * <font color="red"><ul></font> + * \ + * <font color="red"><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> + * <body> + * | + * <b><div></b> + * / \ + * <p> <p> + * </pre> + * + * <p>Invoking <code>insertAfterStart(elem, + * "<ul><li>")</code> results in the following structure + * (new elements are <font color="red">in red</font>).</p> + * + * <pre> + * <body> + * | + * <b><div></b> + * / | \ + * <font color="red"><ul></font> <p> <p> + * / + * <font color="red"><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> + * <body> + * | + * <b><div></b> + * / \ + * <p> <p> + * </pre> + * + * <p>Invoking <code>insertBeforeEnd(elem, "<ul><li>")</code> + * results in the following structure (new elements are <font + * color="red">in red</font>).</p> + * + * <pre> + * <body> + * | + * <b><div></b> + * / | \ + * <p> <p> <font color="red"><ul></font> + * \ + * <font color="red"><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> + * <body> + * | + * <b><div></b> + * / \ + * <p> <p> + * </pre> + * + * <p>Invoking <code>insertBeforeStart(elem, + * "<ul><li>")</code> results in the following structure + * (new elements are <font color="red">in red</font>).</p> + * + * <pre> + * <body> + * / \ + * <font color="red"><ul></font> <b><div></b> + * / / \ + * <font color="red"><li></font> <p> <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> + * <body> + * | + * <b><div></b> + * / \ + * <p> <p> + * </pre> + * + * <p>Invoking <code>insertAfterEnd(elem, "<ul><li>")</code> + * results in the following structure (new elements are <font + * color="red">in red</font>).</p> + * + * <pre> + * <body> + * / \ + * <b><div></b> <font color="red"><ul></font> + * / \ \ + * <p> <p> <font color="red"><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><a id="HelloThere"></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, <APPLET>, <PARAM>, <MAP>, <AREA>, <LINK>, + * <SCRIPT> and <STYLE> 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 </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 <INPUT>, <TEXTAREA>, <SELECT>, + * and <OPTION> 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 <SELECT> tag is being processed, this + * model will be a reference to the model being filled + * with the <OPTION> elements (which produce + * objects of type <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 <applet> tag is not supported, but some support is provided + * for the <object> tag. + * <p> + * There are several goals of the HTML EditorKit provided, that have + * an effect upon the way that HTML is modeled. These + * have influenced its design in a substantial way. + * <dl> + * <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 '<>foo<\t>' + * contains an implied html element and an implied body element. + * + * @since 1.3 + */ + public static final Object IMPLIED = "_implied_"; + + + public void flush() throws BadLocationException { + } + + public void handleText(char[] data, int pos) { + } + + public void handleComment(char[] data, int pos) { + } + + public void handleStartTag(HTML.Tag t, MutableAttributeSet a, int pos) { + } + + public void handleEndTag(HTML.Tag t, int pos) { + } + + public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) { + } + + public void handleError(String errorMsg, int pos){ + } + + /** + * This is invoked after the stream has been parsed, but before + * <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 <table><tr><td></td></tr></table>. + * <p>There is also an option to supply an alternate parentTag and + * addTag. These will be checked for if there is no parentTag at + * offset. + */ + 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 + * <b>, <i>, <font>, and <a>. + */ + private Vector tags = new Vector(10); + + /** + * Values for the tags. + */ + private Vector tagValues = new Vector(10); + + /** + * Used when writing out content. + */ + private Segment segment; + + /* + * This is used in closeOutUnwantedEmbeddedTags. + */ + private Vector tagsToRemove = new Vector(10); + + /** + * Set to true after the head has been output. + */ + private boolean wroteHead; + + /** + * Set to true when entities (such as <) should be replaced. + */ + private boolean replaceEntities; + + /** + * Temporary buffer. + */ + private char[] tempChars; + + + /** + * Creates a new HTMLWriter. + * + * @param w a Writer + * @param doc an HTMLDocument + * + */ + public HTMLWriter(Writer w, HTMLDocument doc) { + this(w, doc, 0, doc.getLength()); + } + + /** + * Creates a new HTMLWriter. + * + * @param w a Writer + * @param doc an HTMLDocument + * @param pos the document location from which to fetch the content + * @param len the amount to write out + */ + public HTMLWriter(Writer w, HTMLDocument doc, int pos, int len) { + super(w, doc, pos, len); + completeDoc = (pos == 0 && len == doc.getLength()); + setLineLength(80); + } + + /** + * Iterates over the + * Element tree and controls the writing out of + * all the tags and its attributes. + * + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + * + */ + public void write() throws IOException, BadLocationException { + ElementIterator it = getElementIterator(); + Element current = null; + Element next = null; + + wroteHead = false; + setCurrentLineLength(0); + replaceEntities = false; + setCanWrapLines(false); + if (segment == null) { + segment = new Segment(); + } + inPre = false; + boolean forcedBody = false; + while ((next = it.next()) != null) { + if (!inRange(next)) { + if (completeDoc && next.getAttributes().getAttribute( + StyleConstants.NameAttribute) == HTML.Tag.BODY) { + forcedBody = true; + } + else { + continue; + } + } + if (current != null) { + + /* + if next is child of current increment indent + */ + + if (indentNeedsIncrementing(current, next)) { + incrIndent(); + } else if (current.getParentElement() != next.getParentElement()) { + /* + next and current are not siblings + so emit end tags for items on the stack until the + item on top of the stack, is the parent of the + next. + */ + Element top = (Element)blockElementStack.peek(); + while (top != next.getParentElement()) { + /* + pop() will return top. + */ + blockElementStack.pop(); + if (!synthesizedElement(top)) { + AttributeSet attrs = top.getAttributes(); + if (!matchNameAttribute(attrs, HTML.Tag.PRE) && + !isFormElementWithContent(attrs)) { + decrIndent(); + } + endTag(top); + } + top = (Element)blockElementStack.peek(); + } + } else if (current.getParentElement() == next.getParentElement()) { + /* + if next and current are siblings the indent level + is correct. But, we need to make sure that if current is + on the stack, we pop it off, and put out its end tag. + */ + Element top = (Element)blockElementStack.peek(); + if (top == current) { + blockElementStack.pop(); + endTag(top); + } + } + } + if (!next.isLeaf() || isFormElementWithContent(next.getAttributes())) { + blockElementStack.push(next); + startTag(next); + } else { + emptyTag(next); + } + current = next; + } + /* Emit all remaining end tags */ + + /* A null parameter ensures that all embedded tags + currently in the tags vector have their + corresponding end tags written out. + */ + closeOutUnwantedEmbeddedTags(null); + + if (forcedBody) { + blockElementStack.pop(); + endTag(current); + } + while (!blockElementStack.empty()) { + current = (Element)blockElementStack.pop(); + if (!synthesizedElement(current)) { + AttributeSet attrs = current.getAttributes(); + if (!matchNameAttribute(attrs, HTML.Tag.PRE) && + !isFormElementWithContent(attrs)) { + decrIndent(); + } + endTag(current); + } + } + + if (completeDoc) { + writeAdditionalComments(); + } + + segment.array = null; + } + + + /** + * Writes out the attribute set. Ignores all + * attributes with a key of type HTML.Tag, + * attributes with a key of type StyleConstants, + * and attributes with a key of type + * HTML.Attribute.ENDTAG. + * + * @param attr an AttributeSet + * @exception IOException on any I/O error + * + */ + protected void writeAttributes(AttributeSet attr) throws IOException { + // translate css attributes to html + convAttr.removeAttributes(convAttr); + convertToHTML32(attr, convAttr); + + Enumeration names = convAttr.getAttributeNames(); + while (names.hasMoreElements()) { + Object name = names.nextElement(); + if (name instanceof HTML.Tag || + name instanceof StyleConstants || + name == HTML.Attribute.ENDTAG) { + continue; + } + write(" " + name + "=\"" + convAttr.getAttribute(name) + "\""); + } + } + + /** + * Writes out all empty elements (all tags that have no + * corresponding end tag). + * + * @param elem an Element + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + */ + protected void emptyTag(Element elem) throws BadLocationException, IOException { + + if (!inContent && !inPre) { + indentSmart(); + } + + AttributeSet attr = elem.getAttributes(); + closeOutUnwantedEmbeddedTags(attr); + writeEmbeddedTags(attr); + + if (matchNameAttribute(attr, HTML.Tag.CONTENT)) { + inContent = true; + text(elem); + } else if (matchNameAttribute(attr, HTML.Tag.COMMENT)) { + comment(elem); + } else { + boolean isBlock = isBlockTag(elem.getAttributes()); + if (inContent && isBlock ) { + writeLineSeparator(); + indentSmart(); + } + + Object nameTag = (attr != null) ? attr.getAttribute + (StyleConstants.NameAttribute) : null; + Object endTag = (attr != null) ? attr.getAttribute + (HTML.Attribute.ENDTAG) : null; + + boolean outputEndTag = false; + // If an instance of an UNKNOWN Tag, or an instance of a + // tag that is only visible during editing + // + if (nameTag != null && endTag != null && + (endTag instanceof String) && + ((String)endTag).equals("true")) { + outputEndTag = true; + } + + if (completeDoc && matchNameAttribute(attr, HTML.Tag.HEAD)) { + if (outputEndTag) { + // Write out any styles. + writeStyles(((HTMLDocument)getDocument()).getStyleSheet()); + } + wroteHead = true; + } + + write('<'); + if (outputEndTag) { + write('/'); + } + write(elem.getName()); + writeAttributes(attr); + write('>'); + if (matchNameAttribute(attr, HTML.Tag.TITLE) && !outputEndTag) { + Document doc = elem.getDocument(); + String title = (String)doc.getProperty(Document.TitleProperty); + write(title); + } else if (!inContent || isBlock) { + writeLineSeparator(); + if (isBlock && inContent) { + indentSmart(); + } + } + } + } + + /** + * Determines if the HTML.Tag associated with the + * element is a block tag. + * + * @param attr an AttributeSet + * @return true if tag is block tag, false otherwise. + */ + protected boolean isBlockTag(AttributeSet attr) { + Object o = attr.getAttribute(StyleConstants.NameAttribute); + if (o instanceof HTML.Tag) { + HTML.Tag name = (HTML.Tag) o; + return name.isBlock(); + } + return false; + } + + + /** + * Writes out a start tag for the element. + * Ignores all synthesized elements. + * + * @param elem an Element + * @exception IOException on any I/O error + */ + protected void startTag(Element elem) throws IOException, BadLocationException { + + if (synthesizedElement(elem)) { + return; + } + + // Determine the name, as an HTML.Tag. + AttributeSet attr = elem.getAttributes(); + Object nameAttribute = attr.getAttribute(StyleConstants.NameAttribute); + HTML.Tag name; + if (nameAttribute instanceof HTML.Tag) { + name = (HTML.Tag)nameAttribute; + } + else { + name = null; + } + + if (name == HTML.Tag.PRE) { + inPre = true; + preEndOffset = elem.getEndOffset(); + } + + // write out end tags for item on stack + closeOutUnwantedEmbeddedTags(attr); + + if (inContent) { + writeLineSeparator(); + inContent = false; + newlineOutputed = false; + } + + if (completeDoc && name == HTML.Tag.BODY && !wroteHead) { + // If the head has not been output, output it and the styles. + wroteHead = true; + indentSmart(); + write("<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 <font> tag attribute. The + * value of the attribute should be a MutableAttributeSet so + * that the attributes can be updated as they are discovered. + */ + private static void createFontAttribute(CSS.Attribute a, AttributeSet from, + MutableAttributeSet to) { + MutableAttributeSet fontAttr = (MutableAttributeSet) + to.getAttribute(HTML.Tag.FONT); + if (fontAttr == null) { + fontAttr = new SimpleAttributeSet(); + to.addAttribute(HTML.Tag.FONT, fontAttr); + } + // edit the parameters to the font tag + String htmlValue = from.getAttribute(a).toString(); + if (a == CSS.Attribute.FONT_FAMILY) { + fontAttr.addAttribute(HTML.Attribute.FACE, htmlValue); + } else if (a == CSS.Attribute.FONT_SIZE) { + fontAttr.addAttribute(HTML.Attribute.SIZE, htmlValue); + } else if (a == CSS.Attribute.COLOR) { + fontAttr.addAttribute(HTML.Attribute.COLOR, htmlValue); + } + } + + /** + * Copies the given AttributeSet to a new set, converting + * any CSS attributes found to arguments of an HTML style + * attribute. + */ + private static void convertToHTML40(AttributeSet from, MutableAttributeSet to) { + Enumeration keys = from.getAttributeNames(); + String value = ""; + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key instanceof CSS.Attribute) { + value = value + " " + key + "=" + from.getAttribute(key) + ";"; + } else { + to.addAttribute(key, from.getAttribute(key)); + } + } + if (value.length() > 0) { + to.addAttribute(HTML.Attribute.STYLE, value); + } + } + + // + // Overrides the writing methods to only break a string when + // canBreakString is true. + // In a future release it is likely AbstractWriter will get this + // functionality. + // + + /** + * Writes the line separator. This is overriden to make sure we don't + * replace the newline content in case it is outside normal ascii. + * @since 1.3 + */ + protected void writeLineSeparator() throws IOException { + boolean oldReplace = replaceEntities; + replaceEntities = false; + super.writeLineSeparator(); + replaceEntities = oldReplace; + indented = false; + } + + /** + * This method is overriden to map any character entities, such as + * < to &lt;. <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("<"); + break; + case '>': + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + output(">"); + break; + case '&': + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + output("&"); + break; + case '"': + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + output("""); + break; + // Special characters + case '\n': + case '\t': + case '\r': + break; + default: + if (chars[counter] < ' ' || chars[counter] > 127) { + if (counter > last) { + super.output(chars, last, counter - last); + } + last = counter + 1; + // If the character is outside of ascii, write the + // numeric value. + output("&#"); + output(String.valueOf((int)chars[counter])); + output(";"); + } + break; + } + } + if (last < length) { + super.output(chars, last, length - last); + } + } + + /** + * This directly invokes super's <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 <IMG> tag. + * Supports scaling via the HEIGHT and WIDTH attributes of the tag. + * If the image is unable to be loaded any text specified via the + * <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 <ISINDEX< tag. This is implemented + * as a JPanel that contains + * + * @author Sunita Mani + */ + +class IsindexView extends ComponentView implements ActionListener { + + JTextField textField; + + /** + * Creates an IsindexView + */ + public IsindexView(Element elem) { + super(elem); + } + + /** + * Creates the components necessary to to implement + * this view. The component returned is a <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> + * <html> + * <head> + * <style> + * <!-- list of named styles + * p.normal { + * font-family: SansSerif; + * margin-height: 0; + * font-size: 14 + * } + * --> + * </style> + * </head> + * <body> + * <p style=normal> + * <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 <span> tag. + * The syntax is similar to inline styles.</b> + * </p> + * </body> + * </html> + * </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 + * <span> tag is not emitted. + */ + private AttributeSet fontAttributes; + + /** + * Maps from style name as held by the Document, to the archived + * style name (style name written out). These may differ. + */ + private Hashtable styleNameMapping; + + /** + * Creates a new MinimalHTMLWriter. + * + * @param w Writer + * @param doc StyledDocument + * + */ + public MinimalHTMLWriter(Writer w, StyledDocument doc) { + super(w, doc); + } + + /** + * Creates a new MinimalHTMLWriter. + * + * @param w Writer + * @param doc StyledDocument + * @param pos The location in the document to fetch the + * content. + * @param len The amount to write out. + * + */ + public MinimalHTMLWriter(Writer w, StyledDocument doc, int pos, int len) { + super(w, doc, pos, len); + } + + /** + * Generates HTML output + * from a StyledDocument. + * + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + * + */ + public void write() throws IOException, BadLocationException { + styleNameMapping = new Hashtable(); + writeStartTag("<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 <head> and <style> + * tags, and then invokes writeStyles() to write + * out all the named styles as the content of the + * <style> tag. The content is surrounded by + * valid HTML comment markers to ensure that the + * document is viewable in applications/browsers + * that do not support the tag. + * + * @exception IOException on any I/O error + */ + protected void writeHeader() throws IOException { + writeStartTag("<head>"); + writeStartTag("<style>"); + writeStartTag("<!--"); + writeStyles(); + writeEndTag("-->"); + writeEndTag("</style>"); + writeEndTag("</head>"); + } + + + + /** + * Writes out all the named styles as the + * content of the <style> tag. + * + * @exception IOException on any I/O error + */ + protected void writeStyles() throws IOException { + /* + * Access to DefaultStyledDocument done to workaround + * a missing API in styled document to access the + * stylenames. + */ + DefaultStyledDocument styledDoc = ((DefaultStyledDocument)getDocument()); + Enumeration styleNames = styledDoc.getStyleNames(); + + while (styleNames.hasMoreElements()) { + Style s = styledDoc.getStyle((String)styleNames.nextElement()); + + /** PENDING: Once the name attribute is removed + from the list we check check for 0. **/ + if (s.getAttributeCount() == 1 && + s.isDefined(StyleConstants.NameAttribute)) { + continue; + } + indent(); + write("p." + addStyleName(s.getName())); + write(" {\n"); + incrIndent(); + writeAttributes(s); + decrIndent(); + indent(); + write("}\n"); + } + } + + + /** + * Iterates over the elements in the document + * and processes elements based on whether they are + * branch elements or leaf elements. This method specially handles + * leaf elements that are text. + * + * @exception IOException on any I/O error + */ + protected void writeBody() throws IOException, BadLocationException { + ElementIterator it = getElementIterator(); + + /* + This will be a section element for a styled document. + We represent this element in HTML as the body tags. + Therefore we ignore it. + */ + it.current(); + + Element next = null; + + writeStartTag("<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 <p> + * tag. Before writing out the tag, this method ensures + * that all other tags that have been opened are + * appropriately closed off. + * + * @exception IOException on any I/O error + */ + protected void writeEndParagraph() throws IOException { + writeEndMask(fontMask); + if (inFontTag()) { + endSpanTag(); + } else { + write(NEWLINE); + } + writeEndTag("</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 + * <p> tag and sets its value to be the name of the + * style. + * + * @exception IOException on any I/O error + */ + protected void writeStartParagraph(Element elem) throws IOException { + AttributeSet attr = elem.getAttributes(); + Object resolveAttr = attr.getAttribute(StyleConstants.ResolveAttribute); + if (resolveAttr instanceof StyleContext.NamedStyle) { + writeStartTag("<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 <img> tag, how should you + * represent the src attribute (the location of the image)? + * In certain cases it could be a URL, in others it could + * be read from a stream. + * + * @param elem element of type StyleConstants.IconElementName + */ + protected void writeImage(Element elem) throws IOException { + } + + + /** + * Responsible for handling Component Elements; + * deliberately unimplemented. + * How this method is implemented is a matter of policy. + */ + protected void writeComponent(Element elem) throws IOException { + } + + + /** + * Returns true if the element is a text element. + * + */ + protected boolean isText(Element elem) { + return (elem.getName() == AbstractDocument.ContentElementName); + } + + + /** + * Writes out the attribute set + * in an HTML-compliant manner. + * + * @exception IOException on any I/O error + * @exception BadLocationException if pos represents an invalid + * location within the document. + */ + protected void writeContent(Element elem, boolean needsIndenting) + throws IOException, BadLocationException { + + AttributeSet attr = elem.getAttributes(); + writeNonHTMLAttributes(attr); + if (needsIndenting) { + indent(); + } + writeHTMLTags(attr); + text(elem); + } + + + /** + * Generates + * bold <b>, italic <i>, and <u> tags for the + * text based on its attribute settings. + * + * @exception IOException on any I/O error + */ + + protected void writeHTMLTags(AttributeSet attr) throws IOException { + + int oldMask = fontMask; + setFontMask(attr); + + int endMask = 0; + int startMask = 0; + if ((oldMask & BOLD) != 0) { + if ((fontMask & BOLD) == 0) { + endMask |= BOLD; + } + } else if ((fontMask & BOLD) != 0) { + startMask |= BOLD; + } + + if ((oldMask & ITALIC) != 0) { + if ((fontMask & ITALIC) == 0) { + endMask |= ITALIC; + } + } else if ((fontMask & ITALIC) != 0) { + startMask |= ITALIC; + } + + if ((oldMask & UNDERLINE) != 0) { + if ((fontMask & UNDERLINE) == 0) { + endMask |= UNDERLINE; + } + } else if ((fontMask & UNDERLINE) != 0) { + startMask |= UNDERLINE; + } + writeEndMask(endMask); + writeStartMask(startMask); + } + + + /** + * Tweaks the appropriate bits of fontMask + * to reflect whether the text is to be displayed in + * bold, italic, and/or with an underline. + * + */ + private void setFontMask(AttributeSet attr) { + if (StyleConstants.isBold(attr)) { + fontMask |= BOLD; + } + + if (StyleConstants.isItalic(attr)) { + fontMask |= ITALIC; + } + + if (StyleConstants.isUnderline(attr)) { + fontMask |= UNDERLINE; + } + } + + + + + /** + * Writes out start tags <u>, <i>, and <b> based on + * the mask settings. + * + * @exception IOException on any I/O error + */ + private void writeStartMask(int mask) throws IOException { + if (mask != 0) { + if ((mask & UNDERLINE) != 0) { + write("<u>"); + } + if ((mask & ITALIC) != 0) { + write("<i>"); + } + if ((mask & BOLD) != 0) { + write("<b>"); + } + } + } + + /** + * Writes out end tags for <u>, <i>, and <b> based on + * the mask settings. + * + * @exception IOException on any I/O error + */ + private void writeEndMask(int mask) throws IOException { + if (mask != 0) { + if ((mask & BOLD) != 0) { + write("</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 <span> tag is generated and its + * style attribute is set to contain the list of remaining + * attributes just like inline styles. + * + * @exception IOException on any I/O error + */ + protected void writeNonHTMLAttributes(AttributeSet attr) throws IOException { + + String style = ""; + String separator = "; "; + + if (inFontTag() && fontAttributes.isEqual(attr)) { + return; + } + + boolean first = true; + Color color = (Color)attr.getAttribute(StyleConstants.Foreground); + if (color != null) { + style += "color: " + css.styleConstantsValueToCSSValue + ((StyleConstants)StyleConstants.Foreground, + color); + first = false; + } + Integer size = (Integer)attr.getAttribute(StyleConstants.FontSize); + if (size != null) { + if (!first) { + style += separator; + } + style += "font-size: " + size.intValue() + "pt"; + first = false; + } + + String family = (String)attr.getAttribute(StyleConstants.FontFamily); + if (family != null) { + if (!first) { + style += separator; + } + style += "font-family: " + family; + first = false; + } + + if (style.length() > 0) { + if (fontMask != 0) { + writeEndMask(fontMask); + fontMask = 0; + } + startSpanTag(style); + fontAttributes = attr; + } + else if (fontAttributes != null) { + writeEndMask(fontMask); + fontMask = 0; + endSpanTag(); + } + } + + + /** + * Returns true if we are currently in a <font> tag. + */ + protected boolean inFontTag() { + return (fontAttributes != null); + } + + /** + * This is no longer used, instead <span> will be written out. + * <p> + * Writes out an end tag for the <font> 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 <span> will be written out. + * <p> + * Writes out a start tag for the <font> tag. + * Because font tags cannot be nested, + * this method closes out + * any enclosing font tag before writing out a + * new start tag. + * + * @exception IOException on any I/O error + */ + protected void startFontTag(String style) throws IOException { + boolean callIndent = false; + if (inFontTag()) { + endFontTag(); + callIndent = true; + } + writeStartTag("<font style=\"" + style + "\">"); + if (callIndent) { + indent(); + } + } + + /** + * Writes out a start tag for the <font> tag. + * Because font tags cannot be nested, + * this method closes out + * any enclosing font tag before writing out a + * new start tag. + * + * @exception IOException on any I/O error + */ + private void startSpanTag(String style) throws IOException { + boolean callIndent = false; + if (inFontTag()) { + endSpanTag(); + callIndent = true; + } + writeStartTag("<span style=\"" + style + "\">"); + if (callIndent) { + indent(); + } + } + + /** + * Writes out an end tag for the <span> tag. + * + * @exception IOException on any I/O error + */ + private void endSpanTag() throws IOException { + write(NEWLINE); + writeEndTag("</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 <object> 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 <object> element are expected to + * be present on the associated elements attribute set as simple + * strings. Each bean property will be queried as a key on + * the AttributeSet, with the expectation that a non-null value + * (of type String) will be present if there was a parameter + * specification for the property. Reflection is used to + * set the parameter. Currently, this is limited to a very + * simple single parameter of type String. + * <p> + * A simple example HTML invocation is: + * <pre> + * <object classid="javax.swing.JLabel"> + * <param name="text" value="sample text"> + * </object> + * </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 <param> elements in the corresponding + * <object> element. + */ + private void setParameters(Component comp, AttributeSet attr) { + Class k = comp.getClass(); + BeanInfo bi; + try { + bi = Introspector.getBeanInfo(k); + } catch (IntrospectionException ex) { + System.err.println("introspector failed, ex: "+ex); + return; // quit for now + } + PropertyDescriptor props[] = bi.getPropertyDescriptors(); + for (int i=0; i < props.length; i++) { + // System.err.println("checking on props[i]: "+props[i].getName()); + Object v = attr.getAttribute(props[i].getName()); + if (v instanceof String) { + // found a property parameter + String value = (String) v; + Method writer = props[i].getWriteMethod(); + if (writer == null) { + // read-only property. ignore + return; // for now + } + Class[] params = writer.getParameterTypes(); + if (params.length != 1) { + // zero or more than one argument, ignore + return; // for now + } + Object [] args = { value }; + try { + writer.invoke(comp, args); + } catch (Exception ex) { + System.err.println("Invocation failed"); + // invocation code + } + } + } + } + +} diff --git a/src/share/classes/javax/swing/text/html/Option.java b/src/share/classes/javax/swing/text/html/Option.java new file mode 100644 index 000000000..1cafbdff5 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/Option.java @@ -0,0 +1,120 @@ +/* + * Copyright 1998-2006 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + */ +package javax.swing.text.html; + +import java.io.Serializable; +import javax.swing.text.*; + +/** + * Value for the ListModel used to represent + * <option> elements. This is the object + * installed as items of the DefaultComboBoxModel + * used to represent the <select> element. + * <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> + * + * import java.util.*; + * import javax.swing.text.*; + * import javax.swing.text.html.*; + * + * public class ShowStyles { + * + * public static void main(String[] args) { + * HTMLEditorKit kit = new HTMLEditorKit(); + * HTMLDocument doc = (HTMLDocument) kit.createDefaultDocument(); + * StyleSheet styles = doc.getStyleSheet(); + * + * Enumeration rules = styles.getStyleNames(); + * while (rules.hasMoreElements()) { + * String name = (String) rules.nextElement(); + * Style rule = styles.getStyle(name); + * System.out.println(rule.toString()); + * } + * System.exit(0); + * } + * } + * + * </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><body bgcolor=red></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 <style> tag. + */ + public void addRule(String rule) { + if (rule != null) { + //tweaks to control display properties + //see BasicEditorPaneUI + final String baseUnitsDisable = "BASE_SIZE_DISABLE"; + final String baseUnits = "BASE_SIZE "; + final String w3cLengthUnitsEnable = "W3C_LENGTH_UNITS_ENABLE"; + final String w3cLengthUnitsDisable = "W3C_LENGTH_UNITS_DISABLE"; + if (rule == baseUnitsDisable) { + sizeMap = sizeMapDefault; + } else if (rule.startsWith(baseUnits)) { + rebaseSizeMap(Integer. + parseInt(rule.substring(baseUnits.length()))); + } else if (rule == w3cLengthUnitsEnable) { + w3cLengthUnits = true; + } else if (rule == w3cLengthUnitsDisable) { + w3cLengthUnits = false; + } else { + CssParser parser = new CssParser(); + try { + parser.parse(getBase(), new StringReader(rule), false, false); + } catch (IOException ioe) { } + } + } + } + + /** + * Translates a CSS declaration to an AttributeSet that represents + * the CSS declaration. Typically this would be called as a + * result of encountering an HTML style attribute. + */ + public AttributeSet getDeclaration(String decl) { + if (decl == null) { + return SimpleAttributeSet.EMPTY; + } + CssParser parser = new CssParser(); + return parser.parseDeclaration(decl); + } + + /** + * Loads a set of rules that have been specified in terms of + * CSS1 grammar. If there are collisions with existing rules, + * the newly specified rule will win. + * + * @param in the stream to read the CSS grammar from + * @param ref the reference URL. This value represents the + * location of the stream and may be null. All relative + * URLs specified in the stream will be based upon this + * parameter. + */ + public void loadRules(Reader in, URL ref) throws IOException { + CssParser parser = new CssParser(); + parser.parse(ref, in, false, false); + } + + /** + * Fetches a set of attributes to use in the view for + * displaying. This is basically a set of attributes that + * can be used for View.getAttributes. + */ + public AttributeSet getViewAttributes(View v) { + return new ViewAttributeSet(v); + } + + /** + * Removes a named style previously added to the document. + * + * @param nm the name of the style to remove + */ + public void removeStyle(String nm) { + Style aStyle = getStyle(nm); + + if (aStyle != null) { + String selector = cleanSelectorString(nm); + String[] selectors = getSimpleSelectors(selector); + synchronized(this) { + SelectorMapping mapping = getRootSelectorMapping(); + for (int i = selectors.length - 1; i >= 0; i--) { + mapping = mapping.getChildSelectorMapping(selectors[i], + true); + } + Style rule = mapping.getStyle(); + if (rule != null) { + mapping.setStyle(null); + if (resolvedStyles.size() > 0) { + Enumeration values = resolvedStyles.elements(); + while (values.hasMoreElements()) { + ResolvedStyle style = (ResolvedStyle)values. + nextElement(); + style.removeStyle(rule); + } + } + } + } + } + super.removeStyle(nm); + } + + /** + * Adds the rules from the StyleSheet <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 (<p>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 <p foo=bar class=neat> has two attributes. foo is + * not defined in HTML.Attribute, where as class is, therefore the + * AttributeSet will have two values in it, HTML.Attribute.CLASS with + * a String value of 'neat' and the String key 'foo' with a String value of + * 'bar'. + * <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><foo blah></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><!ATTLIST OPTION selected (selected) #IMPLIED></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 &nbsp to force the space to + * be displayed): + * <p> + * '<b>blah <i> <strike> foo' which can be treated as: + * '<b>blah <i><strike>foo' + * <p>as well as: + * '<p><a href="xx"> <em>Using</em></a></p>' + * which appears to be treated as: + * '<p><a href="xx"><em>Using</em></a></p>' + * <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: + * '<b>blah <i> <strike> foo' which can be treated as: + * '<b>blah <i><strike>foo' + * as well as: + * '<p><a href="xx"> <em>Using</em></a></p>' + * which appears to be treated as: + * '<p><a href="xx"><em>Using</em></a></p>' + * <p> + * When a tag that breaks flow, or trailing whitespace is encountered + * ignoreSpace is set to true. From then on, all whitespace will be + * ignored. + * ignoreSpace will be set back to false the first time a + * non whitespace character is encountered. This appears to give + * behavior closer to the popular browsers. + */ + private boolean ignoreSpace; + + /** + * This flag determines whether or not the Parser will be strict + * in enforcing SGML compatibility. If false, it will be lenient + * with certain common classes of erroneous HTML constructs. + * Strict or not, in either case an error will be recorded. + * + */ + protected boolean strict = false; + + + /** Number of \r\n's encountered. */ + private int crlfCount; + /** Number of \r's encountered. A \r\n will not increment this. */ + private int crCount; + /** Number of \n's encountered. A \r\n will not increment this. */ + private int lfCount; + + // + // To correctly identify the start of a tag/comment/text we need two + // ivars. Two are needed as handleText isn't invoked until the tag + // after the text has been parsed, that is the parser parses the text, + // then a tag, then invokes handleText followed by handleStart. + // + /** The start position of the current block. Block is overloaded here, + * it really means the current start position for the current comment, + * tag, text. Use getBlockStartPosition to access this. */ + private int currentBlockStartPos; + /** Start position of the last block. */ + private int lastBlockStartPos; + + /** + * array for mapping numeric references in range + * 130-159 to displayable Unicode characters. + */ + private static final char[] cp1252Map = { + 8218, // ‚ + 402, // ƒ + 8222, // „ + 8230, // … + 8224, // † + 8225, // ‡ + 710, // ˆ + 8240, // ‰ + 352, // Š + 8249, // ‹ + 338, // Œ + 141, //  + 142, // Ž + 143, //  + 144, //  + 8216, // ‘ + 8217, // ’ + 8220, // “ + 8221, // ” + 8226, // • + 8211, // – + 8212, // — + 732, // ˜ + 8482, // ™ + 353, // š + 8250, // › + 339, // œ + 157, //  + 158, // ž + 376 // Ÿ + }; + + public Parser(DTD dtd) { + this.dtd = dtd; + } + + + /** + * @return the line number of the line currently being parsed + */ + protected int getCurrentLine() { + return ln; + } + + /** + * Returns the start position of the current block. Block is + * overloaded here, it really means the current start position for + * the current comment tag, text, block.... This is provided for + * subclassers that wish to know the start of the current block when + * called with one of the handleXXX methods. + */ + int getBlockStartPosition() { + return Math.max(0, lastBlockStartPos - 1); + } + + /** + * Makes a TagElement. + */ + protected TagElement makeTag(Element elem, boolean fictional) { + return new TagElement(elem, fictional); + } + + protected TagElement makeTag(Element elem) { + return makeTag(elem, false); + } + + protected SimpleAttributeSet getAttributes() { + return attributes; + } + + protected void flushAttributes() { + attributes.removeAttributes(attributes); + } + + /** + * Called when PCDATA is encountered. + */ + protected void handleText(char text[]) { + } + + /** + * Called when an HTML title tag is encountered. + */ + protected void handleTitle(char text[]) { + // default behavior is to call handleText. Subclasses + // can override if necessary. + handleText(text); + } + + /** + * Called when an HTML comment is encountered. + */ + protected void handleComment(char text[]) { + } + + protected void handleEOFInComment() { + // We've reached EOF. Our recovery strategy is to + // see if we have more than one line in the comment; + // if so, we pretend that the comment was an unterminated + // single line comment, and reparse the lines after the + // first line as normal HTML content. + + int commentEndPos = strIndexOf('\n'); + if (commentEndPos >= 0) { + handleComment(getChars(0, commentEndPos)); + try { + in.close(); + in = new CharArrayReader(getChars(commentEndPos + 1)); + ch = '>'; + } catch (IOException e) { + error("ioexception"); + } + + resetStrBuffer(); + } else { + // no newline, so signal an error + error("eof.comment"); + } + } + + /** + * Called when an empty tag is encountered. + */ + protected void handleEmptyTag(TagElement tag) throws ChangedCharSetException { + } + + /** + * Called when a start tag is encountered. + */ + protected void handleStartTag(TagElement tag) { + } + + /** + * Called when an end tag is encountered. + */ + protected void handleEndTag(TagElement tag) { + } + + /** + * An error has occurred. + */ + protected void handleError(int ln, String msg) { + /* + Thread.dumpStack(); + System.out.println("**** " + stack); + System.out.println("line " + ln + ": error: " + msg); + System.out.println(); + */ + } + + /** + * Output text. + */ + void handleText(TagElement tag) { + if (tag.breaksFlow()) { + space = false; + if (!strict) { + ignoreSpace = true; + } + } + if (textpos == 0) { + if ((!space) || (stack == null) || last.breaksFlow() || + !stack.advance(dtd.pcdata)) { + last = tag; + space = false; + lastBlockStartPos = currentBlockStartPos; + return; + } + } + if (space) { + if (!ignoreSpace) { + // enlarge buffer if needed + if (textpos + 1 > text.length) { + char newtext[] = new char[text.length + 200]; + System.arraycopy(text, 0, newtext, 0, text.length); + text = newtext; + } + + // output pending space + text[textpos++] = ' '; + if (!strict && !tag.getElement().isEmpty()) { + ignoreSpace = true; + } + } + space = false; + } + char newtext[] = new char[textpos]; + System.arraycopy(text, 0, newtext, 0, textpos); + // Handles cases of bad html where the title tag + // was getting lost when we did error recovery. + if (tag.getElement().getName().equals("title")) { + handleTitle(newtext); + } else { + handleText(newtext); + } + lastBlockStartPos = currentBlockStartPos; + textpos = 0; + last = tag; + space = false; + } + + /** + * Invoke the error handler. + */ + protected void error(String err, String arg1, String arg2, + String arg3) { + handleError(ln, err + " " + arg1 + " " + arg2 + " " + arg3); + } + + protected void error(String err, String arg1, String arg2) { + error(err, arg1, arg2, "?"); + } + protected void error(String err, String arg1) { + error(err, arg1, "?", "?"); + } + protected void error(String err) { + error(err, "?", "?", "?"); + } + + + /** + * Handle a start tag. The new tag is pushed + * onto the tag stack. The attribute list is + * checked for required attributes. + */ + protected void startTag(TagElement tag) throws ChangedCharSetException { + Element elem = tag.getElement(); + + // If the tag is an empty tag and texpos != 0 + // this implies that there is text before the + // start tag that needs to be processed before + // handling the tag. + // + if (!elem.isEmpty() || + ((last != null) && !last.breaksFlow()) || + (textpos != 0)) { + handleText(tag); + } else { + // this variable gets updated in handleText(). + // Since in this case we do not call handleText() + // we need to update it here. + // + last = tag; + // Note that we should really check last.breakFlows before + // assuming this should be false. + space = false; + } + lastBlockStartPos = currentBlockStartPos; + + // check required attributes + for (AttributeList a = elem.atts ; a != null ; a = a.next) { + if ((a.modifier == REQUIRED) && + ((attributes.isEmpty()) || + ((!attributes.isDefined(a.name)) && + (!attributes.isDefined(HTML.getAttributeKey(a.name)))))) { + error("req.att ", a.getName(), elem.getName()); + } + } + + if (elem.isEmpty()) { + handleEmptyTag(tag); + /* + } else if (elem.getName().equals("form")) { + handleStartTag(tag); + */ + } else { + recent = elem; + stack = new TagStack(tag, stack); + handleStartTag(tag); + } + } + + /** + * Handle an end tag. The end tag is popped + * from the tag stack. + */ + protected void endTag(boolean omitted) { + handleText(stack.tag); + + if (omitted && !stack.elem.omitEnd()) { + error("end.missing", stack.elem.getName()); + } else if (!stack.terminate()) { + error("end.unexpected", stack.elem.getName()); + } + + // handle the tag + handleEndTag(stack.tag); + stack = stack.next; + recent = (stack != null) ? stack.elem : null; + } + + + boolean ignoreElement(Element elem) { + + String stackElement = stack.elem.getName(); + String elemName = elem.getName(); + /* We ignore all elements that are not valid in the context of + a table except <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 Binary files differnew file mode 100644 index 000000000..a48d51ce8 --- /dev/null +++ b/src/share/classes/javax/swing/text/html/parser/html32.bdtd 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> |