/* * 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 23.01.2005. */ package com.scriptographer.ai; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.PathIterator; import java.io.File; import java.io.FileNotFoundException; import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.Map; import com.scratchdisk.list.Lists; import com.scratchdisk.list.ReadOnlyList; import com.scratchdisk.script.ChangeReceiver; import com.scratchdisk.util.ArrayList; import com.scratchdisk.util.IntegerEnumUtils; import com.scratchdisk.util.SoftIntMap; import com.scriptographer.CommitManager; import com.scriptographer.ScriptographerEngine; import com.scriptographer.ScriptographerException; /** * The Document item refers to an Illustrator document. * * The currently active document can be accessed through the global {@code * document} variable. * * An array of all open documents is accessible through the global {@code * documents} variable. * * @author lehni */ public class Document extends NativeObject implements ChangeReceiver { /* * These flags are just here to test the undo history code. They * should be removed once that works well. */ protected static final boolean trackUndoHistory = true; protected static final boolean reportUndoHistory = false; private LayerList layers = null; private DocumentViewList views = null; private SymbolList symbols = null; private SwatchList swatches = null; private ArtboardList artboards = null; private Dictionary data = null; private Item currentStyleItem = null; /** * The current level in the undo history. */ private int undoLevel; /** * The "future" of the undo history, in case the user went back through undos. */ private int redoLevel; protected long historyVersion; private int maxHistoryBranch; protected HashMap<Long, HistoryBranch> history; private HistoryBranch historyBranch; /** * Internal list that keeps track of wrapped objects that have no clear * creation level. These need to be checked if they are valid in each undo. */ protected ArrayList<SoftReference<Item>> checkItems = new ArrayList<SoftReference<Item>>(); private ArrayList<Item> createdItems = new ArrayList<Item>(); private ArrayList<Item> modifiedItems = new ArrayList<Item>(); private ArrayList<Item> removedItems = new ArrayList<Item>(); // Keep track of state changes private boolean createdState = false; private boolean modifiedState = false; private boolean removedState = false; protected Document(int handle) { super(handle); // Initialise history data for this document. resetHistory(); } /** * Opens an existing document. * * Sample code: * <code> * var file = new File('/path/to/poster.ai'); * var poster = new Document(file); * </code> * * @param file the file to read from * @param colorModel the document's desired color model {@default 'cmyk'} * @param dialogStatus how dialogs should be handled {@default 'none'} * @throws FileNotFoundException */ public Document(File file, ColorModel colorModel, DialogStatus dialogStatus) throws FileNotFoundException { this(nativeCreate(file, (colorModel != null ? colorModel : ColorModel.CMYK).value, (dialogStatus != null ? dialogStatus : DialogStatus.NONE).value)); if (handle == 0) { if (!file.exists()) throw new FileNotFoundException( "Unable to create document from non existing file: " + file); throw new ScriptographerException( "Unable to create document from file: " + file); } } public Document(File file, ColorModel colorModel) throws FileNotFoundException { this(file, colorModel, null); } public Document(File file) throws FileNotFoundException { this(file, null, null); } /** * Creates a new document. * * Sample code: * <code> * // Create a new document named 'poster' * // with a width of 100pt and a height of 200pt: * var doc = new Document('poster', 100, 200);; * </code> * * <code> * // Create a document with a CMYK color mode * // and show Illustrator's 'New Document' dialog: * var doc = new Document('poster', 100, 200, 'cmyk', 'on'); * </code> * * @param title the title of the document * @param width the width of the document * @param height the height of the document * @param colorModel the document's desired color model {@default 'cmyk'} * @param dialogStatus how dialogs should be handled {@default 'none'} */ public Document(String title, float width, float height, ColorModel colorModel, DialogStatus dialogStatus) { this(nativeCreate(title, width, height, (colorModel != null ? colorModel : ColorModel.CMYK).value, (dialogStatus != null ? dialogStatus : DialogStatus.NONE).value)); } public Document(String title, float width, float height, ColorModel colorModel) { this(title, width, height, colorModel, null); } public Document(String title, float width, float height) { this(title, width, height, null, null); } private static native int nativeCreate(java.io.File file, int colorModel, int dialogStatus); private static native int nativeCreate(String title, float width, float height, int colorModel, int dialogStatus); // use a SoftIntMap to keep track of already wrapped documents: private static SoftIntMap<Document> documents = new SoftIntMap<Document>(); protected static Document wrapHandle(int handle) { if (handle == 0) return null; Document doc = documents.get(handle); if (doc == null) { doc = new Document(handle); documents.put(handle, doc); } return doc; } private static native int nativeGetActiveDocumentHandle(); private static native int nativeGetWorkingDocumentHandle(); /** * @jshide */ public static Document getActiveDocument() { return Document.wrapHandle(nativeGetActiveDocumentHandle()); } /** * @jshide */ public static Document getWorkingDocument() { return Document.wrapHandle(nativeGetWorkingDocumentHandle()); } /* * Undo / Redo History Tracking * * Illustrator's native handles are not versioned. Through the undo / redo * functionality, an item that we have a handle to might become invalid at a * certain point, or a redo command might make a previously invalid item * valid again. * * Unfortunately, Illustrator does not give us a way to tie into this system * and to easily know when and if a certain item is valid. In order to solve * this, Scriptographer implements its own undo history tracking system that * internally represents the history in a tree structure, with branches both * representing future possible changes (through redo) and past changes that * are undoable. Going back in history and branching off on a different * branch makes whole future branches invalid. * * All this is kept track of through Illustrator's facility to find out the * current amount of undo transactions: * sAIUndo->CountTransactions(&undoLevel, &redoLevel); * * In the code below we tie into Illustrator's internal undo processes * through a row of callbacks: onClosed, onRevert, onSelectionChanged, * onUndo, onRedo, onClear. The native side uses different approaches to get * these notifications. On each of them, the history tree is kept up to date * and checked. * * At the same time, Scriptographer keeps track of creation, deletion and * modification of items, and marks these with a versioned id at the end of * an undo cycle. These numbers consist of 64bit of information: 32bit for * the branch number, and 32bit for the level within that branch at which * the change happened. These ids then offer an easy and efficient way to * check at any time if an item is currently valid or not. * * The same mechanism then is also used by the Timer class to know of items * have changed in the meantime and decide how to handle / define the undo * cycle type. */ private class HistoryBranch { long branch; // the branch number long level; // the current level within this branch, if it is active long start; // the start level of this branch long end; // the maximum level available in this branch HistoryBranch previous; HistoryBranch next; HistoryBranch(HistoryBranch previous, long start) { // make sure we're not reusing branch numbers that were used for // previous branches before, by continuously adding to // maxHistoryBranch. this.branch = ++maxHistoryBranch; this.previous = previous; if (previous != null) { this.start = start; // If a previous "future" branch is cleared, remove it from the // history. if (previous.next != null) history.remove(previous.next.branch); previous.next = this; } else { start = 0; } } long getVersion(long level) { return (branch << 32) | level; } public String toString() { return "{ branch: " + branch + ", level: " + level + ", start: " + start + ", end: " + end + " }"; } } private void resetHistory() { undoLevel = -1; redoLevel = -1; historyVersion = 0; maxHistoryBranch = -1; historyBranch = new HistoryBranch(null, 0); history = new HashMap<Long, HistoryBranch>(); history.put(historyBranch.branch, historyBranch); } private int historyShift = 0; private void setHistoryLevels(int undoLevel, int redoLevel, boolean checkLevel) { // Illustrator has a maximum of 200 undo levels. When we get over // that, the current undoLevel gets decreased to 199, without // increasing the redolevel. We can detect this and adjust a // correcting shift factor accordingly. Tricky stuff indeed! undoLevel += historyShift; if (undoLevel != this.undoLevel || redoLevel != this.redoLevel) { // Detect maximum of undo levels reached. The undoLevel then stays // the same, and the redoLevel does not change. if (undoLevel == this.undoLevel - 1 && redoLevel == this.redoLevel) { historyShift++; undoLevel++; if (reportUndoHistory) ScriptographerEngine.logConsole("Increasing History Shift: " + historyShift); } boolean updateItems = false; if (checkLevel && undoLevel > this.undoLevel) { // A new history cycle was completed. if (this.redoLevel > redoLevel) { // A previous branch was broken off by going back in the // undo history first and then starting a new branch. // Store the level of the current history branch first. historyBranch.level = this.undoLevel; // Create a new branch historyBranch = new HistoryBranch(historyBranch, undoLevel); history.put(historyBranch.branch, historyBranch); } // Update the current historyEntry's future to the new level // This is the maximum possible level for a branch historyBranch.end = undoLevel; // Update newly created and modified items, after new // historyVersion is set. updateItems = true; } // Set new history version, to be used by items for setting of // creation / modification version and execution of checks. historyVersion = historyBranch.getVersion(undoLevel); if (updateItems) { // Scan through newly created, modified and deleted items and // update versions. We cannot set this while they are created or // modified, since that will still be during the old // history cycle, with the old version, and anticipating // the new version would break isValid and needsUpdate calls // during that cycle. if (!createdItems.isEmpty()) { for (Item item : createdItems) { item.creationVersion = historyVersion; item.modificationVersion = historyVersion; } createdItems.clear(); } if (!modifiedItems.isEmpty()) { for (Item item : modifiedItems) item.updateModified(historyVersion); modifiedItems.clear(); } if (!removedItems.isEmpty()) { for (Item item : removedItems) item.deletionVersion = historyVersion; removedItems.clear(); } } this.undoLevel = undoLevel; this.redoLevel = redoLevel; // Update the current historyEntry level to the current level historyBranch.level = undoLevel; if (reportUndoHistory) ScriptographerEngine.logConsole("undoLevel = " + undoLevel + ", redoLevel = " + redoLevel + ", current = " + historyBranch + ", previous = " + historyBranch.previous + ", version = " + historyVersion); } } protected boolean isValidVersion(long version) { // We first need to check that document handle is not 0, because if it // is all items inside are invalid. handle is set to 0 in onClosed(). if (handle == 0) return false; if (version == -1 || !trackUndoHistory) return true; // Branch = upper 32 bits long branch = (version >> 32) & 0xffffffffl; // First see if this branch is still around HistoryBranch entry = history.get(branch); if (entry != null) { // Level = lower 32 bits long level = version & 0xffffffffl; // See if the item is valid by comparing levels. If it is above // the current branch level, it will only be valid in the future, // if the user would go back there through redos. // But most of all the main undoLevel needs to be matched, as // otherwise we would also validate objects in future branches boolean validLevel = level <= undoLevel && level <= entry.level && level >= entry.start; return validLevel; } return false; } /* * Methods to be called by the Item class to keep track of created, modified * and removed items, and to mark them accordingly at the end of the current * undo cycle. */ protected void addCreatedItem(Item item) { createdItems.add(item); createdState = true; } protected void addModifiedItem(Item item) { modifiedItems.add(item); modifiedState = true; } protected void addRemovedItem(Item item) { removedItems.add(item); removedState = true; } /* * Methods to access the current internal state since the last time it was * accessed, used by the Timer class to decide how to define the undo cycle * type. */ protected boolean hasCreatedState() { return createdState; } protected boolean hasModifiedState() { return modifiedState; } protected boolean hasRemovedState() { return removedState; } protected boolean hasChangedSates() { return createdState || modifiedState || removedState; } protected void clearChangedStates() { createdState = false; modifiedState = false; removedState = false; } /* * Undo History Tracking related callbacks */ protected void onClosed() { // Since AI reused document handles, we have to manually remove wrappers // when documents get closed. This happens through a // kDocumentClosedNotifier on the native side. documents.remove(handle); handle = 0; // Closing this document has invalidated depending Dictionaries. // We need to call releaseInvalid now before the invalid dictionaries // would cause crashes when released next time this method is called. Dictionary.releaseInvalid(); } protected void onRevert() { if (reportUndoHistory) ScriptographerEngine.logConsole("Revert"); resetHistory(); Item.checkItems(this, Long.MAX_VALUE); } /** * Called from the native environment. */ protected void onSelectionChanged(int[] artHandles, int undoLevel, int redoLevel) { if (artHandles != null) Item.updateIfWrapped(artHandles); // TODO: Look into making CommitManager.version document dependent? CommitManager.version++; if (trackUndoHistory) { setHistoryLevels(undoLevel, redoLevel, true); } } protected void onUndo(int undoLevel, int redoLevel) { if (reportUndoHistory) ScriptographerEngine.logConsole("Undo"); if (trackUndoHistory) { // Check if we were going back to a previous branch, and if so, // switch back. if (historyBranch.previous != null && undoLevel <= historyBranch.previous.level) historyBranch = historyBranch.previous; long previousVersion = historyVersion; // Set levels. This also sets historyVersion correctly setHistoryLevels(undoLevel, redoLevel, false); // Scan through all the wrappers without a defined creationLevel and // set it to the previous historyVersion if they are not valid // anymore. Do this after the new historyLevel is set, as this // updates handles form the handleHistory in modified items. Item.checkItems(this, previousVersion); } } protected void onRedo(int undoLevel, int redoLevel) { if (reportUndoHistory) ScriptographerEngine.logConsole("Redo"); if (trackUndoHistory) { // Check if we were going forward to a "future" branch, and if so, // switch again. if (historyBranch.next != null && undoLevel > historyBranch.end) { if (reportUndoHistory) ScriptographerEngine.logConsole("Back to the future: " + historyBranch.next); historyBranch = historyBranch.next; } setHistoryLevels(undoLevel, redoLevel, false); } } protected void onClear(int[] artHandles) { if (reportUndoHistory) ScriptographerEngine.logConsole("Clear"); if (artHandles != null) Item.removeIfWrapped(artHandles, false); } private static native void nativeBeginExecution(boolean topDownCoordinates, boolean updateCoordinates, int[] returnValues); /** * Called before AI functions are executed. * @param system * * @return The current undo level * * @jshide */ public static void beginExecution(boolean topDownCoordinates, boolean updateCoordinates) { // Use an array as a simple way to receive values back from the native // side. int[] returnValues = new int[3]; // docHandle, undoLevel, redoLevel nativeBeginExecution(topDownCoordinates, updateCoordinates, returnValues); Document document = wrapHandle(returnValues[0]); if (document != null) document.setHistoryLevels(returnValues[1], returnValues[2], true); } /** * Called after AI functions are executed * * @jshide */ public static native void endExecution(); /* * Undo Cycle manipulation */ // AIUndoContextKind protected static final int /** * A standard context results in the addition of a new transaction which * can be undone/redone by the user. */ UNDO_STANDARD = 0, /** * A silent context does not cause redos to be discarded and is skipped * over when undoing and redoing. An example is a selection change. */ UNDO_SILENT = 1, /** * An appended context is like a standard context, except that it is * combined with the preceding transaction. It does not appear as a * separate transaction. Used, for example, to collect sequential * changes to the color of an object into a single undo/redo transaction. */ UNDO_MERGE = 2; protected native void setUndoType(int ype); /* * Normal document methods */ /** * Activates this document, so all newly created items will be placed * in it. * * @param focus When set to true, the document window is brought to the * front, otherwise the window sequence remains the same. * @param forCreation if set to true, the internal pointer gActiveDoc will * not be modified, but gCreationDoc will be set, which then is only * used once in the next call to Document_activate() (native stuff). */ protected void activate(boolean focus, boolean forCreation) { nativeActivate(focus, forCreation); if (forCreation) commitCurrentStyle(); } private native void nativeActivate(boolean focus, boolean forCreation); /** * Activates this document, so all newly created items will be placed * in it. * * @param focus When set to {@code true}, the document window is * brought to the front, otherwise the window sequence remains the * same. Default is {@code true}. */ public void activate(boolean focus) { activate(focus, false); } /** * Activates this document and brings its window to the front */ public void activate() { activate(true, false); } /** * Checks whether the document contains any selected items. * * @return {@code true} if the document contains selected items, * false otherwise. * * @jshide */ public native boolean hasSelectedItems(); /** * The selected items contained within the document. */ public native ItemList getSelectedItems(); protected native ItemList getMatchingItems(Class type, int whichAttributes, int attributes); /** * Returns all items that match a set of attributes, as specified by the * passed map. For each of the keys in the map, the demanded value can * either be true or false. * * Sample code: * <code> * // All selected paths and rasters contained in the document. * var selectedItems = document.getItems({ * type: [Path, Raster], * selected: true * }); * * // All visible Paths contained in the document. * var visibleItems = document.getItems({ * type: Path, * hidden: false * }); * </code> * * @param attributes an object containing the various attributes to check * for. */ public ItemList getItems(ItemAttributes attributes) { return attributes != null ? attributes.getItems(this) : null; } /** * @jshide */ public ItemList getItems(Class type) { ItemAttributes attributes = new ItemAttributes(); attributes.setType(type); return getItems(attributes); } /** * @jshide */ public ItemList getItems(Class[] types) { ItemAttributes attributes = new ItemAttributes(); attributes.setType(types); return getItems(attributes); } /** * @deprecated */ public ItemList getItems(Class[] types, ItemAttributes attributes) { if (attributes != null) { if (types != null) attributes.setType(types); return attributes.getItems(this); } return null; } /** * @deprecated */ public ItemList getItems(Class type, ItemAttributes attributes) { if (attributes != null) { if (type != null) attributes.setType(type); return attributes.getItems(this); } return null; } /** * Returns the selected items that are instances of one of the passed classes. * * Sample code: * <code> * // Get all selected groups and paths: * var items = document.getSelectedItems([Group, Path]); * </code> * * @param types * * @deprecated */ public ItemList getSelectedItems(Class[] types) { if (types == null) { return getSelectedItems(); } else { ItemAttributes attributes = new ItemAttributes(); attributes.setSelected(true); return getItems(types, attributes); } } /** * Returns the selected items that are an instance of the passed class. * * Sample code: * <code> * // Get all selected rasters: * var items = document.getSelectedItems(Raster); * </code> * * @param types * * @deprecated */ public ItemList getSelectedItems(Class type) { return getSelectedItems(new Class[] { type }); } private Item getCurrentStyleItem() { // This is a bit of a hack: We use a special handle HANDLE_CURRENT_STYLE // to tell the native side that this is in fact the current style, not // an item handle... if (currentStyleItem == null) currentStyleItem = new Item(Item.HANDLE_CURRENT_STYLE, this, false, true); // Update version so style gets refetched from native side. currentStyleItem.version = CommitManager.version; return currentStyleItem; } /** * The currently active Illustrator path style. All selected items and newly * created items will be styled with this style. */ public PathStyle getCurrentStyle() { return getCurrentStyleItem().getStyle(); } public void setCurrentStyle(PathStyle style) { getCurrentStyleItem().setStyle(style); } protected void commitCurrentStyle() { // Make sure style change gets committed before selection changes, // since it affects the selection. if (currentStyleItem != null) CommitManager.commit(currentStyleItem); } /** * The point of the lower left corner of the imageable page, relative to the * ruler origin. */ public native Point getPageOrigin(); public native void setPageOrigin(Point pt); /** * The point of the ruler origin of the document, relative to the bottom * left of the artboard. */ public native Point getRulerOrigin(); public native void setRulerOrigin(Point pt); /** * The size of the document. * Setting size only works while reading a document! */ public native Size getSize(); /** * @jshide */ public native void setSize(double width, double height); public void setSize(Size size) { if (size != null) setSize(size.width, size.height); } public Rectangle getBounds() { return getArtboards().getFirst().getBounds(); } public void setBounds(Rectangle bounds) { getArtboards().getFirst().setBounds(bounds); } private native int nativeGetColormodel(); private native void nativeSetColormodel(int model); public ColorModel getColorModel() { return IntegerEnumUtils.get(ColorModel.class, nativeGetColormodel()); } public void setColorModel(ColorModel model) { if (model != null) nativeSetColormodel(model.value); } /** * Specifies if the document has been edited since it was last saved. When * set to {@code true}, closing the document will present the user * with a dialog box asking to save the file. */ public native boolean isModified(); public native void setModified(boolean modified); /** * The file associated with the document. */ public native File getFile(); private native int nativeGetFileFormat(); private native void nativeSetFileFormat(int handle); public FileFormat getFileFormat() { return FileFormat.getFormat(nativeGetFileFormat()); } public void setFileFormat(FileFormat format) { nativeSetFileFormat(format != null ? format.handle : 0); } private native int nativeGetData(); /** * An object contained within the document which can be used to store data. * The values in this object can be accessed even after the file has been * closed and opened again. Since these values are stored in a native * structure, only a limited amount of value types are supported: Number, * String, Boolean, Item, Point, Matrix. * * Sample code: * <code> * document.data.point = new Point(50, 50); * print(document.data.point); // {x: 50, y: 50} * </code> * */ public Dictionary getData() { // We need to check if existing data references are still valid, // as Dictionary.releaseAll() is invalidating them after each // history cycle. See Dictionary.releaseAll() for more explanations if (data == null || !data.isValid()) data = Dictionary.wrapHandle(nativeGetData(), this, this); return data; } public void setData(Map<String, Object> map) { Dictionary data = getData(); if (map != data) { data.clear(); data.putAll(map); } } /** * {@grouptitle Document Hierarchy} * * The layers contained within the document. * * Sample code: * <code> * // When you create a new Document it always contains * // a layer called 'Layer 1' * print(document.layers); // Layer (Layer 1) * * // Create a new layer called 'test' in the document * var newLayer = new Layer(); * newLayer.name = 'test'; * * print(document.layers); // Layer (test), Layer (Layer 1) * print(document.layers[0]); // Layer (test) * print(document.layers.test); // Layer (test) * print(document.layers['Layer 1']); // Layer (Layer 1) * </code> */ public LayerList getLayers() { if (layers == null) layers = new LayerList(this); return layers; } /** * The layer which is currently active. The active layer is indicated in the * Layers palette by a black triangle. New items will be created on this * layer by default. * @return The layer which is currently active */ public native Layer getActiveLayer(); /** * The symbols contained within the document. */ public SymbolList getSymbols() { if (symbols == null) symbols = new SymbolList(this); return symbols; } private native int getActiveSymbolHandle(); /** * The symbol which is selected in the Symbols menu. */ public Symbol getActiveSymbol() { return Symbol.wrapHandle(getActiveSymbolHandle(), this); } /** * The swatches contained within the document. * * Sample code: * <code> * var firstSwatch = document.swatches[0]; * var namedSwatch = document.swatches['CMYK Blue']; * </code> */ public SwatchList getSwatches() { if (swatches == null) swatches = new SwatchList(this); return swatches; } /** * The artboards contained in the document. */ public ArtboardList getArtboards() { if (artboards == null) artboards = new ArtboardList(this); else artboards.update(); return artboards; } public void setArtboards(ReadOnlyList<Artboard> boards) { ArtboardList artboards = getArtboards(); for (int i = 0, l = boards.size(); i < l; i++) artboards.set(i, boards.get(i)); artboards.setSize(boards.size()); } public void setArtboards(Artboard[] boards) { setArtboards(Lists.asList(boards)); } private native int getActiveArtboardIndex(); private native void setActiveArtboardIndex(int index); public Artboard getActiveArtboard() { return getArtboards().get(getActiveArtboardIndex()); } public void setActiveArtboard(Artboard board) { setActiveArtboardIndex(board.getIndex()); } /** * The document views contained within the document. */ public DocumentViewList getViews() { if (views == null) views = new DocumentViewList(this); return views; } // getActiveView can not be native as there is no wrapViewHandle defined // nativeGetActiveView returns the handle, that still needs to be wrapped // here. as this is only used once, that's the prefered way (just like // DocumentList.getActiveDocument private native int getActiveViewHandle(); /** * The document view which is currently active. */ public DocumentView getActiveView() { return DocumentView.wrapHandle(getActiveViewHandle(), this); } // TODO: getActiveSwatch, getActiveGradient private TextStoryList stories = null; /** * The stories contained within the document. */ public TextStoryList getStories() { // See getStories(int storyHandle, boolean dispose) for explanations: ItemList items = getItems(TextItem.class); TextItem item = items.size() > 0 ? (TextItem) items.getFirst() : null; return getStories(item, true); } protected TextStoryList getStories(TextStoryProvider storyProvider, boolean release) { // We need to have a storyHandle to fetch the document's stories from. // We could use document.getItems() to get one, but there are situations // where this code seems to not work, e.g. when a text item was just // removed from the document (but is still valid during the cycle and // could be introduced in the DOM again) // So let's be on the save side when directly working with existing // items and always provide the context. // Also we need to version TextStoryLists, since document handles seem // to not be unique: // When there is only one document, closing it and opening a new one // results in the same document handle. Versioning seems the only way to // keep story lists updated. if (stories == null || stories.version != CommitManager.version) { int handle = storyProvider != null ? nativeGetStories(storyProvider.getStoryHandle(), release) : 0; if (stories == null) stories = new TextStoryList(handle, this); else stories.changeHandle(handle); } return stories; } private native int nativeGetStories(int storyHandle, boolean release); /** * Prints the document. * * @param status */ public void print(DialogStatus status) { nativePrint(status.value); } public void print() { print(DialogStatus.OFF); } private native void nativePrint(int status); /** * Saves the document. */ public native void save(); /** * Closes the document. */ public native void close(); /** * Forces the document to be redrawn. */ public native void redraw(); public native void undo(); public native void redo(); /** * Places a file in the document. * * Sample code: * <code> * var file = new File('/path/to/image.jpg'); * var item = document.place(file); * </code> * * @param file the file to place * @param linked when set to {@code true}, the placed object is a * link to the file, otherwise it is embedded within the document */ public native Item place(File file, boolean linked); public Item place(File file) { return place(file, true); } /** * Invalidates the rectangle in artwork coordinates. This will cause all * views of the document that contain the given rectangle to update at the * next opportunity. */ public native void invalidate(Rectangle rect); private native boolean nativeWrite(File file, int formatHandle, boolean ask); /** * @jshide */ public boolean write(File file, FileFormat format, boolean ask) { if (format == null) { // Try to get format by extension String name = file.getName(); int pos = name.lastIndexOf('.'); format = FileFormatList.getInstance().get(name.substring(pos + 1)); if (format == null) format = this.getFileFormat(); } return nativeWrite(file, format != null ? format.handle : 0, ask); } /** * @jshide */ public boolean write(File file, FileFormat format) { return write(file, format, false); } /** * @jshide */ public boolean write(File file, String format, boolean ask) { return write(file, FileFormatList.getInstance().get(format), ask); } /** * @jshide */ public boolean write(File file, String format) { return write(file, format, false); } public boolean write(File file, boolean ask) { return write(file, (FileFormat) null, ask); } public boolean write(File file) { return write(file, false); } /** * The selected text as a text range. * * Sample code: * <code> * var range = document.selectedTextRange; * * // Check if there is a selected range: * if(range) { * range.characterStyle.fontSize += 15; * } * </code> */ public native TextRange getSelectedTextRange(); private native void nativeSelectAll(); /** * Selects all items in the document. */ public void selectAll() { commitCurrentStyle(); nativeSelectAll(); } private native void nativeDeselectAll(); /** * Deselects all selected items in the document. */ public void deselectAll() { commitCurrentStyle(); nativeDeselectAll(); } /* TODO: make these public Item getInsertionItem(); public int getInsertionOrder(); public boolean isInsertionEditable(); */ protected Path createPath() { activate(false, true); return new Path(); } protected CompoundPath createCompoundPath() { activate(false, true); return new CompoundPath(); } /** * Creates a PathItem from a given Java2D PathIterator. Determines weather a * CompoundPath or simple Path is sufficient. */ protected PathItem createPathItem(PathIterator iter) { float[] f = new float[6]; Path path = null; CompoundPath compound = null; while (!iter.isDone()) { switch (iter.currentSegment(f)) { case PathIterator.SEG_MOVETO: { // See if we used a simple Path so far, and turn it into // a compound path once there is more than one MOVETO // command. if (path != null && compound == null) { compound = createCompoundPath(); compound.appendTop(path); } path = createPath(); if (compound != null) compound.appendTop(path); path.moveTo(f[0], f[1]); break; } case PathIterator.SEG_LINETO: path.lineTo(f[0], f[1]); break; case PathIterator.SEG_QUADTO: path.quadraticCurveTo(f[0], f[1], f[2], f[3]); break; case PathIterator.SEG_CUBICTO: path.cubicCurveTo(f[0], f[1], f[2], f[3], f[4], f[5]); break; case PathIterator.SEG_CLOSE: path.closePath(); break; } iter.next(); } return compound != null ? compound : path; } /** * Creates a PathItem from a given Java2D Shape. Determines weather a * CompoundPath or simple Path is sufficient. */ protected PathItem createPathItem(Shape shape) { return createPathItem(shape.getPathIterator(null)); } /** * Creates a Path Item with two anchor points forming a line. * * Sample code: * <code> * var path = new Path.Line(new Point(20, 20, new Point(100, 100)); * </code> * * @param pt1 the first anchor point of the path * @param pt2 the second anchor point of the path * @return the newly created path * * @jshide */ public Path createLine(Point pt1, Point pt2) { Path path = createPath(); path.moveTo(pt1); path.lineTo(pt2); return path; } /** * Creates a Path Item with two anchor points forming a line. * * Sample code: * <code> * var path = new Path.Line(20, 20, 100, 100); * </code> * * @param x1 the x position of the first point * @param y1 the y position of the first point * @param x2 the x position of the second point * @param y2 the y position of the second point * @return the newly created path * * @jshide */ public Path createLine(double x1, double y1, double x2, double y2) { return createLine(new Point(x1, y1), new Point(x2, y2)); } private native Path nativeCreateRectangle(Rectangle rect); /** * Creates a rectangular shaped Path Item. * * Sample code: * <code> * var rectangle = new Rectangle(new Point(100, 100), new Size(100, 100)); * var path = new Path.Rectangle(rectangle); * </code> * * @param rect * @return the newly created path * * @jshide */ public Path createRectangle(Rectangle rect) { activate(false, true); return nativeCreateRectangle(rect); } /** * Creates a rectangular shaped Path Item. * * Sample code: * <code> * var path = new Path.Rectangle(100, 100, 10, 10); * </code> * * @jshide */ public Path createRectangle(double x, double y, double width, double height) { return createRectangle(new Rectangle(x, y, width, height)); } /** * Creates a rectangle shaped Path Item. * * Sample code: * <code> * var path = new Path.Rectangle(new Point(100, 100), new Size(10, 10)); * </code> * * @param point the bottom left point of the rectangle * @param size the size of the rectangle * @return the newly created path * * @jshide */ public Path createRectangle(Point point, Size size) { return createRectangle(new Rectangle(point, size)); } /** * Creates a rectangle shaped Path Item from the passed points. These do not * necessarily need to be the top left and bottom right corners, the * constructor figures out how to fit a rectangle between them. * * Sample code: * <code> * var path = new Path.Rectangle(new Point(100, 100), new Point(200, 300)); * </code> * * @param point1 The first point defining the rectangle * @param point2 The second point defining the rectangle * @return the newly created path * * @jshide */ public Path createRectangle(Point point1, Point point2) { return createRectangle(new Rectangle(point1, point2)); } private native Path nativeCreateRoundRectangle(Rectangle rect, Size size); /** * Creates a rectangular Path Item with rounded corners. * * Sample code: * <code> * var rectangle = new Rectangle(new Point(100, 100), new Size(100, 100)); * var path = new Path.RoundRectangle(rectangle, new Size(30, 30)); * </code> * * @param rect * @param cornerSize the size of the rounded corners * @return the newly created path * * @jshide */ public Path createRoundRectangle(Rectangle rect, Size cornerSize) { activate(false, true); return nativeCreateRoundRectangle(rect, cornerSize); } /** * Creates a rectangular Path Item with rounded corners. * * Sample code: * <code> * var path = new Path.RoundRectangle(new Point(100, 100), * new Size(100, 100), new Size(30, 30)); * </code> * * @param point the bottom left point of the rectangle * @param size the size of the rectangle * @param cornerSize the size of the rounded corners * @return the newly created path * * @jshide */ public Path createRoundRectangle(Point point, Size size, Size cornerSize) { activate(false, true); return nativeCreateRoundRectangle(new Rectangle(point, size), cornerSize); } /** * Creates a rectangular Path Item with rounded corners. * * Sample code: * <code> * var path = new Path.RoundRectangle(50, 50, 100, 100, 30, 30); * </code> * * @param x the left position of the rectangle * @param y the bottom position of the rectangle * @param width the width of the rectangle * @param height the height of the rectangle * @param hor the horizontal size of the rounder corners * @param ver the vertical size of the rounded corners * @return the newly created path * * @jshide */ public Path createRoundRectangle(double x, double y, double width, double height, float hor, float ver) { return createRoundRectangle(new Rectangle(x, y, width, height), new Size(hor, ver)); } private native Path nativeCreateOval(Rectangle rect, boolean circumscribed); /** * Creates an oval shaped Path Item. * * Sample code: * <code> * var rectangle = new Rectangle(new Point(100, 100), new Size(150, 100)); * var path = new Path.Oval(rectangle); * </code> * * @param rect * @param circumscribed if this is set to true the oval shaped path will be * created so the rectangle fits into it. If it's set to false the * oval path will fit within the rectangle. {@default false} * @return the newly created path * * @jshide */ public Path createOval(Rectangle rect, boolean circumscribed) { activate(false, true); return nativeCreateOval(rect, circumscribed); } /** * @jshide */ public Path createOval(Rectangle rect) { return createOval(rect, false); } /** * Creates an oval shaped Path Item. * * Sample code: * <code> * var rectangle = new Rectangle(100, 100, 150, 100); * var path = new Path.Oval(rectangle); * </code> * * @param x * @param y * @param width * @param height * @param circumscribed if this is set to true the oval shaped path will be * created so the rectangle fits into it. If it's set to false the * oval path will fit within the rectangle. {@default false} * @return the newly created path * * @jshide */ public Path createOval(double x, double y, double width, double height, boolean circumscribed) { return createOval(new Rectangle(x, y, width, height), circumscribed); } /** * @jshide */ public Path createOval(double x, double y, double width, double height) { return createOval(x, y, width, height); } /** * Creates a circle shaped Path Item. * * Sample code: * <code> * var path = new Path.Circle(new Point(100, 100), 50); * </code> * * @param center the center point of the circle * @param radius the radius of the circle * @return the newly created path * * @jshide */ public Path createCircle(Point center, float radius) { return createOval(new Rectangle(center.subtract(radius, radius), center .add(radius, radius))); } /** * Creates a circle shaped Path Item. * * Sample code: * * <code> * var path = new Path.Circle(100, 100, 50); * </code> * * @param x the horizontal center position of the circle * @param y the vertical center position of the circle * @param radius the radius of the circle * @return the newly created path * * @jshide */ public Path createCircle(float x, float y, float radius) { return createCircle(new Point(x, y), radius); } /** * Creates a circular arc shaped Path Item. * * Sample code: * * <code> * var path = new Path.Arc(new Point(0, 0), new Point(100, 100), * new Point(200, 150)); * </code> * * @param from the starting point of the circular arc * @param through the point the arc passes through * @param to the end point of the arc * @return the newly created path * * @jshide */ public Path createArc(Point from, Point through, Point to) { Path path = createPath(); path.moveTo(from); path.arcTo(through, to); return path; } private native Path nativeCreateRegularPolygon(Point center, int numSides, float radius); /** * Creates a regular polygon shaped Path Item. * * Sample code: * <code> * // Create a triangle shaped path * var triangle = new Path.RegularPolygon(new Point(100, 100), 3, 50); * * // Create a decahedron shaped path * var decahedron = new Path.RegularPolygon(new Point(200, 100), 10, 50); * </code> * * @param center the center point of the polygon * @param numSides the number of sides of the polygon * @param radius the radius of the polygon * @return the newly created path * * @jshide */ public Path createRegularPolygon(Point center, int numSides, float radius) { activate(false, true); return nativeCreateRegularPolygon(center, numSides, radius); } private native Path nativeCreateStar(Point center, int numPoints, float radius1, float radius2); /** * Creates a star shaped Path Item. * * The largest of {@code radius1} and {@code radius2} will be the outer * radius of the star. The smallest of radius1 and radius2 will be the inner * radius. * * Sample code: * <code> * var center = new Point(100, 100); * var points = 6; * var innerRadius = 20; * var outerRadius = 50; * var path = new Path.Star(center, points, innerRadius, outerRadius); * </code> * * @param center the center point of the star * @param numPoints the number of points of the star * @param radius1 * @param radius2 * @return the newly created path * * @jshide */ public Path createStar(Point center, int numPoints, float radius1, float radius2) { activate(false, true); return nativeCreateStar(center, numPoints, radius1, radius2); } private native Path nativeCreateSpiral(Point firstArcCenter, Point start, float decayPercent, int numQuarterTurns, boolean clockwiseFromOutside); /** * Creates a spiral shaped Path Item. * * Sample code: * <code> * var firstArcCenter = new Point(100, 100); * var start = new Point(50, 50); * var decayPercent = 90; * var numQuarterTurns = 25; * * var path = new Path.Spiral(firstArcCenter, start, decayPercent, * numQuarterTurns, true); * </code> * * @param firstArcCenter the center point of the first arc * @param start the starting point of the spiral * @param decayPercent the percentage by which each succeeding arc will be * scaled * @param numQuarterTurns the number of quarter turns (arcs) * @param clockwiseFromOutside if this is set to {@code true} the spiral * will spiral in a clockwise direction from the first point. If it's * set to {@code false} it will spiral in a counter clockwise * direction * @return the newly created path * * @jshide */ public Path createSpiral(Point firstArcCenter, Point start, float decayPercent, int numQuarterTurns, boolean clockwiseFromOutside) { activate(false, true); return nativeCreateSpiral(firstArcCenter, start, decayPercent, numQuarterTurns, clockwiseFromOutside); } private native HitResult nativeHitTest(Point point, int request, float tolerance, Item item); protected HitResult hitTest(Point point, HitRequest request, float tolerance, Item item) { return nativeHitTest(point, (request != null ? request : HitRequest.ALL).value, tolerance, item); } /** * @param point * @param request * @param tolerance the hit-test tolerance in view coordinates (pixels at * the current zoom factor). correct results for large values are not * guaranteed {@default 2} */ public HitResult hitTest(Point point, HitRequest request, float tolerance) { return hitTest(point, request, tolerance, null); } public HitResult hitTest(Point point, HitRequest request) { return hitTest(point, request, HitResult.DEFAULT_TOLERANCE); } public HitResult hitTest(Point point) { return hitTest(point, HitRequest.ALL, HitResult.DEFAULT_TOLERANCE); } public HitResult hitTest(Point point, float tolerance) { return hitTest(point, HitRequest.ALL, tolerance); } /** * Text reflow is suspended during script execution. when reflowText() is * called, the reflow of text is forced. */ public native void reflowText(); /** * Checks whether the document is valid, i.e. it hasn't been closed. * * Sample code: * <code> * var doc = document; * print(doc.isValid()); // true * doc.close(); * print(doc.isValid()); // false * </code> * * @return {@true if the document is valid} */ public boolean isValid() { return handle != 0; } /** * {@grouptitle Clipboard Functions} * * Cuts the selected items to the clipboard. */ public native void cut(); /** * Copies the selected items to the clipboard. */ public native void copy(); /** * Pastes the contents of the clipboard into the active layer of the * document. */ public native void paste(); /** * Returns a Graphics2D object that can be used to draw into the AI * document. Useful for conversions. * * @jshide */ public DocumentGraphics2D getGraphics2D() { return new DocumentGraphics2D(this, false); } /** * Draws the document's content into a Graphics2D object. Useful for * conversions. * * @jshide */ public void paint(Graphics2D graphics) { LayerList layers = getLayers(); for (int i = layers.size() - 1; i >= 0; i--) { layers.get(i).paint(graphics); } } }