Converting colors now has to be done in modal scope?

Prior to PS 23.4.1, we could just use the following descriptor to convert between color spaces:

{
    _obj: "convertColorToSpace",
    _target: { _ref: "application" },
    color,
    colorSpace: {
      _enum: "colorSpace",
      _value: colorSpace
    }

After the update, it now results in

convertColorToSpace may modify the state of Photoshop. Such events are only allowed from inside a modal scope

Is it a final decision that this now has to happen inside a modal scope, or did that happen on accident?
I don’t think it modifies the PS state (similar to a getter) :thinking:

Interesting, I just wrap all my code in a ExecuteAsModal model and don’t bother :joy: as I am still learning coding by doing :grinning:

Yes, that what I had to do here as a fix. However, it’s not really pretty, since executeAsModal runs the code you pass in the callback and returning something from a callback (to the outer function) requires you to return a promise instead which you can resolve in the callback. Here’s the new code:

async function convertColorSpace(color, colorSpace) {
  return new Promise((resolve) => {
    photoshop.core.executeAsModal(async() => {
      resolve(await photoshop.action.batchPlay([
        {
          _obj: "convertColorToSpace",
          _target: { _ref: "application" },
          color,
          colorSpace: {
            _enum: "colorSpace",
            _value: colorSpace
          }
        }
      ],{})[0].color)
    })
  })
}

As a consequence of that, I had to also make all other helper functions async that called this function and so on…

How exactly are you or anybody else doing that? I really find it annoying for debugging.

For example I can’t just set my layer visibility to false with:

const app = require('photoshop').app;
app.activeDocument.activeLayers[0].visible = false

Instead I am doing:

const app = require('photoshop').app;
const ps = require('photoshop');
const executeAsModal = ps.core.executeAsModal;
// setting the executeasmodel function
await executeAsModal(async () => {})
// now running the code
executeAsModal(function layervis(){app.activeDocument.layers[0].visible = false;})

Now whenever I want to change something which can only be used inside a model scope I need to wrap it inside a function with the executeAsModal function like:

executeAsModal(function rename(){app.activeDocument.layers[0].name = "test";})

OR

function rename(){
    app.activeDocument.layers[0].name = "success";
}

executeAsModal(rename)

OR:

const app = require('photoshop').app;
const ps = require('photoshop');
const executeAsModal = ps.core.executeAsModal;

// Running the code within the executeAsModal function
await executeAsModal(async () => {
    app.activeDocument.layers[0].name = "te2st"
    // any other code
})

Is that how you properly do it?
Or is there a easier way I am missing?

Any suggestions by other users are also very welcome!

it’s a matter of preference really.

I find my self leaning toward wrapping my code snippets into small functions with clear names

and calling them in the desired order inside an anonymous function used by execAsModal

this way I start one modal state and can update progress bar after every task

Modal state prevents someone else plugins to disrupt the sequence of your command… so they have to simply wait until the modal state is finished. If you split each action into its own modal state then someone else plugin can use that gap and run its own routine.

One thing to add is that executeAsModal also gives you access to the executionContext, which you can use to control history. I typically decide the granularity not only based on having exclusive access to Photoshop for a sequence of operations but also based on what I want to bundle together as a history state (within a modal state you can easily have multiple history states, but I don’t think you can across modal states). So in my opinion it’s not ‘wrap all of your code’ vs. ‘wrap every little function’, it really depends on the control flow and so the answer (at least in my humble opinion) lies in the middle. Also I find debugging quite straight forward if you use proper exception throwing/handling. The one tricky thing is that executeAsModal seems to convert errors into strings, so if you are wrapping your exceptions for more context (with custom errors, etc.) you have to re-throw exceptions.

2 Likes