You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2745 lines
104 KiB
2745 lines
104 KiB
/*
|
|
* Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
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 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.
|
|
* <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
|
|
*/
|
|
public class DefaultStyledDocument extends AbstractDocument implements StyledDocument {
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @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 <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.
|
|
*
|
|
* @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
|
|
* <code>getDefaultRootElement</code> method. If the
|
|
* document contained any data it will first be removed.
|
|
*/
|
|
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.
|
|
* <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.
|
|
*
|
|
* <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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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 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.
|
|
* <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.
|
|
*
|
|
* @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.
|
|
* <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.
|
|
* <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 <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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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 <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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*/
|
|
ChangeListener createStyleChangeListener() {
|
|
return new StyleChangeHandler(this);
|
|
}
|
|
|
|
/**
|
|
* Returns a new instance of StyleContextChangeHandler.
|
|
*/
|
|
ChangeListener createStyleContextChangeListener() {
|
|
return new StyleContextChangeHandler(this);
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
Vector v = (Vector)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 = (Style)v.elementAt(counter);
|
|
aStyle.removeChangeListener(styleChangeListener);
|
|
}
|
|
if (listeningStyles.size() == 0) {
|
|
styleChangeListener = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void readObject(ObjectInputStream s)
|
|
throws ClassNotFoundException, IOException {
|
|
listeningStyles = new Vector<Style>();
|
|
s.defaultReadObject();
|
|
// 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.
|
|
*/
|
|
public static final int BUFFER_SIZE_DEFAULT = 4096;
|
|
|
|
protected ElementBuffer buffer;
|
|
|
|
/** Styles listening to. */
|
|
private transient Vector<Style> listeningStyles;
|
|
|
|
/** Listens to Styles. */
|
|
private transient ChangeListener styleChangeListener;
|
|
|
|
/** Listens to Styles. */
|
|
private transient ChangeListener styleContextChangeListener;
|
|
|
|
/** 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.
|
|
* <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}.
|
|
*/
|
|
protected class SectionElement extends BranchElement {
|
|
|
|
/**
|
|
* Creates a new SectionElement.
|
|
*/
|
|
public SectionElement() {
|
|
super(null, null);
|
|
}
|
|
|
|
/**
|
|
* Gets the name of the element.
|
|
*
|
|
* @return the name
|
|
*/
|
|
public String getName() {
|
|
return SectionElementName;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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}.
|
|
*/
|
|
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.
|
|
*/
|
|
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.
|
|
*/
|
|
public static final short EndTagType = 2;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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.
|
|
*/
|
|
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.
|
|
*/
|
|
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.
|
|
*/
|
|
public static final short JoinFractureDirection = 7;
|
|
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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;
|
|
this.offs = offs;
|
|
this.len = len;
|
|
this.direction = OriginateDirection;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @return the type of the element (StartTagType, EndTagType,
|
|
* ContentType)
|
|
*/
|
|
public short getType() {
|
|
return type;
|
|
}
|
|
|
|
/**
|
|
* Sets the direction.
|
|
*
|
|
* @param direction the direction (JoinPreviousDirection,
|
|
* JoinNextDirection)
|
|
*/
|
|
public void setDirection(short direction) {
|
|
this.direction = direction;
|
|
}
|
|
|
|
/**
|
|
* Gets the direction.
|
|
*
|
|
* @return the direction (JoinPreviousDirection, JoinNextDirection)
|
|
*/
|
|
public short getDirection() {
|
|
return direction;
|
|
}
|
|
|
|
/**
|
|
* Gets the element attributes.
|
|
*
|
|
* @return the attribute set
|
|
*/
|
|
public AttributeSet getAttributes() {
|
|
return attr;
|
|
}
|
|
|
|
/**
|
|
* Gets the array of characters.
|
|
*
|
|
* @return the array
|
|
*/
|
|
public char[] getArray() {
|
|
return data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the starting offset.
|
|
*
|
|
* @return the offset >= 0
|
|
*/
|
|
public int getOffset() {
|
|
return offs;
|
|
}
|
|
|
|
/**
|
|
* Gets the length.
|
|
*
|
|
* @return the length >= 0
|
|
*/
|
|
public int getLength() {
|
|
return len;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <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}.
|
|
*/
|
|
public class ElementBuffer implements Serializable {
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @return the root element
|
|
*/
|
|
public Element getRootElement() {
|
|
return root;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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.
|
|
*/
|
|
protected void removeUpdate() {
|
|
removeElements(root, offset, offset + length);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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
|
|
*/
|
|
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.
|
|
* @param record Whether or not the push should be
|
|
* recorded as an element change or not.
|
|
* @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.
|
|
*/
|
|
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 <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?
|
|
*/
|
|
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.
|
|
*/
|
|
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.
|
|
*
|
|
* @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.
|
|
*/
|
|
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. <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 <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 <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 <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.
|
|
*/
|
|
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. */
|
|
transient ElemChanges[] insertPath;
|
|
/** Only for insert, set to true when the fracture has been created. */
|
|
transient boolean createdFracture;
|
|
/** Parent that contains the fractured child. */
|
|
transient Element fracturedParent;
|
|
/** Fractured child. */
|
|
transient Element fracturedChild;
|
|
/** 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. */
|
|
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.
|
|
*/
|
|
public static class AttributeUndoableEdit extends AbstractUndoableEdit {
|
|
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.
|
|
*
|
|
* @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.
|
|
*
|
|
* @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!
|
|
protected AttributeSet newAttributes;
|
|
// Copy of the AttributeSet the Element contained.
|
|
protected AttributeSet copy;
|
|
// true if all the attributes in the element were removed first.
|
|
protected boolean isReplacing;
|
|
// Efected Element.
|
|
protected Element 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.
|
|
*
|
|
* @exception CannotRedoException if the change cannot be redone
|
|
*/
|
|
public void redo() throws CannotRedoException {
|
|
super.redo();
|
|
element.setResolveParent(newStyle);
|
|
}
|
|
|
|
/**
|
|
* 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. */
|
|
protected AbstractElement element;
|
|
/** New style. */
|
|
protected Style newStyle;
|
|
/** Old style, before setting newStyle. */
|
|
protected AttributeSet oldStyle;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
ChangeListener getListener() {
|
|
return AbstractChangeHandler.this;
|
|
}
|
|
}
|
|
|
|
/** Class-specific reference queues. */
|
|
private final static Map<Class, ReferenceQueue<DefaultStyledDocument>> queueMap
|
|
= new HashMap<Class, ReferenceQueue<DefaultStyledDocument>>();
|
|
|
|
/** 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.
|
|
*/
|
|
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.
|
|
*/
|
|
public void stateChanged(ChangeEvent e) {
|
|
DefaultStyledDocument d = doc.get();
|
|
if (d != null) {
|
|
fireStateChanged(d, e);
|
|
}
|
|
}
|
|
|
|
/** 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.
|
|
*/
|
|
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
|
|
* <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.
|
|
*/
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|