/*
* Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javax.swing.text;
import java.awt.Color;
import java.awt.Font;
import java.awt.font.TextAttribute;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.Vector;
import java.util.ArrayList;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Arrays;
import javax.swing.event.*;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEdit;
import javax.swing.SwingUtilities;
import static sun.swing.SwingUtilities2.IMPLIED_CR;
A document that can be marked up with character and paragraph
styles in a manner similar to the Rich Text Format. The element
structure for this document represents style crossings for
style runs. These style runs are mapped into a paragraph element
structure (which may reside in some other structure). The
style runs break at paragraph boundaries since logical styles are
assigned to paragraph boundaries.
Warning:
Serialized objects of this class will not be compatible with
future Swing releases. The current serialization support is
appropriate for short term storage or RMI between applications running
the same version of Swing. As of 1.4, support for long term storage
of all JavaBeans™
has been added to the java.beans
package. Please see XMLEncoder
.
Author: Timothy Prinzing See Also:
/**
* A document that can be marked up with character and paragraph
* styles in a manner similar to the Rich Text Format. The element
* structure for this document represents style crossings for
* style runs. These style runs are mapped into a paragraph element
* structure (which may reside in some other structure). The
* style runs break at paragraph boundaries since logical styles are
* assigned to paragraph boundaries.
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans™
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*
* @author Timothy Prinzing
* @see Document
* @see AbstractDocument
*/
@SuppressWarnings("serial") // Same-version serialization only
public class DefaultStyledDocument extends AbstractDocument implements StyledDocument {
Constructs a styled document.
Params: - c – the container for the content
- styles – resources and style definitions which may
be shared across documents
/**
* Constructs a styled document.
*
* @param c the container for the content
* @param styles resources and style definitions which may
* be shared across documents
*/
public DefaultStyledDocument(Content c, StyleContext styles) {
super(c, styles);
listeningStyles = new Vector<Style>();
buffer = new ElementBuffer(createDefaultRoot());
Style defaultStyle = styles.getStyle(StyleContext.DEFAULT_STYLE);
setLogicalStyle(0, defaultStyle);
}
Constructs a styled document with the default content
storage implementation and a shared set of styles.
Params: - styles – the styles
/**
* Constructs a styled document with the default content
* storage implementation and a shared set of styles.
*
* @param styles the styles
*/
public DefaultStyledDocument(StyleContext styles) {
this(new GapContent(BUFFER_SIZE_DEFAULT), styles);
}
Constructs a default styled document. This buffers
input content by a size of BUFFER_SIZE_DEFAULT
and has a style context that is scoped by the lifetime
of the document and is not shared with other documents.
/**
* Constructs a default styled document. This buffers
* input content by a size of <em>BUFFER_SIZE_DEFAULT</em>
* and has a style context that is scoped by the lifetime
* of the document and is not shared with other documents.
*/
public DefaultStyledDocument() {
this(new GapContent(BUFFER_SIZE_DEFAULT), new StyleContext());
}
Gets the default root element.
See Also: Returns: the root
/**
* Gets the default root element.
*
* @return the root
* @see Document#getDefaultRootElement
*/
public Element getDefaultRootElement() {
return buffer.getRootElement();
}
Initialize the document to reflect the given element
structure (i.e. the structure reported by the
getDefaultRootElement
method. If the
document contained any data it will first be removed.
Params: - data – the element data
/**
* Initialize the document to reflect the given element
* structure (i.e. the structure reported by the
* <code>getDefaultRootElement</code> method. If the
* document contained any data it will first be removed.
* @param data the element data
*/
protected void create(ElementSpec[] data) {
try {
if (getLength() != 0) {
remove(0, getLength());
}
writeLock();
// install the content
Content c = getContent();
int n = data.length;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
ElementSpec es = data[i];
if (es.getLength() > 0) {
sb.append(es.getArray(), es.getOffset(), es.getLength());
}
}
UndoableEdit cEdit = c.insertString(0, sb.toString());
// build the event and element structure
int length = sb.length();
DefaultDocumentEvent evnt =
new DefaultDocumentEvent(0, length, DocumentEvent.EventType.INSERT);
evnt.addEdit(cEdit);
buffer.create(length, data, evnt);
// update bidi (possibly)
super.insertUpdate(evnt, null);
// notify the listeners
evnt.end();
fireInsertUpdate(evnt);
fireUndoableEditUpdate(new UndoableEditEvent(this, evnt));
} catch (BadLocationException ble) {
throw new StateInvariantError("problem initializing");
} finally {
writeUnlock();
}
}
Inserts new elements in bulk. This is useful to allow
parsing with the document in an unlocked state and
prepare an element structure modification. This method
takes an array of tokens that describe how to update an
element structure so the time within a write lock can
be greatly reduced in an asynchronous update situation.
This method is thread safe, although most Swing methods
are not. Please see
Concurrency
in Swing for more information.
Params: - offset – the starting offset >= 0
- data – the element data
Throws: - BadLocationException – for an invalid starting offset
/**
* Inserts new elements in bulk. This is useful to allow
* parsing with the document in an unlocked state and
* prepare an element structure modification. This method
* takes an array of tokens that describe how to update an
* element structure so the time within a write lock can
* be greatly reduced in an asynchronous update situation.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html">Concurrency
* in Swing</A> for more information.
*
* @param offset the starting offset >= 0
* @param data the element data
* @exception BadLocationException for an invalid starting offset
*/
protected void insert(int offset, ElementSpec[] data) throws BadLocationException {
if (data == null || data.length == 0) {
return;
}
try {
writeLock();
// install the content
Content c = getContent();
int n = data.length;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
ElementSpec es = data[i];
if (es.getLength() > 0) {
sb.append(es.getArray(), es.getOffset(), es.getLength());
}
}
if (sb.length() == 0) {
// Nothing to insert, bail.
return;
}
UndoableEdit cEdit = c.insertString(offset, sb.toString());
// create event and build the element structure
int length = sb.length();
DefaultDocumentEvent evnt =
new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.INSERT);
evnt.addEdit(cEdit);
buffer.insert(offset, length, data, evnt);
// update bidi (possibly)
super.insertUpdate(evnt, null);
// notify the listeners
evnt.end();
fireInsertUpdate(evnt);
fireUndoableEditUpdate(new UndoableEditEvent(this, evnt));
} finally {
writeUnlock();
}
}
Removes an element from this document.
The element is removed from its parent element, as well as the text in the range identified by the element. If the element isn't associated with the document,
IllegalArgumentException
is thrown.
As empty branch elements are not allowed in the document, if the
element is the sole child, its parent element is removed as well,
recursively. This means that when replacing all the children of a
particular element, new children should be added before
removing old children.
Element removal results in two events being fired, the DocumentEvent
for changes in element structure and
UndoableEditEvent
for changes in document content.
If the element contains end-of-content mark (the last
"\n"
character in document), this character is not removed; instead, preceding leaf element is extended to cover the character. If the last leaf already ends with "\n",
it is included in content removal.
If the element is null,
NullPointerException
is thrown. If the element structure would become invalid after the removal, for example if the element is the document root element,
IllegalArgumentException
is thrown. If the current element structure is invalid, IllegalStateException
is thrown.
Params: - elem – the element to remove
Throws: - NullPointerException – if the element is
null
- IllegalArgumentException – if the element could not be removed
- IllegalStateException – if the element structure is invalid
Since: 1.7
/**
* Removes an element from this document.
*
* <p>The element is removed from its parent element, as well as
* the text in the range identified by the element. If the
* element isn't associated with the document, {@code
* IllegalArgumentException} is thrown.</p>
*
* <p>As empty branch elements are not allowed in the document, if the
* element is the sole child, its parent element is removed as well,
* recursively. This means that when replacing all the children of a
* particular element, new children should be added <em>before</em>
* removing old children.
*
* <p>Element removal results in two events being fired, the
* {@code DocumentEvent} for changes in element structure and {@code
* UndoableEditEvent} for changes in document content.</p>
*
* <p>If the element contains end-of-content mark (the last {@code
* "\n"} character in document), this character is not removed;
* instead, preceding leaf element is extended to cover the
* character. If the last leaf already ends with {@code "\n",} it is
* included in content removal.</p>
*
* <p>If the element is {@code null,} {@code NullPointerException} is
* thrown. If the element structure would become invalid after the removal,
* for example if the element is the document root element, {@code
* IllegalArgumentException} is thrown. If the current element structure is
* invalid, {@code IllegalStateException} is thrown.</p>
*
* @param elem the element to remove
* @throws NullPointerException if the element is {@code null}
* @throws IllegalArgumentException if the element could not be removed
* @throws IllegalStateException if the element structure is invalid
*
* @since 1.7
*/
public void removeElement(Element elem) {
try {
writeLock();
removeElementImpl(elem);
} finally {
writeUnlock();
}
}
private void removeElementImpl(Element elem) {
if (elem.getDocument() != this) {
throw new IllegalArgumentException("element doesn't belong to document");
}
BranchElement parent = (BranchElement) elem.getParentElement();
if (parent == null) {
throw new IllegalArgumentException("can't remove the root element");
}
int startOffset = elem.getStartOffset();
int removeFrom = startOffset;
int endOffset = elem.getEndOffset();
int removeTo = endOffset;
int lastEndOffset = getLength() + 1;
Content content = getContent();
boolean atEnd = false;
boolean isComposedText = Utilities.isComposedTextElement(elem);
if (endOffset >= lastEndOffset) {
// element includes the last "\n" character, needs special handling
if (startOffset <= 0) {
throw new IllegalArgumentException("can't remove the whole content");
}
removeTo = lastEndOffset - 1; // last "\n" must not be removed
try {
if (content.getString(startOffset - 1, 1).charAt(0) == '\n') {
removeFrom--; // preceding leaf ends with "\n", remove it
}
} catch (BadLocationException ble) { // can't happen
throw new IllegalStateException(ble);
}
atEnd = true;
}
int length = removeTo - removeFrom;
DefaultDocumentEvent dde = new DefaultDocumentEvent(removeFrom,
length, DefaultDocumentEvent.EventType.REMOVE);
UndoableEdit ue = null;
// do not leave empty branch elements
while (parent.getElementCount() == 1) {
elem = parent;
parent = (BranchElement) parent.getParentElement();
if (parent == null) { // shouldn't happen
throw new IllegalStateException("invalid element structure");
}
}
Element[] removed = { elem };
Element[] added = {};
int index = parent.getElementIndex(startOffset);
parent.replace(index, 1, added);
dde.addEdit(new ElementEdit(parent, index, removed, added));
if (length > 0) {
try {
ue = content.remove(removeFrom, length);
if (ue != null) {
dde.addEdit(ue);
}
} catch (BadLocationException ble) {
// can only happen if the element structure is severely broken
throw new IllegalStateException(ble);
}
lastEndOffset -= length;
}
if (atEnd) {
// preceding leaf element should be extended to cover orphaned "\n"
Element prevLeaf = parent.getElement(parent.getElementCount() - 1);
while ((prevLeaf != null) && !prevLeaf.isLeaf()) {
prevLeaf = prevLeaf.getElement(prevLeaf.getElementCount() - 1);
}
if (prevLeaf == null) { // shouldn't happen
throw new IllegalStateException("invalid element structure");
}
int prevStartOffset = prevLeaf.getStartOffset();
BranchElement prevParent = (BranchElement) prevLeaf.getParentElement();
int prevIndex = prevParent.getElementIndex(prevStartOffset);
Element newElem;
newElem = createLeafElement(prevParent, prevLeaf.getAttributes(),
prevStartOffset, lastEndOffset);
Element[] prevRemoved = { prevLeaf };
Element[] prevAdded = { newElem };
prevParent.replace(prevIndex, 1, prevAdded);
dde.addEdit(new ElementEdit(prevParent, prevIndex,
prevRemoved, prevAdded));
}
postRemoveUpdate(dde);
dde.end();
fireRemoveUpdate(dde);
if (! (isComposedText && (ue != null))) {
// do not fire UndoabeEdit event for composed text edit (unsupported)
fireUndoableEditUpdate(new UndoableEditEvent(this, dde));
}
}
Adds a new style into the logical style hierarchy. Style attributes
resolve from bottom up so an attribute specified in a child
will override an attribute specified in the parent.
Params: - nm – the name of the style (must be unique within the
collection of named styles). The name may be null if the style
is unnamed, but the caller is responsible
for managing the reference returned as an unnamed style can't
be fetched by name. An unnamed style may be useful for things
like character attribute overrides such as found in a style
run.
- parent – the parent style. This may be null if unspecified
attributes need not be resolved in some other style.
Returns: the style
/**
* Adds a new style into the logical style hierarchy. Style attributes
* resolve from bottom up so an attribute specified in a child
* will override an attribute specified in the parent.
*
* @param nm the name of the style (must be unique within the
* collection of named styles). The name may be null if the style
* is unnamed, but the caller is responsible
* for managing the reference returned as an unnamed style can't
* be fetched by name. An unnamed style may be useful for things
* like character attribute overrides such as found in a style
* run.
* @param parent the parent style. This may be null if unspecified
* attributes need not be resolved in some other style.
* @return the style
*/
public Style addStyle(String nm, Style parent) {
StyleContext styles = (StyleContext) getAttributeContext();
return styles.addStyle(nm, parent);
}
Removes a named style previously added to the document.
Params: - nm – the name of the style to remove
/**
* Removes a named style previously added to the document.
*
* @param nm the name of the style to remove
*/
public void removeStyle(String nm) {
StyleContext styles = (StyleContext) getAttributeContext();
styles.removeStyle(nm);
}
Fetches a named style previously added.
Params: - nm – the name of the style
Returns: the style
/**
* Fetches a named style previously added.
*
* @param nm the name of the style
* @return the style
*/
public Style getStyle(String nm) {
StyleContext styles = (StyleContext) getAttributeContext();
return styles.getStyle(nm);
}
Fetches the list of style names.
Returns: all the style names
/**
* Fetches the list of style names.
*
* @return all the style names
*/
public Enumeration<?> getStyleNames() {
return ((StyleContext) getAttributeContext()).getStyleNames();
}
Sets the logical style to use for the paragraph at the
given position. If attributes aren't explicitly set
for character and paragraph attributes they will resolve
through the logical style assigned to the paragraph, which
in turn may resolve through some hierarchy completely
independent of the element hierarchy in the document.
This method is thread safe, although most Swing methods
are not. Please see
Concurrency
in Swing for more information.
Params: - pos – the offset from the start of the document >= 0
- s – the logical style to assign to the paragraph, null if none
/**
* Sets the logical style to use for the paragraph at the
* given position. If attributes aren't explicitly set
* for character and paragraph attributes they will resolve
* through the logical style assigned to the paragraph, which
* in turn may resolve through some hierarchy completely
* independent of the element hierarchy in the document.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html">Concurrency
* in Swing</A> for more information.
*
* @param pos the offset from the start of the document >= 0
* @param s the logical style to assign to the paragraph, null if none
*/
public void setLogicalStyle(int pos, Style s) {
Element paragraph = getParagraphElement(pos);
if ((paragraph != null) && (paragraph instanceof AbstractElement)) {
try {
writeLock();
StyleChangeUndoableEdit edit = new StyleChangeUndoableEdit((AbstractElement)paragraph, s);
((AbstractElement)paragraph).setResolveParent(s);
int p0 = paragraph.getStartOffset();
int p1 = paragraph.getEndOffset();
DefaultDocumentEvent e =
new DefaultDocumentEvent(p0, p1 - p0, DocumentEvent.EventType.CHANGE);
e.addEdit(edit);
e.end();
fireChangedUpdate(e);
fireUndoableEditUpdate(new UndoableEditEvent(this, e));
} finally {
writeUnlock();
}
}
}
Fetches the logical style assigned to the paragraph
represented by the given position.
Params: - p – the location to translate to a paragraph
and determine the logical style assigned >= 0. This
is an offset from the start of the document.
Returns: the style, null if none
/**
* Fetches the logical style assigned to the paragraph
* represented by the given position.
*
* @param p the location to translate to a paragraph
* and determine the logical style assigned >= 0. This
* is an offset from the start of the document.
* @return the style, null if none
*/
public Style getLogicalStyle(int p) {
Style s = null;
Element paragraph = getParagraphElement(p);
if (paragraph != null) {
AttributeSet a = paragraph.getAttributes();
AttributeSet parent = a.getResolveParent();
if (parent instanceof Style) {
s = (Style) parent;
}
}
return s;
}
Sets attributes for some part of the document.
A write lock is held by this operation while changes
are being made, and a DocumentEvent is sent to the listeners
after the change has been successfully completed.
This method is thread safe, although most Swing methods
are not. Please see
Concurrency
in Swing for more information.
Params: - offset – the offset in the document >= 0
- length – the length >= 0
- s – the attributes
- replace – true if the previous attributes should be replaced
before setting the new attributes
/**
* Sets attributes for some part of the document.
* A write lock is held by this operation while changes
* are being made, and a DocumentEvent is sent to the listeners
* after the change has been successfully completed.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html">Concurrency
* in Swing</A> for more information.
*
* @param offset the offset in the document >= 0
* @param length the length >= 0
* @param s the attributes
* @param replace true if the previous attributes should be replaced
* before setting the new attributes
*/
public void setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace) {
if (length == 0) {
return;
}
try {
writeLock();
DefaultDocumentEvent changes =
new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
// split elements that need it
buffer.change(offset, length, changes);
AttributeSet sCopy = s.copyAttributes();
// PENDING(prinz) - this isn't a very efficient way to iterate
int lastEnd;
for (int pos = offset; pos < (offset + length); pos = lastEnd) {
Element run = getCharacterElement(pos);
lastEnd = run.getEndOffset();
if (pos == lastEnd) {
// offset + length beyond length of document, bail.
break;
}
MutableAttributeSet attr = (MutableAttributeSet) run.getAttributes();
changes.addEdit(new AttributeUndoableEdit(run, sCopy, replace));
if (replace) {
attr.removeAttributes(attr);
}
attr.addAttributes(s);
}
changes.end();
fireChangedUpdate(changes);
fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
} finally {
writeUnlock();
}
}
Sets attributes for a paragraph.
This method is thread safe, although most Swing methods
are not. Please see
Concurrency
in Swing for more information.
Params: - offset – the offset into the paragraph >= 0
- length – the number of characters affected >= 0
- s – the attributes
- replace – whether to replace existing attributes, or merge them
/**
* Sets attributes for a paragraph.
* <p>
* This method is thread safe, although most Swing methods
* are not. Please see
* <A HREF="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html">Concurrency
* in Swing</A> for more information.
*
* @param offset the offset into the paragraph >= 0
* @param length the number of characters affected >= 0
* @param s the attributes
* @param replace whether to replace existing attributes, or merge them
*/
public void setParagraphAttributes(int offset, int length, AttributeSet s,
boolean replace) {
try {
writeLock();
DefaultDocumentEvent changes =
new DefaultDocumentEvent(offset, length, DocumentEvent.EventType.CHANGE);
AttributeSet sCopy = s.copyAttributes();
// PENDING(prinz) - this assumes a particular element structure
Element section = getDefaultRootElement();
int index0 = section.getElementIndex(offset);
int index1 = section.getElementIndex(offset + ((length > 0) ? length - 1 : 0));
boolean isI18N = Boolean.TRUE.equals(getProperty(I18NProperty));
boolean hasRuns = false;
for (int i = index0; i <= index1; i++) {
Element paragraph = section.getElement(i);
MutableAttributeSet attr = (MutableAttributeSet) paragraph.getAttributes();
changes.addEdit(new AttributeUndoableEdit(paragraph, sCopy, replace));
if (replace) {
attr.removeAttributes(attr);
}
attr.addAttributes(s);
if (isI18N && !hasRuns) {
hasRuns = (attr.getAttribute(TextAttribute.RUN_DIRECTION) != null);
}
}
if (hasRuns) {
updateBidi( changes );
}
changes.end();
fireChangedUpdate(changes);
fireUndoableEditUpdate(new UndoableEditEvent(this, changes));
} finally {
writeUnlock();
}
}
Gets the paragraph element at the offset pos
.
A paragraph consists of at least one child Element, which is usually
a leaf.
Params: - pos – the starting offset >= 0
Returns: the element
/**
* Gets the paragraph element at the offset <code>pos</code>.
* A paragraph consists of at least one child Element, which is usually
* a leaf.
*
* @param pos the starting offset >= 0
* @return the element
*/
public Element getParagraphElement(int pos) {
Element e;
for (e = getDefaultRootElement(); ! e.isLeaf(); ) {
int index = e.getElementIndex(pos);
e = e.getElement(index);
}
if(e != null)
return e.getParentElement();
return e;
}
Gets a character element based on a position.
Params: - pos – the position in the document >= 0
Returns: the element
/**
* Gets a character element based on a position.
*
* @param pos the position in the document >= 0
* @return the element
*/
public Element getCharacterElement(int pos) {
Element e;
for (e = getDefaultRootElement(); ! e.isLeaf(); ) {
int index = e.getElementIndex(pos);
e = e.getElement(index);
}
return e;
}
// --- local methods -------------------------------------------------
Updates document structure as a result of text insertion. This
will happen within a write lock. This implementation simply
parses the inserted content for line breaks and builds up a set
of instructions for the element buffer.
Params: - chng – a description of the document change
- attr – the attributes
/**
* Updates document structure as a result of text insertion. This
* will happen within a write lock. This implementation simply
* parses the inserted content for line breaks and builds up a set
* of instructions for the element buffer.
*
* @param chng a description of the document change
* @param attr the attributes
*/
protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
int offset = chng.getOffset();
int length = chng.getLength();
if (attr == null) {
attr = SimpleAttributeSet.EMPTY;
}
// Paragraph attributes should come from point after insertion.
// You really only notice this when inserting at a paragraph
// boundary.
Element paragraph = getParagraphElement(offset + length);
AttributeSet pattr = paragraph.getAttributes();
// Character attributes should come from actual insertion point.
Element pParagraph = getParagraphElement(offset);
Element run = pParagraph.getElement(pParagraph.getElementIndex
(offset));
int endOffset = offset + length;
boolean insertingAtBoundry = (run.getEndOffset() == endOffset);
AttributeSet cattr = run.getAttributes();
try {
Segment s = new Segment();
Vector<ElementSpec> parseBuffer = new Vector<ElementSpec>();
ElementSpec lastStartSpec = null;
boolean insertingAfterNewline = false;
short lastStartDirection = ElementSpec.OriginateDirection;
// Check if the previous character was a newline.
if (offset > 0) {
getText(offset - 1, 1, s);
if (s.array[s.offset] == '\n') {
// Inserting after a newline.
insertingAfterNewline = true;
lastStartDirection = createSpecsForInsertAfterNewline
(paragraph, pParagraph, pattr, parseBuffer,
offset, endOffset);
for(int counter = parseBuffer.size() - 1; counter >= 0;
counter--) {
ElementSpec spec = parseBuffer.elementAt(counter);
if(spec.getType() == ElementSpec.StartTagType) {
lastStartSpec = spec;
break;
}
}
}
}
// If not inserting after a new line, pull the attributes for
// new paragraphs from the paragraph under the insertion point.
if(!insertingAfterNewline)
pattr = pParagraph.getAttributes();
getText(offset, length, s);
char[] txt = s.array;
int n = s.offset + s.count;
int lastOffset = s.offset;
for (int i = s.offset; i < n; i++) {
if (txt[i] == '\n') {
int breakOffset = i + 1;
parseBuffer.addElement(
new ElementSpec(attr, ElementSpec.ContentType,
breakOffset - lastOffset));
parseBuffer.addElement(
new ElementSpec(null, ElementSpec.EndTagType));
lastStartSpec = new ElementSpec(pattr, ElementSpec.
StartTagType);
parseBuffer.addElement(lastStartSpec);
lastOffset = breakOffset;
}
}
if (lastOffset < n) {
parseBuffer.addElement(
new ElementSpec(attr, ElementSpec.ContentType,
n - lastOffset));
}
ElementSpec first = parseBuffer.firstElement();
int docLength = getLength();
// Check for join previous of first content.
if(first.getType() == ElementSpec.ContentType &&
cattr.isEqual(attr)) {
first.setDirection(ElementSpec.JoinPreviousDirection);
}
// Do a join fracture/next for last start spec if necessary.
if(lastStartSpec != null) {
if(insertingAfterNewline) {
lastStartSpec.setDirection(lastStartDirection);
}
// Join to the fracture if NOT inserting at the end
// (fracture only happens when not inserting at end of
// paragraph).
else if(pParagraph.getEndOffset() != endOffset) {
lastStartSpec.setDirection(ElementSpec.
JoinFractureDirection);
}
// Join to next if parent of pParagraph has another
// element after pParagraph, and it isn't a leaf.
else {
Element parent = pParagraph.getParentElement();
int pParagraphIndex = parent.getElementIndex(offset);
if((pParagraphIndex + 1) < parent.getElementCount() &&
!parent.getElement(pParagraphIndex + 1).isLeaf()) {
lastStartSpec.setDirection(ElementSpec.
JoinNextDirection);
}
}
}
// Do a JoinNext for last spec if it is content, it doesn't
// already have a direction set, no new paragraphs have been
// inserted or a new paragraph has been inserted and its join
// direction isn't originate, and the element at endOffset
// is a leaf.
if(insertingAtBoundry && endOffset < docLength) {
ElementSpec last = parseBuffer.lastElement();
if(last.getType() == ElementSpec.ContentType &&
last.getDirection() != ElementSpec.JoinPreviousDirection &&
((lastStartSpec == null && (paragraph == pParagraph ||
insertingAfterNewline)) ||
(lastStartSpec != null && lastStartSpec.getDirection() !=
ElementSpec.OriginateDirection))) {
Element nextRun = paragraph.getElement(paragraph.
getElementIndex(endOffset));
// Don't try joining to a branch!
if(nextRun.isLeaf() &&
attr.isEqual(nextRun.getAttributes())) {
last.setDirection(ElementSpec.JoinNextDirection);
}
}
}
// If not inserting at boundary and there is going to be a
// fracture, then can join next on last content if cattr
// matches the new attributes.
else if(!insertingAtBoundry && lastStartSpec != null &&
lastStartSpec.getDirection() ==
ElementSpec.JoinFractureDirection) {
ElementSpec last = parseBuffer.lastElement();
if(last.getType() == ElementSpec.ContentType &&
last.getDirection() != ElementSpec.JoinPreviousDirection &&
attr.isEqual(cattr)) {
last.setDirection(ElementSpec.JoinNextDirection);
}
}
// Check for the composed text element. If it is, merge the character attributes
// into this element as well.
if (Utilities.isComposedTextAttributeDefined(attr)) {
MutableAttributeSet mattr = (MutableAttributeSet) attr;
mattr.addAttributes(cattr);
mattr.addAttribute(AbstractDocument.ElementNameAttribute,
AbstractDocument.ContentElementName);
// Assure that the composed text element is named properly
// and doesn't have the CR attribute defined.
mattr.addAttribute(StyleConstants.NameAttribute,
AbstractDocument.ContentElementName);
if (mattr.isDefined(IMPLIED_CR)) {
mattr.removeAttribute(IMPLIED_CR);
}
}
ElementSpec[] spec = new ElementSpec[parseBuffer.size()];
parseBuffer.copyInto(spec);
buffer.insert(offset, length, spec, chng);
} catch (BadLocationException bl) {
}
super.insertUpdate( chng, attr );
}
This is called by insertUpdate when inserting after a new line.
It generates, in parseBuffer
, ElementSpecs that will
position the stack in paragraph
.
It returns the direction the last StartSpec should have (this don't
necessarily create the last start spec).
/**
* This is called by insertUpdate when inserting after a new line.
* It generates, in <code>parseBuffer</code>, ElementSpecs that will
* position the stack in <code>paragraph</code>.<p>
* It returns the direction the last StartSpec should have (this don't
* necessarily create the last start spec).
*/
short createSpecsForInsertAfterNewline(Element paragraph,
Element pParagraph, AttributeSet pattr, Vector<ElementSpec> parseBuffer,
int offset, int endOffset) {
// Need to find the common parent of pParagraph and paragraph.
if(paragraph.getParentElement() == pParagraph.getParentElement()) {
// The simple (and common) case that pParagraph and
// paragraph have the same parent.
ElementSpec spec = new ElementSpec(pattr, ElementSpec.EndTagType);
parseBuffer.addElement(spec);
spec = new ElementSpec(pattr, ElementSpec.StartTagType);
parseBuffer.addElement(spec);
if(pParagraph.getEndOffset() != endOffset)
return ElementSpec.JoinFractureDirection;
Element parent = pParagraph.getParentElement();
if((parent.getElementIndex(offset) + 1) < parent.getElementCount())
return ElementSpec.JoinNextDirection;
}
else {
// Will only happen for text with more than 2 levels.
// Find the common parent of a paragraph and pParagraph
Vector<Element> leftParents = new Vector<Element>();
Vector<Element> rightParents = new Vector<Element>();
Element e = pParagraph;
while(e != null) {
leftParents.addElement(e);
e = e.getParentElement();
}
e = paragraph;
int leftIndex = -1;
while(e != null && (leftIndex = leftParents.indexOf(e)) == -1) {
rightParents.addElement(e);
e = e.getParentElement();
}
if(e != null) {
// e identifies the common parent.
// Build the ends.
for(int counter = 0; counter < leftIndex;
counter++) {
parseBuffer.addElement(new ElementSpec
(null, ElementSpec.EndTagType));
}
// And the starts.
ElementSpec spec;
for(int counter = rightParents.size() - 1;
counter >= 0; counter--) {
spec = new ElementSpec(rightParents.elementAt(counter).getAttributes(),
ElementSpec.StartTagType);
if(counter > 0)
spec.setDirection(ElementSpec.JoinNextDirection);
parseBuffer.addElement(spec);
}
// If there are right parents, then we generated starts
// down the right subtree and there will be an element to
// join to.
if(rightParents.size() > 0)
return ElementSpec.JoinNextDirection;
// No right subtree, e.getElement(endOffset) is a
// leaf. There will be a facture.
return ElementSpec.JoinFractureDirection;
}
// else: Could throw an exception here, but should never get here!
}
return ElementSpec.OriginateDirection;
}
Updates document structure as a result of text removal.
Params: - chng – a description of the document change
/**
* Updates document structure as a result of text removal.
*
* @param chng a description of the document change
*/
protected void removeUpdate(DefaultDocumentEvent chng) {
super.removeUpdate(chng);
buffer.remove(chng.getOffset(), chng.getLength(), chng);
}
Creates the root element to be used to represent the
default document structure.
Returns: the element base
/**
* Creates the root element to be used to represent the
* default document structure.
*
* @return the element base
*/
protected AbstractElement createDefaultRoot() {
// grabs a write-lock for this initialization and
// abandon it during initialization so in normal
// operation we can detect an illegitimate attempt
// to mutate attributes.
writeLock();
BranchElement section = new SectionElement();
BranchElement paragraph = new BranchElement(section, null);
LeafElement brk = new LeafElement(paragraph, null, 0, 1);
Element[] buff = new Element[1];
buff[0] = brk;
paragraph.replace(0, 0, buff);
buff[0] = paragraph;
section.replace(0, 0, buff);
writeUnlock();
return section;
}
Gets the foreground color from an attribute set.
Params: - attr – the attribute set
Returns: the color
/**
* Gets the foreground color from an attribute set.
*
* @param attr the attribute set
* @return the color
*/
public Color getForeground(AttributeSet attr) {
StyleContext styles = (StyleContext) getAttributeContext();
return styles.getForeground(attr);
}
Gets the background color from an attribute set.
Params: - attr – the attribute set
Returns: the color
/**
* Gets the background color from an attribute set.
*
* @param attr the attribute set
* @return the color
*/
public Color getBackground(AttributeSet attr) {
StyleContext styles = (StyleContext) getAttributeContext();
return styles.getBackground(attr);
}
Gets the font from an attribute set.
Params: - attr – the attribute set
Returns: the font
/**
* Gets the font from an attribute set.
*
* @param attr the attribute set
* @return the font
*/
public Font getFont(AttributeSet attr) {
StyleContext styles = (StyleContext) getAttributeContext();
return styles.getFont(attr);
}
Called when any of this document's styles have changed.
Subclasses may wish to be intelligent about what gets damaged.
Params: - style – The Style that has changed.
/**
* Called when any of this document's styles have changed.
* Subclasses may wish to be intelligent about what gets damaged.
*
* @param style The Style that has changed.
*/
protected void styleChanged(Style style) {
// Only propagate change updated if have content
if (getLength() != 0) {
// lazily create a ChangeUpdateRunnable
if (updateRunnable == null) {
updateRunnable = new ChangeUpdateRunnable();
}
// We may get a whole batch of these at once, so only
// queue the runnable if it is not already pending
synchronized(updateRunnable) {
if (!updateRunnable.isPending) {
SwingUtilities.invokeLater(updateRunnable);
updateRunnable.isPending = true;
}
}
}
}
Adds a document listener for notification of any changes.
Params: - listener – the listener
See Also:
/**
* Adds a document listener for notification of any changes.
*
* @param listener the listener
* @see Document#addDocumentListener
*/
public void addDocumentListener(DocumentListener listener) {
synchronized(listeningStyles) {
int oldDLCount = listenerList.getListenerCount
(DocumentListener.class);
super.addDocumentListener(listener);
if (oldDLCount == 0) {
if (styleContextChangeListener == null) {
styleContextChangeListener =
createStyleContextChangeListener();
}
if (styleContextChangeListener != null) {
StyleContext styles = (StyleContext)getAttributeContext();
List<ChangeListener> staleListeners =
AbstractChangeHandler.getStaleListeners(styleContextChangeListener);
for (ChangeListener l: staleListeners) {
styles.removeChangeListener(l);
}
styles.addChangeListener(styleContextChangeListener);
}
updateStylesListeningTo();
}
}
}
Removes a document listener.
Params: - listener – the listener
See Also:
/**
* Removes a document listener.
*
* @param listener the listener
* @see Document#removeDocumentListener
*/
public void removeDocumentListener(DocumentListener listener) {
synchronized(listeningStyles) {
super.removeDocumentListener(listener);
if (listenerList.getListenerCount(DocumentListener.class) == 0) {
for (int counter = listeningStyles.size() - 1; counter >= 0;
counter--) {
listeningStyles.elementAt(counter).
removeChangeListener(styleChangeListener);
}
listeningStyles.removeAllElements();
if (styleContextChangeListener != null) {
StyleContext styles = (StyleContext)getAttributeContext();
styles.removeChangeListener(styleContextChangeListener);
}
}
}
}
Returns a new instance of StyleChangeHandler.
/**
* Returns a new instance of StyleChangeHandler.
*/
ChangeListener createStyleChangeListener() {
return new StyleChangeHandler(this);
}
Returns a new instance of StyleContextChangeHandler.
/**
* Returns a new instance of StyleContextChangeHandler.
*/
ChangeListener createStyleContextChangeListener() {
return new StyleContextChangeHandler(this);
}
Adds a ChangeListener to new styles, and removes ChangeListener from
old styles.
/**
* Adds a ChangeListener to new styles, and removes ChangeListener from
* old styles.
*/
void updateStylesListeningTo() {
synchronized(listeningStyles) {
StyleContext styles = (StyleContext)getAttributeContext();
if (styleChangeListener == null) {
styleChangeListener = createStyleChangeListener();
}
if (styleChangeListener != null && styles != null) {
Enumeration<?> styleNames = styles.getStyleNames();
@SuppressWarnings("unchecked")
Vector<Style> v = (Vector<Style>)listeningStyles.clone();
listeningStyles.removeAllElements();
List<ChangeListener> staleListeners =
AbstractChangeHandler.getStaleListeners(styleChangeListener);
while (styleNames.hasMoreElements()) {
String name = (String)styleNames.nextElement();
Style aStyle = styles.getStyle(name);
int index = v.indexOf(aStyle);
listeningStyles.addElement(aStyle);
if (index == -1) {
for (ChangeListener l: staleListeners) {
aStyle.removeChangeListener(l);
}
aStyle.addChangeListener(styleChangeListener);
}
else {
v.removeElementAt(index);
}
}
for (int counter = v.size() - 1; counter >= 0; counter--) {
Style aStyle = v.elementAt(counter);
aStyle.removeChangeListener(styleChangeListener);
}
if (listeningStyles.size() == 0) {
styleChangeListener = null;
}
}
}
}
private void readObject(ObjectInputStream s)
throws ClassNotFoundException, IOException {
listeningStyles = new Vector<>();
ObjectInputStream.GetField f = s.readFields();
buffer = (ElementBuffer) f.get("buffer", null);
// Reinstall style listeners.
if (styleContextChangeListener == null &&
listenerList.getListenerCount(DocumentListener.class) > 0) {
styleContextChangeListener = createStyleContextChangeListener();
if (styleContextChangeListener != null) {
StyleContext styles = (StyleContext)getAttributeContext();
styles.addChangeListener(styleContextChangeListener);
}
updateStylesListeningTo();
}
}
// --- member variables -----------------------------------------------------------
The default size of the initial content buffer.
/**
* The default size of the initial content buffer.
*/
public static final int BUFFER_SIZE_DEFAULT = 4096;
The element buffer.
/**
* The element buffer.
*/
protected ElementBuffer buffer;
Styles listening to. /** Styles listening to. */
private transient Vector<Style> listeningStyles;
Listens to Styles. /** Listens to Styles. */
private transient ChangeListener styleChangeListener;
Listens to Styles. /** Listens to Styles. */
private transient ChangeListener styleContextChangeListener;
Run to create a change event for the document /** Run to create a change event for the document */
private transient ChangeUpdateRunnable updateRunnable;
Default root element for a document... maps out the
paragraphs/lines contained.
Warning:
Serialized objects of this class will not be compatible with
future Swing releases. The current serialization support is
appropriate for short term storage or RMI between applications running
the same version of Swing. As of 1.4, support for long term storage
of all JavaBeans™
has been added to the java.beans
package. Please see XMLEncoder
.
/**
* Default root element for a document... maps out the
* paragraphs/lines contained.
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans™
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*/
@SuppressWarnings("serial") // Same-version serialization only
protected class SectionElement extends BranchElement {
Creates a new SectionElement.
/**
* Creates a new SectionElement.
*/
public SectionElement() {
super(null, null);
}
Gets the name of the element.
Returns: the name
/**
* Gets the name of the element.
*
* @return the name
*/
public String getName() {
return SectionElementName;
}
}
Specification for building elements.
Warning:
Serialized objects of this class will not be compatible with
future Swing releases. The current serialization support is
appropriate for short term storage or RMI between applications running
the same version of Swing. As of 1.4, support for long term storage
of all JavaBeans™
has been added to the java.beans
package. Please see XMLEncoder
.
/**
* Specification for building elements.
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans™
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*/
@SuppressWarnings("serial") // Same-version serialization only
public static class ElementSpec {
A possible value for getType. This specifies
that this record type is a start tag and
represents markup that specifies the start
of an element.
/**
* A possible value for getType. This specifies
* that this record type is a start tag and
* represents markup that specifies the start
* of an element.
*/
public static final short StartTagType = 1;
A possible value for getType. This specifies
that this record type is a end tag and
represents markup that specifies the end
of an element.
/**
* A possible value for getType. This specifies
* that this record type is a end tag and
* represents markup that specifies the end
* of an element.
*/
public static final short EndTagType = 2;
A possible value for getType. This specifies
that this record type represents content.
/**
* A possible value for getType. This specifies
* that this record type represents content.
*/
public static final short ContentType = 3;
A possible value for getDirection. This specifies
that the data associated with this record should
be joined to what precedes it.
/**
* A possible value for getDirection. This specifies
* that the data associated with this record should
* be joined to what precedes it.
*/
public static final short JoinPreviousDirection = 4;
A possible value for getDirection. This specifies
that the data associated with this record should
be joined to what follows it.
/**
* A possible value for getDirection. This specifies
* that the data associated with this record should
* be joined to what follows it.
*/
public static final short JoinNextDirection = 5;
A possible value for getDirection. This specifies
that the data associated with this record should
be used to originate a new element. This would be
the normal value.
/**
* A possible value for getDirection. This specifies
* that the data associated with this record should
* be used to originate a new element. This would be
* the normal value.
*/
public static final short OriginateDirection = 6;
A possible value for getDirection. This specifies
that the data associated with this record should
be joined to the fractured element.
/**
* A possible value for getDirection. This specifies
* that the data associated with this record should
* be joined to the fractured element.
*/
public static final short JoinFractureDirection = 7;
Constructor useful for markup when the markup will not
be stored in the document.
Params: - a – the attributes for the element
- type – the type of the element (StartTagType, EndTagType,
ContentType)
/**
* Constructor useful for markup when the markup will not
* be stored in the document.
*
* @param a the attributes for the element
* @param type the type of the element (StartTagType, EndTagType,
* ContentType)
*/
public ElementSpec(AttributeSet a, short type) {
this(a, type, null, 0, 0);
}
Constructor for parsing inside the document when
the data has already been added, but len information
is needed.
Params: - a – the attributes for the element
- type – the type of the element (StartTagType, EndTagType,
ContentType)
- len – the length >= 0
/**
* Constructor for parsing inside the document when
* the data has already been added, but len information
* is needed.
*
* @param a the attributes for the element
* @param type the type of the element (StartTagType, EndTagType,
* ContentType)
* @param len the length >= 0
*/
public ElementSpec(AttributeSet a, short type, int len) {
this(a, type, null, 0, len);
}
Constructor for creating a spec externally for batch
input of content and markup into the document.
Params: - a – the attributes for the element
- type – the type of the element (StartTagType, EndTagType,
ContentType)
- txt – the text for the element
- offs – the offset into the text >= 0
- len – the length of the text >= 0
/**
* Constructor for creating a spec externally for batch
* input of content and markup into the document.
*
* @param a the attributes for the element
* @param type the type of the element (StartTagType, EndTagType,
* ContentType)
* @param txt the text for the element
* @param offs the offset into the text >= 0
* @param len the length of the text >= 0
*/
public ElementSpec(AttributeSet a, short type, char[] txt,
int offs, int len) {
attr = a;
this.type = type;
this.data = txt == null ? null : Arrays.copyOf(txt, txt.length);
this.offs = offs;
this.len = len;
this.direction = OriginateDirection;
}
Sets the element type.
Params: - type – the type of the element (StartTagType, EndTagType,
ContentType)
/**
* Sets the element type.
*
* @param type the type of the element (StartTagType, EndTagType,
* ContentType)
*/
public void setType(short type) {
this.type = type;
}
Gets the element type.
Returns: the type of the element (StartTagType, EndTagType,
ContentType)
/**
* Gets the element type.
*
* @return the type of the element (StartTagType, EndTagType,
* ContentType)
*/
public short getType() {
return type;
}
Sets the direction.
Params: - direction – the direction (JoinPreviousDirection,
JoinNextDirection)
/**
* Sets the direction.
*
* @param direction the direction (JoinPreviousDirection,
* JoinNextDirection)
*/
public void setDirection(short direction) {
this.direction = direction;
}
Gets the direction.
Returns: the direction (JoinPreviousDirection, JoinNextDirection)
/**
* Gets the direction.
*
* @return the direction (JoinPreviousDirection, JoinNextDirection)
*/
public short getDirection() {
return direction;
}
Gets the element attributes.
Returns: the attribute set
/**
* Gets the element attributes.
*
* @return the attribute set
*/
public AttributeSet getAttributes() {
return attr;
}
Gets the array of characters.
Returns: the array
/**
* Gets the array of characters.
*
* @return the array
*/
public char[] getArray() {
return data == null ? null : Arrays.copyOf(data, data.length);
}
Gets the starting offset.
Returns: the offset >= 0
/**
* Gets the starting offset.
*
* @return the offset >= 0
*/
public int getOffset() {
return offs;
}
Gets the length.
Returns: the length >= 0
/**
* Gets the length.
*
* @return the length >= 0
*/
public int getLength() {
return len;
}
Converts the element to a string.
Returns: the string
/**
* Converts the element to a string.
*
* @return the string
*/
public String toString() {
String tlbl = "??";
String plbl = "??";
switch(type) {
case StartTagType:
tlbl = "StartTag";
break;
case ContentType:
tlbl = "Content";
break;
case EndTagType:
tlbl = "EndTag";
break;
}
switch(direction) {
case JoinPreviousDirection:
plbl = "JoinPrevious";
break;
case JoinNextDirection:
plbl = "JoinNext";
break;
case OriginateDirection:
plbl = "Originate";
break;
case JoinFractureDirection:
plbl = "Fracture";
break;
}
return tlbl + ":" + plbl + ":" + getLength();
}
private AttributeSet attr;
private int len;
private short type;
private short direction;
private int offs;
private char[] data;
}
Class to manage changes to the element
hierarchy.
Warning:
Serialized objects of this class will not be compatible with
future Swing releases. The current serialization support is
appropriate for short term storage or RMI between applications running
the same version of Swing. As of 1.4, support for long term storage
of all JavaBeans™
has been added to the java.beans
package. Please see XMLEncoder
.
/**
* Class to manage changes to the element
* hierarchy.
* <p>
* <strong>Warning:</strong>
* Serialized objects of this class will not be compatible with
* future Swing releases. The current serialization support is
* appropriate for short term storage or RMI between applications running
* the same version of Swing. As of 1.4, support for long term storage
* of all JavaBeans™
* has been added to the <code>java.beans</code> package.
* Please see {@link java.beans.XMLEncoder}.
*/
@SuppressWarnings("serial") // Same-version serialization only
public class ElementBuffer implements Serializable {
Creates a new ElementBuffer.
Params: - root – the root element
Since: 1.4
/**
* Creates a new ElementBuffer.
*
* @param root the root element
* @since 1.4
*/
public ElementBuffer(Element root) {
this.root = root;
changes = new Vector<ElemChanges>();
path = new Stack<ElemChanges>();
}
Gets the root element.
Returns: the root element
/**
* Gets the root element.
*
* @return the root element
*/
public Element getRootElement() {
return root;
}
Inserts new content.
Params: - offset – the starting offset >= 0
- length – the length >= 0
- data – the data to insert
- de – the event capturing this edit
/**
* Inserts new content.
*
* @param offset the starting offset >= 0
* @param length the length >= 0
* @param data the data to insert
* @param de the event capturing this edit
*/
public void insert(int offset, int length, ElementSpec[] data,
DefaultDocumentEvent de) {
if (length == 0) {
// Nothing was inserted, no structure change.
return;
}
insertOp = true;
beginEdits(offset, length);
insertUpdate(data);
endEdits(de);
insertOp = false;
}
void create(int length, ElementSpec[] data, DefaultDocumentEvent de) {
insertOp = true;
beginEdits(offset, length);
// PENDING(prinz) this needs to be fixed to create a new
// root element as well, but requires changes to the
// DocumentEvent to inform the views that there is a new
// root element.
// Recreate the ending fake element to have the correct offsets.
Element elem = root;
int index = elem.getElementIndex(0);
while (! elem.isLeaf()) {
Element child = elem.getElement(index);
push(elem, index);
elem = child;
index = elem.getElementIndex(0);
}
ElemChanges ec = path.peek();
Element child = ec.parent.getElement(ec.index);
ec.added.addElement(createLeafElement(ec.parent,
child.getAttributes(), getLength(),
child.getEndOffset()));
ec.removed.addElement(child);
while (path.size() > 1) {
pop();
}
int n = data.length;
// Reset the root elements attributes.
AttributeSet newAttrs = null;
if (n > 0 && data[0].getType() == ElementSpec.StartTagType) {
newAttrs = data[0].getAttributes();
}
if (newAttrs == null) {
newAttrs = SimpleAttributeSet.EMPTY;
}
MutableAttributeSet attr = (MutableAttributeSet)root.
getAttributes();
de.addEdit(new AttributeUndoableEdit(root, newAttrs, true));
attr.removeAttributes(attr);
attr.addAttributes(newAttrs);
// fold in the specified subtree
for (int i = 1; i < n; i++) {
insertElement(data[i]);
}
// pop the remaining path
while (path.size() != 0) {
pop();
}
endEdits(de);
insertOp = false;
}
Removes content.
Params: - offset – the starting offset >= 0
- length – the length >= 0
- de – the event capturing this edit
/**
* Removes content.
*
* @param offset the starting offset >= 0
* @param length the length >= 0
* @param de the event capturing this edit
*/
public void remove(int offset, int length, DefaultDocumentEvent de) {
beginEdits(offset, length);
removeUpdate();
endEdits(de);
}
Changes content.
Params: - offset – the starting offset >= 0
- length – the length >= 0
- de – the event capturing this edit
/**
* Changes content.
*
* @param offset the starting offset >= 0
* @param length the length >= 0
* @param de the event capturing this edit
*/
public void change(int offset, int length, DefaultDocumentEvent de) {
beginEdits(offset, length);
changeUpdate();
endEdits(de);
}
Inserts an update into the document.
Params: - data – the elements to insert
/**
* Inserts an update into the document.
*
* @param data the elements to insert
*/
protected void insertUpdate(ElementSpec[] data) {
// push the path
Element elem = root;
int index = elem.getElementIndex(offset);
while (! elem.isLeaf()) {
Element child = elem.getElement(index);
push(elem, (child.isLeaf() ? index : index+1));
elem = child;
index = elem.getElementIndex(offset);
}
// Build a copy of the original path.
insertPath = new ElemChanges[path.size()];
path.copyInto(insertPath);
// Haven't created the fracture yet.
createdFracture = false;
// Insert the first content.
int i;
recreateLeafs = false;
if(data[0].getType() == ElementSpec.ContentType) {
insertFirstContent(data);
pos += data[0].getLength();
i = 1;
}
else {
fractureDeepestLeaf(data);
i = 0;
}
// fold in the specified subtree
int n = data.length;
for (; i < n; i++) {
insertElement(data[i]);
}
// Fracture, if we haven't yet.
if(!createdFracture)
fracture(-1);
// pop the remaining path
while (path.size() != 0) {
pop();
}
// Offset the last index if necessary.
if(offsetLastIndex && offsetLastIndexOnReplace) {
insertPath[insertPath.length - 1].index++;
}
// Make sure an edit is going to be created for each of the
// original path items that have a change.
for(int counter = insertPath.length - 1; counter >= 0;
counter--) {
ElemChanges change = insertPath[counter];
if(change.parent == fracturedParent)
change.added.addElement(fracturedChild);
if((change.added.size() > 0 ||
change.removed.size() > 0) && !changes.contains(change)) {
// PENDING(sky): Do I need to worry about order here?
changes.addElement(change);
}
}
// An insert at 0 with an initial end implies some elements
// will have no children (the bottomost leaf would have length 0)
// this will find what element need to be removed and remove it.
if (offset == 0 && fracturedParent != null &&
data[0].getType() == ElementSpec.EndTagType) {
int counter = 0;
while (counter < data.length &&
data[counter].getType() == ElementSpec.EndTagType) {
counter++;
}
ElemChanges change = insertPath[insertPath.length -
counter - 1];
change.removed.insertElementAt(change.parent.getElement
(--change.index), 0);
}
}
Updates the element structure in response to a removal from the
associated sequence in the document. Any elements consumed by the
span of the removal are removed.
/**
* Updates the element structure in response to a removal from the
* associated sequence in the document. Any elements consumed by the
* span of the removal are removed.
*/
protected void removeUpdate() {
removeElements(root, offset, offset + length);
}
Updates the element structure in response to a change in the
document.
/**
* Updates the element structure in response to a change in the
* document.
*/
protected void changeUpdate() {
boolean didEnd = split(offset, length);
if (! didEnd) {
// need to do the other end
while (path.size() != 0) {
pop();
}
split(offset + length, 0);
}
while (path.size() != 0) {
pop();
}
}
boolean split(int offs, int len) {
boolean splitEnd = false;
// push the path
Element e = root;
int index = e.getElementIndex(offs);
while (! e.isLeaf()) {
push(e, index);
e = e.getElement(index);
index = e.getElementIndex(offs);
}
ElemChanges ec = path.peek();
Element child = ec.parent.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.parent.getEndOffset()) && (len != 0)) {
// it's a range split in the same parent
index1 = ec.parent.getElementIndex(offs+len);
if (index1 == index0) {
// it's a three-way split
ec.removed.addElement(child);
e = createLeafElement(ec.parent, child.getAttributes(),
child.getStartOffset(), offs);
ec.added.addElement(e);
e = createLeafElement(ec.parent, child.getAttributes(),
offs, offs + len);
ec.added.addElement(e);
e = createLeafElement(ec.parent, child.getAttributes(),
offs + len, child.getEndOffset());
ec.added.addElement(e);
return true;
} else {
child = ec.parent.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.parent.getElement(index0);
ec.removed.addElement(child);
e = createLeafElement(ec.parent, child.getAttributes(),
child.getStartOffset(), pos);
ec.added.addElement(e);
e = createLeafElement(ec.parent, child.getAttributes(),
pos, child.getEndOffset());
ec.added.addElement(e);
// pick up things in the middle
for (int i = index0 + 1; i < index1; i++) {
child = ec.parent.getElement(i);
ec.removed.addElement(child);
ec.added.addElement(child);
}
if (index1 != index0) {
child = ec.parent.getElement(index1);
pos = offs + len;
ec.removed.addElement(child);
e = createLeafElement(ec.parent, child.getAttributes(),
child.getStartOffset(), pos);
ec.added.addElement(e);
e = createLeafElement(ec.parent, child.getAttributes(),
pos, child.getEndOffset());
ec.added.addElement(e);
}
}
return splitEnd;
}
Creates the UndoableEdit record for the edits made
in the buffer.
/**
* Creates the UndoableEdit record for the edits made
* in the buffer.
*/
void endEdits(DefaultDocumentEvent de) {
int n = changes.size();
for (int i = 0; i < n; i++) {
ElemChanges ec = changes.elementAt(i);
Element[] removed = new Element[ec.removed.size()];
ec.removed.copyInto(removed);
Element[] added = new Element[ec.added.size()];
ec.added.copyInto(added);
int index = ec.index;
((BranchElement) ec.parent).replace(index, removed.length, added);
ElementEdit ee = new ElementEdit(ec.parent, index, removed, added);
de.addEdit(ee);
}
changes.removeAllElements();
path.removeAllElements();
/*
for (int i = 0; i < n; i++) {
ElemChanges ec = (ElemChanges) changes.elementAt(i);
System.err.print("edited: " + ec.parent + " at: " + ec.index +
" removed " + ec.removed.size());
if (ec.removed.size() > 0) {
int r0 = ((Element) ec.removed.firstElement()).getStartOffset();
int r1 = ((Element) ec.removed.lastElement()).getEndOffset();
System.err.print("[" + r0 + "," + r1 + "]");
}
System.err.print(" added " + ec.added.size());
if (ec.added.size() > 0) {
int p0 = ((Element) ec.added.firstElement()).getStartOffset();
int p1 = ((Element) ec.added.lastElement()).getEndOffset();
System.err.print("[" + p0 + "," + p1 + "]");
}
System.err.println("");
}
*/
}
Initialize the buffer
/**
* Initialize the buffer
*/
void beginEdits(int offset, int length) {
this.offset = offset;
this.length = length;
this.endOffset = offset + length;
pos = offset;
if (changes == null) {
changes = new Vector<ElemChanges>();
} else {
changes.removeAllElements();
}
if (path == null) {
path = new Stack<ElemChanges>();
} else {
path.removeAllElements();
}
fracturedParent = null;
fracturedChild = null;
offsetLastIndex = offsetLastIndexOnReplace = false;
}
Pushes a new element onto the stack that represents
the current path.
Params: - isFracture – true if pushing on an element that was created
as the result of a fracture.
/**
* Pushes a new element onto the stack that represents
* the current path.
* @param isFracture true if pushing on an element that was created
* as the result of a fracture.
*/
void push(Element e, int index, boolean isFracture) {
ElemChanges ec = new ElemChanges(e, index, isFracture);
path.push(ec);
}
void push(Element e, int index) {
push(e, index, false);
}
void pop() {
ElemChanges ec = path.peek();
path.pop();
if ((ec.added.size() > 0) || (ec.removed.size() > 0)) {
changes.addElement(ec);
} else if (! path.isEmpty()) {
Element e = ec.parent;
if(e.getElementCount() == 0) {
// if we pushed a branch element that didn't get
// used, make sure its not marked as having been added.
ec = path.peek();
ec.added.removeElement(e);
}
}
}
move the current offset forward by n.
/**
* move the current offset forward by n.
*/
void advance(int n) {
pos += n;
}
void insertElement(ElementSpec es) {
ElemChanges ec = path.peek();
switch(es.getType()) {
case ElementSpec.StartTagType:
switch(es.getDirection()) {
case ElementSpec.JoinNextDirection:
// Don't create a new element, use the existing one
// at the specified location.
Element parent = ec.parent.getElement(ec.index);
if(parent.isLeaf()) {
// This happens if inserting into a leaf, followed
// by a join next where next sibling is not a leaf.
if((ec.index + 1) < ec.parent.getElementCount())
parent = ec.parent.getElement(ec.index + 1);
else
throw new StateInvariantError("Join next to leaf");
}
// Not really a fracture, but need to treat it like
// one so that content join next will work correctly.
// We can do this because there will never be a join
// next followed by a join fracture.
push(parent, 0, true);
break;
case ElementSpec.JoinFractureDirection:
if(!createdFracture) {
// Should always be something on the stack!
fracture(path.size() - 1);
}
// If parent isn't a fracture, fracture will be
// fracturedChild.
if(!ec.isFracture) {
push(fracturedChild, 0, true);
}
else
// Parent is a fracture, use 1st element.
push(ec.parent.getElement(0), 0, true);
break;
default:
Element belem = createBranchElement(ec.parent,
es.getAttributes());
ec.added.addElement(belem);
push(belem, 0);
break;
}
break;
case ElementSpec.EndTagType:
pop();
break;
case ElementSpec.ContentType:
int len = es.getLength();
if (es.getDirection() != ElementSpec.JoinNextDirection) {
Element leaf = createLeafElement(ec.parent, es.getAttributes(),
pos, pos + len);
ec.added.addElement(leaf);
}
else {
// JoinNext on tail is only applicable if last element
// and attributes come from that of first element.
// With a little extra testing it would be possible
// to NOT due this again, as more than likely fracture()
// created this element.
if(!ec.isFracture) {
Element first = null;
if(insertPath != null) {
for(int counter = insertPath.length - 1;
counter >= 0; counter--) {
if(insertPath[counter] == ec) {
if(counter != (insertPath.length - 1))
first = ec.parent.getElement(ec.index);
break;
}
}
}
if(first == null)
first = ec.parent.getElement(ec.index + 1);
Element leaf = createLeafElement(ec.parent, first.
getAttributes(), pos, first.getEndOffset());
ec.added.addElement(leaf);
ec.removed.addElement(first);
}
else {
// Parent was fractured element.
Element first = ec.parent.getElement(0);
Element leaf = createLeafElement(ec.parent, first.
getAttributes(), pos, first.getEndOffset());
ec.added.addElement(leaf);
ec.removed.addElement(first);
}
}
pos += len;
break;
}
}
Remove the elements from elem
in range
rmOffs0
, rmOffs1
. This uses
canJoin
and join
to handle joining
the endpoints of the insertion.
Returns: true if elem will no longer have any elements.
/**
* Remove the elements from <code>elem</code> in range
* <code>rmOffs0</code>, <code>rmOffs1</code>. This uses
* <code>canJoin</code> and <code>join</code> to handle joining
* the endpoints of the insertion.
*
* @return true if elem will no longer have any elements.
*/
boolean removeElements(Element elem, int rmOffs0, int rmOffs1) {
if (! elem.isLeaf()) {
// update path for changes
int index0 = elem.getElementIndex(rmOffs0);
int index1 = elem.getElementIndex(rmOffs1);
push(elem, index0);
ElemChanges ec = path.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.addElement(child0);
}
else if(removeElements(child0, rmOffs0, rmOffs1)) {
ec.removed.addElement(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.addElement(elem.getElement(i));
}
Element e = join(elem, child0, child1, rmOffs0, rmOffs1);
ec.added.addElement(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.addElement(elem.getElement(i));
}
if (child0 != null) {
if(removeElements(child0, rmOffs0, rmOffs1)) {
ec.removed.insertElementAt(child0, 0);
ec.index = index0;
}
}
if (child1 != null) {
if(removeElements(child1, rmOffs0, rmOffs1)) {
ec.removed.addElement(child1);
}
}
}
}
// publish changes
pop();
// Return true if we no longer have any children.
if(elem.getElementCount() == (ec.removed.size() -
ec.added.size())) {
return true;
}
}
return false;
}
Can the two given elements be coelesced together
into one element?
/**
* Can the two given elements be coelesced together
* into one element?
*/
boolean canJoin(Element e0, Element e1) {
if ((e0 == null) || (e1 == null)) {
return false;
}
// Don't join a leaf to a branch.
boolean leaf0 = e0.isLeaf();
boolean leaf1 = e1.isLeaf();
if(leaf0 != leaf1) {
return false;
}
if (leaf0) {
// Only join leaves if the attributes match, otherwise
// style information will be lost.
return e0.getAttributes().isEqual(e1.getAttributes());
}
// 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) {
return name0.equals(name1);
}
if (name1 != null) {
return name1.equals(name0);
}
// Both names null, treat as equal.
return true;
}
Joins the two elements carving out a hole for the
given removed range.
/**
* Joins the two elements carving out a hole for the
* given removed range.
*/
Element join(Element p, Element left, Element right, int rmOffs0, int rmOffs1) {
if (left.isLeaf() && right.isLeaf()) {
return 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.
Element to = 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;
}
Vector<Element> children = new Vector<Element>();
// transfer the left
for (int i = 0; i < ljIndex; i++) {
children.addElement(clone(to, left.getElement(i)));
}
// transfer the join/middle
if (canJoin(lj, rj)) {
Element e = join(to, lj, rj, rmOffs0, rmOffs1);
children.addElement(e);
} else {
if (lj != null) {
children.addElement(cloneAsNecessary(to, lj, rmOffs0, rmOffs1));
}
if (rj != null) {
children.addElement(cloneAsNecessary(to, rj, rmOffs0, rmOffs1));
}
}
// transfer the right
int n = right.getElementCount();
for (int i = (rj == null) ? rjIndex : rjIndex + 1; i < n; i++) {
children.addElement(clone(to, right.getElement(i)));
}
// install the children
Element[] c = new Element[children.size()];
children.copyInto(c);
((BranchElement)to).replace(0, 0, c);
return to;
} else {
throw new StateInvariantError(
"No support to join leaf element with non-leaf element");
}
}
Creates a copy of this element, with a different
parent.
Params: - parent – the parent element
- clonee – the element to be cloned
Returns: the copy
/**
* Creates a copy of this element, with a different
* parent.
*
* @param parent the parent element
* @param clonee the element to be cloned
* @return the copy
*/
public Element clone(Element parent, Element clonee) {
if (clonee.isLeaf()) {
return createLeafElement(parent, clonee.getAttributes(),
clonee.getStartOffset(),
clonee.getEndOffset());
}
Element e = createBranchElement(parent, clonee.getAttributes());
int n = clonee.getElementCount();
Element[] children = new Element[n];
for (int i = 0; i < n; i++) {
children[i] = clone(e, clonee.getElement(i));
}
((BranchElement)e).replace(0, 0, children);
return e;
}
Creates a copy of this element, with a different
parent. Children of this element included in the
removal range will be discarded.
/**
* Creates a copy of this element, with a different
* parent. Children of this element included in the
* removal range will be discarded.
*/
Element cloneAsNecessary(Element parent, Element clonee, int rmOffs0, int rmOffs1) {
if (clonee.isLeaf()) {
return createLeafElement(parent, clonee.getAttributes(),
clonee.getStartOffset(),
clonee.getEndOffset());
}
Element e = createBranchElement(parent, clonee.getAttributes());
int n = clonee.getElementCount();
ArrayList<Element> childrenList = new ArrayList<Element>(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 = childrenList.toArray(children);
((BranchElement)e).replace(0, 0, children);
return e;
}
Determines if a fracture needs to be performed. A fracture
can be thought of as moving the right part of a tree to a
new location, where the right part is determined by what has
been inserted. depth
is used to indicate a
JoinToFracture is needed to an element at a depth
of depth
. Where the root is 0, 1 is the children
of the root...
This will invoke fractureFrom
if it is determined
a fracture needs to happen.
/**
* Determines if a fracture needs to be performed. A fracture
* can be thought of as moving the right part of a tree to a
* new location, where the right part is determined by what has
* been inserted. <code>depth</code> is used to indicate a
* JoinToFracture is needed to an element at a depth
* of <code>depth</code>. Where the root is 0, 1 is the children
* of the root...
* <p>This will invoke <code>fractureFrom</code> if it is determined
* a fracture needs to happen.
*/
void fracture(int depth) {
int cLength = insertPath.length;
int lastIndex = -1;
boolean needRecreate = recreateLeafs;
ElemChanges lastChange = insertPath[cLength - 1];
// Use childAltered to determine when a child has been altered,
// that is the point of insertion is less than the element count.
boolean childAltered = ((lastChange.index + 1) <
lastChange.parent.getElementCount());
int deepestAlteredIndex = (needRecreate) ? cLength : -1;
int lastAlteredIndex = cLength - 1;
createdFracture = true;
// Determine where to start recreating from.
// Start at - 2, as first one is indicated by recreateLeafs and
// childAltered.
for(int counter = cLength - 2; counter >= 0; counter--) {
ElemChanges change = insertPath[counter];
if(change.added.size() > 0 || counter == depth) {
lastIndex = counter;
if(!needRecreate && childAltered) {
needRecreate = true;
if(deepestAlteredIndex == -1)
deepestAlteredIndex = lastAlteredIndex + 1;
}
}
if(!childAltered && change.index <
change.parent.getElementCount()) {
childAltered = true;
lastAlteredIndex = counter;
}
}
if(needRecreate) {
// Recreate all children to right of parent starting
// at lastIndex.
if(lastIndex == -1)
lastIndex = cLength - 1;
fractureFrom(insertPath, lastIndex, deepestAlteredIndex);
}
}
Recreates the elements to the right of the insertion point.
This starts at startIndex
in changed
,
and calls duplicate to duplicate existing elements.
This will also duplicate the elements along the insertion
point, until a depth of endFractureIndex
is
reached, at which point only the elements to the right of
the insertion point are duplicated.
/**
* Recreates the elements to the right of the insertion point.
* This starts at <code>startIndex</code> in <code>changed</code>,
* and calls duplicate to duplicate existing elements.
* This will also duplicate the elements along the insertion
* point, until a depth of <code>endFractureIndex</code> is
* reached, at which point only the elements to the right of
* the insertion point are duplicated.
*/
void fractureFrom(ElemChanges[] changed, int startIndex,
int endFractureIndex) {
// Recreate the element representing the inserted index.
ElemChanges change = changed[startIndex];
Element child;
Element newChild;
int changeLength = changed.length;
if((startIndex + 1) == changeLength)
child = change.parent.getElement(change.index);
else
child = change.parent.getElement(change.index - 1);
if(child.isLeaf()) {
newChild = createLeafElement(change.parent,
child.getAttributes(), Math.max(endOffset,
child.getStartOffset()), child.getEndOffset());
}
else {
newChild = createBranchElement(change.parent,
child.getAttributes());
}
fracturedParent = change.parent;
fracturedChild = newChild;
// Recreate all the elements to the right of the
// insertion point.
Element parent = newChild;
while(++startIndex < endFractureIndex) {
boolean isEnd = ((startIndex + 1) == endFractureIndex);
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.
change = changed[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 = change.parent.getElement(change.index);
}
else {
child = change.parent.getElement(change.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 kidsToMove = change.parent.getElementCount() -
change.index;
Element[] kids;
int moveStartIndex;
int kidStartIndex = 1;
if(newChild == null) {
// Last part of fracture.
if(isEndLeaf) {
kidsToMove--;
moveStartIndex = change.index + 1;
}
else {
moveStartIndex = change.index;
}
kidStartIndex = 0;
kids = new Element[kidsToMove];
}
else {
if(!isEnd) {
// Branch.
kidsToMove++;
moveStartIndex = change.index;
}
else {
// Last leaf, need to recreate part of it.
moveStartIndex = change.index + 1;
}
kids = new Element[kidsToMove];
kids[0] = newChild;
}
for(int counter = kidStartIndex; counter < kidsToMove;
counter++) {
Element toMove =change.parent.getElement(moveStartIndex++);
kids[counter] = recreateFracturedElement(parent, toMove);
change.removed.addElement(toMove);
}
((BranchElement)parent).replace(0, 0, kids);
parent = newChild;
}
}
Recreates toDuplicate
. This is called when an
element needs to be created as the result of an insertion. This
will recurse and create all the children. This is similar to
clone
, but deteremines the offsets differently.
/**
* Recreates <code>toDuplicate</code>. This is called when an
* element needs to be created as the result of an insertion. This
* will recurse and create all the children. This is similar to
* <code>clone</code>, but deteremines the offsets differently.
*/
Element recreateFracturedElement(Element parent, Element toDuplicate) {
if(toDuplicate.isLeaf()) {
return createLeafElement(parent, toDuplicate.getAttributes(),
Math.max(toDuplicate.getStartOffset(),
endOffset),
toDuplicate.getEndOffset());
}
// Not a leaf
Element newParent = createBranchElement(parent, toDuplicate.
getAttributes());
int childCount = toDuplicate.getElementCount();
Element[] newKids = new Element[childCount];
for(int counter = 0; counter < childCount; counter++) {
newKids[counter] = recreateFracturedElement(newParent,
toDuplicate.getElement(counter));
}
((BranchElement)newParent).replace(0, 0, newKids);
return newParent;
}
Splits the bottommost leaf in path
.
This is called from insert when the first element is NOT content.
/**
* Splits the bottommost leaf in <code>path</code>.
* This is called from insert when the first element is NOT content.
*/
void fractureDeepestLeaf(ElementSpec[] specs) {
// Split the bottommost leaf. It will be recreated elsewhere.
ElemChanges ec = path.peek();
Element child = ec.parent.getElement(ec.index);
// Inserts at offset 0 do not need to recreate child (it would
// have a length of 0!).
if (offset != 0) {
Element newChild = createLeafElement(ec.parent,
child.getAttributes(),
child.getStartOffset(),
offset);
ec.added.addElement(newChild);
}
ec.removed.addElement(child);
if(child.getEndOffset() != endOffset)
recreateLeafs = true;
else
offsetLastIndex = true;
}
Inserts the first content. This needs to be separate to handle
joining.
/**
* Inserts the first content. This needs to be separate to handle
* joining.
*/
void insertFirstContent(ElementSpec[] specs) {
ElementSpec firstSpec = specs[0];
ElemChanges ec = path.peek();
Element child = ec.parent.getElement(ec.index);
int firstEndOffset = offset + firstSpec.getLength();
boolean isOnlyContent = (specs.length == 1);
switch(firstSpec.getDirection()) {
case ElementSpec.JoinPreviousDirection:
if(child.getEndOffset() != firstEndOffset &&
!isOnlyContent) {
// Create the left split part containing new content.
Element newE = createLeafElement(ec.parent,
child.getAttributes(), child.getStartOffset(),
firstEndOffset);
ec.added.addElement(newE);
ec.removed.addElement(child);
// Remainder will be created later.
if(child.getEndOffset() != endOffset)
recreateLeafs = true;
else
offsetLastIndex = true;
}
else {
offsetLastIndex = true;
offsetLastIndexOnReplace = true;
}
// else Inserted at end, and is total length.
// Update index incase something added/removed.
break;
case ElementSpec.JoinNextDirection:
if(offset != 0) {
// Recreate the first element, its offset will have
// changed.
Element newE = createLeafElement(ec.parent,
child.getAttributes(), child.getStartOffset(),
offset);
ec.added.addElement(newE);
// Recreate the second, merge part. We do no checking
// to see if JoinNextDirection is valid here!
Element nextChild = ec.parent.getElement(ec.index + 1);
if(isOnlyContent)
newE = createLeafElement(ec.parent, nextChild.
getAttributes(), offset, nextChild.getEndOffset());
else
newE = createLeafElement(ec.parent, nextChild.
getAttributes(), offset, firstEndOffset);
ec.added.addElement(newE);
ec.removed.addElement(child);
ec.removed.addElement(nextChild);
}
// else nothin to do.
// PENDING: if !isOnlyContent could raise here!
break;
default:
// Inserted into middle, need to recreate split left
// new content, and split right.
if(child.getStartOffset() != offset) {
Element newE = createLeafElement(ec.parent,
child.getAttributes(), child.getStartOffset(),
offset);
ec.added.addElement(newE);
}
ec.removed.addElement(child);
// new content
Element newE = createLeafElement(ec.parent,
firstSpec.getAttributes(),
offset, firstEndOffset);
ec.added.addElement(newE);
if(child.getEndOffset() != endOffset) {
// Signals need to recreate right split later.
recreateLeafs = true;
}
else {
offsetLastIndex = true;
}
break;
}
}
Element root;
transient int pos; // current position
transient int offset;
transient int length;
transient int endOffset;
transient Vector<ElemChanges> changes;
transient Stack<ElemChanges> path;
transient boolean insertOp;
transient boolean recreateLeafs; // For insert.
For insert, path to inserted elements. /** For insert, path to inserted elements. */
transient ElemChanges[] insertPath;
Only for insert, set to true when the fracture has been created. /** Only for insert, set to true when the fracture has been created. */
transient boolean createdFracture;
Parent that contains the fractured child. /** Parent that contains the fractured child. */
transient Element fracturedParent;
Fractured child. /** Fractured child. */
transient Element fracturedChild;
Used to indicate when fracturing that the last leaf should be
skipped. /** Used to indicate when fracturing that the last leaf should be
* skipped. */
transient boolean offsetLastIndex;
Used to indicate that the parent of the deepest leaf should
offset the index by 1 when adding/removing elements in an
insert. /** Used to indicate that the parent of the deepest leaf should
* offset the index by 1 when adding/removing elements in an
* insert. */
transient boolean offsetLastIndexOnReplace;
/*
* Internal record used to hold element change specifications
*/
class ElemChanges {
ElemChanges(Element parent, int index, boolean isFracture) {
this.parent = parent;
this.index = index;
this.isFracture = isFracture;
added = new Vector<Element>();
removed = new Vector<Element>();
}
public String toString() {
return "added: " + added + "\nremoved: " + removed + "\n";
}
Element parent;
int index;
Vector<Element> added;
Vector<Element> removed;
boolean isFracture;
}
}
An UndoableEdit used to remember AttributeSet changes to an
Element.
/**
* An UndoableEdit used to remember AttributeSet changes to an
* Element.
*/
public static class AttributeUndoableEdit extends AbstractUndoableEdit {
Constructs an AttributeUndoableEdit
. Params: - element – the element
- newAttributes – the new attributes
- isReplacing – true if all the attributes in the element were removed first.
/**
* Constructs an {@code AttributeUndoableEdit}.
* @param element the element
* @param newAttributes the new attributes
* @param isReplacing true if all the attributes in the element were removed first.
*/
public AttributeUndoableEdit(Element element, AttributeSet newAttributes,
boolean isReplacing) {
super();
this.element = element;
this.newAttributes = newAttributes;
this.isReplacing = isReplacing;
// If not replacing, it may be more efficient to only copy the
// changed values...
copy = element.getAttributes().copyAttributes();
}
Redoes a change.
Throws: - CannotRedoException – if the change cannot be redone
/**
* Redoes a change.
*
* @exception CannotRedoException if the change cannot be redone
*/
public void redo() throws CannotRedoException {
super.redo();
MutableAttributeSet as = (MutableAttributeSet)element
.getAttributes();
if(isReplacing)
as.removeAttributes(as);
as.addAttributes(newAttributes);
}
Undoes a change.
Throws: - CannotUndoException – if the change cannot be undone
/**
* Undoes a change.
*
* @exception CannotUndoException if the change cannot be undone
*/
public void undo() throws CannotUndoException {
super.undo();
MutableAttributeSet as = (MutableAttributeSet)element.getAttributes();
as.removeAttributes(as);
as.addAttributes(copy);
}
AttributeSet containing additional entries, must be non-mutable!
/**
* AttributeSet containing additional entries, must be non-mutable!
*/
protected AttributeSet newAttributes;
Copy of the AttributeSet the Element contained.
/**
* Copy of the AttributeSet the Element contained.
*/
protected AttributeSet copy;
true if all the attributes in the element were removed first.
/**
* true if all the attributes in the element were removed first.
*/
protected boolean isReplacing;
Affected Element.
/**
* Affected Element.
*/
protected Element element;
}
UndoableEdit for changing the resolve parent of an Element.
/**
* UndoableEdit for changing the resolve parent of an Element.
*/
static class StyleChangeUndoableEdit extends AbstractUndoableEdit {
public StyleChangeUndoableEdit(AbstractElement element,
Style newStyle) {
super();
this.element = element;
this.newStyle = newStyle;
oldStyle = element.getResolveParent();
}
Redoes a change.
Throws: - CannotRedoException – if the change cannot be redone
/**
* Redoes a change.
*
* @exception CannotRedoException if the change cannot be redone
*/
public void redo() throws CannotRedoException {
super.redo();
element.setResolveParent(newStyle);
}
Undoes a change.
Throws: - CannotUndoException – if the change cannot be undone
/**
* Undoes a change.
*
* @exception CannotUndoException if the change cannot be undone
*/
public void undo() throws CannotUndoException {
super.undo();
element.setResolveParent(oldStyle);
}
Element to change resolve parent of. /** Element to change resolve parent of. */
protected AbstractElement element;
New style. /** New style. */
protected Style newStyle;
Old style, before setting newStyle. /** Old style, before setting newStyle. */
protected AttributeSet oldStyle;
}
Base class for style change handlers with support for stale objects detection.
/**
* Base class for style change handlers with support for stale objects detection.
*/
abstract static class AbstractChangeHandler implements ChangeListener {
/* This has an implicit reference to the handler object. */
private class DocReference extends WeakReference<DefaultStyledDocument> {
DocReference(DefaultStyledDocument d, ReferenceQueue<DefaultStyledDocument> q) {
super(d, q);
}
Return a reference to the style change handler object.
/**
* Return a reference to the style change handler object.
*/
ChangeListener getListener() {
return AbstractChangeHandler.this;
}
}
Class-specific reference queues. /** Class-specific reference queues. */
private static final Map<Class<?>, ReferenceQueue<DefaultStyledDocument>> queueMap
= new HashMap<Class<?>, ReferenceQueue<DefaultStyledDocument>>();
A weak reference to the document object. /** A weak reference to the document object. */
private DocReference doc;
AbstractChangeHandler(DefaultStyledDocument d) {
Class<?> c = getClass();
ReferenceQueue<DefaultStyledDocument> q;
synchronized (queueMap) {
q = queueMap.get(c);
if (q == null) {
q = new ReferenceQueue<DefaultStyledDocument>();
queueMap.put(c, q);
}
}
doc = new DocReference(d, q);
}
Return a list of stale change listeners.
A change listener becomes "stale" when its document is cleaned by GC.
/**
* Return a list of stale change listeners.
*
* A change listener becomes "stale" when its document is cleaned by GC.
*/
static List<ChangeListener> getStaleListeners(ChangeListener l) {
List<ChangeListener> staleListeners = new ArrayList<ChangeListener>();
ReferenceQueue<DefaultStyledDocument> q = queueMap.get(l.getClass());
if (q != null) {
DocReference r;
synchronized (q) {
while ((r = (DocReference) q.poll()) != null) {
staleListeners.add(r.getListener());
}
}
}
return staleListeners;
}
The ChangeListener wrapper which guards against dead documents.
/**
* The ChangeListener wrapper which guards against dead documents.
*/
public void stateChanged(ChangeEvent e) {
DefaultStyledDocument d = doc.get();
if (d != null) {
fireStateChanged(d, e);
}
}
Run the actual class-specific stateChanged() method. /** Run the actual class-specific stateChanged() method. */
abstract void fireStateChanged(DefaultStyledDocument d, ChangeEvent e);
}
Added to all the Styles. When instances of this receive a
stateChanged method, styleChanged is invoked.
/**
* Added to all the Styles. When instances of this receive a
* stateChanged method, styleChanged is invoked.
*/
static class StyleChangeHandler extends AbstractChangeHandler {
StyleChangeHandler(DefaultStyledDocument d) {
super(d);
}
void fireStateChanged(DefaultStyledDocument d, ChangeEvent e) {
Object source = e.getSource();
if (source instanceof Style) {
d.styleChanged((Style) source);
} else {
d.styleChanged(null);
}
}
}
Added to the StyleContext. When the StyleContext changes, this invokes
updateStylesListeningTo
.
/**
* Added to the StyleContext. When the StyleContext changes, this invokes
* <code>updateStylesListeningTo</code>.
*/
static class StyleContextChangeHandler extends AbstractChangeHandler {
StyleContextChangeHandler(DefaultStyledDocument d) {
super(d);
}
void fireStateChanged(DefaultStyledDocument d, ChangeEvent e) {
d.updateStylesListeningTo();
}
}
When run this creates a change event for the complete document
and fires it.
/**
* When run this creates a change event for the complete document
* and fires it.
*/
class ChangeUpdateRunnable implements Runnable {
boolean isPending = false;
public void run() {
synchronized(this) {
isPending = false;
}
try {
writeLock();
DefaultDocumentEvent dde = new DefaultDocumentEvent(0,
getLength(),
DocumentEvent.EventType.CHANGE);
dde.end();
fireChangedUpdate(dde);
} finally {
writeUnlock();
}
}
}
}