| /* AbstractDocument.java -- | |
| Copyright (C) 2002, 2004, 2005, 2006 Free Software Foundation, Inc. | |
| This file is part of GNU Classpath. | |
| GNU Classpath is free software; you can redistribute it and/or modify | |
| it under the terms of the GNU General Public License as published by | |
| the Free Software Foundation; either version 2, or (at your option) | |
| any later version. | |
| GNU Classpath is distributed in the hope that it will be useful, but | |
| WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| General Public License for more details. | |
| You should have received a copy of the GNU General Public License | |
| along with GNU Classpath; see the file COPYING. If not, write to the | |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
| 02110-1301 USA. | |
| Linking this library statically or dynamically with other modules is | |
| making a combined work based on this library. Thus, the terms and | |
| conditions of the GNU General Public License cover the whole | |
| combination. | |
| As a special exception, the copyright holders of this library give you | |
| permission to link this library with independent modules to produce an | |
| executable, regardless of the license terms of these independent | |
| modules, and to copy and distribute the resulting executable under | |
| terms of your choice, provided that you also meet, for each linked | |
| independent module, the terms and conditions of the license of that | |
| module. An independent module is a module which is not derived from | |
| or based on this library. If you modify this library, you may extend | |
| this exception to your version of the library, but you are not | |
| obligated to do so. If you do not wish to do so, delete this | |
| exception statement from your version. */ | |
| package javax.swing.text; | |
| import gnu.java.lang.CPStringBuilder; | |
| import java.awt.font.TextAttribute; | |
| import java.io.PrintStream; | |
| import java.io.Serializable; | |
| import java.text.Bidi; | |
| import java.util.ArrayList; | |
| import java.util.Dictionary; | |
| import java.util.Enumeration; | |
| import java.util.EventListener; | |
| import java.util.HashMap; | |
| import java.util.Hashtable; | |
| import java.util.Vector; | |
| import javax.swing.event.DocumentEvent; | |
| import javax.swing.event.DocumentListener; | |
| import javax.swing.event.EventListenerList; | |
| import javax.swing.event.UndoableEditEvent; | |
| import javax.swing.event.UndoableEditListener; | |
| import javax.swing.text.DocumentFilter; | |
| import javax.swing.tree.TreeNode; | |
| import javax.swing.undo.AbstractUndoableEdit; | |
| import javax.swing.undo.CompoundEdit; | |
| import javax.swing.undo.UndoableEdit; | |
| /** | |
| * An abstract base implementation for the {@link Document} interface. | |
| * This class provides some common functionality for all <code>Element</code>s, | |
| * most notably it implements a locking mechanism to make document modification | |
| * thread-safe. | |
| * | |
| * @author original author unknown | |
| * @author Roman Kennke (roman@kennke.org) | |
| */ | |
| public abstract class AbstractDocument implements Document, Serializable | |
| { | |
| /** The serialization UID (compatible with JDK1.5). */ | |
| private static final long serialVersionUID = 6842927725919637215L; | |
| /** | |
| * Standard error message to indicate a bad location. | |
| */ | |
| protected static final String BAD_LOCATION = "document location failure"; | |
| /** | |
| * Standard name for unidirectional <code>Element</code>s. | |
| */ | |
| public static final String BidiElementName = "bidi level"; | |
| /** | |
| * Standard name for content <code>Element</code>s. These are usually | |
| * {@link LeafElement}s. | |
| */ | |
| public static final String ContentElementName = "content"; | |
| /** | |
| * Standard name for paragraph <code>Element</code>s. These are usually | |
| * {@link BranchElement}s. | |
| */ | |
| public static final String ParagraphElementName = "paragraph"; | |
| /** | |
| * Standard name for section <code>Element</code>s. These are usually | |
| * {@link DefaultStyledDocument.SectionElement}s. | |
| */ | |
| public static final String SectionElementName = "section"; | |
| /** | |
| * Attribute key for storing the element name. | |
| */ | |
| public static final String ElementNameAttribute = "$ename"; | |
| /** | |
| * Standard name for the bidi root element. | |
| */ | |
| private static final String BidiRootName = "bidi root"; | |
| /** | |
| * Key for storing the asynchronous load priority. | |
| */ | |
| private static final String AsyncLoadPriority = "load priority"; | |
| /** | |
| * Key for storing the I18N state. | |
| */ | |
| private static final String I18N = "i18n"; | |
| /** | |
| * The actual content model of this <code>Document</code>. | |
| */ | |
| Content content; | |
| /** | |
| * The AttributeContext for this <code>Document</code>. | |
| */ | |
| AttributeContext context; | |
| /** | |
| * The currently installed <code>DocumentFilter</code>. | |
| */ | |
| DocumentFilter documentFilter; | |
| /** | |
| * The documents properties. | |
| */ | |
| Dictionary properties; | |
| /** | |
| * Manages event listeners for this <code>Document</code>. | |
| */ | |
| protected EventListenerList listenerList = new EventListenerList(); | |
| /** | |
| * Stores the current writer thread. Used for locking. | |
| */ | |
| private Thread currentWriter = null; | |
| /** | |
| * The number of readers. Used for locking. | |
| */ | |
| private int numReaders = 0; | |
| /** | |
| * The number of current writers. If this is > 1 then the same thread entered | |
| * the write lock more than once. | |
| */ | |
| private int numWriters = 0; | |
| /** An instance of a DocumentFilter.FilterBypass which allows calling | |
| * the insert, remove and replace method without checking for an installed | |
| * document filter. | |
| */ | |
| private DocumentFilter.FilterBypass bypass; | |
| /** | |
| * The bidi root element. | |
| */ | |
| private BidiRootElement bidiRoot; | |
| /** | |
| * True when we are currently notifying any listeners. This is used | |
| * to detect illegal situations in writeLock(). | |
| */ | |
| private transient boolean notifyListeners; | |
| /** | |
| * Creates a new <code>AbstractDocument</code> with the specified | |
| * {@link Content} model. | |
| * | |
| * @param doc the <code>Content</code> model to be used in this | |
| * <code>Document<code> | |
| * | |
| * @see GapContent | |
| * @see StringContent | |
| */ | |
| protected AbstractDocument(Content doc) | |
| { | |
| this(doc, StyleContext.getDefaultStyleContext()); | |
| } | |
| /** | |
| * Creates a new <code>AbstractDocument</code> with the specified | |
| * {@link Content} model and {@link AttributeContext}. | |
| * | |
| * @param doc the <code>Content</code> model to be used in this | |
| * <code>Document<code> | |
| * @param ctx the <code>AttributeContext</code> to use | |
| * | |
| * @see GapContent | |
| * @see StringContent | |
| */ | |
| protected AbstractDocument(Content doc, AttributeContext ctx) | |
| { | |
| content = doc; | |
| context = ctx; | |
| // FIXME: Fully implement bidi. | |
| bidiRoot = new BidiRootElement(); | |
| // FIXME: This is determined using a Mauve test. Make the document | |
| // actually use this. | |
| putProperty(I18N, Boolean.FALSE); | |
| // Add one child to the bidi root. | |
| writeLock(); | |
| try | |
| { | |
| Element[] children = new Element[1]; | |
| children[0] = new BidiElement(bidiRoot, 0, 1, 0); | |
| bidiRoot.replace(0, 0, children); | |
| } | |
| finally | |
| { | |
| writeUnlock(); | |
| } | |
| } | |
| /** Returns the DocumentFilter.FilterBypass instance for this | |
| * document and create it if it does not exist yet. | |
| * | |
| * @return This document's DocumentFilter.FilterBypass instance. | |
| */ | |
| private DocumentFilter.FilterBypass getBypass() | |
| { | |
| if (bypass == null) | |
| bypass = new Bypass(); | |
| return bypass; | |
| } | |
| /** | |
| * Returns the paragraph {@link Element} that holds the specified position. | |
| * | |
| * @param pos the position for which to get the paragraph element | |
| * | |
| * @return the paragraph {@link Element} that holds the specified position | |
| */ | |
| public abstract Element getParagraphElement(int pos); | |
| /** | |
| * Returns the default root {@link Element} of this <code>Document</code>. | |
| * Usual <code>Document</code>s only have one root element and return this. | |
| * However, there may be <code>Document</code> implementations that | |
| * support multiple root elements, they have to return a default root element | |
| * here. | |
| * | |
| * @return the default root {@link Element} of this <code>Document</code> | |
| */ | |
| public abstract Element getDefaultRootElement(); | |
| /** | |
| * Creates and returns a branch element with the specified | |
| * <code>parent</code> and <code>attributes</code>. Note that the new | |
| * <code>Element</code> is linked to the parent <code>Element</code> | |
| * through {@link Element#getParentElement}, but it is not yet added | |
| * to the parent <code>Element</code> as child. | |
| * | |
| * @param parent the parent <code>Element</code> for the new branch element | |
| * @param attributes the text attributes to be installed in the new element | |
| * | |
| * @return the new branch <code>Element</code> | |
| * | |
| * @see BranchElement | |
| */ | |
| protected Element createBranchElement(Element parent, | |
| AttributeSet attributes) | |
| { | |
| return new BranchElement(parent, attributes); | |
| } | |
| /** | |
| * Creates and returns a leaf element with the specified | |
| * <code>parent</code> and <code>attributes</code>. Note that the new | |
| * <code>Element</code> is linked to the parent <code>Element</code> | |
| * through {@link Element#getParentElement}, but it is not yet added | |
| * to the parent <code>Element</code> as child. | |
| * | |
| * @param parent the parent <code>Element</code> for the new branch element | |
| * @param attributes the text attributes to be installed in the new element | |
| * | |
| * @return the new branch <code>Element</code> | |
| * | |
| * @see LeafElement | |
| */ | |
| protected Element createLeafElement(Element parent, AttributeSet attributes, | |
| int start, int end) | |
| { | |
| return new LeafElement(parent, attributes, start, end); | |
| } | |
| /** | |
| * Creates a {@link Position} that keeps track of the location at the | |
| * specified <code>offset</code>. | |
| * | |
| * @param offset the location in the document to keep track by the new | |
| * <code>Position</code> | |
| * | |
| * @return the newly created <code>Position</code> | |
| * | |
| * @throws BadLocationException if <code>offset</code> is not a valid | |
| * location in the documents content model | |
| */ | |
| public synchronized Position createPosition(final int offset) | |
| throws BadLocationException | |
| { | |
| return content.createPosition(offset); | |
| } | |
| /** | |
| * Notifies all registered listeners when the document model changes. | |
| * | |
| * @param event the <code>DocumentEvent</code> to be fired | |
| */ | |
| protected void fireChangedUpdate(DocumentEvent event) | |
| { | |
| notifyListeners = true; | |
| try | |
| { | |
| DocumentListener[] listeners = getDocumentListeners(); | |
| for (int index = 0; index < listeners.length; ++index) | |
| listeners[index].changedUpdate(event); | |
| } | |
| finally | |
| { | |
| notifyListeners = false; | |
| } | |
| } | |
| /** | |
| * Notifies all registered listeners when content is inserted in the document | |
| * model. | |
| * | |
| * @param event the <code>DocumentEvent</code> to be fired | |
| */ | |
| protected void fireInsertUpdate(DocumentEvent event) | |
| { | |
| notifyListeners = true; | |
| try | |
| { | |
| DocumentListener[] listeners = getDocumentListeners(); | |
| for (int index = 0; index < listeners.length; ++index) | |
| listeners[index].insertUpdate(event); | |
| } | |
| finally | |
| { | |
| notifyListeners = false; | |
| } | |
| } | |
| /** | |
| * Notifies all registered listeners when content is removed from the | |
| * document model. | |
| * | |
| * @param event the <code>DocumentEvent</code> to be fired | |
| */ | |
| protected void fireRemoveUpdate(DocumentEvent event) | |
| { | |
| notifyListeners = true; | |
| try | |
| { | |
| DocumentListener[] listeners = getDocumentListeners(); | |
| for (int index = 0; index < listeners.length; ++index) | |
| listeners[index].removeUpdate(event); | |
| } | |
| finally | |
| { | |
| notifyListeners = false; | |
| } | |
| } | |
| /** | |
| * Notifies all registered listeners when an <code>UndoableEdit</code> has | |
| * been performed on this <code>Document</code>. | |
| * | |
| * @param event the <code>UndoableEditEvent</code> to be fired | |
| */ | |
| protected void fireUndoableEditUpdate(UndoableEditEvent event) | |
| { | |
| UndoableEditListener[] listeners = getUndoableEditListeners(); | |
| for (int index = 0; index < listeners.length; ++index) | |
| listeners[index].undoableEditHappened(event); | |
| } | |
| /** | |
| * Returns the asynchronous loading priority. Returns <code>-1</code> if this | |
| * document should not be loaded asynchronously. | |
| * | |
| * @return the asynchronous loading priority | |
| */ | |
| public int getAsynchronousLoadPriority() | |
| { | |
| Object val = getProperty(AsyncLoadPriority); | |
| int prio = -1; | |
| if (val != null) | |
| prio = ((Integer) val).intValue(); | |
| return prio; | |
| } | |
| /** | |
| * Returns the {@link AttributeContext} used in this <code>Document</code>. | |
| * | |
| * @return the {@link AttributeContext} used in this <code>Document</code> | |
| */ | |
| protected final AttributeContext getAttributeContext() | |
| { | |
| return context; | |
| } | |
| /** | |
| * Returns the root element for bidirectional content. | |
| * | |
| * @return the root element for bidirectional content | |
| */ | |
| public Element getBidiRootElement() | |
| { | |
| return bidiRoot; | |
| } | |
| /** | |
| * Returns the {@link Content} model for this <code>Document</code> | |
| * | |
| * @return the {@link Content} model for this <code>Document</code> | |
| * | |
| * @see GapContent | |
| * @see StringContent | |
| */ | |
| protected final Content getContent() | |
| { | |
| return content; | |
| } | |
| /** | |
| * Returns the thread that currently modifies this <code>Document</code> | |
| * if there is one, otherwise <code>null</code>. This can be used to | |
| * distinguish between a method call that is part of an ongoing modification | |
| * or if it is a separate modification for which a new lock must be aquired. | |
| * | |
| * @return the thread that currently modifies this <code>Document</code> | |
| * if there is one, otherwise <code>null</code> | |
| */ | |
| protected final synchronized Thread getCurrentWriter() | |
| { | |
| return currentWriter; | |
| } | |
| /** | |
| * Returns the properties of this <code>Document</code>. | |
| * | |
| * @return the properties of this <code>Document</code> | |
| */ | |
| public Dictionary<Object, Object> getDocumentProperties() | |
| { | |
| // FIXME: make me thread-safe | |
| if (properties == null) | |
| properties = new Hashtable(); | |
| return properties; | |
| } | |
| /** | |
| * Returns a {@link Position} which will always mark the end of the | |
| * <code>Document</code>. | |
| * | |
| * @return a {@link Position} which will always mark the end of the | |
| * <code>Document</code> | |
| */ | |
| public final Position getEndPosition() | |
| { | |
| Position p; | |
| try | |
| { | |
| p = createPosition(content.length()); | |
| } | |
| catch (BadLocationException ex) | |
| { | |
| // Shouldn't really happen. | |
| p = null; | |
| } | |
| return p; | |
| } | |
| /** | |
| * Returns the length of this <code>Document</code>'s content. | |
| * | |
| * @return the length of this <code>Document</code>'s content | |
| */ | |
| public int getLength() | |
| { | |
| // We return Content.getLength() -1 here because there is always an | |
| // implicit \n at the end of the Content which does count in Content | |
| // but not in Document. | |
| return content.length() - 1; | |
| } | |
| /** | |
| * Returns all registered listeners of a given listener type. | |
| * | |
| * @param listenerType the type of the listeners to be queried | |
| * | |
| * @return all registered listeners of the specified type | |
| */ | |
| public <T extends EventListener> T[] getListeners(Class<T> listenerType) | |
| { | |
| return listenerList.getListeners(listenerType); | |
| } | |
| /** | |
| * Returns a property from this <code>Document</code>'s property list. | |
| * | |
| * @param key the key of the property to be fetched | |
| * | |
| * @return the property for <code>key</code> or <code>null</code> if there | |
| * is no such property stored | |
| */ | |
| public final Object getProperty(Object key) | |
| { | |
| // FIXME: make me thread-safe | |
| Object value = null; | |
| if (properties != null) | |
| value = properties.get(key); | |
| return value; | |
| } | |
| /** | |
| * Returns all root elements of this <code>Document</code>. By default | |
| * this just returns the single root element returned by | |
| * {@link #getDefaultRootElement()}. <code>Document</code> implementations | |
| * that support multiple roots must override this method and return all roots | |
| * here. | |
| * | |
| * @return all root elements of this <code>Document</code> | |
| */ | |
| public Element[] getRootElements() | |
| { | |
| Element[] elements = new Element[2]; | |
| elements[0] = getDefaultRootElement(); | |
| elements[1] = getBidiRootElement(); | |
| return elements; | |
| } | |
| /** | |
| * Returns a {@link Position} which will always mark the beginning of the | |
| * <code>Document</code>. | |
| * | |
| * @return a {@link Position} which will always mark the beginning of the | |
| * <code>Document</code> | |
| */ | |
| public final Position getStartPosition() | |
| { | |
| Position p; | |
| try | |
| { | |
| p = createPosition(0); | |
| } | |
| catch (BadLocationException ex) | |
| { | |
| // Shouldn't really happen. | |
| p = null; | |
| } | |
| return p; | |
| } | |
| /** | |
| * Returns a piece of this <code>Document</code>'s content. | |
| * | |
| * @param offset the start offset of the content | |
| * @param length the length of the content | |
| * | |
| * @return the piece of content specified by <code>offset</code> and | |
| * <code>length</code> | |
| * | |
| * @throws BadLocationException if <code>offset</code> or <code>offset + | |
| * length</code> are invalid locations with this | |
| * <code>Document</code> | |
| */ | |
| public String getText(int offset, int length) throws BadLocationException | |
| { | |
| return content.getString(offset, length); | |
| } | |
| /** | |
| * Fetches a piece of this <code>Document</code>'s content and stores | |
| * it in the given {@link Segment}. | |
| * | |
| * @param offset the start offset of the content | |
| * @param length the length of the content | |
| * @param segment the <code>Segment</code> to store the content in | |
| * | |
| * @throws BadLocationException if <code>offset</code> or <code>offset + | |
| * length</code> are invalid locations with this | |
| * <code>Document</code> | |
| */ | |
| public void getText(int offset, int length, Segment segment) | |
| throws BadLocationException | |
| { | |
| content.getChars(offset, length, segment); | |
| } | |
| /** | |
| * Inserts a String into this <code>Document</code> at the specified | |
| * position and assigning the specified attributes to it. | |
| * | |
| * <p>If a {@link DocumentFilter} is installed in this document, the | |
| * corresponding method of the filter object is called.</p> | |
| * | |
| * <p>The method has no effect when <code>text</code> is <code>null</code> | |
| * or has a length of zero.</p> | |
| * | |
| * | |
| * @param offset the location at which the string should be inserted | |
| * @param text the content to be inserted | |
| * @param attributes the text attributes to be assigned to that string | |
| * | |
| * @throws BadLocationException if <code>offset</code> is not a valid | |
| * location in this <code>Document</code> | |
| */ | |
| public void insertString(int offset, String text, AttributeSet attributes) | |
| throws BadLocationException | |
| { | |
| // Bail out if we have a bogus insertion (Behavior observed in RI). | |
| if (text == null || text.length() == 0) | |
| return; | |
| writeLock(); | |
| try | |
| { | |
| if (documentFilter == null) | |
| insertStringImpl(offset, text, attributes); | |
| else | |
| documentFilter.insertString(getBypass(), offset, text, attributes); | |
| } | |
| finally | |
| { | |
| writeUnlock(); | |
| } | |
| } | |
| void insertStringImpl(int offset, String text, AttributeSet attributes) | |
| throws BadLocationException | |
| { | |
| // Just return when no text to insert was given. | |
| if (text == null || text.length() == 0) | |
| return; | |
| DefaultDocumentEvent event = | |
| new DefaultDocumentEvent(offset, text.length(), | |
| DocumentEvent.EventType.INSERT); | |
| UndoableEdit undo = content.insertString(offset, text); | |
| if (undo != null) | |
| event.addEdit(undo); | |
| // Check if we need bidi layout. | |
| if (getProperty(I18N).equals(Boolean.FALSE)) | |
| { | |
| Object dir = getProperty(TextAttribute.RUN_DIRECTION); | |
| if (TextAttribute.RUN_DIRECTION_RTL.equals(dir)) | |
| putProperty(I18N, Boolean.TRUE); | |
| else | |
| { | |
| char[] chars = text.toCharArray(); | |
| if (Bidi.requiresBidi(chars, 0, chars.length)) | |
| putProperty(I18N, Boolean.TRUE); | |
| } | |
| } | |
| insertUpdate(event, attributes); | |
| fireInsertUpdate(event); | |
| if (undo != null) | |
| fireUndoableEditUpdate(new UndoableEditEvent(this, undo)); | |
| } | |
| /** | |
| * Called to indicate that text has been inserted into this | |
| * <code>Document</code>. The default implementation does nothing. | |
| * This method is executed within a write lock. | |
| * | |
| * @param chng the <code>DefaultDocumentEvent</code> describing the change | |
| * @param attr the attributes of the changed content | |
| */ | |
| protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) | |
| { | |
| if (Boolean.TRUE.equals(getProperty(I18N))) | |
| updateBidi(chng); | |
| } | |
| /** | |
| * Called after some content has been removed from this | |
| * <code>Document</code>. The default implementation does nothing. | |
| * This method is executed within a write lock. | |
| * | |
| * @param chng the <code>DefaultDocumentEvent</code> describing the change | |
| */ | |
| protected void postRemoveUpdate(DefaultDocumentEvent chng) | |
| { | |
| if (Boolean.TRUE.equals(getProperty(I18N))) | |
| updateBidi(chng); | |
| } | |
| /** | |
| * Stores a property in this <code>Document</code>'s property list. | |
| * | |
| * @param key the key of the property to be stored | |
| * @param value the value of the property to be stored | |
| */ | |
| public final void putProperty(Object key, Object value) | |
| { | |
| // FIXME: make me thread-safe | |
| if (properties == null) | |
| properties = new Hashtable(); | |
| if (value == null) | |
| properties.remove(key); | |
| else | |
| properties.put(key, value); | |
| // Update bidi structure if the RUN_DIRECTION is set. | |
| if (TextAttribute.RUN_DIRECTION.equals(key)) | |
| { | |
| if (TextAttribute.RUN_DIRECTION_RTL.equals(value) | |
| && Boolean.FALSE.equals(getProperty(I18N))) | |
| putProperty(I18N, Boolean.TRUE); | |
| if (Boolean.TRUE.equals(getProperty(I18N))) | |
| { | |
| writeLock(); | |
| try | |
| { | |
| DefaultDocumentEvent ev = | |
| new DefaultDocumentEvent(0, getLength(), | |
| DocumentEvent.EventType.INSERT); | |
| updateBidi(ev); | |
| } | |
| finally | |
| { | |
| writeUnlock(); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Updates the bidi element structure. | |
| * | |
| * @param ev the document event for the change | |
| */ | |
| private void updateBidi(DefaultDocumentEvent ev) | |
| { | |
| // Determine start and end offset of the paragraphs to be scanned. | |
| int start = 0; | |
| int end = 0; | |
| DocumentEvent.EventType type = ev.getType(); | |
| if (type == DocumentEvent.EventType.INSERT | |
| || type == DocumentEvent.EventType.CHANGE) | |
| { | |
| int offs = ev.getOffset(); | |
| int endOffs = offs + ev.getLength(); | |
| start = getParagraphElement(offs).getStartOffset(); | |
| end = getParagraphElement(endOffs).getEndOffset(); | |
| } | |
| else if (type == DocumentEvent.EventType.REMOVE) | |
| { | |
| Element par = getParagraphElement(ev.getOffset()); | |
| start = par.getStartOffset(); | |
| end = par.getEndOffset(); | |
| } | |
| else | |
| assert false : "Unknown event type"; | |
| // Determine the bidi levels for the affected range. | |
| Bidi[] bidis = getBidis(start, end); | |
| int removeFrom = 0; | |
| int removeTo = 0; | |
| int offs = 0; | |
| int lastRunStart = 0; | |
| int lastRunEnd = 0; | |
| int lastRunLevel = 0; | |
| ArrayList newEls = new ArrayList(); | |
| for (int i = 0; i < bidis.length; i++) | |
| { | |
| Bidi bidi = bidis[i]; | |
| int numRuns = bidi.getRunCount(); | |
| for (int r = 0; r < numRuns; r++) | |
| { | |
| if (r == 0 && i == 0) | |
| { | |
| if (start > 0) | |
| { | |
| // Try to merge with the previous element if it has the | |
| // same bidi level as the first run. | |
| int prevElIndex = bidiRoot.getElementIndex(start - 1); | |
| removeFrom = prevElIndex; | |
| Element prevEl = bidiRoot.getElement(prevElIndex); | |
| AttributeSet atts = prevEl.getAttributes(); | |
| int prevElLevel = StyleConstants.getBidiLevel(atts); | |
| if (prevElLevel == bidi.getRunLevel(r)) | |
| { | |
| // Merge previous element with current run. | |
| lastRunStart = prevEl.getStartOffset() - start; | |
| lastRunEnd = bidi.getRunLimit(r); | |
| lastRunLevel = bidi.getRunLevel(r); | |
| } | |
| else if (prevEl.getEndOffset() > start) | |
| { | |
| // Split previous element and replace by 2 new elements. | |
| lastRunStart = 0; | |
| lastRunEnd = bidi.getRunLimit(r); | |
| lastRunLevel = bidi.getRunLevel(r); | |
| newEls.add(new BidiElement(bidiRoot, | |
| prevEl.getStartOffset(), | |
| start, prevElLevel)); | |
| } | |
| else | |
| { | |
| // Simply start new run at start location. | |
| lastRunStart = 0; | |
| lastRunEnd = bidi.getRunLimit(r); | |
| lastRunLevel = bidi.getRunLevel(r); | |
| removeFrom++; | |
| } | |
| } | |
| else | |
| { | |
| // Simply start new run at start location. | |
| lastRunStart = 0; | |
| lastRunEnd = bidi.getRunLimit(r); | |
| lastRunLevel = bidi.getRunLevel(r); | |
| removeFrom = 0; | |
| } | |
| } | |
| if (i == bidis.length - 1 && r == numRuns - 1) | |
| { | |
| if (end <= getLength()) | |
| { | |
| // Try to merge last element with next element. | |
| int nextIndex = bidiRoot.getElementIndex(end); | |
| Element nextEl = bidiRoot.getElement(nextIndex); | |
| AttributeSet atts = nextEl.getAttributes(); | |
| int nextLevel = StyleConstants.getBidiLevel(atts); | |
| int level = bidi.getRunLevel(r); | |
| if (lastRunLevel == level && level == nextLevel) | |
| { | |
| // Merge runs together. | |
| if (lastRunStart + start == nextEl.getStartOffset()) | |
| removeTo = nextIndex - 1; | |
| else | |
| { | |
| newEls.add(new BidiElement(bidiRoot, start + lastRunStart, | |
| nextEl.getEndOffset(), level)); | |
| removeTo = nextIndex; | |
| } | |
| } | |
| else if (lastRunLevel == level) | |
| { | |
| // Merge current and last run. | |
| int endOffs = offs + bidi.getRunLimit(r); | |
| newEls.add(new BidiElement(bidiRoot, start + lastRunStart, | |
| start + endOffs, level)); | |
| if (start + endOffs == nextEl.getStartOffset()) | |
| removeTo = nextIndex - 1; | |
| else | |
| { | |
| newEls.add(new BidiElement(bidiRoot, start + endOffs, | |
| nextEl.getEndOffset(), | |
| nextLevel)); | |
| removeTo = nextIndex; | |
| } | |
| } | |
| else if (level == nextLevel) | |
| { | |
| // Merge current and next run. | |
| newEls.add(new BidiElement(bidiRoot, start + lastRunStart, | |
| start + lastRunEnd, | |
| lastRunLevel)); | |
| newEls.add(new BidiElement(bidiRoot, start + lastRunEnd, | |
| nextEl.getEndOffset(), level)); | |
| removeTo = nextIndex; | |
| } | |
| else | |
| { | |
| // Split next element. | |
| int endOffs = offs + bidi.getRunLimit(r); | |
| newEls.add(new BidiElement(bidiRoot, start + lastRunStart, | |
| start + lastRunEnd, | |
| lastRunLevel)); | |
| newEls.add(new BidiElement(bidiRoot, start + lastRunEnd, | |
| start + endOffs, level)); | |
| newEls.add(new BidiElement(bidiRoot, start + endOffs, | |
| nextEl.getEndOffset(), | |
| nextLevel)); | |
| removeTo = nextIndex; | |
| } | |
| } | |
| else | |
| { | |
| removeTo = bidiRoot.getElementIndex(end); | |
| int level = bidi.getRunLevel(r); | |
| int runEnd = offs + bidi.getRunLimit(r); | |
| if (level == lastRunLevel) | |
| { | |
| // Merge with previous. | |
| lastRunEnd = offs + runEnd; | |
| newEls.add(new BidiElement(bidiRoot, | |
| start + lastRunStart, | |
| start + runEnd, level)); | |
| } | |
| else | |
| { | |
| // Create element for last run and current run. | |
| newEls.add(new BidiElement(bidiRoot, start + lastRunStart, | |
| start + lastRunEnd, | |
| lastRunLevel)); | |
| newEls.add(new BidiElement(bidiRoot, | |
| start + lastRunEnd, | |
| start + runEnd, | |
| level)); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| int level = bidi.getRunLevel(r); | |
| int runEnd = bidi.getRunLimit(r); | |
| if (level == lastRunLevel) | |
| { | |
| // Merge with previous. | |
| lastRunEnd = offs + runEnd; | |
| } | |
| else | |
| { | |
| // Create element for last run and update values for | |
| // current run. | |
| newEls.add(new BidiElement(bidiRoot, start + lastRunStart, | |
| start + lastRunEnd, | |
| lastRunLevel)); | |
| lastRunStart = lastRunEnd; | |
| lastRunEnd = offs + runEnd; | |
| lastRunLevel = level; | |
| } | |
| } | |
| } | |
| offs += bidi.getLength(); | |
| } | |
| // Determine the bidi elements which are to be removed. | |
| int numRemoved = 0; | |
| if (bidiRoot.getElementCount() > 0) | |
| numRemoved = removeTo - removeFrom + 1; | |
| Element[] removed = new Element[numRemoved]; | |
| for (int i = 0; i < numRemoved; i++) | |
| removed[i] = bidiRoot.getElement(removeFrom + i); | |
| Element[] added = new Element[newEls.size()]; | |
| added = (Element[]) newEls.toArray(added); | |
| // Update the event. | |
| ElementEdit edit = new ElementEdit(bidiRoot, removeFrom, removed, added); | |
| ev.addEdit(edit); | |
| // Update the structure. | |
| bidiRoot.replace(removeFrom, numRemoved, added); | |
| } | |
| /** | |
| * Determines the Bidi objects for the paragraphs in the specified range. | |
| * | |
| * @param start the start of the range | |
| * @param end the end of the range | |
| * | |
| * @return the Bidi analysers for the paragraphs in the range | |
| */ | |
| private Bidi[] getBidis(int start, int end) | |
| { | |
| // Determine the default run direction from the document property. | |
| Boolean defaultDir = null; | |
| Object o = getProperty(TextAttribute.RUN_DIRECTION); | |
| if (o instanceof Boolean) | |
| defaultDir = (Boolean) o; | |
| // Scan paragraphs and add their level arrays to the overall levels array. | |
| ArrayList bidis = new ArrayList(); | |
| Segment s = new Segment(); | |
| for (int i = start; i < end;) | |
| { | |
| Element par = getParagraphElement(i); | |
| int pStart = par.getStartOffset(); | |
| int pEnd = par.getEndOffset(); | |
| // Determine the default run direction of the paragraph. | |
| Boolean dir = defaultDir; | |
| o = par.getAttributes().getAttribute(TextAttribute.RUN_DIRECTION); | |
| if (o instanceof Boolean) | |
| dir = (Boolean) o; | |
| // Bidi over the paragraph. | |
| try | |
| { | |
| getText(pStart, pEnd - pStart, s); | |
| } | |
| catch (BadLocationException ex) | |
| { | |
| assert false : "Must not happen"; | |
| } | |
| int flag = Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; | |
| if (dir != null) | |
| { | |
| if (TextAttribute.RUN_DIRECTION_LTR.equals(dir)) | |
| flag = Bidi.DIRECTION_LEFT_TO_RIGHT; | |
| else | |
| flag = Bidi.DIRECTION_RIGHT_TO_LEFT; | |
| } | |
| Bidi bidi = new Bidi(s.array, s.offset, null, 0, s.count, flag); | |
| bidis.add(bidi); | |
| i = pEnd; | |
| } | |
| Bidi[] ret = new Bidi[bidis.size()]; | |
| ret = (Bidi[]) bidis.toArray(ret); | |
| return ret; | |
| } | |
| /** | |
| * Blocks until a read lock can be obtained. Must block if there is | |
| * currently a writer modifying the <code>Document</code>. | |
| */ | |
| public final synchronized void readLock() | |
| { | |
| try | |
| { | |
| while (currentWriter != null) | |
| { | |
| if (currentWriter == Thread.currentThread()) | |
| return; | |
| wait(); | |
| } | |
| numReaders++; | |
| } | |
| catch (InterruptedException ex) | |
| { | |
| throw new Error("Interrupted during grab read lock"); | |
| } | |
| } | |
| /** | |
| * Releases the read lock. If this was the only reader on this | |
| * <code>Document</code>, writing may begin now. | |
| */ | |
| public final synchronized void readUnlock() | |
| { | |
| // Note we could have a problem here if readUnlock was called without a | |
| // prior call to readLock but the specs simply warn users to ensure that | |
| // balance by using a finally block: | |
| // readLock() | |
| // try | |
| // { | |
| // doSomethingHere | |
| // } | |
| // finally | |
| // { | |
| // readUnlock(); | |
| // } | |
| // All that the JDK seems to check for is that you don't call unlock | |
| // more times than you've previously called lock, but it doesn't make | |
| // sure that the threads calling unlock were the same ones that called lock | |
| // If the current thread holds the write lock, and attempted to also obtain | |
| // a readLock, then numReaders hasn't been incremented and we don't need | |
| // to unlock it here. | |
| if (currentWriter == Thread.currentThread()) | |
| return; | |
| // FIXME: the reference implementation throws a | |
| // javax.swing.text.StateInvariantError here | |
| if (numReaders <= 0) | |
| throw new IllegalStateException("document lock failure"); | |
| // If currentWriter is not null, the application code probably had a | |
| // writeLock and then tried to obtain a readLock, in which case | |
| // numReaders wasn't incremented | |
| numReaders--; | |
| notify(); | |
| } | |
| /** | |
| * Removes a piece of content from this <code>Document</code>. | |
| * | |
| * <p>If a {@link DocumentFilter} is installed in this document, the | |
| * corresponding method of the filter object is called. The | |
| * <code>DocumentFilter</code> is called even if <code>length</code> | |
| * is zero. This is different from {@link #replace}.</p> | |
| * | |
| * <p>Note: When <code>length</code> is zero or below the call is not | |
| * forwarded to the underlying {@link AbstractDocument.Content} instance | |
| * of this document and no exception is thrown.</p> | |
| * | |
| * @param offset the start offset of the fragment to be removed | |
| * @param length the length of the fragment to be removed | |
| * | |
| * @throws BadLocationException if <code>offset</code> or | |
| * <code>offset + length</code> or invalid locations within this | |
| * document | |
| */ | |
| public void remove(int offset, int length) throws BadLocationException | |
| { | |
| writeLock(); | |
| try | |
| { | |
| DocumentFilter f = getDocumentFilter(); | |
| if (f == null) | |
| removeImpl(offset, length); | |
| else | |
| f.remove(getBypass(), offset, length); | |
| } | |
| finally | |
| { | |
| writeUnlock(); | |
| } | |
| } | |
| void removeImpl(int offset, int length) throws BadLocationException | |
| { | |
| // The RI silently ignores all requests that have a negative length. | |
| // Don't ask my why, but that's how it is. | |
| if (length > 0) | |
| { | |
| if (offset < 0 || offset > getLength()) | |
| throw new BadLocationException("Invalid remove position", offset); | |
| if (offset + length > getLength()) | |
| throw new BadLocationException("Invalid remove length", offset); | |
| DefaultDocumentEvent event = | |
| new DefaultDocumentEvent(offset, length, | |
| DocumentEvent.EventType.REMOVE); | |
| // The order of the operations below is critical! | |
| removeUpdate(event); | |
| UndoableEdit temp = content.remove(offset, length); | |
| postRemoveUpdate(event); | |
| fireRemoveUpdate(event); | |
| } | |
| } | |
| /** | |
| * Replaces a piece of content in this <code>Document</code> with | |
| * another piece of content. | |
| * | |
| * <p>If a {@link DocumentFilter} is installed in this document, the | |
| * corresponding method of the filter object is called.</p> | |
| * | |
| * <p>The method has no effect if <code>length</code> is zero (and | |
| * only zero) and, at the same time, <code>text</code> is | |
| * <code>null</code> or has zero length.</p> | |
| * | |
| * @param offset the start offset of the fragment to be removed | |
| * @param length the length of the fragment to be removed | |
| * @param text the text to replace the content with | |
| * @param attributes the text attributes to assign to the new content | |
| * | |
| * @throws BadLocationException if <code>offset</code> or | |
| * <code>offset + length</code> or invalid locations within this | |
| * document | |
| * | |
| * @since 1.4 | |
| */ | |
| public void replace(int offset, int length, String text, | |
| AttributeSet attributes) | |
| throws BadLocationException | |
| { | |
| // Bail out if we have a bogus replacement (Behavior observed in RI). | |
| if (length == 0 | |
| && (text == null || text.length() == 0)) | |
| return; | |
| writeLock(); | |
| try | |
| { | |
| if (documentFilter == null) | |
| { | |
| // It is important to call the methods which again do the checks | |
| // of the arguments and the DocumentFilter because subclasses may | |
| // have overridden these methods and provide crucial behavior | |
| // which would be skipped if we call the non-checking variants. | |
| // An example for this is PlainDocument where insertString can | |
| // provide a filtering of newlines. | |
| remove(offset, length); | |
| insertString(offset, text, attributes); | |
| } | |
| else | |
| documentFilter.replace(getBypass(), offset, length, text, attributes); | |
| } | |
| finally | |
| { | |
| writeUnlock(); | |
| } | |
| } | |
| void replaceImpl(int offset, int length, String text, | |
| AttributeSet attributes) | |
| throws BadLocationException | |
| { | |
| removeImpl(offset, length); | |
| insertStringImpl(offset, text, attributes); | |
| } | |
| /** | |
| * Adds a <code>DocumentListener</code> object to this document. | |
| * | |
| * @param listener the listener to add | |
| */ | |
| public void addDocumentListener(DocumentListener listener) | |
| { | |
| listenerList.add(DocumentListener.class, listener); | |
| } | |
| /** | |
| * Removes a <code>DocumentListener</code> object from this document. | |
| * | |
| * @param listener the listener to remove | |
| */ | |
| public void removeDocumentListener(DocumentListener listener) | |
| { | |
| listenerList.remove(DocumentListener.class, listener); | |
| } | |
| /** | |
| * Returns all registered <code>DocumentListener</code>s. | |
| * | |
| * @return all registered <code>DocumentListener</code>s | |
| */ | |
| public DocumentListener[] getDocumentListeners() | |
| { | |
| return (DocumentListener[]) getListeners(DocumentListener.class); | |
| } | |
| /** | |
| * Adds an {@link UndoableEditListener} to this <code>Document</code>. | |
| * | |
| * @param listener the listener to add | |
| */ | |
| public void addUndoableEditListener(UndoableEditListener listener) | |
| { | |
| listenerList.add(UndoableEditListener.class, listener); | |
| } | |
| /** | |
| * Removes an {@link UndoableEditListener} from this <code>Document</code>. | |
| * | |
| * @param listener the listener to remove | |
| */ | |
| public void removeUndoableEditListener(UndoableEditListener listener) | |
| { | |
| listenerList.remove(UndoableEditListener.class, listener); | |
| } | |
| /** | |
| * Returns all registered {@link UndoableEditListener}s. | |
| * | |
| * @return all registered {@link UndoableEditListener}s | |
| */ | |
| public UndoableEditListener[] getUndoableEditListeners() | |
| { | |
| return (UndoableEditListener[]) getListeners(UndoableEditListener.class); | |
| } | |
| /** | |
| * Called before some content gets removed from this <code>Document</code>. | |
| * The default implementation does nothing but may be overridden by | |
| * subclasses to modify the <code>Document</code> structure in response | |
| * to a remove request. The method is executed within a write lock. | |
| * | |
| * @param chng the <code>DefaultDocumentEvent</code> describing the change | |
| */ | |
| protected void removeUpdate(DefaultDocumentEvent chng) | |
| { | |
| // Do nothing here. Subclasses may wish to override this. | |
| } | |
| /** | |
| * Called to render this <code>Document</code> visually. It obtains a read | |
| * lock, ensuring that no changes will be made to the <code>document</code> | |
| * during the rendering process. It then calls the {@link Runnable#run()} | |
| * method on <code>runnable</code>. This method <em>must not</em> attempt | |
| * to modifiy the <code>Document</code>, since a deadlock will occur if it | |
| * tries to obtain a write lock. When the {@link Runnable#run()} method | |
| * completes (either naturally or by throwing an exception), the read lock | |
| * is released. Note that there is nothing in this method related to | |
| * the actual rendering. It could be used to execute arbitrary code within | |
| * a read lock. | |
| * | |
| * @param runnable the {@link Runnable} to execute | |
| */ | |
| public void render(Runnable runnable) | |
| { | |
| readLock(); | |
| try | |
| { | |
| runnable.run(); | |
| } | |
| finally | |
| { | |
| readUnlock(); | |
| } | |
| } | |
| /** | |
| * Sets the asynchronous loading priority for this <code>Document</code>. | |
| * A value of <code>-1</code> indicates that this <code>Document</code> | |
| * should be loaded synchronously. | |
| * | |
| * @param p the asynchronous loading priority to set | |
| */ | |
| public void setAsynchronousLoadPriority(int p) | |
| { | |
| Integer val = p >= 0 ? new Integer(p) : null; | |
| putProperty(AsyncLoadPriority, val); | |
| } | |
| /** | |
| * Sets the properties of this <code>Document</code>. | |
| * | |
| * @param p the document properties to set | |
| */ | |
| public void setDocumentProperties(Dictionary<Object, Object> p) | |
| { | |
| // FIXME: make me thread-safe | |
| properties = p; | |
| } | |
| /** | |
| * Blocks until a write lock can be obtained. Must wait if there are | |
| * readers currently reading or another thread is currently writing. | |
| */ | |
| protected synchronized final void writeLock() | |
| { | |
| try | |
| { | |
| while (numReaders > 0 || currentWriter != null) | |
| { | |
| if (Thread.currentThread() == currentWriter) | |
| { | |
| if (notifyListeners) | |
| throw new IllegalStateException("Mutation during notify"); | |
| numWriters++; | |
| return; | |
| } | |
| wait(); | |
| } | |
| currentWriter = Thread.currentThread(); | |
| numWriters = 1; | |
| } | |
| catch (InterruptedException ex) | |
| { | |
| throw new Error("Interupted during grab write lock"); | |
| } | |
| } | |
| /** | |
| * Releases the write lock. This allows waiting readers or writers to | |
| * obtain the lock. | |
| */ | |
| protected final synchronized void writeUnlock() | |
| { | |
| if (--numWriters <= 0) | |
| { | |
| numWriters = 0; | |
| currentWriter = null; | |
| notifyAll(); | |
| } | |
| } | |
| /** | |
| * Returns the currently installed {@link DocumentFilter} for this | |
| * <code>Document</code>. | |
| * | |
| * @return the currently installed {@link DocumentFilter} for this | |
| * <code>Document</code> | |
| * | |
| * @since 1.4 | |
| */ | |
| public DocumentFilter getDocumentFilter() | |
| { | |
| return documentFilter; | |
| } | |
| /** | |
| * Sets the {@link DocumentFilter} for this <code>Document</code>. | |
| * | |
| * @param filter the <code>DocumentFilter</code> to set | |
| * | |
| * @since 1.4 | |
| */ | |
| public void setDocumentFilter(DocumentFilter filter) | |
| { | |
| this.documentFilter = filter; | |
| } | |
| /** | |
| * Dumps diagnostic information to the specified <code>PrintStream</code>. | |
| * | |
| * @param out the stream to write the diagnostic information to | |
| */ | |
| public void dump(PrintStream out) | |
| { | |
| ((AbstractElement) getDefaultRootElement()).dump(out, 0); | |
| ((AbstractElement) getBidiRootElement()).dump(out, 0); | |
| } | |
| /** | |
| * Defines a set of methods for managing text attributes for one or more | |
| * <code>Document</code>s. | |
| * | |
| * Replicating {@link AttributeSet}s throughout a <code>Document</code> can | |
| * be very expensive. Implementations of this interface are intended to | |
| * provide intelligent management of <code>AttributeSet</code>s, eliminating | |
| * costly duplication. | |
| * | |
| * @see StyleContext | |
| */ | |
| public interface AttributeContext | |
| { | |
| /** | |
| * Returns an {@link AttributeSet} that contains the attributes | |
| * of <code>old</code> plus the new attribute specified by | |
| * <code>name</code> and <code>value</code>. | |
| * | |
| * @param old the attribute set to be merged with the new attribute | |
| * @param name the name of the attribute to be added | |
| * @param value the value of the attribute to be added | |
| * | |
| * @return the old attributes plus the new attribute | |
| */ | |
| AttributeSet addAttribute(AttributeSet old, Object name, Object value); | |
| /** | |
| * Returns an {@link AttributeSet} that contains the attributes | |
| * of <code>old</code> plus the new attributes in <code>attributes</code>. | |
| * | |
| * @param old the set of attributes where to add the new attributes | |
| * @param attributes the attributes to be added | |
| * | |
| * @return an {@link AttributeSet} that contains the attributes | |
| * of <code>old</code> plus the new attributes in | |
| * <code>attributes</code> | |
| */ | |
| AttributeSet addAttributes(AttributeSet old, AttributeSet attributes); | |
| /** | |
| * Returns an empty {@link AttributeSet}. | |
| * | |
| * @return an empty {@link AttributeSet} | |
| */ | |
| AttributeSet getEmptySet(); | |
| /** | |
| * Called to indicate that the attributes in <code>attributes</code> are | |
| * no longer used. | |
| * | |
| * @param attributes the attributes are no longer used | |
| */ | |
| void reclaim(AttributeSet attributes); | |
| /** | |
| * Returns a {@link AttributeSet} that has the attribute with the specified | |
| * <code>name</code> removed from <code>old</code>. | |
| * | |
| * @param old the attribute set from which an attribute is removed | |
| * @param name the name of the attribute to be removed | |
| * | |
| * @return the attributes of <code>old</code> minus the attribute | |
| * specified by <code>name</code> | |
| */ | |
| AttributeSet removeAttribute(AttributeSet old, Object name); | |
| /** | |
| * Removes all attributes in <code>attributes</code> from <code>old</code> | |
| * and returns the resulting <code>AttributeSet</code>. | |
| * | |
| * @param old the set of attributes from which to remove attributes | |
| * @param attributes the attributes to be removed from <code>old</code> | |
| * | |
| * @return the attributes of <code>old</code> minus the attributes in | |
| * <code>attributes</code> | |
| */ | |
| AttributeSet removeAttributes(AttributeSet old, AttributeSet attributes); | |
| /** | |
| * Removes all attributes specified by <code>names</code> from | |
| * <code>old</code> and returns the resulting <code>AttributeSet</code>. | |
| * | |
| * @param old the set of attributes from which to remove attributes | |
| * @param names the names of the attributes to be removed from | |
| * <code>old</code> | |
| * | |
| * @return the attributes of <code>old</code> minus the attributes in | |
| * <code>attributes</code> | |
| */ | |
| AttributeSet removeAttributes(AttributeSet old, Enumeration<?> names); | |
| } | |
| /** | |
| * A sequence of data that can be edited. This is were the actual content | |
| * in <code>AbstractDocument</code>'s is stored. | |
| */ | |
| public interface Content | |
| { | |
| /** | |
| * Creates a {@link Position} that keeps track of the location at | |
| * <code>offset</code>. | |
| * | |
| * @return a {@link Position} that keeps track of the location at | |
| * <code>offset</code>. | |
| * | |
| * @throw BadLocationException if <code>offset</code> is not a valid | |
| * location in this <code>Content</code> model | |
| */ | |
| Position createPosition(int offset) throws BadLocationException; | |
| /** | |
| * Returns the length of the content. | |
| * | |
| * @return the length of the content | |
| */ | |
| int length(); | |
| /** | |
| * Inserts a string into the content model. | |
| * | |
| * @param where the offset at which to insert the string | |
| * @param str the string to be inserted | |
| * | |
| * @return an <code>UndoableEdit</code> or <code>null</code> if undo is | |
| * not supported by this <code>Content</code> model | |
| * | |
| * @throws BadLocationException if <code>where</code> is not a valid | |
| * location in this <code>Content</code> model | |
| */ | |
| UndoableEdit insertString(int where, String str) | |
| throws BadLocationException; | |
| /** | |
| * Removes a piece of content from the content model. | |
| * | |
| * @param where the offset at which to remove content | |
| * @param nitems the number of characters to be removed | |
| * | |
| * @return an <code>UndoableEdit</code> or <code>null</code> if undo is | |
| * not supported by this <code>Content</code> model | |
| * | |
| * @throws BadLocationException if <code>where</code> is not a valid | |
| * location in this <code>Content</code> model | |
| */ | |
| UndoableEdit remove(int where, int nitems) throws BadLocationException; | |
| /** | |
| * Returns a piece of content. | |
| * | |
| * @param where the start offset of the requested fragment | |
| * @param len the length of the requested fragment | |
| * | |
| * @return the requested fragment | |
| * @throws BadLocationException if <code>offset</code> or | |
| * <code>offset + len</code>is not a valid | |
| * location in this <code>Content</code> model | |
| */ | |
| String getString(int where, int len) throws BadLocationException; | |
| /** | |
| * Fetches a piece of content and stores it in <code>txt</code>. | |
| * | |
| * @param where the start offset of the requested fragment | |
| * @param len the length of the requested fragment | |
| * @param txt the <code>Segment</code> where to fragment is stored into | |
| * | |
| * @throws BadLocationException if <code>offset</code> or | |
| * <code>offset + len</code>is not a valid | |
| * location in this <code>Content</code> model | |
| */ | |
| void getChars(int where, int len, Segment txt) throws BadLocationException; | |
| } | |
| /** | |
| * An abstract base implementation of the {@link Element} interface. | |
| */ | |
| public abstract class AbstractElement | |
| implements Element, MutableAttributeSet, TreeNode, Serializable | |
| { | |
| /** The serialization UID (compatible with JDK1.5). */ | |
| private static final long serialVersionUID = 1712240033321461704L; | |
| /** The number of characters that this Element spans. */ | |
| int count; | |
| /** The starting offset of this Element. */ | |
| int offset; | |
| /** The attributes of this Element. */ | |
| AttributeSet attributes; | |
| /** The parent element. */ | |
| Element element_parent; | |
| /** The parent in the TreeNode interface. */ | |
| TreeNode tree_parent; | |
| /** The children of this element. */ | |
| Vector tree_children; | |
| /** | |
| * Creates a new instance of <code>AbstractElement</code> with a | |
| * specified parent <code>Element</code> and <code>AttributeSet</code>. | |
| * | |
| * @param p the parent of this <code>AbstractElement</code> | |
| * @param s the attributes to be assigned to this | |
| * <code>AbstractElement</code> | |
| */ | |
| public AbstractElement(Element p, AttributeSet s) | |
| { | |
| element_parent = p; | |
| AttributeContext ctx = getAttributeContext(); | |
| attributes = ctx.getEmptySet(); | |
| if (s != null) | |
| addAttributes(s); | |
| } | |
| /** | |
| * Returns the child nodes of this <code>Element</code> as an | |
| * <code>Enumeration</code> of {@link TreeNode}s. | |
| * | |
| * @return the child nodes of this <code>Element</code> as an | |
| * <code>Enumeration</code> of {@link TreeNode}s | |
| */ | |
| public abstract Enumeration children(); | |
| /** | |
| * Returns <code>true</code> if this <code>AbstractElement</code> | |
| * allows children. | |
| * | |
| * @return <code>true</code> if this <code>AbstractElement</code> | |
| * allows children | |
| */ | |
| public abstract boolean getAllowsChildren(); | |
| /** | |
| * Returns the child of this <code>AbstractElement</code> at | |
| * <code>index</code>. | |
| * | |
| * @param index the position in the child list of the child element to | |
| * be returned | |
| * | |
| * @return the child of this <code>AbstractElement</code> at | |
| * <code>index</code> | |
| */ | |
| public TreeNode getChildAt(int index) | |
| { | |
| return (TreeNode) tree_children.get(index); | |
| } | |
| /** | |
| * Returns the number of children of this <code>AbstractElement</code>. | |
| * | |
| * @return the number of children of this <code>AbstractElement</code> | |
| */ | |
| public int getChildCount() | |
| { | |
| return tree_children.size(); | |
| } | |
| /** | |
| * Returns the index of a given child <code>TreeNode</code> or | |
| * <code>-1</code> if <code>node</code> is not a child of this | |
| * <code>AbstractElement</code>. | |
| * | |
| * @param node the node for which the index is requested | |
| * | |
| * @return the index of a given child <code>TreeNode</code> or | |
| * <code>-1</code> if <code>node</code> is not a child of this | |
| * <code>AbstractElement</code> | |
| */ | |
| public int getIndex(TreeNode node) | |
| { | |
| return tree_children.indexOf(node); | |
| } | |
| /** | |
| * Returns the parent <code>TreeNode</code> of this | |
| * <code>AbstractElement</code> or <code>null</code> if this element | |
| * has no parent. | |
| * | |
| * @return the parent <code>TreeNode</code> of this | |
| * <code>AbstractElement</code> or <code>null</code> if this | |
| * element has no parent | |
| */ | |
| public TreeNode getParent() | |
| { | |
| return tree_parent; | |
| } | |
| /** | |
| * Returns <code>true</code> if this <code>AbstractElement</code> is a | |
| * leaf element, <code>false</code> otherwise. | |
| * | |
| * @return <code>true</code> if this <code>AbstractElement</code> is a | |
| * leaf element, <code>false</code> otherwise | |
| */ | |
| public abstract boolean isLeaf(); | |
| /** | |
| * Adds an attribute to this element. | |
| * | |
| * @param name the name of the attribute to be added | |
| * @param value the value of the attribute to be added | |
| */ | |
| public void addAttribute(Object name, Object value) | |
| { | |
| attributes = getAttributeContext().addAttribute(attributes, name, value); | |
| } | |
| /** | |
| * Adds a set of attributes to this element. | |
| * | |
| * @param attrs the attributes to be added to this element | |
| */ | |
| public void addAttributes(AttributeSet attrs) | |
| { | |
| attributes = getAttributeContext().addAttributes(attributes, attrs); | |
| } | |
| /** | |
| * Removes an attribute from this element. | |
| * | |
| * @param name the name of the attribute to be removed | |
| */ | |
| public void removeAttribute(Object name) | |
| { | |
| attributes = getAttributeContext().removeAttribute(attributes, name); | |
| } | |
| /** | |
| * Removes a set of attributes from this element. | |
| * | |
| * @param attrs the attributes to be removed | |
| */ | |
| public void removeAttributes(AttributeSet attrs) | |
| { | |
| attributes = getAttributeContext().removeAttributes(attributes, attrs); | |
| } | |
| /** | |
| * Removes a set of attribute from this element. | |
| * | |
| * @param names the names of the attributes to be removed | |
| */ | |
| public void removeAttributes(Enumeration<?> names) | |
| { | |
| attributes = getAttributeContext().removeAttributes(attributes, names); | |
| } | |
| /** | |
| * Sets the parent attribute set against which the element can resolve | |
| * attributes that are not defined in itself. | |
| * | |
| * @param parent the resolve parent to set | |
| */ | |
| public void setResolveParent(AttributeSet parent) | |
| { | |
| attributes = getAttributeContext().addAttribute(attributes, | |
| ResolveAttribute, | |
| parent); | |
| } | |
| /** | |
| * Returns <code>true</code> if this element contains the specified | |
| * attribute. | |
| * | |
| * @param name the name of the attribute to check | |
| * @param value the value of the attribute to check | |
| * | |
| * @return <code>true</code> if this element contains the specified | |
| * attribute | |
| */ | |
| public boolean containsAttribute(Object name, Object value) | |
| { | |
| return attributes.containsAttribute(name, value); | |
| } | |
| /** | |
| * Returns <code>true</code> if this element contains all of the | |
| * specified attributes. | |
| * | |
| * @param attrs the attributes to check | |
| * | |
| * @return <code>true</code> if this element contains all of the | |
| * specified attributes | |
| */ | |
| public boolean containsAttributes(AttributeSet attrs) | |
| { | |
| return attributes.containsAttributes(attrs); | |
| } | |
| /** | |
| * Returns a copy of the attributes of this element. | |
| * | |
| * @return a copy of the attributes of this element | |
| */ | |
| public AttributeSet copyAttributes() | |
| { | |
| return attributes.copyAttributes(); | |
| } | |
| /** | |
| * Returns the attribute value with the specified key. If this attribute | |
| * is not defined in this element and this element has a resolving | |
| * parent, the search goes upward to the resolve parent chain. | |
| * | |
| * @param key the key of the requested attribute | |
| * | |
| * @return the attribute value for <code>key</code> of <code>null</code> | |
| * if <code>key</code> is not found locally and cannot be resolved | |
| * in this element's resolve parents | |
| */ | |
| public Object getAttribute(Object key) | |
| { | |
| Object result = attributes.getAttribute(key); | |
| if (result == null) | |
| { | |
| AttributeSet resParent = getResolveParent(); | |
| if (resParent != null) | |
| result = resParent.getAttribute(key); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Returns the number of defined attributes in this element. | |
| * | |
| * @return the number of defined attributes in this element | |
| */ | |
| public int getAttributeCount() | |
| { | |
| return attributes.getAttributeCount(); | |
| } | |
| /** | |
| * Returns the names of the attributes of this element. | |
| * | |
| * @return the names of the attributes of this element | |
| */ | |
| public Enumeration<?> getAttributeNames() | |
| { | |
| return attributes.getAttributeNames(); | |
| } | |
| /** | |
| * Returns the resolve parent of this element. | |
| * This is taken from the AttributeSet, but if this is null, | |
| * this method instead returns the Element's parent's | |
| * AttributeSet | |
| * | |
| * @return the resolve parent of this element | |
| * | |
| * @see #setResolveParent(AttributeSet) | |
| */ | |
| public AttributeSet getResolveParent() | |
| { | |
| return attributes.getResolveParent(); | |
| } | |
| /** | |
| * Returns <code>true</code> if an attribute with the specified name | |
| * is defined in this element, <code>false</code> otherwise. | |
| * | |
| * @param attrName the name of the requested attributes | |
| * | |
| * @return <code>true</code> if an attribute with the specified name | |
| * is defined in this element, <code>false</code> otherwise | |
| */ | |
| public boolean isDefined(Object attrName) | |
| { | |
| return attributes.isDefined(attrName); | |
| } | |
| /** | |
| * Returns <code>true</code> if the specified <code>AttributeSet</code> | |
| * is equal to this element's <code>AttributeSet</code>, <code>false</code> | |
| * otherwise. | |
| * | |
| * @param attrs the attributes to compare this element to | |
| * | |
| * @return <code>true</code> if the specified <code>AttributeSet</code> | |
| * is equal to this element's <code>AttributeSet</code>, | |
| * <code>false</code> otherwise | |
| */ | |
| public boolean isEqual(AttributeSet attrs) | |
| { | |
| return attributes.isEqual(attrs); | |
| } | |
| /** | |
| * Returns the attributes of this element. | |
| * | |
| * @return the attributes of this element | |
| */ | |
| public AttributeSet getAttributes() | |
| { | |
| return this; | |
| } | |
| /** | |
| * Returns the {@link Document} to which this element belongs. | |
| * | |
| * @return the {@link Document} to which this element belongs | |
| */ | |
| public Document getDocument() | |
| { | |
| return AbstractDocument.this; | |
| } | |
| /** | |
| * Returns the child element at the specified <code>index</code>. | |
| * | |
| * @param index the index of the requested child element | |
| * | |
| * @return the requested element | |
| */ | |
| public abstract Element getElement(int index); | |
| /** | |
| * Returns the name of this element. | |
| * | |
| * @return the name of this element | |
| */ | |
| public String getName() | |
| { | |
| return (String) attributes.getAttribute(ElementNameAttribute); | |
| } | |
| /** | |
| * Returns the parent element of this element. | |
| * | |
| * @return the parent element of this element | |
| */ | |
| public Element getParentElement() | |
| { | |
| return element_parent; | |
| } | |
| /** | |
| * Returns the offset inside the document model that is after the last | |
| * character of this element. | |
| * | |
| * @return the offset inside the document model that is after the last | |
| * character of this element | |
| */ | |
| public abstract int getEndOffset(); | |
| /** | |
| * Returns the number of child elements of this element. | |
| * | |
| * @return the number of child elements of this element | |
| */ | |
| public abstract int getElementCount(); | |
| /** | |
| * Returns the index of the child element that spans the specified | |
| * offset in the document model. | |
| * | |
| * @param offset the offset for which the responsible element is searched | |
| * | |
| * @return the index of the child element that spans the specified | |
| * offset in the document model | |
| */ | |
| public abstract int getElementIndex(int offset); | |
| /** | |
| * Returns the start offset if this element inside the document model. | |
| * | |
| * @return the start offset if this element inside the document model | |
| */ | |
| public abstract int getStartOffset(); | |
| /** | |
| * Prints diagnostic output to the specified stream. | |
| * | |
| * @param stream the stream to write to | |
| * @param indent the indentation level | |
| */ | |
| public void dump(PrintStream stream, int indent) | |
| { | |
| CPStringBuilder b = new CPStringBuilder(); | |
| for (int i = 0; i < indent; ++i) | |
| b.append(' '); | |
| b.append('<'); | |
| b.append(getName()); | |
| // Dump attributes if there are any. | |
| if (getAttributeCount() > 0) | |
| { | |
| b.append('\n'); | |
| Enumeration attNames = getAttributeNames(); | |
| while (attNames.hasMoreElements()) | |
| { | |
| for (int i = 0; i < indent + 2; ++i) | |
| b.append(' '); | |
| Object attName = attNames.nextElement(); | |
| b.append(attName); | |
| b.append('='); | |
| Object attribute = getAttribute(attName); | |
| b.append(attribute); | |
| b.append('\n'); | |
| } | |
| } | |
| if (getAttributeCount() > 0) | |
| { | |
| for (int i = 0; i < indent; ++i) | |
| b.append(' '); | |
| } | |
| b.append(">\n"); | |
| // Dump element content for leaf elements. | |
| if (isLeaf()) | |
| { | |
| for (int i = 0; i < indent + 2; ++i) | |
| b.append(' '); | |
| int start = getStartOffset(); | |
| int end = getEndOffset(); | |
| b.append('['); | |
| b.append(start); | |
| b.append(','); | |
| b.append(end); | |
| b.append("]["); | |
| try | |
| { | |
| b.append(getDocument().getText(start, end - start)); | |
| } | |
| catch (BadLocationException ex) | |
| { | |
| AssertionError err = new AssertionError("BadLocationException " | |
| + "must not be thrown " | |
| + "here."); | |
| err.initCause(ex); | |
| throw err; | |
| } | |
| b.append("]\n"); | |
| } | |
| stream.print(b.toString()); | |
| // Dump child elements if any. | |
| int count = getElementCount(); | |
| for (int i = 0; i < count; ++i) | |
| { | |
| Element el = getElement(i); | |
| if (el instanceof AbstractElement) | |
| ((AbstractElement) el).dump(stream, indent + 2); | |
| } | |
| } | |
| } | |
| /** | |
| * An implementation of {@link Element} to represent composite | |
| * <code>Element</code>s that contain other <code>Element</code>s. | |
| */ | |
| public class BranchElement extends AbstractElement | |
| { | |
| /** The serialization UID (compatible with JDK1.5). */ | |
| private static final long serialVersionUID = -6037216547466333183L; | |
| /** | |
| * The child elements of this BranchElement. | |
| */ | |
| private Element[] children; | |
| /** | |
| * The number of children in the branch element. | |
| */ | |
| private int numChildren; | |
| /** | |
| * The last found index in getElementIndex(). Used for faster searching. | |
| */ | |
| private int lastIndex; | |
| /** | |
| * Creates a new <code>BranchElement</code> with the specified | |
| * parent and attributes. | |
| * | |
| * @param parent the parent element of this <code>BranchElement</code> | |
| * @param attributes the attributes to set on this | |
| * <code>BranchElement</code> | |
| */ | |
| public BranchElement(Element parent, AttributeSet attributes) | |
| { | |
| super(parent, attributes); | |
| children = new Element[1]; | |
| numChildren = 0; | |
| lastIndex = -1; | |
| } | |
| /** | |
| * Returns the children of this <code>BranchElement</code>. | |
| * | |
| * @return the children of this <code>BranchElement</code> | |
| */ | |
| public Enumeration children() | |
| { | |
| if (numChildren == 0) | |
| return null; | |
| Vector tmp = new Vector(); | |
| for (int index = 0; index < numChildren; ++index) | |
| tmp.add(children[index]); | |
| return tmp.elements(); | |
| } | |
| /** | |
| * Returns <code>true</code> since <code>BranchElements</code> allow | |
| * child elements. | |
| * | |
| * @return <code>true</code> since <code>BranchElements</code> allow | |
| * child elements | |
| */ | |
| public boolean getAllowsChildren() | |
| { | |
| return true; | |
| } | |
| /** | |
| * Returns the child element at the specified <code>index</code>. | |
| * | |
| * @param index the index of the requested child element | |
| * | |
| * @return the requested element | |
| */ | |
| public Element getElement(int index) | |
| { | |
| if (index < 0 || index >= numChildren) | |
| return null; | |
| return children[index]; | |
| } | |
| /** | |
| * Returns the number of child elements of this element. | |
| * | |
| * @return the number of child elements of this element | |
| */ | |
| public int getElementCount() | |
| { | |
| return numChildren; | |
| } | |
| /** | |
| * Returns the index of the child element that spans the specified | |
| * offset in the document model. | |
| * | |
| * @param offset the offset for which the responsible element is searched | |
| * | |
| * @return the index of the child element that spans the specified | |
| * offset in the document model | |
| */ | |
| public int getElementIndex(int offset) | |
| { | |
| // Implemented using an improved linear search. | |
| // This makes use of the fact that searches are not random but often | |
| // close to the previous search. So we try to start the binary | |
| // search at the last found index. | |
| int i0 = 0; // The lower bounds. | |
| int i1 = numChildren - 1; // The upper bounds. | |
| int index = -1; // The found index. | |
| int p0 = getStartOffset(); | |
| int p1; // Start and end offset local variables. | |
| if (numChildren == 0) | |
| index = 0; | |
| else if (offset >= getEndOffset()) | |
| index = numChildren - 1; | |
| else | |
| { | |
| // Try lastIndex. | |
| if (lastIndex >= i0 && lastIndex <= i1) | |
| { | |
| Element last = getElement(lastIndex); | |
| p0 = last.getStartOffset(); | |
| p1 = last.getEndOffset(); | |
| if (offset >= p0 && offset < p1) | |
| index = lastIndex; | |
| else | |
| { | |
| // Narrow the search bounds using the lastIndex, even | |
| // if it hasn't been a hit. | |
| if (offset < p0) | |
| i1 = lastIndex; | |
| else | |
| i0 = lastIndex; | |
| } | |
| } | |
| // The actual search. | |
| int i = 0; | |
| while (i0 <= i1 && index == -1) | |
| { | |
| i = i0 + (i1 - i0) / 2; | |
| Element el = getElement(i); | |
| p0 = el.getStartOffset(); | |
| p1 = el.getEndOffset(); | |
| if (offset >= p0 && offset < p1) | |
| { | |
| // Found it! | |
| index = i; | |
| } | |
| else if (offset < p0) | |
| i1 = i - 1; | |
| else | |
| i0 = i + 1; | |
| } | |
| if (index == -1) | |
| { | |
| // Didn't find it. Return the boundary index. | |
| if (offset < p0) | |
| index = i; | |
| else | |
| index = i + 1; | |
| } | |
| lastIndex = index; | |
| } | |
| return index; | |
| } | |
| /** | |
| * Returns the offset inside the document model that is after the last | |
| * character of this element. | |
| * This is the end offset of the last child element. If this element | |
| * has no children, this method throws a <code>NullPointerException</code>. | |
| * | |
| * @return the offset inside the document model that is after the last | |
| * character of this element | |
| * | |
| * @throws NullPointerException if this branch element has no children | |
| */ | |
| public int getEndOffset() | |
| { | |
| // This might accss one cached element or trigger an NPE for | |
| // numChildren == 0. This is checked by a Mauve test. | |
| Element child = numChildren > 0 ? children[numChildren - 1] | |
| : children[0]; | |
| return child.getEndOffset(); | |
| } | |
| /** | |
| * Returns the name of this element. This is {@link #ParagraphElementName} | |
| * in this case. | |
| * | |
| * @return the name of this element | |
| */ | |
| public String getName() | |
| { | |
| return ParagraphElementName; | |
| } | |
| /** | |
| * Returns the start offset of this element inside the document model. | |
| * This is the start offset of the first child element. If this element | |
| * has no children, this method throws a <code>NullPointerException</code>. | |
| * | |
| * @return the start offset of this element inside the document model | |
| * | |
| * @throws NullPointerException if this branch element has no children and | |
| * no startOffset value has been cached | |
| */ | |
| public int getStartOffset() | |
| { | |
| // Do not explicitly throw an NPE here. If the first element is null | |
| // then the NPE gets thrown anyway. If it isn't, then it either | |
| // holds a real value (for numChildren > 0) or a cached value | |
| // (for numChildren == 0) as we don't fully remove elements in replace() | |
| // when removing single elements. | |
| // This is checked by a Mauve test. | |
| return children[0].getStartOffset(); | |
| } | |
| /** | |
| * Returns <code>false</code> since <code>BranchElement</code> are no | |
| * leafes. | |
| * | |
| * @return <code>false</code> since <code>BranchElement</code> are no | |
| * leafes | |
| */ | |
| public boolean isLeaf() | |
| { | |
| return false; | |
| } | |
| /** | |
| * Returns the <code>Element</code> at the specified <code>Document</code> | |
| * offset. | |
| * | |
| * @return the <code>Element</code> at the specified <code>Document</code> | |
| * offset | |
| * | |
| * @see #getElementIndex(int) | |
| */ | |
| public Element positionToElement(int position) | |
| { | |
| // XXX: There is surely a better algorithm | |
| // as beginning from first element each time. | |
| for (int index = 0; index < numChildren; ++index) | |
| { | |
| Element elem = children[index]; | |
| if ((elem.getStartOffset() <= position) | |
| && (position < elem.getEndOffset())) | |
| return elem; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Replaces a set of child elements with a new set of child elemens. | |
| * | |
| * @param offset the start index of the elements to be removed | |
| * @param length the number of elements to be removed | |
| * @param elements the new elements to be inserted | |
| */ | |
| public void replace(int offset, int length, Element[] elements) | |
| { | |
| int delta = elements.length - length; | |
| int copyFrom = offset + length; // From where to copy. | |
| int copyTo = copyFrom + delta; // Where to copy to. | |
| int numMove = numChildren - copyFrom; // How many elements are moved. | |
| if (numChildren + delta > children.length) | |
| { | |
| // Gotta grow the array. | |
| int newSize = Math.max(2 * children.length, numChildren + delta); | |
| Element[] target = new Element[newSize]; | |
| System.arraycopy(children, 0, target, 0, offset); | |
| System.arraycopy(elements, 0, target, offset, elements.length); | |
| System.arraycopy(children, copyFrom, target, copyTo, numMove); | |
| children = target; | |
| } | |
| else | |
| { | |
| System.arraycopy(children, copyFrom, children, copyTo, numMove); | |
| System.arraycopy(elements, 0, children, offset, elements.length); | |
| } | |
| numChildren += delta; | |
| } | |
| /** | |
| * Returns a string representation of this element. | |
| * | |
| * @return a string representation of this element | |
| */ | |
| public String toString() | |
| { | |
| return ("BranchElement(" + getName() + ") " | |
| + getStartOffset() + "," + getEndOffset() + "\n"); | |
| } | |
| } | |
| /** | |
| * Stores the changes when a <code>Document</code> is beeing modified. | |
| */ | |
| public class DefaultDocumentEvent extends CompoundEdit | |
| implements DocumentEvent | |
| { | |
| /** The serialization UID (compatible with JDK1.5). */ | |
| private static final long serialVersionUID = 5230037221564563284L; | |
| /** | |
| * The threshold that indicates when we switch to using a Hashtable. | |
| */ | |
| private static final int THRESHOLD = 10; | |
| /** The starting offset of the change. */ | |
| private int offset; | |
| /** The length of the change. */ | |
| private int length; | |
| /** The type of change. */ | |
| private DocumentEvent.EventType type; | |
| /** | |
| * Maps <code>Element</code> to their change records. This is only | |
| * used when the changes array gets too big. We can use an | |
| * (unsync'ed) HashMap here, since changes to this are (should) always | |
| * be performed inside a write lock. | |
| */ | |
| private HashMap changes; | |
| /** | |
| * Indicates if this event has been modified or not. This is used to | |
| * determine if this event is thrown. | |
| */ | |
| private boolean modified; | |
| /** | |
| * Creates a new <code>DefaultDocumentEvent</code>. | |
| * | |
| * @param offset the starting offset of the change | |
| * @param length the length of the change | |
| * @param type the type of change | |
| */ | |
| public DefaultDocumentEvent(int offset, int length, | |
| DocumentEvent.EventType type) | |
| { | |
| this.offset = offset; | |
| this.length = length; | |
| this.type = type; | |
| modified = false; | |
| } | |
| /** | |
| * Adds an UndoableEdit to this <code>DocumentEvent</code>. If this | |
| * edit is an instance of {@link ElementEdit}, then this record can | |
| * later be fetched by calling {@link #getChange}. | |
| * | |
| * @param edit the undoable edit to add | |
| */ | |
| public boolean addEdit(UndoableEdit edit) | |
| { | |
| // Start using Hashtable when we pass a certain threshold. This | |
| // gives a good memory/performance compromise. | |
| if (changes == null && edits.size() > THRESHOLD) | |
| { | |
| changes = new HashMap(); | |
| int count = edits.size(); | |
| for (int i = 0; i < count; i++) | |
| { | |
| Object o = edits.elementAt(i); | |
| if (o instanceof ElementChange) | |
| { | |
| ElementChange ec = (ElementChange) o; | |
| changes.put(ec.getElement(), ec); | |
| } | |
| } | |
| } | |
| if (changes != null && edit instanceof ElementChange) | |
| { | |
| ElementChange elEdit = (ElementChange) edit; | |
| changes.put(elEdit.getElement(), elEdit); | |
| } | |
| return super.addEdit(edit); | |
| } | |
| /** | |
| * Returns the document that has been modified. | |
| * | |
| * @return the document that has been modified | |
| */ | |
| public Document getDocument() | |
| { | |
| return AbstractDocument.this; | |
| } | |
| /** | |
| * Returns the length of the modification. | |
| * | |
| * @return the length of the modification | |
| */ | |
| public int getLength() | |
| { | |
| return length; | |
| } | |
| /** | |
| * Returns the start offset of the modification. | |
| * | |
| * @return the start offset of the modification | |
| */ | |
| public int getOffset() | |
| { | |
| return offset; | |
| } | |
| /** | |
| * Returns the type of the modification. | |
| * | |
| * @return the type of the modification | |
| */ | |
| public DocumentEvent.EventType getType() | |
| { | |
| return type; | |
| } | |
| /** | |
| * Returns the changes for an element. | |
| * | |
| * @param elem the element for which the changes are requested | |
| * | |
| * @return the changes for <code>elem</code> or <code>null</code> if | |
| * <code>elem</code> has not been changed | |
| */ | |
| public ElementChange getChange(Element elem) | |
| { | |
| ElementChange change = null; | |
| if (changes != null) | |
| { | |
| change = (ElementChange) changes.get(elem); | |
| } | |
| else | |
| { | |
| int count = edits.size(); | |
| for (int i = 0; i < count && change == null; i++) | |
| { | |
| Object o = edits.get(i); | |
| if (o instanceof ElementChange) | |
| { | |
| ElementChange ec = (ElementChange) o; | |
| if (elem.equals(ec.getElement())) | |
| change = ec; | |
| } | |
| } | |
| } | |
| return change; | |
| } | |
| /** | |
| * Returns a String description of the change event. This returns the | |
| * toString method of the Vector of edits. | |
| */ | |
| public String toString() | |
| { | |
| return edits.toString(); | |
| } | |
| } | |
| /** | |
| * An implementation of {@link DocumentEvent.ElementChange} to be added | |
| * to {@link DefaultDocumentEvent}s. | |
| */ | |
| public static class ElementEdit extends AbstractUndoableEdit | |
| implements DocumentEvent.ElementChange | |
| { | |
| /** The serial version UID of ElementEdit. */ | |
| private static final long serialVersionUID = -1216620962142928304L; | |
| /** | |
| * The changed element. | |
| */ | |
| private Element elem; | |
| /** | |
| * The index of the change. | |
| */ | |
| private int index; | |
| /** | |
| * The removed elements. | |
| */ | |
| private Element[] removed; | |
| /** | |
| * The added elements. | |
| */ | |
| private Element[] added; | |
| /** | |
| * Creates a new <code>ElementEdit</code>. | |
| * | |
| * @param elem the changed element | |
| * @param index the index of the change | |
| * @param removed the removed elements | |
| * @param added the added elements | |
| */ | |
| public ElementEdit(Element elem, int index, | |
| Element[] removed, Element[] added) | |
| { | |
| this.elem = elem; | |
| this.index = index; | |
| this.removed = removed; | |
| this.added = added; | |
| } | |
| /** | |
| * Returns the added elements. | |
| * | |
| * @return the added elements | |
| */ | |
| public Element[] getChildrenAdded() | |
| { | |
| return added; | |
| } | |
| /** | |
| * Returns the removed elements. | |
| * | |
| * @return the removed elements | |
| */ | |
| public Element[] getChildrenRemoved() | |
| { | |
| return removed; | |
| } | |
| /** | |
| * Returns the changed element. | |
| * | |
| * @return the changed element | |
| */ | |
| public Element getElement() | |
| { | |
| return elem; | |
| } | |
| /** | |
| * Returns the index of the change. | |
| * | |
| * @return the index of the change | |
| */ | |
| public int getIndex() | |
| { | |
| return index; | |
| } | |
| } | |
| /** | |
| * An implementation of {@link Element} that represents a leaf in the | |
| * document structure. This is used to actually store content. | |
| */ | |
| public class LeafElement extends AbstractElement | |
| { | |
| /** The serialization UID (compatible with JDK1.5). */ | |
| private static final long serialVersionUID = -8906306331347768017L; | |
| /** | |
| * Manages the start offset of this element. | |
| */ | |
| private Position startPos; | |
| /** | |
| * Manages the end offset of this element. | |
| */ | |
| private Position endPos; | |
| /** | |
| * Creates a new <code>LeafElement</code>. | |
| * | |
| * @param parent the parent of this <code>LeafElement</code> | |
| * @param attributes the attributes to be set | |
| * @param start the start index of this element inside the document model | |
| * @param end the end index of this element inside the document model | |
| */ | |
| public LeafElement(Element parent, AttributeSet attributes, int start, | |
| int end) | |
| { | |
| super(parent, attributes); | |
| try | |
| { | |
| startPos = createPosition(start); | |
| endPos = createPosition(end); | |
| } | |
| catch (BadLocationException ex) | |
| { | |
| AssertionError as; | |
| as = new AssertionError("BadLocationException thrown " | |
| + "here. start=" + start | |
| + ", end=" + end | |
| + ", length=" + getLength()); | |
| as.initCause(ex); | |
| throw as; | |
| } | |
| } | |
| /** | |
| * Returns <code>null</code> since <code>LeafElement</code>s cannot have | |
| * children. | |
| * | |
| * @return <code>null</code> since <code>LeafElement</code>s cannot have | |
| * children | |
| */ | |
| public Enumeration children() | |
| { | |
| return null; | |
| } | |
| /** | |
| * Returns <code>false</code> since <code>LeafElement</code>s cannot have | |
| * children. | |
| * | |
| * @return <code>false</code> since <code>LeafElement</code>s cannot have | |
| * children | |
| */ | |
| public boolean getAllowsChildren() | |
| { | |
| return false; | |
| } | |
| /** | |
| * Returns <code>null</code> since <code>LeafElement</code>s cannot have | |
| * children. | |
| * | |
| * @return <code>null</code> since <code>LeafElement</code>s cannot have | |
| * children | |
| */ | |
| public Element getElement(int index) | |
| { | |
| return null; | |
| } | |
| /** | |
| * Returns <code>0</code> since <code>LeafElement</code>s cannot have | |
| * children. | |
| * | |
| * @return <code>0</code> since <code>LeafElement</code>s cannot have | |
| * children | |
| */ | |
| public int getElementCount() | |
| { | |
| return 0; | |
| } | |
| /** | |
| * Returns <code>-1</code> since <code>LeafElement</code>s cannot have | |
| * children. | |
| * | |
| * @return <code>-1</code> since <code>LeafElement</code>s cannot have | |
| * children | |
| */ | |
| public int getElementIndex(int offset) | |
| { | |
| return -1; | |
| } | |
| /** | |
| * Returns the end offset of this <code>Element</code> inside the | |
| * document. | |
| * | |
| * @return the end offset of this <code>Element</code> inside the | |
| * document | |
| */ | |
| public int getEndOffset() | |
| { | |
| return endPos.getOffset(); | |
| } | |
| /** | |
| * Returns the name of this <code>Element</code>. This is | |
| * {@link #ContentElementName} in this case. | |
| * | |
| * @return the name of this <code>Element</code> | |
| */ | |
| public String getName() | |
| { | |
| String name = super.getName(); | |
| if (name == null) | |
| name = ContentElementName; | |
| return name; | |
| } | |
| /** | |
| * Returns the start offset of this <code>Element</code> inside the | |
| * document. | |
| * | |
| * @return the start offset of this <code>Element</code> inside the | |
| * document | |
| */ | |
| public int getStartOffset() | |
| { | |
| return startPos.getOffset(); | |
| } | |
| /** | |
| * Returns <code>true</code>. | |
| * | |
| * @return <code>true</code> | |
| */ | |
| public boolean isLeaf() | |
| { | |
| return true; | |
| } | |
| /** | |
| * Returns a string representation of this <code>Element</code>. | |
| * | |
| * @return a string representation of this <code>Element</code> | |
| */ | |
| public String toString() | |
| { | |
| return ("LeafElement(" + getName() + ") " | |
| + getStartOffset() + "," + getEndOffset() + "\n"); | |
| } | |
| } | |
| /** | |
| * The root element for bidirectional text. | |
| */ | |
| private class BidiRootElement | |
| extends BranchElement | |
| { | |
| /** | |
| * Creates a new bidi root element. | |
| */ | |
| BidiRootElement() | |
| { | |
| super(null, null); | |
| } | |
| /** | |
| * Returns the name of the element. | |
| * | |
| * @return the name of the element | |
| */ | |
| public String getName() | |
| { | |
| return BidiRootName; | |
| } | |
| } | |
| /** | |
| * A leaf element for the bidi structure. | |
| */ | |
| private class BidiElement | |
| extends LeafElement | |
| { | |
| /** | |
| * Creates a new BidiElement. | |
| * | |
| * @param parent the parent element | |
| * @param start the start offset | |
| * @param end the end offset | |
| * @param level the bidi level | |
| */ | |
| BidiElement(Element parent, int start, int end, int level) | |
| { | |
| super(parent, new SimpleAttributeSet(), start, end); | |
| addAttribute(StyleConstants.BidiLevel, new Integer(level)); | |
| } | |
| /** | |
| * Returns the name of the element. | |
| * | |
| * @return the name of the element | |
| */ | |
| public String getName() | |
| { | |
| return BidiElementName; | |
| } | |
| } | |
| /** A class whose methods delegate to the insert, remove and replace methods | |
| * of this document which do not check for an installed DocumentFilter. | |
| */ | |
| class Bypass extends DocumentFilter.FilterBypass | |
| { | |
| public Document getDocument() | |
| { | |
| return AbstractDocument.this; | |
| } | |
| public void insertString(int offset, String string, AttributeSet attr) | |
| throws BadLocationException | |
| { | |
| AbstractDocument.this.insertStringImpl(offset, string, attr); | |
| } | |
| public void remove(int offset, int length) | |
| throws BadLocationException | |
| { | |
| AbstractDocument.this.removeImpl(offset, length); | |
| } | |
| public void replace(int offset, int length, String string, | |
| AttributeSet attrs) | |
| throws BadLocationException | |
| { | |
| AbstractDocument.this.replaceImpl(offset, length, string, attrs); | |
| } | |
| } | |
| } |