How can I programmatically enable and disable Extras?

I’m trying to implement a simple feature in my UXP plugin: Enabling and Disabling “Extras” from the View menu.

const {executeAsModal} = require("photoshop").core;
const {batchPlay} = require("photoshop").action;
const app = require('photoshop').app

async function enableExtras() {
   const result = await batchPlay(
      [
         {
            _obj: "select",
            _target: [
               {
                  _ref: "menuItemClass",
                  _enum: "menuItemType",
                  _value: "toggleShowExtras"
               }
            ],
            _options: {
               dialogOptions: "dontDisplay"
            }
         }
      ],
      {
         synchronousExecution: false,
         modalBehavior: "execute"
      }
   );
}

async function btnDisableExtras_Click() {
   await executeAsModal(disableExtras, {"commandName": "Disable Extras"});
}

async function btnEnableExtras_Click() {
   await executeAsModal(enableExtras, {"commandName": "Enable Extras"});
}

document.getElementById("btnDisableExtras").addEventListener("click", btnDisableExtras_Click);
document.getElementById("btnEnableExtras").addEventListener("click", btnEnableExtras_Click);

The problem is that there is no value to explicitly disable or enable extras, just a toggle:

_value: "toggleShowExtras"

So I can make a “Toggle Extras” button, but I can’t seem to make two buttons that explicitly disable and enable the extras. Does anyone know how to get around this limitation? There are a lot of toggle based BatchPlay _value: commands, but nothing binary (on/off).

Any help greatly appreciated.

1 Like

in my search I couldn’t find a way to set the checked value of a menu item so I went with 2nd best thing.

I get the current state of the menu item and and toggle only if needed.

async function showExtras(state){
    let extrasIsChecked = (await batchPlay([{
        "_obj": "get",
        "_target": [{"_property": "menuBarInfo"},{"_ref": "application", "_enum": "ordinal", "_value": "targetEnum"},]
    }], {}))[0].menuBarInfo.submenu.find( m => m.name == "View").submenu.find( m => m.name == "Extras").checked
    if (extrasIsChecked != state) {
        await core.performMenuCommand({commandID: 3500})
    }
}

that function can be used as follows:

//Show Extras
await showExtras(true);

//Hide Extras
await showExtras(false);

EDIT:
as pointed out the previous function may fail on non English versions of PS, so here’s another way using IDs instead of names:

async function showExtras(state){
    let extrasIsChecked = (await batchPlay([{
        "_obj": "get",
        "_target": [{"_property": "menuBarInfo"},{"_ref": "application", "_enum": "ordinal", "_value": "targetEnum"},]
    }], {}))[0].menuBarInfo.submenu.find( m => m.menuID== 9).submenu.find( m => m.command == 3500).checked
    if (extrasIsChecked != state) {
        await core.performMenuCommand({commandID: 3500})
    }
}

This will fail if the language in PS is not English.

how about menu IDs, are they reliable?

Positive IDs should be reliable, but Adobe proved that’s not always the case when they completely changed “Select Subject” menu :slight_smile: Negative IDs may always change (even on Ps restart IIRC)

Another option is to use localization strings to generate translated strings and then you compare in any language. But that could also be a subject of change but it is much safer.

Also you could look into these:
https://developer.adobe.com/photoshop/uxp/2022/ps_reference/media/photoshopcore/#performmenucommand

https://developer.adobe.com/photoshop/uxp/2022/ps_reference/media/photoshopcore/#getmenucommandstate

well, we do have core.getMenuCommandTitle but it uses command id anyway, how would you get localization strings?

getMenuCommandState is pretty much useless

Returns whether a command menu item is available for invoking.

it does not return if the menu item is “checked” but if it’s “available”

I’m also interested in localization strings. Never managed to figure out how to use them or how they work.

I am working on small plugin to help you with an errors and the way it detects specific error dialog is this:
image

2 Likes

But where do we get full list of these string paths? I mean here you know this error message translations uses this path-ID, but where did you find it? How did you figure it’s the one you need?

just found them

C:\Program Files\Adobe\Adobe Photoshop 2023\Locales\xx_XX\Support Files

there should be a .dat file there

for this specific case:

async function showExtras(state){
    let viewLocalString = core.translateUIString("$$$/Menu/View");
    let extrasLocalString = core.translateUIString("$$$/Menu/View/Extras");
    let extrasIsChecked = (await batchPlay([{
        "_obj": "get",
        "_target": [{"_property": "menuBarInfo"},{"_ref": "application", "_enum": "ordinal", "_value": "targetEnum"},]
    }], {}))[0].menuBarInfo.submenu.find( m => m.name == viewLocalString ).submenu.find( m => m.name == extrasLocalString).checked
    if (extrasIsChecked != state) {
        await core.performMenuCommand({commandID: 3500})
    }
}

thanks @Jarda for the valuable insight!

1 Like

The last supplied function fails silently. Here’s the code (ignore irrelevant functions):

const {executeAsModal} = require("photoshop").core;
const {batchPlay} = require("photoshop").action;
const app = require('photoshop').app

async function toggleSnapToPixels(executionContext, toggle) {
   const result = await batchPlay(
      [
         {
            _obj: "set",
            _target: [
               {
                  _ref: "property",
                  _property: "generalPreferences"
               },
               {
                  _ref: "application",
                  _enum: "ordinal",
                  _value: "targetEnum"
               }
            ],
            to: {
               _obj: "generalPreferences",
               transformsSnapToPixels: toggle
            }
         }
      ],{}
   );
}

