How to manipulate the flyout menu in an InDesign plugin panel

With InDesign v20 we have the flyout menu in a plugin panel. The documentation suggests that you can programmatically insert and remove menu items, change a menu item’s label, check/uncheck it, enable/disable it, etc.

https://developer.adobe.com/indesign/uxp/reference/uxp-api/reference-js/Modules/uxp/Entry%20Points/UxpMenuItems/

and

https://developer.adobe.com/indesign/uxp/reference/uxp-api/reference-js/Modules/uxp/Entry%20Points/UxpMenuItem/

Nothing I have tried works! Has anyone else tried it yet? With any success? Is there some magic trick to it? Can anyone manipulate a flyout menu in InDesign in any way whatsoever?

Here is some code. This is entirely silly (of no real world use at all). It is just to show what I have that doesn’t work.

Set up of the flyout menu (in the entrypoints.setup() function, under panels > flyoutPanel):

	menuItems: [
		{id:'one', label:'One'},
		{id:'two', label:'Two'}
	],
	invokeMenu(id) {
		handleFlyout(id);
	}

Then the handler to handle the invoking of a flyout menu item:

function handleFlyout(id) {
	const panel = entrypoints.getPanel('flyoutPanel');
	const menuItems = panel.menuItems;
	const menuItem = menuItems.getItem(id);
	const currentLabel = menuItem.label;
	menuItem.label = currentLabel + 'x';
}

Totally silly – it should just add an ‘x’ to a menu item label every time that menu item is invoked.

It does nothing – nothing at all.

I had assumed that I had got the syntax wrong or something, but I have found that this same syntax in a Photoshop plugin does exactly what it should – an ‘x’ is added to the menu item label on every invocation.

Philip

1 Like

It might be that the magic trick is to act on the menuItems rather than directly on the menuItem because the methods on the menuItems (insertAt(index, newItem) etc.) seem to work. I will report back again if I get any further.

Philip

A menu item in a flyout menu has an enabled property, a checked property, a label property. When I’ve got a menu item, I really don’t understand why I can’t simply say myMenuItem.enabled = false (to disable it), or myMenuItem.checked = true (to check it), or myMenuItem.label = 'Whatever' (to set its label).

The only way I have found to do any of these things is using this function that is available on the collection of menu items that contain the menu item:

insertAt(index, newItem)

It’s a slightly curious function. If a menu item exists at the index you pass, it replaces the existing item with the new item you’re passing. If you pass an index that is 1 greater than the index of the last item, it appends the new item. In other words, with this function you can replace menu items and you can add menu items to the end of a collection of menu items. (There is no function that I have found to, for instance, insert a new menu item at the beginning of a collection or in the middle of a collection – before or after an existing item for example.)

Using this function in its replacing form, my original idea was to get the current properties of the existing menu item, create a new menu item with the same properties, change the property I am interested in (e.g. change ‘enabled’ from true to false), and then insert the new menu item back into the same place.

That essentially is the way I have found to do it, but it is a bit more complicated . . .

The function requires a new menu item to be passed (as the second parameter). What I have discovered is that you can’t replace a menu item with a new menu item if they both have the same id (an error to that effect is thrown).

So, as well as changing the particular property I want to change, I have to change the id each time I call the function.

I’ve done that by appending a number to the end of my id which I increment before I pass it to the function. This is less than ideal because you normally handle the invocation of a menu item in a switch statement on the id. That can’t be done as normally if the id is constantly changing but can be got round by stripping off the incrementing numbers and performing the switch on the original id which remains the same.

The other parameter that this function requires is the index. If your menu item is at the root level of the flyout menu, the index will be its index in relation to the root, but if your menu item is in a submenu, the index is the index in relation to the submenu containing it, and the function itself needs to be called on the submenu menu items (not on the menu items of the flyout as a whole).

