| /* 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(); | |
| } | |
| } | |
| } |