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