async function showExtras(state){
   let viewLocalString = core.translateUIString("$$$/Menu/View");
   let extrasLocalString = core.translateUIString("$$$/Menu/View/Extras");
   let extrasIsChecked = (await batchPlay([{
       "_obj": "get",
       "_target": [{"_property": "menuBarInfo"},{"_ref": "application", "_enum": "ordinal", "_value": "targetEnum"},]
   }], {}))[0].menuBarInfo.submenu.find( m => m.name == viewLocalString ).submenu.find( m => m.name == extrasLocalString).checked
   if (extrasIsChecked != state) {
       await core.performMenuCommand({commandID: 3500})
   }
}

async function openNeuralFilters() {
   const result = await batchPlay(
      [
         {
            _obj: "neuralGalleryFilters",
            _options: {
               dialogOptions: "display"
            }
         }
      ],
      {
         synchronousExecution: false,
         modalBehavior: "wait"
      }
   );
}

async function btnDisableSnapToPixels_Click() {
   await executeAsModal(toggleSnapToPixels, {"commandName": "Disable Snap to Pixels", "descriptor": false});
}

async function btnEnableSnapToPixels_Click() {
   await executeAsModal(toggleSnapToPixels(true), {"commandName": "Enable Snap to Pixels", "descriptor": true});
}

async function btnDisableExtras_Click() {
   await executeAsModal(showExtras, {"commandName": "Disable Extras", "descriptor": false});
}

async function btnEnableExtras_Click() {
   await executeAsModal(showExtras, {"commandName": "Enable Extras", "descriptor": true});
}

async function btnOpenNeuralFilters_Click() {
   await executeAsModal(openNeuralFilters, {"commandName": "Open Neural Filters"});
}

document.getElementById("btnEnableSnapToPixels").addEventListener("click", btnEnableSnapToPixels_Click);
document.getElementById("btnDisableSnapToPixels").addEventListener("click", btnDisableSnapToPixels_Click);
document.getElementById("btnOpenNeuralFilters").addEventListener("click", btnOpenNeuralFilters_Click);
document.getElementById("btnDisableExtras").addEventListener("click", btnDisableExtras_Click);
document.getElementById("btnEnableExtras").addEventListener("click", btnEnableExtras_Click);

I don’t think state is being passed correctly. I don’t know. I’ll try to do some debugging.

Any further help would be massively appreciated.

Jay

1 Like

Also, when I add the following into the function:

alert(state);

I just get a generic result of [object Object]. How can I get more information than that when I want to inspect a variable?

Never use this within modal execution. Because it will wait forever.

Ok, gotcha. When should I use "Wait"? Is there a documentation/Photoshop UXP API page where I can get clarification on this?

Should I change it to "Execute" in this case?

Thanks for the help.

Jay

"execute" all lower case as it is case sensitive. I don’t see any documentation yet. This was more usefull before in DOM version 1 when execute as modal did not exist yet. Right now it makes only sense for “get” command as it can be executed without modal state. So if user would have some dialog open… it would wait until user close the dialog and then read value for you.

Still having some trouble solving this problem. Here is my current code:

const {executeAsModal} = require("photoshop").core;
const {batchPlay} = require("photoshop").action;
const app = require('photoshop').app
const PhotoshopCore = require('photoshop').core;

async function showExtras(toggle) {

   const batchCommands = {
      _obj: "get",
      _target: [
         {
            _property: "menuBarInfo"
         },
         {
            _ref: "application",
            _enum: "ordinal",
            _value: "targetEnum"
         }
      ],
   };

   let state = (await require('photoshop').action.batchPlay([batchCommands], {}));
   let viewMenu   = (state[0].menuBarInfo.submenu.find(m => m.name == 'View'));
   let extrasMenu = (viewMenu.submenu.find(m => m.name == 'Extras'));
   let extrasIsChecked = extrasMenu.checked;

   if (extrasIsChecked != toggle) {
       PhotoshopCore.performMenuCommand({ commandID: 3500 });
   }

   console.log(state);
   console.log(viewMenu);
   console.log(extrasMenu);
   console.log(extrasIsChecked);

}


document.getElementById("btnDisableExtras").addEventListener('click', async () => {

   await showExtras(false);
   
   // console.log("result:" + result);

   // // a message in the result means error
   // if (result[0].message) {
   //    showAlert(result[0].message);
   // }
})

document.getElementById("btnEnableExtras").addEventListener('click', async () => {
   await showExtras(true);

   //console.log("result:" + result);

   // // a message in the result means error
   // if (result[0].message) {
   //    showAlert(result[0].message);
   // }
})

All of my logging is reporting correct information from the end of the showExtras function:

let state = (await require('photoshop').action.batchPlay([batchCommands], {}));
let viewMenu   = (state[0].menuBarInfo.submenu.find(m => m.name == 'View'));
let extrasMenu = (viewMenu.submenu.find(m => m.name == 'Extras'));
let extrasIsChecked = extrasMenu.checked;

Is returning:

Despite this, when I run the following at the end of the function:

if (extrasIsChecked != toggle) {
       PhotoshopCore.performMenuCommand({ commandID: 3500 });
}

PhotoshopCore.performMenuCommand({ commandID: 3500 }); seems to do absolutely nothing. It doesn’t deselect (or re-select) the Extras menu item. Right now it’s always set to true. Hitting my “Disable Extras” button doesn’t deselect extras.

I’m sure the commandID of 3500 is correct - Here’s the devtools output:

have you tried running the code as it is with no modification.

I tested it and it does work.

also make sure that checked is returning the correct value. strangely in my tests sometimes it reported the opposite of visible state.

also try wrapping your code with a try/catch it may help debug the issue