Copy link to clipboard
Copied
I have a palette written with ScriptUI that uses Bridgetalk to invoke its functions.
Now I wanted to call this script from a CEP panel (rather than via File / Scripting menu) but it cannot find its functions
Basically the code is structured like
Myscript = (function() {
function start() {....}
function func1() {...}
return {start:start, func1:func1, ....}
})();
The buttons are created inside start() and invoke Myscript.func1() via Bridgetalk
xxx.addEventListener('click', function() {
var bt = new BridgeTalk();
bt.target = 'illustrator';
bt.body = 'Myscript.func1();'
bt.onError = function(err) {alert(err.body)};
bt.send();
});
The CEP panel uses evalScript to call Myscript.start() - this works fine
The buttons fail with "Myscript not found". I would assume that CEP panels use a different namespace or need a different target that code running from the scripting menu
Copy link to clipboard
Copied
The entire script must be passed in as a string to BridgeTalk, this is true even if you are using the script in Illustrator to talk only to Illustrator. You may be able to get the string of your Myscript script and bake it into the bt.body portion inside the scriptui event listener.
I created this script to do some exploration and I found some interesting results. I wanted to demonstrate the pattern of using one script to send to itself, along with two arguments: the name of a specific function inside the main function and an optional argument.
First I made the pallette window as usual and re-wrote for the 100th time a BT-calling function in a different way from some previous times. When I got the two buttons working and creating either a circle or square with hard-coded arguments, I wanted to make a simulation of how a CEP evalScript call could trigger a method in the script.
I knew that using Window.find it was possible to get a reference to a SUI 'window' in Illustrator and it was probably possible to even reach some properties which were added to it previously via custom code.
In the bottom of the dialog function I stuck in a property which contains the methods available for use within this script. However, I made a special different set of methods to be called externally because while the buttons are meant to demonstrate a controlled higer-level control over lower-level methods within the UI; external calls I wanted to ensure would be used with dynamic arguments. Using a for-in loop was disastrous as for some reason my Window.find call managed to always determine that my method of choice was 'makeACircle' while I was plainly sending in .makeASquare(). Even though I was not even using BT but rather calling the methods directly and it was miraculously working, I also wanted to record the result in the textbox on the pallette. And since this was done in an onResult method, at that point I created new methods 'callMakeACircle' and 'callMakeASquare' which wrapped the original BT-using onClick handlers, switched the onClick handlers to call the methods 'makeASquare' and 'makeACircle' with the hard-coded arguments and it worked!
But, why, and should it have worked? As simply using Window.find technique was launching the commands and affecting the document, couldn't we skip the whole BT thing anyway and have the onClick handlers do Window.find() to bypass it? It turns out that no, it cannot be skipped. The code running in this script must have a BT-enabled document-manipulation method running in UI control handlers. Running from an external JSX file (which is executing within Adobe Illustrator) and doing Window.find makes sense because this was a command launched not from the SUI palette but the back-end ExtendScript.
And now this brings me to one point, which is, why not just use CEP for the UI and redo all SUI things using browser HTML and javascript? No matter how complex the SUI is, normal HTML and things are much faster to make a really robust UI with. There's a million frameworks for HTML and CSS, but if you just find something basic and work with Flexbox for css positions, it's kind of like SUI. You can make containers that go row/column for orientation but unlike SUI you can make this change based on how wide the view gets as people resize your CEP panel.
Otherwise it's really doing more data transferring and executing in all these different places while CEP can just do evalScript and the UI is more reliable than our SUI.
//@target illustrator
//@targetengine main
function BTSample () {
"object"!=typeof JSON&&(JSON={}),function(){"use strict";function f(t){return 10>t?"0"+t:t}function quote(t){
return escapable.lastIndex=0,escapable.test(t)?'"'+t.replace(escapable,function(t){var e=meta[t];
return"string"==typeof e?e:"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+t+'"'}
function str(t,e){var n,r,o,f,u,i=gap,p=e[t];switch(p&&"object"==typeof p&&"function"==typeof p.toJSON&&(p=p.toJSON(t)),
"function"==typeof rep&&(p=rep.call(e,t,p)),typeof p){case"string":return quote(p);case"number":return isFinite(p)?String(p):"null";
case"boolean":case"null":return String(p);case"object":if(!p)return"null";if(gap+=indent,u=[],"[object Array]"===Object.prototype.toString.apply(p)){
for(f=p.length,n=0;f>n;n+=1)u[n]=str(n,p)||"null";return o=0===u.length?"[]":gap?"[\n"+gap+u.join(",\n"+gap)+"\n"+i+"]":"["+u.join(",")+"]",gap=i,o}
if(rep&&"object"==typeof rep)for(f=rep.length,n=0;f>n;n+=1)"string"==typeof rep[n]&&(r=rep[n],o=str(r,p),o&&u.push(quote(r)+(gap?": ":":")+o));
else for(r in p)Object.prototype.hasOwnProperty.call(p,r)&&(o=str(r,p),o&&u.push(quote(r)+(gap?": ":":")+o));return o=0===u.length?"{}":gap?"{\n"+gap+
u.join(",\n"+gap)+"\n"+i+"}":"{"+u.join(",")+"}",gap=i,o}}"function"!=typeof Date.prototype.toJSON&&(Date.prototype.toJSON=function(){
return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+
f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){
return this.valueOf()});var cx,escapable,gap,indent,meta,rep;"function"!=typeof JSON.stringify&&
(escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},JSON.stringify=function(t,e,n){var r;
if(gap="",indent="","number"==typeof n)for(r=0;n>r;r+=1)indent+=" ";else"string"==typeof n&&(indent=n);if(rep=e,
e&&"function"!=typeof e&&("object"!=typeof e||"number"!=typeof e.length))throw new Error("JSON.stringify");return str("",{"":t})}),
"function"!=typeof JSON.parse&&(cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
JSON.parse=function(text,reviver){function walk(t,e){var n,r,o=t[e];if(o&&"object"==typeof o)for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&
(r=walk(o,n),void 0!==r?o[n]=r:delete o[n]);return reviver.call(t,e,o)}var j;if(text=String(text),cx.lastIndex=0,cx.test(text)&&
(text=text.replace(cx,function(t){return"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})),
/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@")
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]")
.replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),"function"==typeof reviver?walk({"":j},""):j;
throw new SyntaxError("JSON.parse")})}();
// * One line to easily feed into BT call.
function hexDecode (str) { var r=''; for (var i = 0; i < str.length; i += 2){ r += unescape('%' + str.substr(i, 2));} return r; }
function hexEncode (str) {
var r = ''; var i = 0; var h; while (i < str.length) { h = str.charCodeAt(i++).toString(16); while (h.length < 2) { h = h; } r += h; } return r;
}
function getTrimmedError (errorMessage) {
var displayMessage = errorMessage;
if (displayMessage.length > 200) {
displayMessage = displayMessage.substring(0, 200);
}
return displayMessage;
}
function styleThePath (pathItem) {
pathItem.stroked = true;
pathItem.strokeWidth = 4;
pathItem.strokeColor = app.activeDocument.swatches.getByName("Black").color;
}
function checkDocument () {
if (app.documents.length == 0) {
throw new Error(BTSample.name + ": No documents are open.");
}
}
function makeACircle (xy) {
checkDocument();
var doc = app.activeDocument;
var x = xy[0]; // * Array argument example.
var y = xy[1];
var circle = doc.pathItems.ellipse(y, x, 60, 60);
styleThePath(circle);
return { circleResult : "A circle was made at [" + x + ", " + y + "]." };
}
function makeASquare (xyObj) {
checkDocument();
var x = xyObj.x;
var y = xyObj.y;
var doc = app.activeDocument;
var square = doc.pathItems.rectangle(y, x, 60, 60);
styleThePath(square);
return { squareResult : "A square was made at [" + x + ", " + y + "]." };
}
function checkMethod (methodName) {
if (!methodName || typeof methodName !== "string") {
throw new Error("Method is invalid");
}
var availableMethods = "|" + [
makeACircle.name,
makeASquare.name
].join("||") + "|";
if (availableMethods.indexOf("|" + methodName + "|") == -1) {
throw new Error("Method '" + methodName + "' is not among the available methods: [" + availableMethods.split("||").replace(/\|/g, "") + "].");
}
return true;
}
// * An object which is a dictionary of methods and their names. A method can be called by using its name and accessing this object.
const METHODS = {
"makeASquare" : makeASquare,
"makeACircle" : makeACircle,
};
/**
* The method which sends this entire script and running instructions via BridgeTalk.
* @Param {string} methodName - Method that will execute out of this script.
* @Param { Record<string, unknown> | any[] | boolean | string | number } arguments - An argument which will be serialized into JSON.
* @Param {(res: { body: string }) => void} [onResult] - Optional method to perform a follow-up action in the ScriptUI window.
*/
function makeBTCall (methodName, arguments, onResult) {
checkMethod(methodName);
var argString = "";
if (typeof arguments == "object" && arguments !== null) {
argString = JSON.stringify(arguments);
} else if (arguments instanceof Array) {
argString = JSON.stringify(arguments);
} else if (typeof arguments == "boolean") {
argString = JSON.stringify({ booleanValue : arguments });
} else if (typeof arguments == "string") {
argString = JSON.stringify({ stringValue : arguments });
} else if (typeof arguments == "number") {
argString = JSON.stringify({ numberValue : arguments });
}
if (argString) {
argString = "JSON.parse(hexDecode('" + hexEncode(argString) + "'))";
}
var callLine = BTSample.name + "('" + methodName + "'" + ((argString ? ", " + argString : "")) + ");";
var scriptString = hexDecode + "; eval(decodeURI(hexDecode('" + hexEncode(
encodeURI(
BTSample.toString() + "\n" +
callLine
)
) + "')));";
var bt = new BridgeTalk();
bt.target = "illustrator";
bt.onError = function (error) {
alert(getTrimmedError(error.body));
}
bt.onResult = onResult || function (result) {
alert(result.body);
}
bt.body = scriptString;
bt.send();
}
function start () {
var w = new Window("palette", BTSample.name);
var g1 = w.add("group");
var btn_1 = g1.add("button", undefined, "Action 1");
var btn_2 = g1.add("button", undefined, "Action 2");
function callMakeACircle (xyArray) {
makeBTCall(makeACircle.name, xyArray, function (res) {
var parsedBody = JSON.parse(res.body);
e_res.text = parsedBody.result.circleResult || parsedBody.result;
});
};
function callMakeASquare (xyObj) {
makeBTCall(makeASquare.name, xyObj, function (res) {
var parsedBody = JSON.parse(res.body);
e_res.text = parsedBody.result.squareResult || parsedBody.result;
});
};
btn_1.onClick = function () { callMakeACircle([0, 50]); }
btn_2.onClick = function () { callMakeASquare({ x : 50, y : 0 }); }
var g2 = w.add("panel", undefined, "Result:");
var e_res = g2.add("edittext", undefined, "");
e_res.characters = 20;
// * Amazingly, adding this to the window allows the methods to be accessed directly, from a separate JSX file.
// * This is not necessary when calling from CEP where the window-creating code is stored.
// * However, it is necessary to create this object and not assign a function in a loop of `METHODS` keys - then it acts weird and only does `makeACircle`.
w.addedCustomMethods = {
callMakeASquare : callMakeASquare,
callMakeACircle : callMakeACircle,
};
w.show();
}
// * When this script is called for any reason, this entry point decides on how it will be executed.
if (arguments.length == 1) {
if (arguments[0] == start.name) {
start();
}
// * Add any other methods that have no arguments to run them.
} else if (arguments.length == 2) {
var inputMethodString = arguments[0];
checkMethod(inputMethodString);
var inputArgObj = arguments[1];
var result;
try {
result = METHODS[inputMethodString](inputArgObj);
} catch (error) {
result = "ERR_:" + getTrimmedError(error.message);
}
return JSON.stringify({ result : result });
}
}
BTSample("start"); /* // * Cut this part out when using from CEP and just use the "start" as the evalScript method name (Ex: `evalScript("start")`). */
^^^ This is the main script.
//@targetengine main
// Window.find("palette", "BTSample").addedCustomMethods.callMakeASquare({ x : 400, y : -300 });
Window.find("palette", "BTSample").addedCustomMethods.callMakeACircle([200, -400]);
^^^ This is a simulation of how an external call would physically work. In fact, you could pass this as a string to the CEP evalScript method. But, all you would need to pass to evalScript is this: evalScript("BTSample('makeACircle', '" + JSON.stringify([120, 350]) + "')")
It theoretically should work running from the buttons in the SUI pallette, from a CEP evalScript (in these 2 different ways) and also using the Window.find technique from any JSX script that is ran in Illustrator.
Note that once the window is closed, there's no way to show it again unless you restart Illustrator. However, even while the window is closed it can still run the BT methods or regular methods when invoked with the Window.find technique, so externally-triggering the methods still add the shapes to the document. Although, obviously if SUI is doing anything to add text to the edittext input, it's hidden from view. However - even then, it would be possible to use the window reference in Window.find to iterate the window's childen or access other custom properties to gain references to the UI controls and their values or any variables kept running by this window object whether hidden or not.
Anyways, my advice is to just avoid BT here since CEP makes the UI easier and since you've gone down the path of CEP then might as well take advantage of all the benefits - unless your BT functions are indeed needed to go between Photoshop or something. In that case still, no need for SUI or any window techniques at all since a normal BT call to another app would still be viable using the regular evalScript CEP method.
Copy link to clipboard
Copied
Note, I used the word 'arguments' as an argument name. This actually didn't throw any complaints, but was not a good choice for a parameter name. I just did it in a hurry, and it was interesting to see going back over it that the reserved keyword 'arguments' was allowed to slide by without a hitch.
Copy link to clipboard
Copied
Hi,
many thanks for the thorough investigation. I will give sending the actual scripts (in my skeleton this would be calls to func1 with parameters) a quick try and, if that fails. make another CEP panel.
The sad thing; many small scripts can be done much quicker in extendscript and scriptui than in a mixed environment, in particular when there is a need to debug the extendscript side
BTW: with extendscript alone, creating the functions and then just sending a call via BT always seems to work in Illustrator (I have no experience with the other tools)
Copy link to clipboard
Copied
Hi, I came up with this recipe for a single (but possibly nested) function
var scrip = func1.toSource()
scrip = scrip.substr(1, scrip.length-2)
bt.body = scrip+';func1()'
Copy link to clipboard
Copied
I know, and I sure feel the same way. But if you *have* to go CEP, then hey..
But, yea I actually was doing it this way you are trying, 8 years ago and it was ok for the first couple of tries but it didn't end up panning out. I found it best to make the script such that it can run itself.
If it gets some argument, it will run itself one way and other arguments it will run itself another way.
So maybe toSource() is going to accomplish this goal, but it looks like when it's time to pass in arguments you will have to edit this string.
It will still work, so anything that is good and trusted by you and works for you is all and well.
However, at the end of the day, with a function that can run itself based on arguments it's possible to create SUI scripts that asynchronously work with external applications too.
It's very mechanical and I love this kind of thing. Like a Rube Goldberg machine.
Say, you start in Illustrator and run this script that puts a modal dialog. The user clicks something that does an excel calculation. The window disappears, but the jsx script writes a vbs script that has baked-in arguments from the UI, or just in the jsx script. When the VBS runs it will do its thing and following would write a JSX file that has baked-in arguments and guess what it will eval, it would eval the original JSX script and then UI dialog will pop back up and be filled with data that came from the arguments!
This isn't a case of Illustrator waiting on a VBS file, instead it's Illustrator executing VBS, going idle for user interaction, a user may start interacting with it.. and POP: up comes the modal window back up with new data.
^^This possible method is possible, I have used it to get web data via VBS. But, it's very annoying, so for web data situations I have switched to making CEP panels.