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