I just added this to a plugin and it worked flawlessly!
I wrapped the require(âphotoshopâ).action.recordAction line inside a try/catch. That way it will run on the beta release with action recording but not throw an error in the current public release.
I have 3 plugins I am going to release updates for next week with this. My users have been asking for this for a long time so thanks
In the future, one thing that would be nice to have if possible would be for the action step to display the object properties. That way the user can see the plugin settings that were used when the action was recorded. Either way, Iâm still super happy having the feature though.
Iâve added the action recordability into 2 plugins. It works, except I have discovered 2 bugs.
If you double click on the action step itself inside the action, it will process the step. However, after it runs the step, the dropdown from the step goes away and then it just says âScript Actionâ in the step. After that, the step wonât play back anymore.
Running an action that has a step recorded with a UXP plugin wonât play back when running the action from another UXP plugin. I know there was a block in place when trying to run actions that played a JSX script. This seems to do the same thing with actions that have a UXP plugin step.
I have an automation plugin called âHot Folderâ that plays actions. Many of my users would like to record some of my plugins into actions and process with the Hot Folder plugin. Is there some special permission that I need to grant in the manifest file in order for a plugin to be able to play actions that have a recorded UXP plugin step?
For the second, I am wondering if the executeAsModal from the Hot Folder batching plugin is blocking the action step with the recorded UXP plugin from running???
how about this one.
adding a function in global scope.
import { app, action } from "photoshop";
/**
* declaring a function here
* info object is passed from recordAction parameter.
*/
async function actionHandler(executionContext, info) {
try {
console.log("info", info); // {prop: "value" }
const lay = await app.activeDocument.createLayer();
lay.name = "new layer"';
return {"newProp": "newValue"};
} catch (e) {
console.log(e);
}
}
//adding function in global scope.
window.actionHandler = actionHandler;
// you can see global object has actionHandler function.
console.log(window, "window");
// just registering a function on a button which has "action" id
document.getElementById("action").addEventListener("click", () => {
try {
console.log("clicked");
console.log(action);
// recording function as an action.
action.recordAction(
{"name": "My Command", "methodName": "actionHandler"}, {"prop": "having a good pint"}
);
} catch (e) {
console.log(e);
}
});
Iâm experimenting with this but am not making much progress after 3 hours, so better ask questions of the experts.
I want my UXP plugin button id="buttonRUNCODE" to not only record an action of what the button is intended to do (create a new layer), but I also want it to actually do that action (create a new layer) when the button is clicked. The code I came up with for that based on this thread is listed below. I basically just added my code for creating a new layer after the try/catch for recording the action. This doesnât feel right, but itâs as far as I got.
const app = require('photoshop').app;
const action = require('photoshop').action;
async function actionHandler(executionContext, info) {
try {
console.log("info", info); // {prop: "value" }
const lay = await app.activeDocument.createLayer();
lay.name = "new layer";
return {"newProp": "newValue"};
} catch (e) {
console.log(e);
}
}
//adding function in global scope.
window.actionHandler = actionHandler;
// you can see global object has actionHandler function.
console.log(window, "window");
async function actionHandler(executionContext, info) {
try {
console.log("info", info); // {info: "value" }
const lay = await app.activeDocument.createLayer();
lay.name = "new layer";
return {"newProp": "newValue"};
} catch (e) {
console.log(e);
}
}
//adding function in global scope.
window.actionHandler = actionHandler;
// you can see global object has actionHandler function.
console.log(window, "window");
document.getElementById("buttonRUNCODE").addEventListener("click", runCode);
async function runCode() {
try {
console.log("clicked");
console.log(action);
// recording function as an action.
action.recordAction(
{"name": "My Command", "methodName": "actionHandler"}, {"info": "having a good pint"}
);
} catch (e) {
console.log(e);
}
//My code for creating a new layer starts here
try {await exeModal(targetFunction, {"commandName": "Progress...", "interactive": true});}
catch(e) {
if (e.number == 9) {showAlert("Some other plugin is using Photoshop!");}
else {showAlert(e)}
}
async function targetFunction(executionControl) {
let hostControl = executionControl.hostControl;
let documentID = await app.activeDocument._id;
let suspensionID = await hostControl.suspendHistory({ "historyStateInfo": { "name": "Run Code", "target": [{ _ref: "document", _id: documentID }] } });
///////////////////// Start modal execution /////////////////////
await batchPlay([
//Make New Layer
{"_obj": "make","_target": [{"_ref": "layer"}]},
],{});
console.log("Run Code button clicked")
///////////////////// Stop modal execution /////////////////////
await hostControl.resumeHistory(suspensionID);
} // end targetFunction()
};
I also want to create another button to delete a layer, but am totally lost how to do that. The actionHandler(executionContext, info) function is already coded to create a new layer. How can I get it to now delete a layer using the actionHandler(executionContext, info) function? Iâm thinking there must be some way of passing executionContext to the actionHandler(executionContext, info) function, but I canât figure out how to do it, but maybe Iâm going down the wrong rabbit hole.
Hope that makes sense. Any help would be appreciated.
At a high level, youâll need to plan how you want to organize the global function(s) that will be recorded âaction handlersâ.
One that uses values in the argument object to route to the proper control logic (e.g., make or delete)
One function per task.
In your code above, you used actionHandler, following the documentation example.
Going down the one-per route and renaming functions:
User presses the button, makeLayerClickHandler (formerly runCode) runs.
makeLayerClickHandler calls recordAction({name: "mL", methodName: 'makeLayer'}) to register a step with actively recording Action.
makeLayerClickHandler now calls makeLayer which does the DOM or batchPlay call.
For the above you would need window.makeLayer = makeLayer to make it available for Action play.
To do another setup for delete, you can replicate the above structure just swapping out âmakeâ for âdeleteâ. I see that have some abstraction for dealing with executeAsModal. You could even pull that try/catch out to separate function to reuse, as well.
Lastly, you donât need return {"newProp": "newValue"}; unless you want to take advantage of re-recording.
Thanks @samgannaway. Very helpful. I wanted to keep everything within suspended history, so the code below is what I came up with. This way, whether the API/batchPlay is run via a UXP panel button click or from a Ps action, it creates only one history state.
Not entirely sure this is the best way to do this, but it appears to work. Suggestions always appreciated.
//adding function in global scope.
window.myNewLayer = myNewLayer;
async function myNewLayer(executionContext, info) {
//It appears "executionContext" variable doesn't get used. exeModal() function immediately switches to "executionControl", which maybe does the same thing.
try {await exeModal(targetFunction, {"commandName": "Progress...", "interactive": true});}
catch(e) {
if (e.number == 9) {showAlert("Some other plugin is using Photoshop!");}
else {showAlert(e)}
}
async function targetFunction(executionControl) {
let hostControl = executionControl.hostControl;
let documentID = await app.activeDocument._id;
let suspensionID = await hostControl.suspendHistory({ "historyStateInfo": { "name": info.historyStateName, "target": [{ _ref: "document", _id: documentID }] } });
///////////////////// Start modal execution /////////////////////
try {
const lay = await app.activeDocument.createLayer();
lay.name = info.MyLayerName;
return {"newProp": "newValue"};
} catch (e) { console.log(e); }
///////////////////// Stop modal execution /////////////////////
await hostControl.resumeHistory(suspensionID);
} // end targetFunction()
};
// you can see global object has myNewLayer function.
console.log(window, "window");
document.getElementById("buttonACTIONFROMUXP").addEventListener("click", actionFromUXP);
async function actionFromUXP() {
try {
// record function as an action and pass required variables.
action.recordAction(
{"name": "New Layer", "methodName": "myNewLayer"}, {"MyLayerName": "New Layer from action", "historyStateName": "Run My Action"}
);
} catch (e) { console.log(e); }
//Run the action associated with the button when the button with id="buttonACTIONFROMUXP" is clicked and pass the necessary variables
//First variable is an empty object but could potentially be: {"mode":"action","uiMode":"never","hostControl":{}}
await myNewLayer({}, {"MyLayerName": "New Layer from click", "historyStateName": "New Layer From Click"});
};
Just noticed this as well and hope it can eventually be fixed. UXP plugins can be great at helping to organize a messy Photoshop Actions panel into a streamlined workflow-based list of actions. But not being able to run actions recorded from UXP plugins sort of defeats this strategy.
I also noticed I can run batchPlay that changes the image and Photoshop from actions recorded from UXP plugins WITHOUT enclosing the batchPaly in an executeAsModal() function. Not sure of the significance of this or why itâs even possible. Also wondering if this might change in the future.