/*
* Scriptographer
*
* This file is part of Scriptographer, a Scripting Plugin for Adobe Illustrator
* http://scriptographer.org/
*
* Copyright (c) 2002-2010, Juerg Lehni
* http://scratchdisk.com/
*
* All rights reserved. See LICENSE file for details.
*
* File created on 28.10.2005.
*/
package com.scriptographer.ai;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.zip.Adler32;
import com.scriptographer.CommitManager;
import com.scriptographer.Committable;
import com.scriptographer.ScriptographerException;
import com.scratchdisk.list.ExtendedArrayList;
import com.scratchdisk.list.ExtendedList;
import com.scratchdisk.list.ListIterator;
import com.scratchdisk.list.Lists;
import com.scratchdisk.list.ReadOnlyList;
import com.scratchdisk.util.ArrayList;
import com.scratchdisk.util.IntegerEnumUtils;
/**
* @author lehni
*/
public class TextRange extends DocumentObject implements Committable,
TextStoryProvider {
// values for the native environment,
// to cash glyph run references, once their
// found. these values need to be cleared in
// setStart, setEnd ,setRange and finalize
private int glyphRuns;
protected TextStory story = null;
protected int version = CommitManager.version;
protected boolean dirty = false;
// Sub Range lists:
TokenizerList words = null;
TokenizerList paragraphs = null;
CharacterList characters = null;
protected TextRange(int handle, Document document) {
super(handle, document);
}
protected TextRange(int handle, int docHandle) {
this(handle, Document.wrapHandle(docHandle));
}
// Once a range object is created, always return the same reference
// and swap handles instead. like this references in JS remain...
protected void changeHandle(int newHandle) {
nativeRelease(handle); // release old handle
handle = newHandle;
version = CommitManager.version;
// Clear the sub ranges, as the items in them are not valid anymore
words = null;
paragraphs = null;
characters = null;
// Story needs to be updated too, otherwise the old TextItem
// (e.g. before moving) keeps being referenced...
if (story != null)
document.getStories(this, true).changeStoryHandle(story,
getStoryIndex());
}
/**
* markDirty is called when content is changed
*/
protected void markDirty() {
if (!dirty) {
CommitManager.markDirty(getStory(), this);
dirty = true;
}
}
public void commit(boolean endExecution) {
// Committing changes for TextRange does not need more than
// a reflow of the text layout in the document.
// This is needed since otherwise, the TextItem's attributes
// (bounds, etc) are invalid
if (!endExecution)
document.reflowText();
dirty = false;
}
/**
* The text content of the text range.
*/
public native String getContent();
public void setContent(String text) {
remove();
append(text);
}
/**
* The index of the first character of the text range inside the story in
* numbers of characters.
*/
public native int getStart();
/**
* The index of the last character of the text range inside the story in
* numbers of characters.
*/
public native int getEnd();
/**
* The amount of characters in the range.
*/
public native int getLength();
private native int getStoryIndex();
/*
* @see TextStoryProvider.
*/
public native int getStoryHandle();
/**
* {@grouptitle Hierarchy}
*
* The document that the text range belongs to.
*/
public Document getDocument() {
// This is only here for the API document.
// It does exactly the same as the definition in DocumentObject
return document;
}
/**
* The story that the text range belongs to.
*/
public TextStory getStory() {
if (story == null) {
TextStoryList stories = document.getStories(this, true);
int index = getStoryIndex();
if (index >= stories.size())
throw new ScriptographerException("Cannot get text stories from document");
story = stories.get(index);
}
return story;
}
/**
* The first text frame of the story that this text range belongs to.
* @jshide
*/
public native TextItem getFirstTextItem();
/**
* The last text frame of the story that this text range belongs to.
* @jshide
*/
public native TextItem getLastTextItem();
/**
* An array of linked text frames that the text range is contained
* within.
*/
public ReadOnlyList<TextItem> getTextItems() {
ExtendedArrayList<TextItem> list = new ExtendedArrayList<TextItem>();
TextItem item = getFirstTextItem();
TextItem last = getLastTextItem();
if (item != null) {
if (last == null)
last = item;
do {
list.add(item);
item = item.getNextTextItem();
} while (item != null && item != last);
}
return list;
}
protected native void setRange(int start, int end);
private native int nativePrepend(int handle1, String text);
private native int nativeAppend(int handle1, String text);
private native int nativePrepend(int handle1, int handle2);
private native int nativeAppend(int handle1, int handle2);
private void adjustStart(int oldLength) {
if (characters != null)
characters.adjustStart(oldLength);
if (words != null)
words.adjustStart(oldLength);
if (paragraphs != null)
paragraphs.adjustStart(oldLength);
this.markDirty();
}
private void adjustEnd(int oldLength) {
if (characters != null)
characters.adjustEnd(oldLength);
if (words != null)
words.adjustEnd(oldLength);
if (paragraphs != null)
paragraphs.adjustEnd(oldLength);
this.markDirty();
}
/**
* Prepends the supplied text to the text range.
* @param text
*/
public void prepend(String text) {
adjustStart(nativePrepend(handle, text));
}
/**
* Appends the supplied text to this text range.
* @param text
*/
public void append(String text) {
adjustEnd(nativeAppend(handle, text));
}
/**
* Prepends the supplied text range to the text range.
*/
public void prepend(TextRange range) {
adjustStart(nativePrepend(handle, range.handle));
}
/**
* Appends the supplied text range to this text range.
* @param range
*/
public void append(TextRange range) {
adjustEnd(nativeAppend(handle, range.handle));
}
private native boolean nativeRemove(int handle);
/**
* Deletes all the characters in the text range.
*/
public boolean remove() {
if (nativeRemove(handle)) {
if (characters != null)
characters.removeAll();
if (words != null)
words.removeAll();
if (paragraphs != null)
paragraphs.removeAll();
markDirty();
return true;
}
return false;
}
/**
* gets the character style handle and adds reference to it.
* attention! this needs to be wrapped in CharacterStyle so
* the reference gets released in the end.
*/
private native int nativeGetCharacterStyle(int handle);
private native int nativeGetParagraphStyle(int handle);
CharacterStyle characterStyle = null;
ParagraphStyle paragraphStyle = null;
/**
* {@grouptitle Style Properties}
*
* The character style of the text range.
*/
public CharacterStyle getCharacterStyle() {
if (characterStyle == null) {
int styleHandle = nativeGetCharacterStyle(handle);
if (styleHandle != 0)
characterStyle = new CharacterStyle(styleHandle, this);
} else if (characterStyle.version != CommitManager.version) {
characterStyle.changeHandle(nativeGetCharacterStyle(handle));
}
return characterStyle;
}
public void setCharacterStyle(CharacterStyle style) {
if (style != null) {
getCharacterStyle(); // make sure it's created
// create a new handle and set it here
style = (CharacterStyle) style.clone();
characterStyle.changeHandle(style.handle);
characterStyle.markSetStyle();
style.handle = 0; // make sure release doesn't mess up things...
}
}
/**
* The paragraph style of the text range.
*/
public ParagraphStyle getParagraphStyle() {
if (paragraphStyle == null) {
int styleHandle = nativeGetParagraphStyle(handle);
if (styleHandle != 0)
paragraphStyle = new ParagraphStyle(styleHandle, this);
} else if (paragraphStyle.version != CommitManager.version) {
paragraphStyle.changeHandle(nativeGetParagraphStyle(handle));
paragraphStyle.version = CommitManager.version;
}
return paragraphStyle;
}
public void setParagraphStyle(ParagraphStyle style) {
if (style != null) {
getParagraphStyle(); // make sure it's created
// create a new handle and set it here
style = (ParagraphStyle) style.clone();
paragraphStyle.changeHandle(style.handle);
paragraphStyle.version = CommitManager.version;
paragraphStyle.markSetStyle();
style.handle = 0; // make sure release doesn't mess up things...
}
}
protected void commitStyles() {
if (characterStyle != null && characterStyle.dirty)
characterStyle.commit(false);
if (paragraphStyle != null && paragraphStyle.dirty)
paragraphStyle.commit(false);
}
// TODO: move to CharacterStyle
/**
* {@grouptitle Character Range Properties}
*
* The origin points of the characters in the text range.
*/
public native Point[] getOrigins();
/**
* The transformation matrices of the characters in the text range.
*/
// TODO: Rename to getMatrices() ?
public native Matrix[] getTransformations();
/*
TODO: ...
public native int[] getGlyphIds();
public native int getGlyphId();
public native int getCharCount();
public native int getCount();
*/
// TODO: needed?
/**
* @jshide
*/
public native int getSingleGlyph();
/**
* Selects the text range.
*
* @param addToSelection If set to {@code true}, the text range will
* be added to the current selection in the document. If set to
* false, it will clear the current selection in the document and
* only select the text range.
*/
public native void select(boolean addToSelection);
// if addToSelection is true, it will add this range to the current document
// selection.
// if addToSelection is false, it will clear the selection from the document
// and only select this range.
public void select() {
select(false);
}
/**
* Deselects the text range in the document. Note that deselecting a text
* range can cause defragmented selection, if the text range is a subrange
* of the current selection.
*/
public native void deselect();
// This method will remove this range from the selection.
// Note, deselecting a range can cause defregmented selection, if this range
// is a sub range of the current selection.
/**
* Clones the text range.
*/
public Object clone() {
/*
* Cannot use the native code, because when the original range is
* changed in size, the copied one seems to change as well. Use
* getRange(0, getLength()) instead of the native clone...
*/
return getSubRange(0, getLength());
}
/**
* Returns a text range from the story given in indices relative to the current
* text range's start index.
*
* @param start
* @param end
* @return sub range
*/
public TextRange getSubRange(int start, int end) {
int index = getStart();
return getStory().getRange(index + start, index + end);
}
private native boolean nativeChangeCase(int change);
/**
* Changes the case of the text in the text range.
*
* Sample code:
* <code>
* var text = new PointText();
* text.content = 'the title of a book';
* text.range.changeCase('title');
* print(text.content); // 'The Title Of A Book'
* </code>
*
* <code>
* var text = new PointText();
* text.content = 'one two three';
* text.range.words[1].changeCase('upper');
* print(text.content); // 'one TWO three'
* </code>
*
* @param change
*/
public boolean changeCase(TextCase change) {
if (change != null)
return nativeChangeCase(change.value);
return false;
}
/**
* Adjusts the tracking of the text in the text range to fit on one line
* spanning the width of the area text.
*/
public native void fitHeadlines();
private native int nativeGetCharacterType();
/**
* The character type of the text range.
* This range has to be of size equal to 1 character.
*
* Sample code:
* <code>
* var text = new PointText(new Point(50, 100));
* text.content = 'Text.';
*
* var firstCharacter = text.range.characters.first;
* print(firstCharacter.characterType) // 'normal'
*
* var lastCharacter = text.range.characters.last;
* print(lastCharacter.characterType); // 'punctuation'
* </code>
*/
public CharacterType getCharacterType() {
return IntegerEnumUtils.get(CharacterType.class,
nativeGetCharacterType());
}
// TODO:
// ATEErr (*SetStory) ( TextRangeRef textrange, const StoryRef story);
// ATEErr (*SetRange) ( TextRangeRef textrange, ASInt32 start, ASInt32 end);
/// start and end of this range will change depending on direction
/// if direction = CollapseEnd, then end = start
/// if direction = CollapseStart, then start = end
// ATEErr (*Collapse) ( TextRangeRef textrange, CollapseDirection direction);
public native boolean equals(Object obj);
private native void nativeRelease(int handle);
protected void finalize() {
nativeRelease(handle);
handle = 0;
}
/**
* {@grouptitle Sub Ranges}
*
* The text ranges of the words contained within the text range. Note that
* the returned text range includes the trailing whitespace characters of
* the words.
*
* Sample code:
* <code>
* var text = new PointText(new Point(0,0));
* text.content = 'The contents of the point text.';
* var word = text.range.words[1];
* print(word.content) // 'contents ' - note the space after 'contents';
* </code>
*/
public ReadOnlyList<TextRange> getWords() {
if (words == null)
words = new TokenizerList(" \t\n\r\f");
words.update();
return words;
}
/**
* The text ranges of the paragraphs contained within the text range. Note
* that the returned text range includes the trailing paragraph (\r)
* characters of the paragraphs.
*
* Sample code:
* <code>
* var text = new PointText(new Point(0,0));
*
* // ('\r' is the escaped character that specifies a new paragraph)
* text.content = 'First paragraph\rSecond paragraph';
* var paragraph = text.range.paragraphs[1];
* print(paragraph.content) //returns 'Second paragraph';
* </code>
*/
public ReadOnlyList<TextRange> getParagraphs() {
if (paragraphs == null)
paragraphs = new TokenizerList("\r");
paragraphs.update();
return paragraphs;
}
/**
* The text ranges of the characters contained within the text range.
*
* Sample code:
* <code>
* var text = new PointText(new Point(0,0));
* text.content = 'abc';
* var character = text.range.characters[1];
* print(character.content) //returns 'b';
* </code>
*/
public ReadOnlyList<TextRange> getCharacters() {
if (characters == null)
characters = new CharacterList();
characters.update();
return characters;
}
/**
* The base class for all TextRangeList classes
*/
abstract class TextRangeList<E> implements ReadOnlyList<TextRange> {
ArrayList<E> list;
TextRangeList() {
list = new ArrayList<E>();
}
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public void removeAll() {
list.clear();
}
public ExtendedList<TextRange> getSubList(int fromIndex, int toIndex) {
return Lists.createSubList(this, fromIndex, toIndex);
}
public Iterator<TextRange> iterator() {
return new ListIterator<TextRange>(this);
}
public TextRange getFirst() {
return size() > 0 ? get(0) : null;
}
public TextRange getLast() {
int size = size();
return size > 0 ? get(size - 1) : null;
}
public Class<?> getComponentType() {
return TextRange.class;
}
}
/**
* A list that applies a StringTokenizer to the internal text and adds each
* token to the list as a TextRange. The Ranges are only created when
* needed, The delimiter chars are included in the tokens at the end.
*/
class TokenizerList extends TextRangeList<TokenizerList.Token> {
class Token {
int start;
int end;
String text;
TextRange range = null;
TextRange getRange() {
if (range == null)
range = getSubRange(start, end);
return range;
}
void init(int start, int end, String text) {
this.start = start;
this.end = end;
this.text = text;
if (range != null)
range.setRange(start, end);
}
}
String delimiter;
int tokenPos;
int tokenIndex;
Adler32 checksum;
TokenizerList(String delimiter) {
super();
this.delimiter = delimiter;
this.checksum = new Adler32();
}
void update() {
String content = getContent();
// calculate string checksum and compare, only update token list
// if something changed...
// TODO: see how this performs!
long oldChecksum = checksum.getValue();
checksum.reset();
checksum.update(content.getBytes());
// positions in tokens are relative to the start of the containing
// range
int position = 0;
int index = 0;
// this loop reuses tokens
if (checksum.getValue() != oldChecksum) {
StringTokenizer st = new StringTokenizer(content, delimiter, true);
StringBuffer part = new StringBuffer();
while (st.hasMoreTokens()) {
String token = st.nextToken();
// delimiter char?
if (part.length() > 0 && (token.length() > 1 ||
delimiter.indexOf(token.charAt(0)) == -1)) {
position = setToken(index++, position, part.toString());
part.setLength(0);
}
part.append(token);
}
if (part.length() > 0)
position = setToken(index++, position, part.toString());
list.setSize(index);
}
}
public void adjustStart(int oldLength) {
// get text of first token that has changed. split it again and
// adjust list accordingly
if (list.size() > 0) {
TextRange range = get(0);
String content = range.getContent();
StringTokenizer st = new StringTokenizer(content, delimiter, false);
int count = 0;
while (st.hasMoreTokens()) {
st.nextToken();
count++;
}
// in case the added text causes a new split or even several,
// add the amount of empty tokens so the offset is right again
// (in case some ranges where already
// referenced they need to shift properly. then update list
// TODO: consider optimization: only the new tokens should be
// parsed, the rest could be simply offset...
if (count > 1) {
for (int i = 1; i < count; i++)
list.add(0, null);
update();
}
}
}
public void adjustEnd(int oldLength) {
// get text of last token that has changed. split it again and
// adjust list accordingly
int size = list.size();
if (size > 0) {
TextRange range = get(size - 1);
String content = range.getContent();
StringTokenizer st = new StringTokenizer(content, delimiter,
false);
int count = 0;
while (st.hasMoreTokens()) {
st.nextToken();
count++;
}
// in case the added text causes a new split or even several,
// add the amount of empty tokens so the offset is right again.
// then fix range
// TODO: consider optimization: only the new tokens should be
// parsed, the rest could be simply offset...
if (count > 1) {
for (int i = 1; i < count; i++)
list.add(null);
update();
}
}
}
int setToken(int index, int position, String str) {
int len = str.length();
int end = position + len;
Token token;
if (index < list.size()) {
token = list.get(index);
if (token == null) {
token = new Token();
list.set(index, token);
}
} else {
token = new Token();
list.add(token);
}
token.init(position, end, str);
return end;
}
public TextRange get(int index) {
Token token = list.get(index);
return token.getRange();
}
}
/**
* A list of text ranges for each character in this text range.
*/
class CharacterList extends TextRangeList<TextRange> {
void update() {
list.setSize(TextRange.this.getLength());
}
/**
* adjustStart is called when TextRange.prepend changes the start point
* of the ranges update here accordingly
*/
void adjustStart(int oldLength) {
TextRange range = list.get(0);
// the starting point of the range needs to be moved and then removed from the
// list so a new one is return for this index
if (range != null) {
// move the range by the amount of change in the range
int start = getStart() + (size() - oldLength);
range.setRange(start, start + 1);
list.set(0, null);
}
}
/**
* adjustEnd is called when TextRange.append changes the end point of
* the ranges update here accordingly
*
* @param oldLength
*/
void adjustEnd(int oldLength) {
int index = oldLength - 1;
if (index >= 0 && index < list.size()) {
TextRange range = list.get(index);
// the end point of the range needs to be moved so it
// has length 1 again. it can stay in the list
if (range != null) {
int end = getStart() + oldLength;
range.setRange(end - 1, end);
}
}
}
public TextRange get(int index) {
TextRange range = list.get(index);
if (range == null) {
range = getSubRange(index, index + 1);
list.set(index, range);
}
return range;
}
}
/**
* Whenever a style changes happens that affects other styles, the style
* objects need to get replaces with new references, as otherwise the
* updates only seem reflected in the next execution cycle. This method
* takes care of this for us. Currently this is only in use in
* CharacterStyle#setLeading / #setAutoLeading.
*/
protected void updateStyle() {
if (characterStyle != null) {
characterStyle.commit(false);
// Swap with new instance, so changes do get reflected.
characterStyle.changeHandle(nativeGetCharacterStyle(handle));
}
if (paragraphStyle != null) {
paragraphStyle.commit(false);
// Swap with new instance, so changes do get reflected.
paragraphStyle.changeHandle(nativeGetParagraphStyle(handle));
}
}
}