Move Layer to Front through batch-play works differently in PS 23.4.1

Hi,

I was using move “layer to front” in batch-play in many functions, when I wanted to make sure that the layer or the group created by the script will be on top.
I had no issue till the latest Photoshop update 23.4.1. I didn’t have to check if there is another layer on top of the one created.

Now I’m getting an error The command “Move” is not currently available and the batch-play script stops, if there is no other layer in the image.
I understand that this is normal behaviour but I’ve based many of my scripts on the fact that no matter if the move was possible or not, I could include it in the batch-play.

Do you see any simple workaround for that ?

Thanks!

                {
                    _obj: "move",
                    _target: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "targetEnum"
                    },
                    to: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "front"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                }

Get the top-most layer index and move to that index. So far I found it’s the only more or less reliable way to do this.

1 Like

I use the following:

function menuCommand(id) {
    require('photoshop').core.performMenuCommand({
      commandID: id,
      kcanDispatchWhileModal: true,
      _isCommand: false
    });
}

async function move_layer_top(){
    await require("photoshop").core.executeAsModal(function(){menuCommand(2711);});
}
2 Likes

here a way to check if the active layer is member of a group and move it complete to top:

function menuCommand(id) {
    require('photoshop').core.performMenuCommand({
      commandID: id,
      kcanDispatchWhileModal: true,
      _isCommand: false
    });
}

async function check_if_in_group(){
    const batchPlay = require("photoshop").action.batchPlay;
    const result = await batchPlay(
    [
       {
          _obj: "get",
          _target: [
             {
                _ref: "layer",
                _enum: "ordinal",
                _value: "targetEnum"
             }
          ],
          _options: {
             dialogOptions: "dontDisplay"
          }
       }
    ],{
       synchronousExecution: false
    });
    const pinned = result[0].parentLayerID;
    return pinned;
}

async function move_layer_top(){
    if (check_if_in_group != -1){
        await require("photoshop").core.executeAsModal(function(){menuCommand(2711);});
    }
    await require("photoshop").core.executeAsModal(function(){menuCommand(2711);});
}
1 Like

Hi @Karmalakas ,

That’s a very good solution but I don’t know how to use it within bigger batchplay code. Do you maybe have any example?

Thank you!

Hi @assitburn

This looks great, do you maybe know how to use it when “move” have to be done in the middle of bigger batchplay code? I’m really struggling with this.

Thanks!

At risk of missing the point/teaching you to suck eggs:
You can chain batchplay descriptors by passing them to batchplay in an array.

In order lo learn something, could you please give me an example how to do this?

Thanks!

This is a bit dirty as I’m on my phone in the garden!

batchPlay(
    [
       {
          _obj: "get",
          _target: [
             {
                _ref: "layer",
                _enum: "ordinal",
                _value: "targetEnum"
             }
          ],
          _options: {
             dialogOptions: "dontDisplay"
          }
       },
       {
          // another descriptor
       },
       {
          // And another
        }
    ]
)

Thank you @Timothy_Bennett, that’s something I’m already using but the current scenario is:

batchPlay(
    [
       {
          // first descriptor
       },
       {
          // another descriptor
       },

// function placed in the middle of batchplay descriptors (for example the one mentioned by @assitburn, calling menu item and checking if layer is in the group). That's the tricky part for me.

       {
          // And another
        }
    ]
)

So in general, my question would be how to break batchplay descriptors with another function.

Thanks!

Ok I get you; as far as I’m aware that would have to be two separate batchplay calls.

It might be unorthodox, but I’m grabbing my batchplay arrays, and stitching them together like so, which means I can mix them up.

const myBatchPlay1 = [ array in here]
const myBatchPlay2 = [ array in here]
const myBatchPlay3 = [ array in here]
finalBatchPlayRunObject = [...myBatchPlay1, ...myBatchPlay2, ...myBatchPlay3]

Then call batchPlay to run it passing in the combined variable:

require("photoshop").action.batchPlay(finalBatchPlayRunObject)

The final combined batch play looks like:

2 Likes

You’d need to do at least 2 BP calls. What I do:

// Get top-most layer
const topLayer = doc.layers[0]

// Get index of that layer
const topIndex = batchPlay(
    {
        _obj: 'get',
        _target: [
            {_property: "itemIndex"},
            {_ref: 'layer', _id: topLayer.id},
            {_ref: 'document', _id: docId}
        ],
        _options: {dialogOptions: 'dontDisplay'}
    },
    {
        synchronousExecution: true,
        modalBehavior: 'execute'
    }
)[0].itemIndex

