Skip to content

Instantly share code, notes, and snippets.

@joshbduncan
Created August 8, 2022 14:37
Show Gist options
  • Save joshbduncan/33a7da4011b7a174e3350be907c11553 to your computer and use it in GitHub Desktop.
Save joshbduncan/33a7da4011b7a174e3350be907c11553 to your computer and use it in GitHub Desktop.
Ai Command Palette Error Test - Excluded Menu Options
/*
Ai Command Palette
Copyright 2022 Josh Duncan
https://joshbduncan.com
This script is distributed under the MIT License.
See the LICENSE file for details.
*/
// TODO: add the ability to edit custom commands
//@target illustrator
var _title = "Ai Command Palette";
var _version = "0.1.0";
var _copyright = "Copyright 2022 Josh Duncan";
var _website = "joshbduncan.com";
var _github = "https://github.com/joshbduncan";
// setup main script variables
var data, dataFolder, dataFile, commandsData, allCommands, filteredCommands, result;
// ai command palette data object
data = {
commands: {
custom: {},
script: {},
action: getAllActions(),
defaults: {
"Command Palette Settings": {
cmdType: "defaults",
cmdActions: [{ type: "config", value: "paletteSettings" }],
},
},
menu: {},
config: {
"About Ai Command Palette...": {
cmdType: "config",
cmdActions: [{ type: "config", value: "about" }],
},
"Build Custom Command": {
cmdType: "config",
cmdActions: [{ type: "config", value: "buildCustomCommand" }],
},
"Load Script(s) Into Command Palette": {
cmdType: "config",
cmdActions: [{ type: "config", value: "loadScript" }],
},
"Show All Built-In Menu Commands": {
cmdType: "config",
cmdActions: [{ type: "config", value: "showBuiltInMenuCommands" }],
},
"Hide Built-In Command(s)": {
cmdType: "config",
cmdActions: [{ type: "config", value: "hideCommand" }],
},
"UnHide Built-In Command(s)": {
cmdType: "config",
cmdActions: [{ type: "config", value: "unhideCommand" }],
},
"Delete Palette Command(s)": {
cmdType: "config",
cmdActions: [{ type: "config", value: "deleteCommand" }],
},
"Reveal Preferences File": {
cmdType: "config",
cmdActions: [{ type: "config", value: "revealPrefFile" }],
},
},
},
settings: {
hiddenCommands: [],
version: _version,
},
};
// load user data if available and add to data object
dataFolder = setupFolderObject(Folder.userData + "/" + "JBD");
dataFile = setupFileObject(dataFolder, "AiCommandPalette.json");
loadUserData(dataFile);
// setup commands for palette
commandsData = buildCommands(data.commands);
allCommands = getObjectKeys(commandsData);
filteredCommands = filterHiddenCommands(allCommands);
// present the command palette
result = commandPalette(
(arr = filteredCommands),
(title = _title),
(bounds = [0, 0, 500, 182]),
(multiselect = false),
(filter = ["action", "menu", "config"])
);
if (result) processCommandActions(result, commandsData);
/**************************************************
COMMAND EXECUTION
**************************************************/
/** iterate over the actions for the picked command */
function processCommandActions(command, commandsObject) {
var cmdActions = commandsObject[command].cmdActions;
for (var i = 0; i < cmdActions.length; i++) {
executeCommandAction(cmdActions[i]);
}
}
/** execute `action` based on `action.type` */
function executeCommandAction(action) {
var type, f;
type = action.type;
switch (type.toLowerCase()) {
case "config":
try {
configAction(action.value);
} catch (e) {
alert("Error executing " + type + " command: " + action.value + "\n" + e);
}
break;
case "menu":
try {
app.executeMenuCommand(action.value);
} catch (e) {
alert("Error executing " + type + " command: " + action.value + "\n" + e);
}
break;
case "action":
try {
app.doScript(action.value.actionName, action.value.actionSet);
} catch (e) {
alert("Error executing action: " + action.value.actionName + "\n" + e);
}
break;
case "script":
f = new File(action.value.scriptPath);
if (!f.exists) {
alert(
"Script no longer exists.\nOriginal Path: " +
action.value.scriptPath +
"\n\nPLEASE NOTE: Deleted commands will longer work in any custom commands you previously created where they were used as a step."
);
delete data.commands[type]["Script: " + action.value.scriptName];
writeUserData(dataFile);
} else {
try {
$.evalFile(f);
} catch (e) {
alert("Error executing script: " + action.value.scriptName + "\n" + e);
}
}
break;
default:
alert("Sorry, " + type + " isn't a valid command type.");
}
try {
app.redraw();
} catch (e) {
$.writeln(e);
}
}
/**************************************************
CONFIGURATION OPERATIONS
**************************************************/
/** execute command palette configuration actions */
function configAction(action) {
var result;
var write = true;
switch (action) {
case "paletteSettings":
configPaletteSettings();
write = false;
break;
case "about":
aboutDialog();
break;
case "buildCustomCommand":
configBuildCustomCommand();
break;
case "loadScript":
configLoadScript();
break;
case "showBuiltInMenuCommands":
showBuiltInMenuCommands();
write = false;
break;
case "hideCommand":
configHideCommand();
break;
case "unhideCommand":
configUnhideCommand();
break;
case "deleteCommand":
configDeleteCommand();
break;
case "revealPrefFile":
dataFolder.execute();
write = false;
break;
default:
alert("Sorry, " + action + " isn't a valid configuration option.");
}
if (write) writeUserData(dataFile);
}
function aboutDialog() {
var win = new Window("dialog");
win.text = "Ai Command Palette " + _version;
win.alignChildren = "fill";
// script info
var pAbout = win.add("panel", undefined, "About");
pAbout.margins = 20;
pAbout.alignChildren = "fill";
var aboutText =
"If you have worked with Alfred app or VS Code you know how great the 'command palette' is... Well, I wanted that same functionality in Adobe Illustrator so here's what I've come up with.\
\
You can execute:\
- most any Illustrator Menu command\
- any actions from your Actions Palette\
- scripts from anywhere on your filesystem\
\
AND you can build custom commands that chain other commands together!";
var about = pAbout.add("statictext", [0, 0, 500, 150], aboutText, {
multiline: true,
});
// window buttons
var winButtons = win.add("group");
winButtons.orientation = "row";
winButtons.alignChildren = ["center", "center"];
var ok = winButtons.add("button", undefined, "OK");
ok.preferredSize.width = 100;
var cancel = winButtons.add("button", undefined, "Cancel");
cancel.preferredSize.width = 100;
// copyright info
var pCopyright = win.add("panel", undefined);
pCopyright.orientation = "column";
pCopyright.add("statictext", undefined, "Version " + _version + " " + _copyright);
var website = pCopyright.add("statictext", undefined, _website);
website.addEventListener("mousedown", function () {
openURL("https://" + _website);
});
win.show();
}
/** show all config commands in a palette */
function configPaletteSettings() {
var result = commandPalette(
(arr = getObjectKeys(data.commands.config)),
(title = "Palette Settings and Configuration"),
(bounds = [0, 0, 500, 182]),
(multiselect = false),
(filter = [])
);
if (result) processCommandActions(result, commandsData);
}
/** load external scripts into the command palette */
function configLoadScript() {
var files, f, fname;
var ct = 0;
var files = File.openDialog("Load Script File(s)", "", true);
if (files) {
for (var i = 0; i < files.length; i++) {
f = files[i];
fname = decodeURI(f.name);
if (f.name.search(".jsx$|.js$") >= 0) {
if (data.commands.script.hasOwnProperty("Script: " + fname)) {
if (
data.commands.script["Script: " + fname].cmdActions[0].value.scriptPath ==
f.fsName
) {
alert("Script " + fname + " already loaded at exact same file path!");
continue;
} else {
if (
!Window.confirm(
"Replace " + fname + " script that is already loaded?",
"noAsDflt",
"Script Load Conflict"
)
)
continue;
}
}
try {
data.commands.script["Script: " + fname] = {
cmdType: "script",
cmdActions: [
{
type: "script",
value: {
scriptName: fname,
scriptPath: f.fsName,
},
},
],
};
ct++;
} catch (e) {
alert("Error loading script: " + fname + "\nPath: " + f.fsName);
}
}
}
if (ct > 0) buildCommands(data.commands, true);
alert("Total scripts loaded: " + ct);
}
}
/** build custom commands with step from other commands */
function configBuildCustomCommand() {
var command;
var cmdActions = [];
result = customCommandsBuilder(
(arr = filterOutCommands(filteredCommands, ["config"])),
(title = "Custom Command Builder"),
(bounds = [0, 0, 500, 182]),
(multiselect = false)
);
if (result) {
try {
for (var i = 0; i < result.items.length; i++) {
command = result.items[i].text;
for (var a = 0; a < commandsData[command].cmdActions.length; a++) {
cmdActions.push({
type: commandsData[command].cmdActions[a].type,
value: commandsData[command].cmdActions[a].value,
});
}
}
data.commands.custom[result.name] = {
cmdType: "custom",
cmdActions: cmdActions,
};
} catch (e) {
alert("Error saving custom command!");
}
}
}
/** show all builtin menu commands in a palette */
function showBuiltInMenuCommands() {
result = commandPalette(
(arr = getObjectKeys(data.commands.menu)),
(title = "All Built-In Menu Commands"),
(bounds = [0, 0, 500, 182]),
(multiselect = false),
(filter = [])
);
if (result) processCommandActions(result, commandsData);
}
/** hide commands from the command palette results */
function configHideCommand() {
var commands, result;
var ct = 0;
commands = filterOutCommands(getObjectKeys(commandsData), [
"config",
"custom",
"script",
]);
if (commands.length > 0) {
result = commandPalette(
(arr = commands),
(title = "Select Menu Command(s) To Hide"),
(bounds = [0, 0, 500, 182]),
(multiselect = true),
(filter = [])
);
if (result) {
if (
Window.confirm(
"Hide Command(s)?\n" + result.join("\n"),
"noAsDflt",
"Confirm Command(s) To Hide"
)
) {
for (var i = 0; i < result.length; i++) {
data.settings.hiddenCommands.push(result[i].text);
ct++;
}
}
}
if (ct > 0) {
alert("Total commands hidden: " + ct);
}
} else {
alert("Sorry, there are no commands to hide.");
}
}
/** unhide commands from the users settings */
function configUnhideCommand() {
var result;
var ct = 0;
if (data.settings.hiddenCommands.length > 0) {
result = commandPalette(
(arr = data.settings.hiddenCommands),
(title = "Select Menu Command(s) To UnHide"),
(bounds = [0, 0, 500, 182]),
(multiselect = true),
(filter = [])
);
if (result) {
if (
Window.confirm(
"UnHide Command(s)?\n" + result.join("\n"),
"noAsDflt",
"Confirm Command(s) To UnHide"
)
) {
for (var i = 0; i < result.length; i++) {
for (var n = 0; n < data.settings.hiddenCommands.length; n++) {
if (result[i].text == data.settings.hiddenCommands[n]) {
data.settings.hiddenCommands.splice(n, 1);
ct++;
}
}
}
}
}
if (ct > 0) {
alert("Total commands unhidden: " + ct);
}
} else {
alert("Sorry, there are no commands to unhide.");
}
}
/** delete commands from the command palette */
function configDeleteCommand() {
var commands, result, cmdToDelete, type;
var ct = 0;
commands = filterOutCommands(getObjectKeys(commandsData), [
"defaults",
"config",
"action",
"menu",
]);
if (commands.length > 0) {
result = commandPalette(
(arr = commands),
(title = "Select Menu Commands To Delete"),
(bounds = [0, 0, 500, 182]),
(multiselect = true),
(filter = [])
);
if (result) {
if (
Window.confirm(
"Delete Command(s)?\n" +
"PLEASE NOTE: Deleted commands will longer work in any custom commands you previously created where they were used as a step.\n\n" +
result.join("\n"),
"noAsDflt",
"Confirm Command(s) To Delete"
)
) {
for (var i = 0; i < result.length; i++) {
cmdToDelete = result[i].text;
type = commandsData[cmdToDelete].cmdType;
try {
delete data.commands[type][cmdToDelete];
ct++;
} catch (e) {
alert("Couldn't delete command: " + cmdToDelete);
}
}
}
}
if (ct > 0) {
alert("Total commands deleted: " + ct);
}
} else {
alert("Sorry, there are no commands to delete.");
}
}
/**************************************************
USER DIALOGS (and accompanying functions)
**************************************************/
/** show the command palette to the user populated with commands from `arr` */
function commandPalette(arr, title, bounds, multiselect, filter) {
var q, filteredArr, matches, temp;
var visibleListItems = 9;
var frameStart = 0;
var win = new Window("dialog");
win.text = title;
win.alignChildren = "fill";
var q = win.add("edittext");
q.helpTip = "Search for commands, actions, and loaded scripts.";
q.active = true;
if (filter.length > 0) {
filteredArr = filterOutCommands(arr, filter);
} else {
filteredArr = arr;
}
var list = win.add("listbox", bounds, filteredArr, { multiselect: multiselect });
list.selection = 0;
// window buttons
var winButtons = win.add("group");
winButtons.orientation = "row";
winButtons.alignChildren = ["center", "center"];
var ok = winButtons.add("button", undefined, "OK");
ok.preferredSize.width = 100;
var cancel = winButtons.add("button", undefined, "Cancel");
cancel.preferredSize.width = 100;
// as a query is typed update the list box
q.onChanging = function () {
frameStart = 0;
q = this.text;
matches = q === "" ? filteredArr : scoreMatches(q, arr);
if (matches.length > 0) {
temp = win.add("listbox", list.bounds, matches, {
multiselect: list.properties.multiselect,
});
// close window when double-clicking a selection
temp.onDoubleClick = function () {
if (list.selection) win.close(1);
};
win.remove(list);
list = temp;
list.selection = 0;
}
};
/*
Move the listbox frame of visible items when using the
up and down arrow keys while in the `q` edittext.
One problem with this functionality is that when a listbox listitem
is selected via a script the API moves the visible "frame" of items
so that the new selection is at the top. This is not standard behavior,
and not even how the listbox behaves when you use the up and down keys inside
of the actual listbox.
Only works if multiselect if set to false.
*/
q.addEventListener("keydown", function (k) {
if (k.keyName == "Up") {
k.preventDefault();
if (list.selection.index > 0) {
list.selection = list.selection.index - 1;
if (list.selection.index < frameStart) frameStart--;
}
} else if (k.keyName == "Down") {
k.preventDefault();
if (list.selection.index < list.items.length) {
list.selection = list.selection.index + 1;
if (list.selection.index > frameStart + visibleListItems - 1) {
if (frameStart < list.items.length - visibleListItems) {
frameStart++;
} else {
frameStart = frameStart;
}
}
}
}
/*
If a selection is made inside of the actual listbox frame by the user,
the API doesn't offer any way to know which part of the list is currently
visible in the listbox "frame". If the user was to re-enter the `q` edittext
and then hit an arrow key the above event listener will not work correctly so
I just move the next selection (be it up or down) to the middle of the "frame".
*/
if (
list.selection.index < frameStart ||
list.selection.index > frameStart + visibleListItems - 1
)
frameStart = list.selection.index - Math.floor(visibleListItems / 2);
// move the frame by revealing the calculated `frameStart`
list.revealItem(frameStart);
});
// close window when double-clicking a selection
list.onDoubleClick = function () {
if (list.selection) win.close(1);
};
if (win.show() == 1) {
if (list.selection) {
return multiselect ? list.selection : [list.selection];
}
}
return false;
}
/** show the custom commands builder palette populated with commands from `arr` */
function customCommandsBuilder(arr, title, bounds, multiselect) {
var win = new Window("dialog");
win.text = title;
win.alignChildren = "fill";
// command search
var pSearch = win.add("panel", undefined, "Search For Commands");
pSearch.alignChildren = ["fill", "center"];
pSearch.margins = 20;
var q = pSearch.add("edittext");
q.helpTip = "Search for commands, actions, and loaded scripts.";
q.active = true;
var commands = pSearch.add("listbox", bounds, arr, { multiselect: multiselect });
commands.helpTip = "Double-click a command to add it as a custom step below.";
commands.selection = 0;
// custom command steps
var pSteps = win.add("panel", undefined, "Custom Command Steps");
pSteps.alignChildren = ["fill", "center"];
pSteps.margins = 20;
var steps = pSteps.add("listbox", bounds, [], { multiselect: false });
steps.helpTip = "Commands will run in order from top to bottom.";
var stepButtons = pSteps.add("group");
stepButtons.alignment = "center";
var up = stepButtons.add("button", undefined, "Move Up");
up.preferredSize.width = 100;
var down = stepButtons.add("button", undefined, "Move Down");
down.preferredSize.width = 100;
var del = stepButtons.add("button", undefined, "Delete");
del.preferredSize.width = 100;
// command name
var pName = win.add("panel", undefined, "Save Custom Command As");
pName.alignChildren = ["fill", "center"];
pName.margins = 20;
var name = pName.add("edittext", undefined, "");
name.enabled = false;
// window buttons
var winButtons = win.add("group");
winButtons.orientation = "row";
winButtons.alignChildren = ["center", "center"];
var ok = winButtons.add("button", undefined, "OK");
ok.preferredSize.width = 100;
ok.enabled = false;
var cancel = winButtons.add("button", undefined, "Cancel");
cancel.preferredSize.width = 100;
// as a query is typed update the list box
var matches, temp;
q.onChanging = function () {
q = this.text;
matches = q === "" ? arr : scoreMatches(q, arr);
if (matches.length > 0) {
temp = pSearch.add("listbox", commands.bounds, matches, {
multiselect: commands.properties.multiselect,
});
// add command when double-clicking
temp.onDoubleClick = function () {
if (commands.selection) {
steps.add("item", commands.selection);
name.enabled = true;
}
};
pSearch.remove(commands);
commands = temp;
commands.selection = 0;
cur = 0;
}
};
name.onChanging = function () {
if (name.text.length > 0) {
ok.enabled = true;
} else {
ok.enabled = false;
}
};
up.onClick = function () {
var n = steps.selection.index;
if (n > 0) {
swap(steps.items[n - 1], steps.items[n]);
steps.selection = n - 1;
}
};
down.onClick = function () {
var n = steps.selection.index;
if (n < steps.items.length - 1) {
swap(steps.items[n], steps.items[n + 1]);
steps.selection = n + 1;
}
};
del.onClick = function () {
if (steps.selection) {
steps.remove(steps.selection.index);
if (steps.items.length == 0) {
name.enabled = false;
ok.enabled = false;
}
}
};
// add command when double-clicking
commands.onDoubleClick = function () {
if (commands.selection) {
steps.add("item", commands.selection);
name.enabled = true;
}
};
/** swap listbox items in place */
function swap(x, y) {
var t = x.text;
x.text = y.text;
y.text = t;
}
if (win.show() == 1) {
var finalName = "Custom: " + name.text;
return { name: finalName, items: steps.items };
// TODO: check if custom name already exists
}
return false;
}
/**
* Score array items based on string match with query.
* @param {String} q String to search for withing `arr`
* @param {Array} arr String items to try and match.
* @returns {Array} Matching items sorted by score.
*/
function scoreMatches(q, arr) {
var word;
var words = [];
var scores = {};
var words = q.split(" ");
for (var i = 0; i < arr.length; i++) {
var score = 0;
for (var n = 0; n < words.length; n++) {
word = words[n];
if (word != "" && arr[i].match("(?:^|\\s)(" + word + ")", "gi") != null) score++;
}
if (score > 0) scores[arr[i]] = score;
}
return sortKeysByValue(scores, "score", "name");
}
/**
* Sort an objects key by their value.
* @param {Object} obj Simple object with `key`: `value` pairs.
* @returns {Array} Array of sorted keys.
*/
function sortKeysByValue(obj) {
var sorted = [];
for (var key in obj) {
for (var i = 0; i < sorted.length; i++) {
if (obj[key] > obj[sorted[i]]) break;
}
sorted.splice(i, 0, key);
}
return sorted;
}
/**************************************************
SUPPLEMENTAL FUNCTIONS
**************************************************/
/** build overall command object and list objects from `obj` */
function buildCommands(obj) {
var commandsData = {};
for (var commandsType in obj) {
for (var command in obj[commandsType]) {
commandsData[command] = obj[commandsType][command];
}
}
return commandsData;
}
/** get all commands that aren't of specific type or name */
function filterOutCommands(commandsArr, types) {
var filtered = [];
for (var i = 0; i < commandsArr.length; i++) {
if (!includes(types, commandsData[commandsArr[i]].cmdType))
filtered.push(commandsArr[i]);
}
return filtered;
}
/** filter out all commands hidden by user */
function filterHiddenCommands() {
var arr = [];
for (var i = 0; i < allCommands.length; i++) {
if (!includes(data.settings.hiddenCommands, allCommands[i]))
arr.push(allCommands[i]);
}
return arr;
}
/** get all currently installed action sets and actions */
function getAllActions() {
var currentPath, setName, actionCount, actionName;
var actions = {};
var pref = app.preferences;
var path = "plugin/Action/SavedSets/set-";
for (var i = 1; i <= 100; i++) {
currentPath = path + i.toString() + "/";
// get action sets
setName = pref.getStringPreference(currentPath + "name");
if (!setName) {
break;
}
// get actions in set
actionCount = Number(pref.getIntegerPreference(currentPath + "actionCount"));
for (var j = 1; j <= actionCount; j++) {
actionName = pref.getStringPreference(
currentPath + "action-" + j.toString() + "/name"
);
actions["Action: " + actionName + " [" + setName + "]"] = {
cmdType: "action",
cmdActions: [
{
type: "action",
value: {
actionSet: setName,
actionName: actionName,
},
},
],
};
}
}
return actions;
}
/** get all key from `obj` */
function getObjectKeys(obj) {
var keys = [];
for (var k in obj) {
keys.push(k);
}
return keys;
}
/** check to see if `arr` includes `q` */
function includes(arr, q) {
for (var i = 0; i < arr.length; i++) {
if (q === arr[i]) return true;
}
return false;
}
/** open `url` in browser */
function openURL(url) {
var html = new File(Folder.temp.absoluteURI + "/aisLink.html");
html.open("w");
var htmlBody =
'<html><head><META HTTP-EQUIV=Refresh CONTENT="0; URL=' +
url +
'"></head><body> <p></body></html>';
html.write(htmlBody);
html.close();
html.execute();
}
/**************************************************
FILE/FOLDER OPERATIONS
**************************************************/
/** load user saved preferences from disk at `f` and add to `data` object */
function loadUserData(f) {
var userData = {};
if (f.exists) {
userData = readJSONData(f);
for (var prop in userData) {
for (var subProp in userData[prop]) {
data[prop][subProp] = userData[prop][subProp];
}
}
}
return userData;
}
/** write user data to disk */
function writeUserData(f) {
var userData = {
commands: {
custom: data.commands.custom,
script: data.commands.script,
},
settings: data.settings,
};
writeJSONData(userData, f);
}
/** setup a new folder object at `path` and create if doesn't exists */
function setupFolderObject(path) {
var folder = new Folder(path);
if (!folder.exists) folder.create();
return folder;
}
/** setup a new file object with name `fName` at `folderPath` */
function setupFileObject(folderPath, fName) {
return new File(folderPath + "/" + fName);
}
/** read ai "json-like" data from file `f` */
function readJSONData(f) {
var json, obj;
try {
f.encoding = "UTF-8";
f.open("r");
json = f.read();
f.close();
} catch (e) {
alert("Error loading " + f + " file!");
}
obj = eval(json);
return obj;
}
/** write ai "json-like" data in `obj` to file `f` */
function writeJSONData(obj, f) {
var data = obj.toSource();
try {
f.encoding = "UTF-8";
f.open("w");
f.write(data);
f.close();
} catch (e) {
alert("Error writing file " + f + "!");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment