Action recording for UXP plugins

Action recording for UXP plugins is now available in Ps Beta. With this new feature you can make functions from your UXP plugin recordable in Actions.

https://developer.adobe.com/photoshop/uxp/2022/ps_reference/media/action-recording/
https://developer.adobe.com/photoshop/uxp/2022/guides/uxp_guide/uxp-misc/manifest-v4/photoshop-manifest/#enablemenurecording

Try out the menu recording (remember to enable in the manifest) and the direct call, recordAction.

9 Likes

Thanks!!!

Been waiting for this :slight_smile:

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 :slight_smile:

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.

@samgannaway

I’ve added the action recordability into 2 plugins. It works, except I have discovered 2 bugs.

  1. 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.

  2. 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?

Yes, we have a fix for the first one already. That should be in Beta soon.

I have logged the second. You are likely correct in your assessment.

Awesome, thanks.

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???

If using webpack, does anyone know where and how to write actionHandler?

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);
    }
});
1 Like

Thank you, it worked fine. This part was the point.

//adding function in global scope.
window.actionHandler = actionHandler;

I’m experimenting with this but am not making much progress after 3 hours, so better ask questions of the experts.

  1. 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()  
};
  1. 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:

  1. User presses the button, makeLayerClickHandler (formerly runCode) runs.
  2. makeLayerClickHandler calls recordAction({name: "mL", methodName: 'makeLayer'}) to register a step with actively recording Action.
  3. 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.

Sam

1 Like

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.