// Move other layer to that index
executeAsModal(async () => {
    batchPlay(
        {
            _obj: 'move',
            _target: [{_ref: 'layer', _enum: 'ordinal', _value: 'targetEnum'}],
            to: {_ref: 'layer', _index: topIndex},
            adjustment: false
        },
        {
            synchronousExecution: true,
            modalBehavior: 'execute'
        }
    )
}, {"Command name"})

This moves selected layer to top.

NOTE: This is a very simplified example built by gathering pieces of my own plugin code. This example has no checks for properties existence or errors. And I didn’t test it as a standalone code, so there might be minor issues, but idea should be clear I hope

1 Like

hi @Karmalakas

Thanks so much! It looks valid but somehow I can’t implement it in my plugin :frowning:

I’m not sure what I’m doing wrong. For sure, I’m running batchplay in different way. I’m attaching simple function below with marking, where the move should be done. Maybe you’ll notice something that I can’t. It’s just one of many functions where I have to redo the “move” part, sometimes it happens at the beginning of function, sometimes close to the end.
I would be very grateful If you could explain me how to merge your part with mine.

Additional question is if it’s possible to have one history state for the function if batchplay will be divided into two. I’m usually using “historyStateInfo” to hide the steps of batchplay in PS’s history.

Thank you!

async function dustAndScratches() {

    const docexists = () => {
        return Boolean(app.documents?.length)
      }
      const exists = docexists()
     
      
if (exists === true) {

    await core.executeAsModal(() => {

        batchPlay(
            [{
                    _obj: "make",
                    _target: {
                        _ref: "layer"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
// here the move starts
                {
                    _obj: "move",
                    _target: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "targetEnum"
                    },
                    to: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "front"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "move",
                    _target: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "targetEnum"
                    },
                    to: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "front"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
// and ends here (it was done twice in case of groups)
                {
                    _obj: "mergeVisible",
                    duplicate: true,
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },

                {
                    _obj: "set",
                    _target: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "targetEnum"
                    },
                    to: {
                        _obj: "layer",
                        name: "Dust and Scratches"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "select",
                    _target: {
                        _ref: "menuItemClass",
                        _enum: "menuItemType",
                        _value: "view200Percent"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "dustAndScratches",
                    _options: {
                        dialogOptions: "display"
                    }
                },
                {
                    _obj: "make",
                    new: {
                        _class: "channel"
                    },
                    at: {
                        _ref: "channel",
                        _enum: "channel",
                        _value: "mask"
                    },
                    using: {
                        _enum: "userMaskEnabled",
                        _value: "revealAll"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "invert",
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "select",
                    _target: {
                        _ref: "channel",
                        _enum: "channel",
                        _value: "mask"
                    },
                    makeVisible: false,
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "select",
                    _target: {
                        _ref: "paintbrushTool"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "reset",
                    _target: {
                        _ref: "color",
                        _property: "colors"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "select",
                    _target: {
                        _ref: "menuItemClass",
                        _enum: "menuItemType",
                        _value: "fitOnScreen"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                }
            ], {
                "historyStateInfo": {
                    "name": "Dust and Sctartches",
                    "target": {
                        "_ref": "document",
                        "_enum": "ordinal",
                        "_value": "targetEnum"
                    }
                }
            });
    })
}
else {  PhotoshopCore.showAlert({message: 'Open the document first'});}
}
document.querySelector("#btndustAndScratches").addEventListener("click", dustAndScratches);

What exactly is not working? Do you get an error, unexpected behavior or is simply nothing happening?
If nothing happens and you don’t have an error, always make sure to log the batchPlay result and also wrap it in a try/catch, that gives you a hint often times.

it was done twice in case of groups

That’s not really reliable, what about multi-level nesting? (group inside group etc.)
In general, moving to a specific index is the most reliable option, but beware that having a background layer or not will shift the indices by 1.


Regarding history suspension: Since the addition of modal execution, we can now do whatever we want and wrap it inside one history step. Before that, every batchPlay (with N descriptors) would result in one history step each. Here’s the documentation:
https://developer.adobe.com/photoshop/uxp/2022/ps_reference/media/executeasmodal/#history-state-suspension

I recommend writing some helper functions for code you might reuse alot. For example, that’s my helper for modal execution / history suspension:

export async function suspendHistory(fn, historyName = "(Plugin command)", commandName = "Waiting for plugin command...") {
  async function executeAsModal(executionContext) {
    const hostControl = executionContext.hostControl;
    const suspensionID = await hostControl.suspendHistory({
        "historyStateInfo": {
            "name": historyName,
            "target": { _ref: 'document', _enum: 'ordinal', _value: 'targetEnum' }
        }
    });
    try {
      fn(executionContext);  
    } catch(e) {console.error(e)}
   await hostControl.resumeHistory(suspensionID);
  }
  await require("photoshop").core.executeAsModal(executeAsModal, {commandName});
}

The function you pass to executeAsModal (or in the above code to suspendHistory) can have as many batchPlay calls as you want.

For example:


suspendHistory(async () => {
  const result1 = await batchPlay([
    descriptor1(),
    descriptor2(),
    descriptor3(),
  ], {});

  const something = parseBpResult(result1);
  
  const result2 = await batchPlay([
    someGetterBasedOn(something)
  ],{});

  await batchPlay([
    doMoreStuffBasedOn(result2)
  ], {});
}, "Just one step")

This allows to do things like creating new layers and having those new layers selected at the end of the script. Something like:

suspendHistory(async () => {
  const result = await batchPlay([
    __duplicateLayer(),
    __duplicateLayer(),
    __duplicateLayer()
  ], {})
  const newIds = result.map(d => d.ID?.[0]).filter(Boolean)
  batchPlay([
    __selectByIDs(newIds)
  ],{})
}, `Duplicate and Select`)
2 Likes

Hi @simonhenke,

Thank you so much for your help, I really appreciate it!

What exactly is not working? Do you get an error, unexpected behavior or is simply nothing happening?

Unfortunately nothing happened. I didn’t get an error in console as well.
I’m not a developer so I’m doing many things by tests and tries but I’m lacking a lot of basic knowledge :frowning: I’m trying to constantly learn new things and this community (including you) is helping me al lot.
In this case I suppose that it could be a matter of my mistakes in merging my function with @Karmalakas code.
I’ve noticed that we’re calling executeAsModal in different way.

I’m using

await core.executeAsModal(() => {
batchPlay(
            [

and @Karmalakas :

executeAsModal(async () => {
    batchPlay(

I’m also not using this part in my descriptors:

 synchronousExecution: true,
 modalBehavior: 'execute'

Is this the difference in API / manifest version or these methods can be used interchangeably?

I’ve tried to split my batchplay into two and to place @Karmalakas code in the middle like shown below but it didn’t work.


async function dustAndScratches() {

    const docexists = () => {
        return Boolean(app.documents?.length)
      }
      const exists = docexists()
     
      
if (exists === true) {

    await core.executeAsModal(() => {

        batchPlay(
            [{
                    _obj: "make",
                    _target: {
                        _ref: "layer"
                    },
                    layerID: 222,
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                }
            ], {
                "historyStateInfo": {
                    "name": "Dust and Sctartches pt1",
                    "target": {
                        "_ref": "document",
                        "_enum": "ordinal",
                        "_value": "targetEnum"
                    }
                }
            });
// added code


// I guess that this line was missing (you probably had it specified globally):
const doc = app.activeDocument;

// Get top-most layer
const topLayer = doc.layers[0]

// Get index of that layer
const topIndex = batchPlay(
    {
        _obj: 'get',
        _target: [
            {_property: "itemIndex"},
            {_ref: 'layer', _id: topLayer.id},
            {_ref: 'document', _id: docId}
        ],
        _options: {dialogOptions: 'dontDisplay'}
    },
    {
        synchronousExecution: true,
        modalBehavior: 'execute'
    }
)[0].itemIndex

// Move other layer to that index
executeAsModal(async () => {
    batchPlay(
        {
            _obj: 'move',
            _target: [{_ref: 'layer', _enum: 'ordinal', _value: 'targetEnum'}],
            to: {_ref: 'layer', _index: topIndex},
            adjustment: false
        },
        {
            synchronousExecution: true,
            modalBehavior: 'execute'
        }
    )
    // you had here:
    //  }, {"Command name"})
    // but I was getting an error - ':' expected. so I've replaced it with:
    }, {
    "historyStateInfo": {
        "name": "Dust and Sctartches pt2",
        "target": {
            "_ref": "document",
            "_enum": "ordinal",
            "_value": "targetEnum"
        }
    }
        })


// end of added code

            batchPlay(
                [
                {
                    _obj: "move",
                    _target: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "targetEnum"
                    },
                    to: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "front"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "move",
                    _target: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "targetEnum"
                    },
                    to: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "front"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "mergeVisible",
                    duplicate: true,
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },

                {
                    _obj: "set",
                    _target: {
                        _ref: "layer",
                        _enum: "ordinal",
                        _value: "targetEnum"
                    },
                    to: {
                        _obj: "layer",
                        name: "Dust and Scratches"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "select",
                    _target: {
                        _ref: "menuItemClass",
                        _enum: "menuItemType",
                        _value: "view200Percent"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "dustAndScratches",
                    _options: {
                        dialogOptions: "display"
                    }
                },
                {
                    _obj: "make",
                    new: {
                        _class: "channel"
                    },
                    at: {
                        _ref: "channel",
                        _enum: "channel",
                        _value: "mask"
                    },
                    using: {
                        _enum: "userMaskEnabled",
                        _value: "revealAll"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "invert",
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "select",
                    _target: {
                        _ref: "channel",
                        _enum: "channel",
                        _value: "mask"
                    },
                    makeVisible: false,
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "select",
                    _target: {
                        _ref: "paintbrushTool"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "reset",
                    _target: {
                        _ref: "color",
                        _property: "colors"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                },
                {
                    _obj: "select",
                    _target: {
                        _ref: "menuItemClass",
                        _enum: "menuItemType",
                        _value: "fitOnScreen"
                    },
                    _options: {
                        dialogOptions: "dontDisplay"
                    }
                }
            ], {
                "historyStateInfo": {
                    "name": "Dust and Sctartches pt3",
                    "target": {
                        "_ref": "document",
                        "_enum": "ordinal",
                        "_value": "targetEnum"
                    }
                }
            });
    })
}
else {  PhotoshopCore.showAlert({message: 'Open the document first'});}
}
document.querySelector("#btndustAndScratches").addEventListener("click", dustAndScratches);

Your suggestion on history suspension is great, I’ll have to test it as well. It’s a shame though that each BatchPlay instance within one executeAsModal needs to have own history state.

Thanks!

I can confirm 23.4.1 broke BP behavior with multiple descriptors. This completely broke my plugin. It seems even defining synchronousExecution: true performs commands defined in a single descriptors array asynchronously. Looks like some descriptors defined at the end finish before the ones defined at the beginning.

I have a button with basically 3 descriptors in BP:

[
    {
        "_obj": "select",
        "_target": [
            {
                "_ref": "layer",
                "_id": 4
            }
        ],
        "makeVisible": false
    },
    {
        "_obj": "make",
        "new": {
            "_class": "channel"
        },
        "at": {
            "_ref": "channel",
            "_enum": "channel",
            "_value": "mask"
        },
        "using": {
            "_enum": "userMaskEnabled",
            "_value": "revealAll"
        }
    },
    {
        "_obj": "select",
        "_target": [
            {
                "_ref": "layer",
                "_id": 4
            }
        ],
        "makeVisible": false
    }
]

And it’s behavior now is completely random. Most of the time I get:

The command “Make” is not currently available.

But sometimes I get mask created. All I need to do is just keep clicking the button (maybe 1/20 times it works, maybe even less)… So in this case it looks like it tries to create a mask while layer is still not selected

IMO it’s another new bug

Ping @kerrishotts

Same happens on 23.5.0

My plugin is fully broken too :frowning: I’m trying to find the solution but without luck, no matter what I do, I’m getting errors.

Whenever you’re using await in a function, that function has to be async.

You don’t need the modalBehvaior property, it’s default value has always worked fine for me.
If you have synchronousExecution:true, you don’t need await.

It doesn’t, check my reply again.

Interesting, I also updated to 23.4.1 recently before migrating my next plugin and haven’t had any issues so far, although I have batchPlays with ~50-100 descriptors. Maybe it’s a specific event that’s causing the issue?

I’ll run your code example later to see if I get the same results.

I try to debug and see what’s happening, but can’t figure out. I changed button code to split commands for BP, but no luck. Basically I put this into my plugin:

require('photoshop').core.executeAsModal(async () => {
 let result = require("photoshop").action.batchPlay(
  [
   {
    "_obj": "select",
    "_target": [
        {
            "_ref": "layer",
            "_id": 2
        }
    ],
    "makeVisible": false
   }
  ], {
        synchronousExecution: true,
        modalBehavior: "execute"
    });
  console.log("BP1", result)

  result = require("photoshop").action.batchPlay(
  [
   {
    "_obj": "make",
    "new": {
        "_class": "channel"
    },
    "at": {
        "_ref": "channel",
        "_enum": "channel",
        "_value": "mask"
    },
    "using": {
        "_enum": "userMaskEnabled",
        "_value": "revealAll"
    }
   }
  ], {
        synchronousExecution: true,
        modalBehavior: "execute"
    });
  console.log("BP2", result)
}, {"commandName": "test"})

And it works fine. But if I do pretty much the same while clicking a button, I get

The command “Make” is not currently available.

Will continue debugging to figure what’s preventing creating a mask (I have more actions happening before mask creation, but don’t see how they can impact the command). I’m really pissed that almost every Ps update breaks something