Package com.scriptographer.ai

Source Code of com.scriptographer.ai.TextRange$TokenizerList$Token

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

Related Classes of com.scriptographer.ai.TextRange$TokenizerList$Token

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.