mirror of
https://github.com/autc04/Retro68.git
synced 2024-11-28 21:49:33 +00:00
2527 lines
78 KiB
Java
2527 lines
78 KiB
Java
/* 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();
|
|
}
|
|
}
|
|
}
|