/* * 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 04.12.2004. */ package com.scriptographer; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Stack; import java.util.prefs.Preferences; import com.scratchdisk.script.Callable; import com.scratchdisk.script.Scope; import com.scratchdisk.script.ScriptCanceledException; import com.scratchdisk.script.ScriptEngine; import com.scratchdisk.script.ScriptException; import com.scratchdisk.util.ClassUtils; import com.scratchdisk.util.ConversionUtils; import com.scriptographer.adm.Dialog; import com.scriptographer.ai.Annotator; import com.scriptographer.ai.Dictionary; import com.scriptographer.ai.Document; import com.scriptographer.ai.LiveEffect; import com.scriptographer.ai.Timer; import com.scriptographer.sg.AngleUnits; import com.scriptographer.sg.CoordinateSystem; import com.scriptographer.sg.Script; import com.scriptographer.ui.KeyIdentifier; import com.scriptographer.ui.KeyEvent; import com.scriptographer.ui.MenuItem; /** * @author lehni */ public class ScriptographerEngine { private static File pluginDir = null; private static File coreDir = null; private static File[] scriptDirectories = null; private static PrintStream errorLogger = null; private static PrintStream consoleLogger = null; private static Thread mainThread; private static HashMap<String, ArrayList<Scope>> callbackScopes; /* * Create a NumberFormat object to be used wherever numbers are printed to * the user. We use a reasonable amount of fractional digits (5) and the * same symbols for NaN and Infinity as in JavaScript. */ private static final DecimalFormatSymbols numberFormatSymbols = new DecimalFormatSymbols(); static { numberFormatSymbols.setInfinity("Infinity"); numberFormatSymbols.setNaN("NaN"); } public static final NumberFormat numberFormat = new DecimalFormat("0.#####", numberFormatSymbols); // All callback functions to be found and collected in the compiled scopes. private static String[] callbackNames = { "onStartup", "onShutdown", "onActivate", "onDeactivate", "onAbout", "onOwlDragBegin", "onOwlDragEnd", "onKeyDown", "onKeyUp", "onStop" }; // Flags to be used by the AI package, for coordinate systems and angle // units. public static boolean topDownCoordinates = true; public static boolean anglesInDegrees = true; // App Events. Their numbers need to match calbackNames indices. public static final int EVENT_APP_STARTUP = 0; public static final int EVENT_APP_SHUTDOWN = 1; public static final int EVENT_APP_ACTIVATED = 2; public static final int EVENT_APP_DEACTIVATED = 3; public static final int EVENT_APP_ABOUT = 4; public static final int EVENT_OWL_DRAG_BEGIN = 5; public static final int EVENT_OWL_DRAG_END = 6; // Key Events. Their numbers need to match calbackNames indices. public static final int EVENT_KEY_DOWN = 7; public static final int EVENT_KEY_UP = 8; /** * Don't let anyone instantiate this class. */ private ScriptographerEngine() { } public static void init(String pluginPath) { mainThread = Thread.currentThread(); // Redirect system streams to the console. ConsoleOutputStream.enableRedirection(true); pluginDir = new File(pluginPath); errorLogger = getLogger("java.log"); consoleLogger = getLogger("console.log"); // This is needed on Mac, where there is more than one thread and the // Loader is initiated on startup // in the second thread. The ScriptographerEngine get loaded through the // Loader, so getting the ClassLoader from there is save: Thread.currentThread().setContextClassLoader( ScriptographerEngine.class.getClassLoader()); // Compile all core init scripts callbackScopes = new HashMap<String, ArrayList<Scope>>(); coreDir = new File(new File(pluginDir, "Core"), "JavaScript"); if (coreDir.isDirectory()) { // Load the core libraries first. loadLibraries(new File(coreDir, "lib")); compileInitScripts(coreDir); } } public static void destroy() { // We're shutting down, so do not display console stuff any more ConsoleOutputStream.enableOutput(false); ConsoleOutputStream.enableRedirection(false); stopAll(true, true); LiveEffect.removeAll(); MenuItem.removeAll(); Annotator.disposeAll(); try { // This is needed on some versions on Mac CS (CFM?) // as the JVM seems to not shoot down properly, and the // preferences would then not be flushed to file otherwise. getPreferences(null).flush(); } catch (java.util.prefs.BackingStoreException e) { throw new RuntimeException(e); } } public static File getPluginDirectory() { return pluginDir; } public static File getCoreDirectory() { return coreDir; } public static void setScriptDirectories(File[] directories) { scriptDirectories = directories; // When setting script directories for error reporting, also compile // init scripts within them. for (int i = 0, l = scriptDirectories.length; i < l; i++) { compileInitScripts(scriptDirectories[i]); } } public static String[] getScriptPath(File file, boolean hideCore) { ArrayList<String> parts = new ArrayList<String>(); boolean loop = true; while (loop) { parts.add(0, file.getName()); file = file.getParentFile(); if (file == null || file.equals(pluginDir)) break; if (file.equals(coreDir)) { if (hideCore) return null; break; } if (scriptDirectories != null) { for (int i = 0, l = scriptDirectories.length; i < l; i++) { if (file.equals(scriptDirectories[i])) { // Add the script directory name itself too. parts.add(0, file.getName()); loop = false; break; } } } } return parts.toArray(new String[parts.size()]); } /** * Executes all scripts named __init__.* in the given folder * * @param dir * @throws IOException * @throws ScriptException */ protected static void compileInitScripts(File dir) { File []files = dir.listFiles(); if (files != null) { for (int i = 0; i < files.length; i++) { File file = files[i]; String name = file.getName(); if (file.isDirectory() && !name.startsWith(".") && !name.equals("CVS")) { compileInitScripts(file); } else if (name.startsWith("__init__")) { try { ScriptEngine engine = ScriptEngine.getEngineByFile(file); if (engine == null) throw new ScriptException( "Unable to find script engine for " + file); execute(file, engine.createScope()); } catch (Exception e) { reportError(e); } } } } } protected static void loadLibraries(File dir) { File[] files = dir.listFiles(); if (files != null) { for (int i = 0; i < files.length; i++) { File file = files[i]; String name = file.getName(); if (file.isDirectory() && !name.startsWith(".") && !name.equals("CVS")) { loadLibraries(file); } else { try { ScriptEngine engine = ScriptEngine.getEngineByFile(file); if (engine != null) execute(file, engine.getGlobalScope()); } catch (Exception e) { reportError(e); } } } } } public static Preferences getPreferences(Script script) { // The base preferences for Scriptographer are: // com.scriptographer.preferences on Mac, three nodes seem // to be necessary, otherwise things get mixed up... Preferences prefs = Preferences.userNodeForPackage( ScriptographerEngine.class).node("preferences"); if (script == null) return prefs; // Determine preferences for the current executing script // by walking up the file path to the script directory and // using each folder as a preference node. File file = script.getFile(); // Core script preferences are placed in the "core" node, all others // go into the "scripts" node. prefs = prefs.node(script.isCoreScript() ? "core" : "scripts"); String[] parts = getScriptPath(file, false); for (int i = 0, l = parts.length; i < l; i++) prefs = prefs.node(parts[i]); return prefs; } private static PrintStream getLogger(String name) { try { File logDir = new File(pluginDir, "Logs"); if (!logDir.exists()) logDir.mkdir(); return new PrintStream( new FileOutputStream(new File(logDir, name)), true); } catch (Exception e) { // Not allowed to make this log directory or file, so don't log... } return null; } public static void logError(Throwable t) { if (errorLogger != null) { errorLogger.println(new Date()); t.printStackTrace(errorLogger); errorLogger.println(); errorLogger.flush(); } } public static void logError(String str) { if (errorLogger != null) { errorLogger.println(new Date()); errorLogger.println(str); errorLogger.flush(); } } public static void logConsole(String str) { if (consoleLogger != null) { consoleLogger.println(str); consoleLogger.flush(); } } public static void reportError(Throwable t) { try { String error; Throwable cause; if (t instanceof ScriptException) { error = ((ScriptException) t).getFullMessage(); cause = ((ScriptException) t).getWrappedException(); } else { error = t.getMessage(); if (error == null) error = t.toString(); cause = t.getCause(); } // Simplify error messages for Wrapped ScriptographerExceptions: if (cause instanceof ScriptographerException) error = "Error: " + error; else if (cause instanceof UnsupportedOperationException) error = "Unsupported Operation: " + error; // Shorten file names by removing the script directory form it /* // TODO: if (scriptsDir != null) error = StringUtils.replace(error, scriptsDir.getAbsolutePath() + System.getProperty("file.separator"), ""); */ // Add a line break at the end if the error does // not contain one already. // TODO: find out why this regular expression does not work and make // it work instead: // if (!Pattern.compile("(?:\\n\\r|\\n|\\r)$").matcher(error).matches()) String lineBreak = System.getProperty("line.separator"); if (!error.endsWith(lineBreak)) error += lineBreak; System.err.print(error); logError(t); } catch (Throwable e) { // Report an error in reportError code... // This should not happen! e.printStackTrace(); } } static int reloadCount = 0; public static int getReloadCount() { return reloadCount; } public static String reload() { stopAll(true, true); reloadCount++; return nativeReload(); } public static native String nativeReload(); public static void setCallback(ScriptographerCallback cb) { ConsoleOutputStream.setCallback(cb); } private static Stack<Script> scriptStack = new Stack<Script>(); private static boolean allowScriptCancelation = true; private static Throwable lastError; public static Script getCurrentScript() { // There can be 'holes' in the script stack, so find the first non-null // entry and return it. for (int i = scriptStack.size() - 1; i >= 0; i--) { Script last = scriptStack.get(i); if (last != null) return last; } return null; } /** * To be called before AI functions are executed as scripts */ public static void beginExecution(File file, Scope scope) { // Since the interface is done in scripts too and we receive being / // endExecution events for all UI notifications as well, we need to // cheat a bit here. // When file is set, we ignore the current state of "executing", // as we're about to to execute a new script... Script script = scope != null ? (Script) scope.get("script") : null; // Only call Document.beginExecution for the first script in the call // stack. if (scriptStack.empty()) { // Set script coordinate system and angle units on each execution, // at the beginning of the script stack. anglesInDegrees = AngleUnits.DEGREES == (script != null ? script.getAngleUnits() : AngleUnits.DEFAULT); topDownCoordinates = CoordinateSystem.TOP_DOWN == (script != null ? script.getCoordinateSystem() : CoordinateSystem.DEFAULT); // Pass topDownCoordinates value to the client side as well Document.beginExecution(topDownCoordinates, // Do not update coordinate systems for tool scripts, // as this has already happened in Tool.onHandleEvent() script == null || !script.isToolScript()); // Disable output to the console while the script is executed as it // won't get updated anyway // ConsoleOutputStream.enableOutput(false); } if (file != null) { Dialog.destroyAll(false, false); Timer.abortAll(false, false); // Put a script object in the scope to offer the user // access to information about it. if (script == null) { script = new Script(file, file.getPath().startsWith( coreDir.getPath())); scope.put("script", script, true); } } if (scriptStack.empty() || file != null) { if (script != null && !script.getShowProgress()) { closeProgress(); } else if (file == null || !file.getName().startsWith("__")) { showProgress(file != null ? "Executing " + file.getName() + "..." : "Executing..."); } } // Push script even if it is null, as we're always popping again in // endExecution. scriptStack.push(script); } public static void beginExecution() { beginExecution(null, null); } /** * To be called after AI functions were executed. * * @return if any changes to the document were committed. */ public static void endExecution() { if (!scriptStack.empty()) scriptStack.pop(); if (scriptStack.empty()) { try { CommitManager.commit(); } catch(Throwable t) { ScriptographerEngine.reportError(t); } Dictionary.releaseInvalid(); Document.endExecution(); closeProgress(); } } private native static void nativeSetTopDownCoordinates( boolean topDownCoordinates); protected static void setTopDownCoordinates(boolean topDown) { if (topDown ^ topDownCoordinates) { topDownCoordinates = topDown; nativeSetTopDownCoordinates(topDown); } } public static CoordinateSystem getCoordinateSystem() { return topDownCoordinates ? CoordinateSystem.TOP_DOWN : CoordinateSystem.BOTTOM_UP; } public static void setCoordinateSystem(CoordinateSystem system) { setTopDownCoordinates(system == CoordinateSystem.TOP_DOWN); } public static AngleUnits getAngleUnits() { return anglesInDegrees ? AngleUnits.DEGREES : AngleUnits.RADIANS; } public static void setAngleUnits(AngleUnits angleUnits) { anglesInDegrees = angleUnits == AngleUnits.DEGREES; } /** * Invokes the method on the object, passing the arguments to it and calling * beginExecution before and endExecution after it, which commits all * changes after execution. */ public static Object invoke(Callable callable, Object obj, Object... args) { lastError = null; Scope scope; if (obj instanceof Scope) { scope = (Scope) obj; obj = scope.getScope(); } else { scope = callable.getScope(); } beginExecution(null, scope); // Retrieve wrapper object for the native java object, and // call the function on it. Throwable throwable = null; try { return callable.call(obj, args); } catch (Throwable t) { throwable = t; } finally { // Commit all changed objects after a scripting function // has been called! endExecution(); } if (throwable != null) handleException(throwable, null); return null; } /** * Compiles the script file and throws errors if it cannot be compiled. * * @param file * @throws ScriptException * @throws IOException */ public static com.scratchdisk.script.Script compile(File file) throws ScriptException, IOException { ScriptEngine engine = ScriptEngine.getEngineByFile(file); if (engine == null) throw new ScriptException("Unable to find script engine for " + file); com.scratchdisk.script.Script script = engine.compile(file); if (script == null) throw new ScriptException("Unable to compile script " + file); return script; } /** * Executes the specified script file. * * @param file * @throws ScriptException * @throws IOException */ public static Object execute(File file, Scope scope) throws ScriptException, IOException { return execute(compile(file), file, scope); } /** * Executes the compiled script. * * @param script * @param file * @param scope * @throws ScriptException * @throws IOException */ public static Object execute(com.scratchdisk.script.Script script, File file, Scope scope) throws ScriptException, IOException { lastError = null; Object ret = null; Throwable throwable = null; try { if (scope == null) scope = script.getEngine().createScope(); beginExecution(file, scope); ret = script.execute(scope); addCallbacks(scope, file); } catch (Throwable t) { throwable = t; } finally { // Commit all the changes, even when script has caused an error, to // sync with direct changes such as creation of paths, etc. endExecution(); } if (throwable != null) handleException(throwable, file); return ret; } private static void handleException(Throwable throwable, File file) { // Do not allow script cancellation during error reporting, as printing // of errors is handled by coreScripts too allowScriptCancelation = false; // Unwrap ScriptCanceledExceptions Throwable cause = throwable.getCause(); if (cause instanceof ScriptCanceledException) throwable = cause; if (throwable instanceof ScriptException) { ScriptographerEngine.reportError(throwable); } else if (throwable instanceof ScriptCanceledException) { logConsole(file != null ? file.getName() + " canceled" : "Execution canceled"); } lastError = throwable; allowScriptCancelation = true; } public static Throwable getLastError() { return lastError; } private static Script getScript(Scope scope) { return (Script) scope.get("script"); } private static void addCallbacks(Scope scope, File file) { // Scan through callback names and add to callback scope sublists if // found. for (String name : callbackNames) { Callable callback = scope.getCallable(name); if (callback != null) { ArrayList<Scope> list = callbackScopes.get(name); if (list == null) { list = new ArrayList<Scope>(); callbackScopes.put(name, list); } else { // Remove old scope for this script before adding new one for (int i = list.size() - 1; i >= 0; i--) { if (getScript(list.get(i)).getFile().equals(file)) { list.remove(i); break; } } } list.add(scope); } } } private static void removeCallbacks(String name, boolean ignoreKeepAlive) { ArrayList<Scope> list = callbackScopes.get(name); if (list != null) { for (int i = list.size() - 1; i >= 0; i--) { if (getScript(list.get(i)).canRemove(ignoreKeepAlive)) list.remove(i); } } } private static void removeCallbacks(boolean ignoreKeepAlive) { for (String name : callbackScopes.keySet()) removeCallbacks(name, ignoreKeepAlive); } private static boolean callCallbacks(String name, Object[] args) { ArrayList<Scope> list = callbackScopes.get(name); // The first callback handler that returns true stops the others // (and in the case of keyDown / up also the native one!) if (list != null) { for (Scope scope : list) { Callable callback = scope.getCallable(name); Object res = invoke(callback, scope, args); if (ConversionUtils.toBoolean(res)) return true; } } return false; } private static void callCallbacks(String name) { callCallbacks(name, new Object[0]); } public static void stopAll(boolean ignoreKeepAlive, boolean force) { Timer.abortAll(ignoreKeepAlive, force); callCallbacks("onStop"); Dialog.destroyAll(ignoreKeepAlive, force); removeCallbacks(ignoreKeepAlive); } /** * To be called from the native environment. */ public static void onHandleEvent(int type) { // TODO: There is currently no way to use these callbacks in a Java-only // use of the API. Find one? callCallbacks(callbackNames[type]); // Explicitly initialize all dialogs after startup, as otherwise // funny things will happen on CS3 and above. See comment in initializeAll if (type == EVENT_APP_STARTUP) Dialog.initializeAll(); } /** * To be called from the native environment. */ private static boolean onHandleKeyEvent(int type, int identifier, char character, int modifiers) { // TODO: There is currently no way to use these callbacks in a Java-only // use of the API. Find one? return callCallbacks(callbackNames[type], new Object[] { new KeyEvent(type, identifier, character, modifiers) }); } /** * Launches the filename with the default associated editor. * * @param filename */ public static native boolean launch(String filename); public static boolean launch(File file) { return launch(file.getPath()); } /** * Returns the current system time in nano seconds. * This is very useful for high resolution time measurements. * @return the current system time. */ public static native long getNanoTime(); private static native boolean nativeIsDown(int keyCode); public static boolean isKeyDown(KeyIdentifier key) { return key != null ? nativeIsDown(key.value()) : false; } private static long progressCurrent; private static long progressMax; private static boolean progressAutomatic = false; private static boolean progressVisible = false; private static native void nativeSetProgressText(String text); public static void showProgress() { progressVisible = true; progressAutomatic = true; progressCurrent = 0; progressMax = 1 << 8; nativeUpdateProgress(progressCurrent, progressMax, true); } public static void showProgress(String text) { showProgress(); nativeSetProgressText(text); } private static native boolean nativeUpdateProgress(long current, long max, boolean visible); public static boolean updateProgress(long current, long max) { if (progressVisible) { progressCurrent = current; progressMax = max; progressAutomatic = false; } boolean ret = nativeUpdateProgress(current, max, progressVisible); return !allowScriptCancelation || ret; } public static boolean updateProgress() { if (isMainThreadActive()) { boolean ret = nativeUpdateProgress(progressCurrent, progressMax, progressVisible); if (progressVisible && progressAutomatic) { progressCurrent++; progressMax++; } return !allowScriptCancelation || ret; } return true; } private static native void nativeCloseProgress(); public static void closeProgress() { progressVisible = false; nativeCloseProgress(); } public static boolean getProgressVisible() { return progressVisible; } public static void setProgressVisible(boolean visible) { if (progressVisible ^ visible) { if (visible) { progressVisible = true; nativeUpdateProgress(progressCurrent, progressMax, true); } else { closeProgress(); } } } public static boolean isMainThreadActive() { return Thread.currentThread().equals(mainThread); } public static native void dispatchNextEvent(); private static final boolean isWindows, isMacintosh; static { String os = System.getProperty("os.name").toLowerCase(); isWindows = (os.indexOf("windows") != -1); isMacintosh = (os.indexOf("mac os x") != -1); } public static boolean isWindows() { return isWindows; } public static boolean isMacintosh() { return isMacintosh; } public static native boolean isActive(); public static native double getIllustratorVersion(); public static native int getIllustratorRevision(); private static double version = -1; private static int revision = -1; public static double getPluginVersion() { if (version == -1) readVersion(); return version; } public static int getPluginRevision() { if (revision == -1) readVersion(); return revision; } private static void readVersion() { String[] lines = ClassUtils.getServiceInformation( ScriptographerEngine.class); if (lines != null) { version = Double.parseDouble(lines[0]); revision = Integer.parseInt(lines[1]); } } public static void debug() { } }