Calculating the index to pass to the function is somewhat tortuous, albeit logical. If you’re starting with just the id (which you do when starting from the invokeMenu function), you need to first get all the menu items in the flyout menu (you can do that from the panel with entrypoints.getPanel(id).menuItems, passing the panel id). From that you can get the menu item itself (with the getItem(id) function, passing the menu item id). The menu item has a parent property which gives you the containing menu items (might be the menu items of a submenu). Menu items have an outputObject property which, being an array, gives you a way to calculate the index.

I have written the following (proof-of-concept) code. When the user selects a menu item in the flyout menu, if the menu item is checked, it is unchecked, and vice versa. It is proof-of-concept code – it is just to show the only way I have found to change the properties of a menu item in a flyout menu (in InDesign). It is changing the checked property but could equally well change the enabled or label properties. If I were to use this in real code, I would split out parts of the code into discrete functions (in particular the code used to generate a new menu item id each time).

This code should work with any configuration of menu items and submenus, but FWIW here is what I have in the menuItems section of the panel section of the entry points.setup function:

menuItems: [
	{id:'one', label:'One'},
	{id:'submenu-hello', label:'Hello', submenu:[
		{id:'hello-fench', label:'Bonjour'},
		{id:'hello-spanish', label:'Hola'}
	]},
	{id:'two', label:'Two'}
],

My invokeMenu function (in the same place) looks like this:

invokeMenu(menuItemID) {
	const panelID = this.id;
	toggleChecked(menuItemID, panelID);
}				

I am passing the id of the menu item and the id of the panel to this function which checks or unchecks the invoked menu item:

function toggleChecked(theMenuItemID, thePanelID) {
	// we get all the menu items of the panel flyout menu
	const panelMenuItems = entrypoints.getPanel(thePanelID).menuItems;
	// with that we can get the menu item (that has been invoked)
	const menuItem = panelMenuItems.getItem(theMenuItemID);
	// to calculate the index, we get the containing menu items with the parent property of the menu item
	const parentMenuItems = menuItem.parent;
	// the outputObject property of menu items is an array, so we can use that to find the index of our menu item
	const idx = parentMenuItems.outputObject.findIndex(obj => obj.id === theMenuItemID);
	// we can't insert a menu item that has the same id as an already existing menu item
	// so we create a new id ...
	if (!theMenuItemID.includes('||')) theMenuItemID = `${theMenuItemID}||0`;
	let [baseName, incrementingVersion] = theMenuItemID.split('||');
	incrementingVersion = (parseInt(incrementingVersion) + 1).toString();
	const newMenuItemID = `${baseName}||${incrementingVersion}`;
	// if our menu item is checked we want to uncheck it, and vice versa
	const newCheckedState = (menuItem.checked) ? false : true;
	// now we can create a new menu item (as a simple object) for passing to the insertAt function
	const newMenuItem = {
		id:newMenuItemID,
		label:menuItem.label,
		checked:newCheckedState,
		enabled:menuItem.enabled
	};
	// finally we call the insertAt function (on the menu items that contain our menu item) 
	parentMenuItems.insertAt(idx, newMenuItem);
}

It works, but it is an absurd amount of code to have to write when all I want to do is check or uncheck a menu item! Surely there’s a simple way!

Philip

Does InDesign implement it in a different way than Photoshop does?

Yes.

As far as I can tell, the documentation is the same but the implementation is not. So, in Photoshop you can simply say myMenuItem.checked = true and the menu item in the ui is checked. The same code in InDesign and the menu item in the ui remains stubbornly unchecked.

Philip

Thanks for the detailed code
I would also like to remove check marks from other menu items, leaving only the selected menu item.
Is there such a possibility?

For my InDesign plugin, in the end I actually abandoned altogether trying to manipulate the flyout menu from the uxp side – too little worked as it should, there are too many problems. The solution I adopted was to manipulate the menu entirely from the InDesign side. Doing it that way, you can do everything that you might want to (check/uncheck menu items, disable/enable, insert/remove on the fly, etc.). I wrote about how I have done it here:

Philip