diff options
author | upstream source tree <ports@midipix.org> | 2015-03-15 20:14:05 -0400 |
---|---|---|
committer | upstream source tree <ports@midipix.org> | 2015-03-15 20:14:05 -0400 |
commit | 554fd8c5195424bdbcabf5de30fdc183aba391bd (patch) | |
tree | 976dc5ab7fddf506dadce60ae936f43f58787092 /libjava/classpath/javax/swing/text/DefaultStyledDocument.java | |
download | cbb-gcc-4.6.4-554fd8c5195424bdbcabf5de30fdc183aba391bd.tar.bz2 cbb-gcc-4.6.4-554fd8c5195424bdbcabf5de30fdc183aba391bd.tar.xz |
obtained gcc-4.6.4.tar.bz2 from upstream website;upstream
verified gcc-4.6.4.tar.bz2.sig;
imported gcc-4.6.4 source tree from verified upstream tarball.
downloading a git-generated archive based on the 'upstream' tag
should provide you with a source tree that is binary identical
to the one extracted from the above tarball.
if you have obtained the source via the command 'git clone',
however, do note that line-endings of files in your working
directory might differ from line-endings of the respective
files in the upstream repository.
Diffstat (limited to 'libjava/classpath/javax/swing/text/DefaultStyledDocument.java')
-rw-r--r-- | libjava/classpath/javax/swing/text/DefaultStyledDocument.java | 2526 |
1 files changed, 2526 insertions, 0 deletions
diff --git a/libjava/classpath/javax/swing/text/DefaultStyledDocument.java b/libjava/classpath/javax/swing/text/DefaultStyledDocument.java new file mode 100644 index 000000000..9021a1990 --- /dev/null +++ b/libjava/classpath/javax/swing/text/DefaultStyledDocument.java @@ -0,0 +1,2526 @@ +/* DefaultStyledDocument.java -- + Copyright (C) 2004, 2005 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.Color; +import java.awt.Font; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Stack; +import java.util.Vector; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.UndoableEditEvent; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.UndoableEdit; + +/** + * The default implementation of {@link StyledDocument}. The document is + * modeled as an {@link Element} tree, which has a {@link SectionElement} as + * single root, which has one or more {@link AbstractDocument.BranchElement}s + * as paragraph nodes and each paragraph node having one or more + * {@link AbstractDocument.LeafElement}s as content nodes. + * + * @author Michael Koch (konqueror@gmx.de) + * @author Roman Kennke (roman@kennke.org) + */ +public class DefaultStyledDocument extends AbstractDocument implements + StyledDocument +{ + + /** + * An {@link UndoableEdit} that can undo attribute changes to an element. + * + * @author Roman Kennke (kennke@aicas.com) + */ + public static class AttributeUndoableEdit extends AbstractUndoableEdit + { + /** + * A copy of the old attributes. + */ + protected AttributeSet copy; + + /** + * The new attributes. + */ + protected AttributeSet newAttributes; + + /** + * If the new attributes replaced the old attributes or if they only were + * added to them. + */ + protected boolean isReplacing; + + /** + * The element that has changed. + */ + protected Element element; + + /** + * Creates a new <code>AttributeUndoableEdit</code>. + * + * @param el + * the element that changes attributes + * @param newAtts + * the new attributes + * @param replacing + * if the new attributes replace the old or only append to them + */ + public AttributeUndoableEdit(Element el, AttributeSet newAtts, + boolean replacing) + { + element = el; + newAttributes = newAtts; + isReplacing = replacing; + copy = el.getAttributes().copyAttributes(); + } + + /** + * Undos the attribute change. The <code>copy</code> field is set as + * attributes on <code>element</code>. + */ + public void undo() + { + super.undo(); + AttributeSet atts = element.getAttributes(); + if (atts instanceof MutableAttributeSet) + { + MutableAttributeSet mutable = (MutableAttributeSet) atts; + mutable.removeAttributes(atts); + mutable.addAttributes(copy); + } + } + + /** + * Redos an attribute change. This adds <code>newAttributes</code> to the + * <code>element</code>'s attribute set, possibly clearing all attributes + * if <code>isReplacing</code> is true. + */ + public void redo() + { + super.undo(); + AttributeSet atts = element.getAttributes(); + if (atts instanceof MutableAttributeSet) + { + MutableAttributeSet mutable = (MutableAttributeSet) atts; + if (isReplacing) + mutable.removeAttributes(atts); + mutable.addAttributes(newAttributes); + } + } + } + + /** + * Carries specification information for new {@link Element}s that should be + * created in {@link ElementBuffer}. This allows the parsing process to be + * decoupled from the <code>Element</code> creation process. + */ + public static class ElementSpec + { + /** + * This indicates a start tag. This is a possible value for {@link #getType}. + */ + public static final short StartTagType = 1; + + /** + * This indicates an end tag. This is a possible value for {@link #getType}. + */ + public static final short EndTagType = 2; + + /** + * This indicates a content element. This is a possible value for + * {@link #getType}. + */ + public static final short ContentType = 3; + + /** + * This indicates that the data associated with this spec should be joined + * with what precedes it. This is a possible value for {@link #getDirection}. + */ + public static final short JoinPreviousDirection = 4; + + /** + * This indicates that the data associated with this spec should be joined + * with what follows it. This is a possible value for {@link #getDirection}. + */ + public static final short JoinNextDirection = 5; + + /** + * This indicates that the data associated with this spec should be used to + * create a new element. This is a possible value for {@link #getDirection}. + */ + public static final short OriginateDirection = 6; + + /** + * This indicates that the data associated with this spec should be joined + * to the fractured element. This is a possible value for + * {@link #getDirection}. + */ + public static final short JoinFractureDirection = 7; + + /** + * The type of the tag. + */ + short type; + + /** + * The direction of the tag. + */ + short direction; + + /** + * The offset of the content. + */ + int offset; + + /** + * The length of the content. + */ + int length; + + /** + * The actual content. + */ + char[] content; + + /** + * The attributes for the tag. + */ + AttributeSet attributes; + + /** + * Creates a new <code>ElementSpec</code> with no content, length or + * offset. This is most useful for start and end tags. + * + * @param a + * the attributes for the element to be created + * @param type + * the type of the tag + */ + public ElementSpec(AttributeSet a, short type) + { + this(a, type, 0); + } + + /** + * Creates a new <code>ElementSpec</code> that specifies the length but + * not the offset of an element. Such <code>ElementSpec</code>s are + * processed sequentially from a known starting point. + * + * @param a + * the attributes for the element to be created + * @param type + * the type of the tag + * @param len + * the length of the element + */ + public ElementSpec(AttributeSet a, short type, int len) + { + this(a, type, null, 0, len); + } + + /** + * Creates a new <code>ElementSpec</code> with document content. + * + * @param a + * the attributes for the element to be created + * @param type + * the type of the tag + * @param txt + * the actual content + * @param offs + * the offset into the <code>txt</code> array + * @param len + * the length of the element + */ + public ElementSpec(AttributeSet a, short type, char[] txt, int offs, int len) + { + attributes = a; + this.type = type; + offset = offs; + length = len; + content = txt; + direction = OriginateDirection; + } + + /** + * Sets the type of the element. + * + * @param type + * the type of the element to be set + */ + public void setType(short type) + { + this.type = type; + } + + /** + * Returns the type of the element. + * + * @return the type of the element + */ + public short getType() + { + return type; + } + + /** + * Sets the direction of the element. + * + * @param dir + * the direction of the element to be set + */ + public void setDirection(short dir) + { + direction = dir; + } + + /** + * Returns the direction of the element. + * + * @return the direction of the element + */ + public short getDirection() + { + return direction; + } + + /** + * Returns the attributes of the element. + * + * @return the attributes of the element + */ + public AttributeSet getAttributes() + { + return attributes; + } + + /** + * Returns the actual content of the element. + * + * @return the actual content of the element + */ + public char[] getArray() + { + return content; + } + + /** + * Returns the offset of the content. + * + * @return the offset of the content + */ + public int getOffset() + { + return offset; + } + + /** + * Returns the length of the content. + * + * @return the length of the content + */ + public int getLength() + { + return length; + } + + /** + * Returns a String representation of this <code>ElementSpec</code> + * describing the type, direction and length of this + * <code>ElementSpec</code>. + * + * @return a String representation of this <code>ElementSpec</code> + */ + public String toString() + { + CPStringBuilder b = new CPStringBuilder(); + switch (type) + { + case StartTagType: + b.append("StartTag"); + break; + case EndTagType: + b.append("EndTag"); + break; + case ContentType: + b.append("Content"); + break; + default: + b.append("??"); + break; + } + + b.append(':'); + + switch (direction) + { + case JoinPreviousDirection: + b.append("JoinPrevious"); + break; + case JoinNextDirection: + b.append("JoinNext"); + break; + case OriginateDirection: + b.append("Originate"); + break; + case JoinFractureDirection: + b.append("Fracture"); + break; + default: + b.append("??"); + break; + } + + b.append(':'); + b.append(length); + + return b.toString(); + } + } + + /** + * Performs all <em>structural</code> changes to the <code>Element</code> + * hierarchy. This class was implemented with much help from the document: + * http://java.sun.com/products/jfc/tsc/articles/text/element_buffer/index.html. + */ + public class ElementBuffer implements Serializable + { + /** + * Instance of all editing information for an object in the Vector. This class + * is used to add information to the DocumentEvent associated with an + * insertion/removal/change as well as to store the changes that need to be + * made so they can be made all at the same (appropriate) time. + */ + class Edit + { + /** The element to edit . */ + Element e; + + /** The index of the change. */ + int index; + + /** The removed elements. */ + ArrayList removed = new ArrayList(); + + /** The added elements. */ + ArrayList added = new ArrayList(); + + /** + * Indicates if this edit contains a fracture. + */ + boolean isFracture; + + /** + * Creates a new Edit for the specified element at index i. + * + * @param el the element + * @param i the index + */ + Edit(Element el, int i) + { + this(el, i, false); + } + + /** + * Creates a new Edit for the specified element at index i. + * + * @param el the element + * @param i the index + * @param frac if this is a fracture edit or not + */ + Edit(Element el, int i, boolean frac) + { + e = el; + index = i; + isFracture = frac; + } + + } + + /** The serialization UID (compatible with JDK1.5). */ + private static final long serialVersionUID = 1688745877691146623L; + + /** The root element of the hierarchy. */ + private Element root; + + /** Holds the offset for structural changes. */ + private int offset; + + /** Holds the end offset for structural changes. */ + private int endOffset; + + /** Holds the length of structural changes. */ + private int length; + + /** Holds the position of the change. */ + private int pos; + + /** + * The parent of the fracture. + */ + private Element fracturedParent; + + /** + * The fractured child. + */ + private Element fracturedChild; + + /** + * Indicates if a fracture has been created. + */ + private boolean createdFracture; + + /** + * The current position in the element tree. This is used for bulk inserts + * using ElementSpecs. + */ + private Stack elementStack; + + private Edit[] insertPath; + + private boolean recreateLeafs; + + /** + * Vector that contains all the edits. Maybe replace by a HashMap. + */ + private ArrayList edits; + + private boolean offsetLastIndex; + private boolean offsetLastIndexReplace; + + /** + * Creates a new <code>ElementBuffer</code> for the specified + * <code>root</code> element. + * + * @param root + * the root element for this <code>ElementBuffer</code> + */ + public ElementBuffer(Element root) + { + this.root = root; + } + + /** + * Returns the root element of this <code>ElementBuffer</code>. + * + * @return the root element of this <code>ElementBuffer</code> + */ + public Element getRootElement() + { + return root; + } + + /** + * Removes the content. This method sets some internal parameters and + * delegates the work to {@link #removeUpdate}. + * + * @param offs + * the offset from which content is remove + * @param len + * the length of the removed content + * @param ev + * the document event that records the changes + */ + public void remove(int offs, int len, DefaultDocumentEvent ev) + { + prepareEdit(offs, len); + removeUpdate(); + finishEdit(ev); + } + + /** + * Updates the element structure of the document in response to removal of + * content. It removes the affected {@link Element}s from the document + * structure. + */ + protected void removeUpdate() + { + removeElements(root, offset, endOffset); + } + + private boolean removeElements(Element elem, int rmOffs0, int rmOffs1) + { + boolean ret = false; + if (! elem.isLeaf()) + { + // Update stack for changes. + int index0 = elem.getElementIndex(rmOffs0); + int index1 = elem.getElementIndex(rmOffs1); + elementStack.push(new Edit(elem, index0)); + Edit ec = (Edit) elementStack.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.add(child0); + } + else if (removeElements(child0, rmOffs0, rmOffs1)) + { + ec.removed.add(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.add(elem.getElement(i)); + } + Element e = join(elem, child0, child1, rmOffs0, rmOffs1); + ec.added.add(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.add(elem.getElement(i)); + } + if (child0 != null) + { + if(removeElements(child0, rmOffs0, rmOffs1)) + { + ec.removed.add(0, child0); + ec.index = index0; + } + } + if (child1 != null) + { + if(removeElements(child1, rmOffs0, rmOffs1)) + { + ec.removed.add(child1); + } + } + } + } + + // Perform changes. + pop(); + + // Return true if we no longer have any children. + if(elem.getElementCount() == (ec.removed.size() - ec.added.size())) + ret = true; + } + return ret; + } + + /** + * Creates a document in response to a call to + * {@link DefaultStyledDocument#create(ElementSpec[])}. + * + * @param len the length of the inserted text + * @param data the specs for the elements + * @param ev the document event + */ + void create(int len, ElementSpec[] data, DefaultDocumentEvent ev) + { + prepareEdit(offset, len); + Element el = root; + int index = el.getElementIndex(0); + while (! el.isLeaf()) + { + Element child = el.getElement(index); + Edit edit = new Edit(el, index, false); + elementStack.push(edit); + el = child; + index = el.getElementIndex(0); + } + Edit ed = (Edit) elementStack.peek(); + Element child = ed.e.getElement(ed.index); + ed.added.add(createLeafElement(ed.e, child.getAttributes(), getLength(), + child.getEndOffset())); + ed.removed.add(child); + while (elementStack.size() > 1) + pop(); + int n = data.length; + + // Reset root element's attributes. + AttributeSet newAtts = null; + if (n > 0 && data[0].getType() == ElementSpec.StartTagType) + newAtts = data[0].getAttributes(); + if (newAtts == null) + newAtts = SimpleAttributeSet.EMPTY; + MutableAttributeSet mAtts = (MutableAttributeSet) root.getAttributes(); + ev.addEdit(new AttributeUndoableEdit(root, newAtts, true)); + mAtts.removeAttributes(mAtts); + mAtts.addAttributes(newAtts); + + // Insert the specified elements. + for (int i = 1; i < n; i++) + insertElement(data[i]); + + // Pop remaining stack. + while (elementStack.size() > 0) + pop(); + + finishEdit(ev); + } + + private boolean canJoin(Element e0, Element e1) + { + boolean ret = false; + if ((e0 != null) && (e1 != null)) + { + // Don't join a leaf to a branch. + boolean isLeaf0 = e0.isLeaf(); + boolean isLeaf1 = e1.isLeaf(); + if(isLeaf0 == isLeaf1) + { + if (isLeaf0) + { + // Only join leaves if the attributes match, otherwise + // style information will be lost. + ret = e0.getAttributes().isEqual(e1.getAttributes()); + } + else + { + // 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) + ret = name0.equals(name1); + else if (name1 != null) + ret = name1.equals(name0); + else // Both names null. + ret = true; + } + } + } + return ret; + } + + private Element join(Element p, Element left, Element right, int rmOffs0, + int rmOffs1) + { + Element joined = null; + if (left.isLeaf() && right.isLeaf()) + { + joined = 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. + joined = 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; + } + ArrayList children = new ArrayList(); + // Transfer the left. + for (int i = 0; i < ljIndex; i++) + { + children.add(clone(joined, left.getElement(i))); + } + + // Transfer the join/middle. + if (canJoin(lj, rj)) + { + Element e = join(joined, lj, rj, rmOffs0, rmOffs1); + children.add(e); + } + else + { + if (lj != null) + { + children.add(cloneAsNecessary(joined, lj, rmOffs0, rmOffs1)); + } + if (rj != null) + { + children.add(cloneAsNecessary(joined, rj, rmOffs0, rmOffs1)); + } + } + + // Transfer the right. + int n = right.getElementCount(); + for (int i = (rj == null) ? rjIndex : rjIndex + 1; i < n; i++) + { + children.add(clone(joined, right.getElement(i))); + } + + // Install the children. + Element[] c = new Element[children.size()]; + c = (Element[]) children.toArray(c); + ((BranchElement) joined).replace(0, 0, c); + } + else + { + assert false : "Must not happen"; + } + return joined; + } + + /** + * Performs the actual work for {@link #change}. The elements at the + * interval boundaries are split up (if necessary) so that the interval + * boundaries are located at element boundaries. + */ + protected void changeUpdate() + { + boolean didEnd = split(offset, length); + if (! didEnd) + { + // need to do the other end + while (elementStack.size() != 0) + { + pop(); + } + split(offset + length, 0); + } + while (elementStack.size() != 0) + { + pop(); + } + } + + /** + * Modifies the element structure so that the specified interval starts and + * ends at an element boundary. Content and paragraph elements are split and + * created as necessary. This also updates the + * <code>DefaultDocumentEvent</code> to reflect the structural changes. + * The bulk work is delegated to {@link #changeUpdate()}. + * + * @param offset + * the start index of the interval to be changed + * @param length + * the length of the interval to be changed + * @param ev + * the <code>DefaultDocumentEvent</code> describing the change + */ + public void change(int offset, int length, DefaultDocumentEvent ev) + { + prepareEdit(offset, length); + changeUpdate(); + finishEdit(ev); + } + + /** + * Creates and returns a deep clone of the specified <code>clonee</code> + * with the specified parent as new parent. + * + * This method can only clone direct instances of {@link BranchElement} + * or {@link LeafElement}. + * + * @param parent the new parent + * @param clonee the element to be cloned + * + * @return the cloned element with the new parent + */ + public Element clone(Element parent, Element clonee) + { + Element clone = clonee; + // We can only handle AbstractElements here. + if (clonee instanceof BranchElement) + { + BranchElement branchEl = (BranchElement) clonee; + BranchElement branchClone = + new BranchElement(parent, branchEl.getAttributes()); + // Also clone all of the children. + int numChildren = branchClone.getElementCount(); + Element[] cloneChildren = new Element[numChildren]; + for (int i = 0; i < numChildren; ++i) + { + cloneChildren[i] = clone(branchClone, + branchClone.getElement(i)); + } + branchClone.replace(0, 0, cloneChildren); + clone = branchClone; + } + else if (clonee instanceof LeafElement) + { + clone = new LeafElement(parent, clonee.getAttributes(), + clonee.getStartOffset(), + clonee.getEndOffset()); + } + return clone; + } + + private Element cloneAsNecessary(Element parent, Element clonee, + int rmOffs0, int rmOffs1) + { + Element cloned; + if (clonee.isLeaf()) + { + cloned = createLeafElement(parent, clonee.getAttributes(), + clonee.getStartOffset(), + clonee.getEndOffset()); + } + else + { + 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); + cloned = e; + } + return cloned; + } + + /** + * Inserts new <code>Element</code> in the document at the specified + * position. Most of the work is done by {@link #insertUpdate}, after some + * fields have been prepared for it. + * + * @param offset + * the location in the document at which the content is inserted + * @param length + * the length of the inserted content + * @param data + * the element specifications for the content to be inserted + * @param ev + * the document event that is updated to reflect the structural + * changes + */ + public void insert(int offset, int length, ElementSpec[] data, + DefaultDocumentEvent ev) + { + if (length > 0) + { + prepareEdit(offset, length); + insertUpdate(data); + finishEdit(ev); + } + } + + /** + * Prepares the state of this object for performing an insert. + * + * @param offset the offset at which is inserted + * @param length the length of the inserted region + */ + private void prepareEdit(int offset, int length) + { + this.offset = offset; + this.pos = offset; + this.endOffset = offset + length; + this.length = length; + + if (edits == null) + edits = new ArrayList(); + else + edits.clear(); + + if (elementStack == null) + elementStack = new Stack(); + else + elementStack.clear(); + + fracturedParent = null; + fracturedChild = null; + offsetLastIndex = false; + offsetLastIndexReplace = false; + } + + /** + * Finishes an insert. This applies all changes and updates + * the DocumentEvent. + * + * @param ev the document event + */ + private void finishEdit(DefaultDocumentEvent ev) + { + // This for loop applies all the changes that were made and updates the + // DocumentEvent. + for (Iterator i = edits.iterator(); i.hasNext();) + { + Edit edits = (Edit) i.next(); + Element[] removed = new Element[edits.removed.size()]; + removed = (Element[]) edits.removed.toArray(removed); + Element[] added = new Element[edits.added.size()]; + added = (Element[]) edits.added.toArray(added); + int index = edits.index; + BranchElement parent = (BranchElement) edits.e; + parent.replace(index, removed.length, added); + ElementEdit ee = new ElementEdit(parent, index, removed, added); + ev.addEdit(ee); + } + edits.clear(); + elementStack.clear(); + } + + /** + * Inserts new content. + * + * @param data the element specifications for the elements to be inserted + */ + protected void insertUpdate(ElementSpec[] data) + { + // Push the current path to the stack. + Element current = root; + int index = current.getElementIndex(offset); + while (! current.isLeaf()) + { + Element child = current.getElement(index); + int editIndex = child.isLeaf() ? index : index + 1; + Edit edit = new Edit(current, editIndex); + elementStack.push(edit); + current = child; + index = current.getElementIndex(offset); + } + + // Create a copy of the original path. + insertPath = new Edit[elementStack.size()]; + insertPath = (Edit[]) elementStack.toArray(insertPath); + + // No fracture yet. + createdFracture = false; + + // Insert first content tag. + int i = 0; + recreateLeafs = false; + int type = data[0].getType(); + if (type == ElementSpec.ContentType) + { + // If the first tag is content we must treat it separately to allow + // for joining properly to previous Elements and to ensure that + // no extra LeafElements are erroneously inserted. + insertFirstContentTag(data); + pos += data[0].length; + i = 1; + } + else + { + createFracture(data); + i = 0; + } + + // Handle each ElementSpec individually. + for (; i < data.length; i++) + { + insertElement(data[i]); + } + + // Fracture if we haven't done yet. + if (! createdFracture) + fracture(-1); + + // Pop the remaining stack. + while (elementStack.size() != 0) + pop(); + + // Offset last index if necessary. + if (offsetLastIndex && offsetLastIndexReplace) + insertPath[insertPath.length - 1].index++; + + // Make sure we havea an Edit for each path item that has a change. + for (int p = insertPath.length - 1; p >= 0; p--) + { + Edit edit = insertPath[p]; + if (edit.e == fracturedParent) + edit.added.add(fracturedChild); + if ((edit.added.size() > 0 || edit.removed.size() > 0) + && ! edits.contains(edit)) + edits.add(edit); + } + + // Remove element that would be created by an insert at 0 with + // an initial end tag. + if (offset == 0 && fracturedParent != null + && data[0].getType() == ElementSpec.EndTagType) + { + int p; + for (p = 0; + p < data.length && data[p].getType() == ElementSpec.EndTagType; + p++) + ; + + Edit edit = insertPath[insertPath.length - p - 1]; + edit.index--; + edit.removed.add(0, edit.e.getElement(edit.index)); + } + } + + private void pop() + { + Edit edit = (Edit) elementStack.peek(); + elementStack.pop(); + if ((edit.added.size() > 0) || (edit.removed.size() > 0)) + { + edits.add(edit); + } + else if (! elementStack.isEmpty()) + { + Element e = edit.e; + if (e.getElementCount() == 0) + { + // If we pushed a branch element that didn't get + // used, make sure its not marked as having been added. + edit = (Edit) elementStack.peek(); + edit.added.remove(e); + } + } + } + + private void insertElement(ElementSpec spec) + { + if (elementStack.isEmpty()) + return; + + Edit edit = (Edit) elementStack.peek(); + switch (spec.getType()) + { + case ElementSpec.StartTagType: + switch (spec.getDirection()) + { + case ElementSpec.JoinFractureDirection: + // Fracture the tree and ensure the appropriate element + // is on top of the stack. + if (! createdFracture) + { + fracture(elementStack.size() - 1); + } + if (! edit.isFracture) + { + // If the parent isn't a fracture, then the fracture is + // in fracturedChild. + Edit newEdit = new Edit(fracturedChild, 0, true); + elementStack.push(newEdit); + } + else + { + // Otherwise use the parent's first child. + Element el = edit.e.getElement(0); + Edit newEdit = new Edit(el, 0, true); + elementStack.push(newEdit); + } + break; + case ElementSpec.JoinNextDirection: + // Push the next paragraph element onto the stack so + // future insertions are added to it. + Element parent = edit.e.getElement(edit.index); + if (parent.isLeaf()) + { + if (edit.index + 1 < edit.e.getElementCount()) + parent = edit.e.getElement(edit.index + 1); + else + assert false; // Must not happen. + } + elementStack.push(new Edit(parent, 0, true)); + break; + default: + Element branch = createBranchElement(edit.e, + spec.getAttributes()); + edit.added.add(branch); + elementStack.push(new Edit(branch, 0)); + break; + } + break; + case ElementSpec.EndTagType: + pop(); + break; + case ElementSpec.ContentType: + insertContentTag(spec, edit); + break; + } + } + + /** + * Inserts the first tag into the document. + * + * @param data - + * the data to be inserted. + */ + private void insertFirstContentTag(ElementSpec[] data) + { + ElementSpec first = data[0]; + Edit edit = (Edit) elementStack.peek(); + Element current = edit.e.getElement(edit.index); + int firstEndOffset = offset + first.length; + boolean onlyContent = data.length == 1; + switch (first.getDirection()) + { + case ElementSpec.JoinPreviousDirection: + if (current.getEndOffset() != firstEndOffset && ! onlyContent) + { + Element newEl1 = createLeafElement(edit.e, + current.getAttributes(), + current.getStartOffset(), + firstEndOffset); + edit.added.add(newEl1); + edit.removed.add(current); + if (current.getEndOffset() != endOffset) + recreateLeafs = true; + else + offsetLastIndex = true; + } + else + { + offsetLastIndex = true; + offsetLastIndexReplace = true; + } + break; + case ElementSpec.JoinNextDirection: + if (offset != 0) + { + Element newEl1 = createLeafElement(edit.e, + current.getAttributes(), + current.getStartOffset(), + offset); + edit.added.add(newEl1); + Element next = edit.e.getElement(edit.index + 1); + if (onlyContent) + newEl1 = createLeafElement(edit.e, next.getAttributes(), + offset, next.getEndOffset()); + else + { + newEl1 = createLeafElement(edit.e, next.getAttributes(), + offset, firstEndOffset); + } + edit.added.add(newEl1); + edit.removed.add(current); + edit.removed.add(next); + } + break; + default: // OriginateDirection. + if (current.getStartOffset() != offset) + { + Element newEl = createLeafElement(edit.e, + current.getAttributes(), + current.getStartOffset(), + offset); + edit.added.add(newEl); + } + edit.removed.add(current); + Element newEl1 = createLeafElement(edit.e, first.getAttributes(), + offset, firstEndOffset); + edit.added.add(newEl1); + if (current.getEndOffset() != endOffset) + recreateLeafs = true; + else + offsetLastIndex = true; + break; + } + } + + /** + * Inserts a content element into the document structure. + * + * @param tag - + * the element spec + */ + private void insertContentTag(ElementSpec tag, Edit edit) + { + int len = tag.getLength(); + int dir = tag.getDirection(); + if (dir == ElementSpec.JoinNextDirection) + { + if (! edit.isFracture) + { + Element first = null; + if (insertPath != null) + { + for (int p = insertPath.length - 1; p >= 0; p--) + { + if (insertPath[p] == edit) + { + if (p != insertPath.length - 1) + first = edit.e.getElement(edit.index); + break; + } + } + } + if (first == null) + first = edit.e.getElement(edit.index + 1); + Element leaf = createLeafElement(edit.e, first.getAttributes(), + pos, first.getEndOffset()); + edit.added.add(leaf); + edit.removed.add(first); + } + else + { + Element first = edit.e.getElement(0); + Element leaf = createLeafElement(edit.e, first.getAttributes(), + pos, first.getEndOffset()); + edit.added.add(leaf); + edit.removed.add(first); + } + } + else + { + Element leaf = createLeafElement(edit.e, tag.getAttributes(), pos, + pos + len); + edit.added.add(leaf); + } + + pos += len; + + } + + /** + * This method fractures bottomost leaf in the elementStack. This + * happens when the first inserted tag is not content. + * + * @param data + * the ElementSpecs used for the entire insertion + */ + private void createFracture(ElementSpec[] data) + { + Edit edit = (Edit) elementStack.peek(); + Element child = edit.e.getElement(edit.index); + if (offset != 0) + { + Element newChild = createLeafElement(edit.e, child.getAttributes(), + child.getStartOffset(), offset); + edit.added.add(newChild); + } + edit.removed.add(child); + if (child.getEndOffset() != endOffset) + recreateLeafs = true; + else + offsetLastIndex = true; + } + + private void fracture(int depth) + { + int len = insertPath.length; + int lastIndex = -1; + boolean recreate = recreateLeafs; + Edit lastEdit = insertPath[len - 1]; + boolean childChanged = lastEdit.index + 1 < lastEdit.e.getElementCount(); + int deepestChangedIndex = recreate ? len : - 1; + int lastChangedIndex = len - 1; + createdFracture = true; + for (int i = len - 2; i >= 0; i--) + { + Edit edit = insertPath[i]; + if (edit.added.size() > 0 || i == depth) + { + lastIndex = i; + if (! recreate && childChanged) + { + recreate = true; + if (deepestChangedIndex == -1) + deepestChangedIndex = lastChangedIndex + 1; + } + } + if (! childChanged && edit.index < edit.e.getElementCount()) + { + childChanged = true; + lastChangedIndex = i; + } + } + if (recreate) + { + if (lastIndex == -1) + lastIndex = len - 1; + recreate(lastIndex, deepestChangedIndex); + } + } + + private void recreate(int startIndex, int endIndex) + { + // Recreate the element representing the inserted index. + Edit edit = insertPath[startIndex]; + Element child; + Element newChild; + int changeLength = insertPath.length; + + if (startIndex + 1 == changeLength) + child = edit.e.getElement(edit.index); + else + child = edit.e.getElement(edit.index - 1); + + if(child.isLeaf()) + { + newChild = createLeafElement(edit.e, child.getAttributes(), + Math.max(endOffset, child.getStartOffset()), + child.getEndOffset()); + } + else + { + newChild = createBranchElement(edit.e, child.getAttributes()); + } + fracturedParent = edit.e; + fracturedChild = newChild; + + // Recreate all the elements to the right of the insertion point. + Element parent = newChild; + while (++startIndex < endIndex) + { + boolean isEnd = (startIndex + 1) == endIndex; + 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. + edit = insertPath[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 = edit.e.getElement(edit.index); + } + else + { + child = edit.e.getElement(edit.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 childrenToMove = edit.e.getElementCount() - edit.index; + Element[] children; + int moveStartIndex; + int childStartIndex = 1; + + if (newChild == null) + { + // Last part of fracture. + if (isEndLeaf) + { + childrenToMove--; + moveStartIndex = edit.index + 1; + } + else + { + moveStartIndex = edit.index; + } + childStartIndex = 0; + children = new Element[childrenToMove]; + } + else + { + if (! isEnd) + { + // Branch. + childrenToMove++; + moveStartIndex = edit.index; + } + else + { + // Last leaf, need to recreate part of it. + moveStartIndex = edit.index + 1; + } + children = new Element[childrenToMove]; + children[0] = newChild; + } + + for (int c = childStartIndex; c < childrenToMove; c++) + { + Element toMove = edit.e.getElement(moveStartIndex++); + children[c] = recreateFracturedElement(parent, toMove); + edit.removed.add(toMove); + } + ((BranchElement) parent).replace(0, 0, children); + parent = newChild; + } + + } + + private Element recreateFracturedElement(Element parent, Element toCopy) + { + Element recreated; + if(toCopy.isLeaf()) + { + recreated = createLeafElement(parent, toCopy.getAttributes(), + Math.max(toCopy.getStartOffset(), endOffset), + toCopy.getEndOffset()); + } + else + { + Element newParent = createBranchElement(parent, + toCopy.getAttributes()); + int childCount = toCopy.getElementCount(); + Element[] newChildren = new Element[childCount]; + for (int i = 0; i < childCount; i++) + { + newChildren[i] = recreateFracturedElement(newParent, + toCopy.getElement(i)); + } + ((BranchElement) newParent).replace(0, 0, newChildren); + recreated = newParent; + } + return recreated; + } + + private boolean split(int offs, int len) + { + boolean splitEnd = false; + // Push the path to the stack. + Element e = root; + int index = e.getElementIndex(offs); + while (! e.isLeaf()) + { + elementStack.push(new Edit(e, index)); + e = e.getElement(index); + index = e.getElementIndex(offs); + } + + Edit ec = (Edit) elementStack.peek(); + Element child = ec.e.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.e.getEndOffset()) && (len != 0)) + { + // It's a range split in the same parent. + index1 = ec.e.getElementIndex(offs+len); + if (index1 == index0) + { + // It's a three-way split. + ec.removed.add(child); + e = createLeafElement(ec.e, child.getAttributes(), + child.getStartOffset(), offs); + ec.added.add(e); + e = createLeafElement(ec.e, child.getAttributes(), + offs, offs + len); + ec.added.add(e); + e = createLeafElement(ec.e, child.getAttributes(), + offs + len, child.getEndOffset()); + ec.added.add(e); + return true; + } + else + { + child = ec.e.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.e.getElement(index0); + ec.removed.add(child); + e = createLeafElement(ec.e, child.getAttributes(), + child.getStartOffset(), pos); + ec.added.add(e); + e = createLeafElement(ec.e, child.getAttributes(), + pos, child.getEndOffset()); + ec.added.add(e); + + // Pick up things in the middle. + for (int i = index0 + 1; i < index1; i++) + { + child = ec.e.getElement(i); + ec.removed.add(child); + ec.added.add(child); + } + + if (index1 != index0) + { + child = ec.e.getElement(index1); + pos = offs + len; + ec.removed.add(child); + e = createLeafElement(ec.e, child.getAttributes(), + child.getStartOffset(), pos); + ec.added.add(e); + e = createLeafElement(ec.e, child.getAttributes(), + pos, child.getEndOffset()); + + ec.added.add(e); + } + } + return splitEnd; + + } + + } + + + /** + * An element type for sections. This is a simple BranchElement with a unique + * name. + */ + protected class SectionElement extends BranchElement + { + /** + * Creates a new SectionElement. + */ + public SectionElement() + { + super(null, null); + } + + /** + * Returns the name of the element. This method always returns + * "section". + * + * @return the name of the element + */ + public String getName() + { + return SectionElementName; + } + } + + /** + * Receives notification when any of the document's style changes and calls + * {@link DefaultStyledDocument#styleChanged(Style)}. + * + * @author Roman Kennke (kennke@aicas.com) + */ + private class StyleChangeListener implements ChangeListener + { + + /** + * Receives notification when any of the document's style changes and calls + * {@link DefaultStyledDocument#styleChanged(Style)}. + * + * @param event + * the change event + */ + public void stateChanged(ChangeEvent event) + { + Style style = (Style) event.getSource(); + styleChanged(style); + } + } + + /** The serialization UID (compatible with JDK1.5). */ + private static final long serialVersionUID = 940485415728614849L; + + /** + * The default size to use for new content buffers. + */ + public static final int BUFFER_SIZE_DEFAULT = 4096; + + /** + * The <code>EditorBuffer</code> that is used to manage to + * <code>Element</code> hierarchy. + */ + protected DefaultStyledDocument.ElementBuffer buffer; + + /** + * Listens for changes on this document's styles and notifies styleChanged(). + */ + private StyleChangeListener styleChangeListener; + + /** + * Creates a new <code>DefaultStyledDocument</code>. + */ + public DefaultStyledDocument() + { + this(new GapContent(BUFFER_SIZE_DEFAULT), new StyleContext()); + } + + /** + * Creates a new <code>DefaultStyledDocument</code> that uses the specified + * {@link StyleContext}. + * + * @param context + * the <code>StyleContext</code> to use + */ + public DefaultStyledDocument(StyleContext context) + { + this(new GapContent(BUFFER_SIZE_DEFAULT), context); + } + + /** + * Creates a new <code>DefaultStyledDocument</code> that uses the specified + * {@link StyleContext} and {@link Content} buffer. + * + * @param content + * the <code>Content</code> buffer to use + * @param context + * the <code>StyleContext</code> to use + */ + public DefaultStyledDocument(AbstractDocument.Content content, + StyleContext context) + { + super(content, context); + buffer = new ElementBuffer(createDefaultRoot()); + setLogicalStyle(0, context.getStyle(StyleContext.DEFAULT_STYLE)); + } + + /** + * Adds a style into the style hierarchy. Unspecified style attributes can be + * resolved in the <code>parent</code> style, if one is specified. While it + * is legal to add nameless styles (<code>nm == null</code), + * you must be aware that the client application is then responsible + * for managing the style hierarchy, since unnamed styles cannot be + * looked up by their name. + * + * @param nm the name of the style or <code>null</code> if the style should + * be unnamed + * @param parent the parent in which unspecified style attributes are + * resolved, or <code>null</code> if that is not necessary + * + * @return the newly created <code>Style</code> + */ + public Style addStyle(String nm, Style parent) + { + StyleContext context = (StyleContext) getAttributeContext(); + Style newStyle = context.addStyle(nm, parent); + + // Register change listener. + if (styleChangeListener == null) + styleChangeListener = new StyleChangeListener(); + newStyle.addChangeListener(styleChangeListener); + + return newStyle; + } + + /** + * Create the default root element for this kind of <code>Document</code>. + * + * @return the default root element for this kind of <code>Document</code> + */ + protected AbstractDocument.AbstractElement createDefaultRoot() + { + Element[] tmp; + SectionElement section = new SectionElement(); + + BranchElement paragraph = new BranchElement(section, null); + tmp = new Element[1]; + tmp[0] = paragraph; + section.replace(0, 0, tmp); + + Element leaf = new LeafElement(paragraph, null, 0, 1); + tmp = new Element[1]; + tmp[0] = leaf; + paragraph.replace(0, 0, tmp); + + return section; + } + + /** + * Returns the <code>Element</code> that corresponds to the character at the + * specified position. + * + * @param position + * the position of which we query the corresponding + * <code>Element</code> + * @return the <code>Element</code> that corresponds to the character at the + * specified position + */ + public Element getCharacterElement(int position) + { + Element element = getDefaultRootElement(); + + while (!element.isLeaf()) + { + int index = element.getElementIndex(position); + element = element.getElement(index); + } + + return element; + } + + /** + * Extracts a background color from a set of attributes. + * + * @param attributes + * the attributes from which to get a background color + * @return the background color that correspond to the attributes + */ + public Color getBackground(AttributeSet attributes) + { + StyleContext context = (StyleContext) getAttributeContext(); + return context.getBackground(attributes); + } + + /** + * Returns the default root element. + * + * @return the default root element + */ + public Element getDefaultRootElement() + { + return buffer.getRootElement(); + } + + /** + * Extracts a font from a set of attributes. + * + * @param attributes + * the attributes from which to get a font + * @return the font that correspond to the attributes + */ + public Font getFont(AttributeSet attributes) + { + StyleContext context = (StyleContext) getAttributeContext(); + return context.getFont(attributes); + } + + /** + * Extracts a foreground color from a set of attributes. + * + * @param attributes + * the attributes from which to get a foreground color + * @return the foreground color that correspond to the attributes + */ + public Color getForeground(AttributeSet attributes) + { + StyleContext context = (StyleContext) getAttributeContext(); + return context.getForeground(attributes); + } + + /** + * Returns the logical <code>Style</code> for the specified position. + * + * @param position + * the position from which to query to logical style + * @return the logical <code>Style</code> for the specified position + */ + public Style getLogicalStyle(int position) + { + Element paragraph = getParagraphElement(position); + AttributeSet attributes = paragraph.getAttributes(); + AttributeSet a = attributes.getResolveParent(); + // If the resolve parent is not of type Style, we return null. + if (a instanceof Style) + return (Style) a; + return null; + } + + /** + * Returns the paragraph element for the specified position. If the position + * is outside the bounds of the document's root element, then the closest + * element is returned. That is the last paragraph if + * <code>position >= endIndex</code> or the first paragraph if + * <code>position < startIndex</code>. + * + * @param position + * the position for which to query the paragraph element + * @return the paragraph element for the specified position + */ + public Element getParagraphElement(int position) + { + Element e = getDefaultRootElement(); + while (!e.isLeaf()) + e = e.getElement(e.getElementIndex(position)); + + if (e != null) + return e.getParentElement(); + return e; + } + + /** + * Looks up and returns a named <code>Style</code>. + * + * @param nm + * the name of the <code>Style</code> + * @return the found <code>Style</code> of <code>null</code> if no such + * <code>Style</code> exists + */ + public Style getStyle(String nm) + { + StyleContext context = (StyleContext) getAttributeContext(); + return context.getStyle(nm); + } + + /** + * Removes a named <code>Style</code> from the style hierarchy. + * + * @param nm + * the name of the <code>Style</code> to be removed + */ + public void removeStyle(String nm) + { + StyleContext context = (StyleContext) getAttributeContext(); + context.removeStyle(nm); + } + + /** + * Sets text attributes for the fragment specified by <code>offset</code> + * and <code>length</code>. + * + * @param offset + * the start offset of the fragment + * @param length + * the length of the fragment + * @param attributes + * the text attributes to set + * @param replace + * if <code>true</code>, the attributes of the current selection + * are overridden, otherwise they are merged + */ + public void setCharacterAttributes(int offset, int length, + AttributeSet attributes, boolean replace) + { + // Exit early if length is 0, so no DocumentEvent is created or fired. + if (length == 0) + return; + try + { + // Must obtain a write lock for this method. writeLock() and + // writeUnlock() should always be in try/finally block to make + // sure that locking happens in a balanced manner. + writeLock(); + DefaultDocumentEvent ev = new DefaultDocumentEvent(offset, + length, + DocumentEvent.EventType.CHANGE); + + // Modify the element structure so that the interval begins at an + // element + // start and ends at an element end. + buffer.change(offset, length, ev); + + // Visit all paragraph elements within the specified interval + int end = offset + length; + Element curr; + for (int pos = offset; pos < end;) + { + // Get the CharacterElement at offset pos. + curr = getCharacterElement(pos); + if (pos == curr.getEndOffset()) + break; + + MutableAttributeSet a = (MutableAttributeSet) curr.getAttributes(); + ev.addEdit(new AttributeUndoableEdit(curr, attributes, replace)); + // If replace is true, remove all the old attributes. + if (replace) + a.removeAttributes(a); + // Add all the new attributes. + a.addAttributes(attributes); + // Increment pos so we can check the next CharacterElement. + pos = curr.getEndOffset(); + } + fireChangedUpdate(ev); + fireUndoableEditUpdate(new UndoableEditEvent(this, ev)); + } + finally + { + writeUnlock(); + } + } + + /** + * Sets the logical style for the paragraph at the specified position. + * + * @param position + * the position at which the logical style is added + * @param style + * the style to set for the current paragraph + */ + public void setLogicalStyle(int position, Style style) + { + Element el = getParagraphElement(position); + // getParagraphElement doesn't return null but subclasses might so + // we check for null here. + if (el == null) + return; + try + { + writeLock(); + if (el instanceof AbstractElement) + { + AbstractElement ael = (AbstractElement) el; + ael.setResolveParent(style); + int start = el.getStartOffset(); + int end = el.getEndOffset(); + DefaultDocumentEvent ev = new DefaultDocumentEvent(start, + end - start, + DocumentEvent.EventType.CHANGE); + fireChangedUpdate(ev); + fireUndoableEditUpdate(new UndoableEditEvent(this, ev)); + } + else + throw new AssertionError( + "paragraph elements are expected to be" + + "instances of AbstractDocument.AbstractElement"); + } + finally + { + writeUnlock(); + } + } + + /** + * Sets text attributes for the paragraph at the specified fragment. + * + * @param offset + * the beginning of the fragment + * @param length + * the length of the fragment + * @param attributes + * the text attributes to set + * @param replace + * if <code>true</code>, the attributes of the current selection + * are overridden, otherwise they are merged + */ + public void setParagraphAttributes(int offset, int length, + AttributeSet attributes, boolean replace) + { + try + { + // Must obtain a write lock for this method. writeLock() and + // writeUnlock() should always be in try/finally blocks to make + // sure that locking occurs in a balanced manner. + writeLock(); + + // Create a DocumentEvent to use for changedUpdate(). + DefaultDocumentEvent ev = new DefaultDocumentEvent(offset, + length, + DocumentEvent.EventType.CHANGE); + + // Have to iterate through all the _paragraph_ elements that are + // contained or partially contained in the interval + // (offset, offset + length). + Element rootElement = getDefaultRootElement(); + int startElement = rootElement.getElementIndex(offset); + int endElement = rootElement.getElementIndex(offset + length - 1); + if (endElement < startElement) + endElement = startElement; + + for (int i = startElement; i <= endElement; i++) + { + Element par = rootElement.getElement(i); + MutableAttributeSet a = (MutableAttributeSet) par.getAttributes(); + // Add the change to the DocumentEvent. + ev.addEdit(new AttributeUndoableEdit(par, attributes, replace)); + // If replace is true remove the old attributes. + if (replace) + a.removeAttributes(a); + // Add the new attributes. + a.addAttributes(attributes); + } + fireChangedUpdate(ev); + fireUndoableEditUpdate(new UndoableEditEvent(this, ev)); + } + finally + { + writeUnlock(); + } + } + + /** + * Called in response to content insert actions. This is used to update the + * element structure. + * + * @param ev + * the <code>DocumentEvent</code> describing the change + * @param attr + * the attributes for the change + */ + protected void insertUpdate(DefaultDocumentEvent ev, AttributeSet attr) + { + int offs = ev.getOffset(); + int len = ev.getLength(); + int endOffs = offs + len; + if (attr == null) + attr = SimpleAttributeSet.EMPTY; + + // Paragraph attributes are fetched from the point _after_ the insertion. + Element paragraph = getParagraphElement(endOffs); + AttributeSet pAttr = paragraph.getAttributes(); + // Character attributes are fetched from the actual insertion point. + Element paragraph2 = getParagraphElement(offs); + int contIndex = paragraph2.getElementIndex(offs); + Element content = paragraph2.getElement(contIndex); + AttributeSet cAttr = content.getAttributes(); + + boolean insertAtBoundary = content.getEndOffset() == endOffs; + try + { + Segment s = new Segment(); + ArrayList buf = new ArrayList(); + ElementSpec lastStartTag = null; + boolean insertAfterNewline = false; + short lastStartDir = ElementSpec.OriginateDirection; + + // Special handle if we are inserting after a newline. + if (offs > 0) + { + getText(offs - 1, 1, s); + if (s.array[s.offset] == '\n') + { + insertAfterNewline = true; + lastStartDir = insertAfterNewline(paragraph, paragraph2, + pAttr, buf, offs, + endOffs); + // Search last start tag. + for (int i = buf.size() - 1; i >= 0 && lastStartTag == null; + i--) + { + ElementSpec tag = (ElementSpec) buf.get(i); + if (tag.getType() == ElementSpec.StartTagType) + { + lastStartTag = tag; + } + } + } + + } + + // If we are not inserting after a newline, the paragraph attributes + // come from the paragraph under the insertion point. + if (! insertAfterNewline) + pAttr = paragraph2.getAttributes(); + + // Scan text and build up the specs. + getText(offs, len, s); + int end = s.offset + s.count; + int last = s.offset; + for (int i = s.offset; i < end; i++) + { + if (s.array[i] == '\n') + { + int breakOffs = i + 1; + buf.add(new ElementSpec(attr, ElementSpec.ContentType, + breakOffs - last)); + buf.add(new ElementSpec(null, ElementSpec.EndTagType)); + lastStartTag = new ElementSpec(pAttr, + ElementSpec.StartTagType); + buf.add(lastStartTag); + last = breakOffs; + } + } + + // Need to add a tailing content tag if we didn't finish at a boundary. + if (last < end) + { + buf.add(new ElementSpec(attr, ElementSpec.ContentType, + end - last)); + } + + // Now we need to fix up the directions of the specs. + ElementSpec first = (ElementSpec) buf.get(0); + int doclen = getLength(); + + // Maybe join-previous the first tag if it is content and has + // the same attributes as the previous character run. + if (first.getType() == ElementSpec.ContentType && cAttr.isEqual(attr)) + first.setDirection(ElementSpec.JoinPreviousDirection); + + // Join-fracture or join-next the last start tag if necessary. + if (lastStartTag != null) + { + if (insertAfterNewline) + lastStartTag.setDirection(lastStartDir); + else if (paragraph2.getEndOffset() != endOffs) + lastStartTag.setDirection(ElementSpec.JoinFractureDirection); + else + { + Element par = paragraph2.getParentElement(); + int par2Index = par.getElementIndex(offs); + if (par2Index + 1 < par.getElementCount() + && ! par.getElement(par2Index + 1).isLeaf()) + lastStartTag.setDirection(ElementSpec.JoinNextDirection); + } + } + + // Join-next last tag if possible. + if (insertAtBoundary && endOffs < doclen) + { + ElementSpec lastTag = (ElementSpec) buf.get(buf.size() - 1); + if (lastTag.getType() == ElementSpec.ContentType + && ((lastStartTag == null + && (paragraph == paragraph2 || insertAfterNewline)) + || (lastStartTag != null + && lastStartTag.getDirection() != ElementSpec.OriginateDirection))) + { + int nextIndex = paragraph.getElementIndex(endOffs); + Element nextRun = paragraph.getElement(nextIndex); + if (nextRun.isLeaf() && attr.isEqual(nextRun.getAttributes())) + lastTag.setDirection(ElementSpec.JoinNextDirection); + } + } + + else if (! insertAtBoundary && lastStartTag != null + && lastStartTag.getDirection() == ElementSpec.JoinFractureDirection) + { + ElementSpec lastTag = (ElementSpec) buf.get(buf.size() - 1); + if (lastTag.getType() == ElementSpec.ContentType + && lastTag.getDirection() != ElementSpec.JoinPreviousDirection + && attr.isEqual(cAttr)) + { + lastTag.setDirection(ElementSpec.JoinNextDirection); + } + } + + ElementSpec[] specs = new ElementSpec[buf.size()]; + specs = (ElementSpec[]) buf.toArray(specs); + buffer.insert(offs, len, specs, ev); + } + catch (BadLocationException ex) + { + // Ignore this. Comment out for debugging. + ex.printStackTrace(); + } + super.insertUpdate(ev, attr); + } + + private short insertAfterNewline(Element par1, Element par2, + AttributeSet attr, ArrayList buf, + int offs, int endOffs) + { + short dir = 0; + if (par1.getParentElement() == par2.getParentElement()) + { + ElementSpec tag = new ElementSpec(attr, ElementSpec.EndTagType); + buf.add(tag); + tag = new ElementSpec(attr, ElementSpec.StartTagType); + buf.add(tag); + if (par2.getEndOffset() != endOffs) + dir = ElementSpec.JoinFractureDirection; + else + { + Element par = par2.getParentElement(); + if (par.getElementIndex(offs) + 1 < par.getElementCount()) + dir = ElementSpec.JoinNextDirection; + } + } + else + { + // For text with more than 2 levels, find the common parent of + // par1 and par2. + ArrayList parentsLeft = new ArrayList(); + ArrayList parentsRight = new ArrayList(); + Element e = par2; + while (e != null) + { + parentsLeft.add(e); + e = e.getParentElement(); + } + e = par1; + int leftIndex = -1; + while (e != null && (leftIndex = parentsLeft.indexOf(e)) == 1) + { + parentsRight.add(e); + e = e.getParentElement(); + } + + if (e != null) + + { + // e is now the common parent. + // Insert the end tags. + for (int c = 0; c < leftIndex; c++) + { + buf.add(new ElementSpec(null, ElementSpec.EndTagType)); + } + // Insert the start tags. + for (int c = parentsRight.size() - 1; c >= 0; c--) + { + Element el = (Element) parentsRight.get(c); + ElementSpec tag = new ElementSpec(el.getAttributes(), + ElementSpec.StartTagType); + if (c > 0) + tag.setDirection(ElementSpec.JoinNextDirection); + buf.add(tag); + } + if (parentsRight.size() > 0) + dir = ElementSpec.JoinNextDirection; + else + dir = ElementSpec.JoinFractureDirection; + } + else + assert false; + } + return dir; + } + + /** + * A helper method to set up the ElementSpec buffer for the special case of an + * insertion occurring immediately after a newline. + * + * @param specs + * the ElementSpec buffer to initialize. + */ + short handleInsertAfterNewline(Vector specs, int offset, int endOffset, + Element prevParagraph, Element paragraph, + AttributeSet a) + { + if (prevParagraph.getParentElement() == paragraph.getParentElement()) + { + specs.add(new ElementSpec(a, ElementSpec.EndTagType)); + specs.add(new ElementSpec(a, ElementSpec.StartTagType)); + if (paragraph.getStartOffset() != endOffset) + return ElementSpec.JoinFractureDirection; + // If there is an Element after this one, use JoinNextDirection. + Element parent = paragraph.getParentElement(); + if (parent.getElementCount() > (parent.getElementIndex(offset) + 1)) + return ElementSpec.JoinNextDirection; + } + return ElementSpec.OriginateDirection; + } + + /** + * Updates the document structure in response to text removal. This is + * forwarded to the {@link ElementBuffer} of this document. Any changes to the + * document structure are added to the specified document event and sent to + * registered listeners. + * + * @param ev + * the document event that records the changes to the document + */ + protected void removeUpdate(DefaultDocumentEvent ev) + { + super.removeUpdate(ev); + buffer.remove(ev.getOffset(), ev.getLength(), ev); + } + + /** + * Returns an enumeration of all style names. + * + * @return an enumeration of all style names + */ + public Enumeration<?> getStyleNames() + { + StyleContext context = (StyleContext) getAttributeContext(); + return context.getStyleNames(); + } + + /** + * Called when any of this document's styles changes. + * + * @param style + * the style that changed + */ + protected void styleChanged(Style style) + { + // Nothing to do here. This is intended to be overridden by subclasses. + } + + /** + * Inserts a bulk of structured content at once. + * + * @param offset + * the offset at which the content should be inserted + * @param data + * the actual content spec to be inserted + */ + protected void insert(int offset, ElementSpec[] data) + throws BadLocationException + { + if (data == null || data.length == 0) + return; + try + { + // writeLock() and writeUnlock() should always be in a try/finally + // block so that locking balance is guaranteed even if some + // exception is thrown. + writeLock(); + + // First we collect the content to be inserted. + CPStringBuilder contentBuffer = new CPStringBuilder(); + for (int i = 0; i < data.length; i++) + { + // Collect all inserts into one so we can get the correct + // ElementEdit + ElementSpec spec = data[i]; + if (spec.getArray() != null && spec.getLength() > 0) + contentBuffer.append(spec.getArray(), spec.getOffset(), + spec.getLength()); + } + + int length = contentBuffer.length(); + + // If there was no content inserted then exit early. + if (length == 0) + return; + + Content c = getContent(); + UndoableEdit edit = c.insertString(offset, + contentBuffer.toString()); + + // Create the DocumentEvent with the ElementEdit added + DefaultDocumentEvent ev = new DefaultDocumentEvent(offset, + length, + DocumentEvent.EventType.INSERT); + + ev.addEdit(edit); + + // Finally we must update the document structure and fire the insert + // update event. + buffer.insert(offset, length, data, ev); + + super.insertUpdate(ev, null); + + ev.end(); + fireInsertUpdate(ev); + fireUndoableEditUpdate(new UndoableEditEvent(this, ev)); + } + finally + { + writeUnlock(); + } + } + + /** + * Initializes the <code>DefaultStyledDocument</code> with the specified + * data. + * + * @param data + * the specification of the content with which the document is + * initialized + */ + protected void create(ElementSpec[] data) + { + try + { + + // Clear content if there is some. + int len = getLength(); + if (len > 0) + remove(0, len); + + writeLock(); + + // Now we insert the content. + StringBuilder b = new StringBuilder(); + for (int i = 0; i < data.length; ++i) + { + ElementSpec el = data[i]; + if (el.getArray() != null && el.getLength() > 0) + b.append(el.getArray(), el.getOffset(), el.getLength()); + } + Content content = getContent(); + UndoableEdit cEdit = content.insertString(0, b.toString()); + + len = b.length(); + DefaultDocumentEvent ev = + new DefaultDocumentEvent(0, b.length(), + DocumentEvent.EventType.INSERT); + ev.addEdit(cEdit); + + buffer.create(len, data, ev); + + // For the bidi update. + super.insertUpdate(ev, null); + + ev.end(); + fireInsertUpdate(ev); + fireUndoableEditUpdate(new UndoableEditEvent(this, ev)); + } + catch (BadLocationException ex) + { + AssertionError err = new AssertionError("Unexpected bad location"); + err.initCause(ex); + throw err; + } + finally + { + writeUnlock(); + } + } +} |