How to Create your Own InDesign Menus
February 17, 2010 | Tips | en
To make a script available from an InDesign menu, it is necessary to understand how the Menu
object communicates with event listeners through the MenuAction
object.
Menu and MenuAction models
In short, a Menu
/ Submenu
object is a simple container designed like a “composite pattern”. Each menu element (child) belongs to one of three possible categories: MenuItem
, MenuSeparator
, or Submenu
. MenuItem
and MenuSeparator
correspond to the primitive objects, while Submenu
behaves recursively as another menu. Therefore Menu
and Submenu
objects are basically the same thing and expose the same interface, except that Menu
represents the root class, parent of which is the Application
.
Given a (sub)menu, you can access primitive and complex objects uniformly through the .menuElements
property (MenuElements
collection), which is the union of .menuItems
(MenuItems
), .menuSeparators
(MenuSeparators
), and .submenus
(Submenus
) collections.
The Menu
/ Submenu
object provides event listeners (.eventListeners
) intended to handle menu display events ("beforeDisplay"
), but as a general rule you won't use those listeners to trigger your process. The key point is that menu actions are ‘decoupled’ from the menu object. The InDesign JS DOM provides a specific MenuAction
object, and a ScriptMenuAction
twin object, to encapsulate menu actions apart from the menu model. Thus, you can attach a menu action to one or more MenuItem
object, but you could also invoke a menu action irrespective of any menu component.
Let's see how to navigate within the menu and action models:
A MenuItem
(the menu element that you can ‘link’ to an action) mainly contains status and titling properties, plus a reference to a MenuAction
(.associatedMenuAction
) which actually deals with events. As explained in the InDesign CS4 Scripting Guide, chapter 8: “The properties of the menuAction define what happens when the menu item is chosen. In addition to the menuActions defined by the user interface, InDesign scripters can create their own, scriptMenuActions, which associate a script with a menu selection.”
Both the MenuAction
and ScriptMenuAction
objects belong to Application
and have exactly the same structure. However, the MenuAction
instances are persistent and not removable. They reflect InDesign built-in functionalities, such as creating a new document, pasting, or grouping the selected objects. On the other hand the ScriptMenuAction
instances are created by scripts and live only during a session (remember that you need to use the #targetengine
directive to get your code session-persistent).
You can do many sly things with the InDesign existing menu actions. For example, you can invoke most of the UI dialogs from your own script:
// displays the Document Setup dialog var maDocSetup = app.menuActions.item("$ID/Document Setup..."); maDocSetup.invoke();
You can also —at your own risks!— extend a primitive action behaviour by “listening” the beforeInvoke
and/or afterInvoke
event:
#targetengine "ExtendPaste" var maPaste = app.menuActions.item("$ID/Paste"); maPaste.addEventListener( "afterInvoke", function(){alert("You pasted something");} );
The code above installs a custom function which will be called after every Paste action initiated by the user (from the Edit menu, or the Ctrl/Cmd V shortcut).
The Localization Problem
A chink in the armor of the InDesign JavaScript DOM is that menus, submenus, menuItems, and menuActions are all referred to by localized name. It means that something like app.menuActions.item("Paste")
in UK/US installations must be translated into app.menuActions.item("Coller")
in French installation, etc.
If you write scripts for an international audience, you probably want to work around this boring constraint. The solution is to use the $ID/
prefix at the beginning of “locale-independent key strings”, for example: "$ID/Paste"
. The point is that InDesign stores internal key strings in translation tables you can access to via the scripting DOM. The syntax "$ID/<internal_key_string>"
is one way to get a key string translated into the locale InDesign interface, on condition of passing through the InDesign JavaScript object model. If you need to handle a key string from the core JS context (string affectation, alert()
, etc.) then you must use the Application.translateKeyString()
method:
// This WILL work: app.menuActions.item("$ID/Paste").invoke(); // This WON'T display the translated string: alert( "$ID/Paste" ); // This WILL display the translated string: alert( app.translateKeyString("$ID/Paste") );
Conversely, when you work from a non-English InDesign platform, you often need to find the locale-independent form of a localized string, such as "Coller" (FR), or "Einfügen" (DE). Then you use the Application.findKeyStrings()
method, which returns an array of “locale-independent string(s) from the internal string localization database that correspond to the specified string (in the current locale).”
// Extract the "$ID/Paste" key string // from the French corresponding token: alert( app.findKeyStrings("Coller").join('\r') );
Menu and Script Menu Action API
To create a new action, we use the app.scriptMenuActions.add()
method. At this point we pass only a title argument, and an optional properties object. The ScriptMenuAction
instance lives in the session scope. It can now be linked to one or several event listener(s) through the addEventListener()
method. An event listener is a callback mechanism which allows to trigger a specific function (an “event handler”) when a specific event occurs in the target object. A ScriptMenuAction
target supports the following events:
ScriptMenuAction Event | Description |
---|---|
beforeDisplay | Occurs before an internal request for the enabled/checked status of the target. |
beforeInvoke | Occurs before the target action is invoked. |
onInvoke | Occurs when the target action is invoked. |
afterInvoke | Occurs after the onInvoke event. |
Now let's see how to add an event listener observing the onInvoke
event:
#targetengine 'SampleScriptMenuAction' var smaTitle = "Sample Script Menu Action"; // Create a new Script Menu Action var sma = app.scriptMenuActions.add(smaTitle); // Add an Event Listener sma.addEventListener( /*event type*/ 'onInvoke', /*event handler*/ function(){alert('Hello World!');} ); // Internal call sma.invoke();
The internal call —sma.invoke()
— emulates an onInvoke
event within the script menu action. The event is ‘captured’ by the event listener. Then the event listener calls the event handler, which displays the message.
At first sight it's the most stupid way to get a “Hello World!” in JavaScript! But the advantage of using the action event model is not in calling a routine from your script, it is to connect all this stuff with a menu item. To do that, we use the menuItems.add()
method from a Menu
/ Submenu
object:
#targetengine 'Sample Script Menu Action' var smaTitle = "Sample Script Menu Action"; // Create the Script Menu Action (SMA) var sma = app.scriptMenuActions.add(smaTitle); // Add an Event Listener sma.addEventListener( /*event type*/ 'onInvoke', /*event handler*/ function(){alert('Hello World!');} ); // Create a new menu item in the Help submenu var mnu = app.menus.item("$ID/Main").submenus.item("$ID/&Help"); mnu.menuItems.add(sma);
Here is the result:
Note that our “Sample Script Menu Action” is session-persistent thanks to the #targetengine
directive. But it is also possible to use a script File
rather than a direct code as the event handler, in which case you automatically obtain session persistence even from a script scope. Why? Because a new MenuItem
is always registered at the session level, so if the .associatedMenuAction
event handlers reside in script files the connection will remain.
Another side effect to keep in mind is that the script above installs a new clone of the menu item each time it is called! Moreover, if you create your own submenu to manage several menu items the Submenu
object is “application persistent,” that is InDesign will restore it the next session from your user profile. That's why menu scripters need to provide for a clean installation management.
Sample Code: Adding a “Close All” Feature in the File Menu
The basic approach I suggest in menu management is to separate the main process (i.e. the event handlers) from the menu installation. A good thing is to encapsulate the installation within an autoexecutable function returning true
when the job is done. We affect the return value to a variable which tests itself preventing the user from calling once again the installer.
/**************************************************/ /* FileCloseAll.js */ /* */ /* Add a "Close All" feature in the File menu */ /* */ /**************************************************/ #targetengine "FileCloseAll" // THE MAIN PROCESS // ----------------------------------------------- var fcaTitle = "Close All"; var fcaHandlers = { 'beforeDisplay' : function(ev) { ev.target.enabled = (app.documents.length>1); }, 'onInvoke' : function() { var doc; for( var i = app.documents.length-1 ; i>=0 ; i-- ) { doc = app.documents[i]; doc.close(); } } }; // THE MENU INSTALLER // ----------------------------------------------- var fcaMenuInstaller = fcaMenuInstaller|| (function(mnuTitle,mnuHandlers) { // 1. Create the script menu action var mnuAction = app.scriptMenuActions.add(mnuTitle); // 2. Attach the event listener var ev; for( ev in mnuHandlers ) { mnuAction.eventListeners.add(ev,mnuHandlers[ev]); } // 3. Create the menu item var fileMenu = app.menus.item("$ID/Main").submenus.item("$ID/&File"); var refItem = fileMenu.menuItems.item("$ID/&Close"); fileMenu.menuItems.add(mnuAction,LocationOptions.after,refItem); return true; })(fcaTitle, fcaHandlers);
Of course, a good place for that script is the "Startup Scripts" folder if you want to keep at hand a “Close All” feature in every InDesign session.
Comments
Thanks for making the darkness shiny ;-)
Jeez, amazing stuff, Marc! I tried the FileCloseAll, and it worked. Amazing. Thanks!
Your script nags me with the pesky Do-you-want-to-save dialog. When I've opened 23 files, fiddled a little with them, but don't want to save any changes -- saying No 23 times is booooring. Could you please make such a Close All script which just closes all, without the nagging? And yes, I'm a big boy -- so I know how to use such god-like powers for Good alone!
Hi Klaus,
> When I've opened 23 files, fiddled a little with them,
> but don't want to save any changes [...]
Well, you ask for a dangerous power in human hands, but let's do it: line #39 of the js file, replace:
doc.close();
by
doc.close(SaveOptions.NO);
Cool, it works -- thanks!!! I promise to use my powers wisely. Mostly. At least on Sundays.
thanks for ur script.I have to control the quit-indesign option and close option in theIndesign CS4 application,so that the indesign document must be saved and then closed.please reply me as soon as possible.So,please help me.
@shakthi
A way to do the job is to attach an event listener to the 'beforeClose' and/or 'beforeQuit' event.
@+
Marc
I need to control the position of the menu when i create it .For example the menu which i create must come before the edit menu in indesign.then i need to remove the keyboard shorcuts for the already existing menus in indesign
One more thing: it doesn't seem like all menu actions are very well updated. I recently tried toggling the Text Threads view option on and off (View>Extras>Show/Hide Text Threads). It works, but only the first time, if you have clicked the menu list so InDesign can update the command name.
If you toggle the Text Threads view with a quick key command (for instance option-cmd-Y, the default command), and then run the script again, it doesn't update with the current status.
Here is a script I used that works fine and reports isValid=true if I have looked at the menu (no need to click the command, just to look at the menu) before running the script, but not if I have used a quick key command to toggle the view.
turnOn = app.menuActions.item("$ID/Show Text Threads");
turnOff = app.menuActions.item("$ID/Hide Text Threads");
alert(checktextThreads());
if (turnOn.isValid){
alert("turnOn is OK");
turnOn.invoke();
}
if (turnOff.isValid){
alert("turnOff is OK");
turnOff.invoke();
}
function checktextThreads(){
return [turnOn.isValid,turnOff.isValid];
}
Hi zixvelo,
Thanks for your comment. Not sure I understand the shortcut key issue, maybe it's an InDesign bug, but I cannot reproduce the problem on my platform.
Regarding menuActions items that work as switches, you're absolutely right that those whose name/title change can be problematic. What is important to keep in mind is that the DOM layer needs to RESOLVE your menuAction specifier. When you use:
myAction = app.menuActions.item(name);
the itemByName methods is implicitly called and the supplied name needs to be available AT THE MOMENT the specifier is resolved. Hence if the action's name toggles—depending on the ON/OFF status of the function—your script indeed might have to manage invalid specifiers. That's what you do by calling the isValid method on turnOn and turnOff specifiers. The problem is that we don't know exactly at what time InDesign actually updates menuaction names. There are some cases where the scripting process is not synchronized with what is shown in the UI!
But what your snippet does not take in account is the fact that, actually, turnOn and turnOff refer to the same final object. The underlying menuaction item is the same (although its inner name dynamically changes).
So I think you should store and use the id of the menuaction rather that its name. The id offers a stable way to specify the object while you cannot rely on the object's name.
Incidentally, note that your code does not use 'else' after the if(turnOn.isValid) block. If the condition is true, turnOn.invoke() is executed, then turnOff.isValid becomes true. You see what I mean? This—I think—explains why your script has a problem in toggling the menuaction.
So, here is the approach I suggest:
var t;
// First, get a stable menuaction ID whatever the context
// ---
var acID = acID||
((t=app.menuActions.item("$ID/Show Text Threads")).isValid&&t.id)||
((t=app.menuActions.item("$ID/Hide Text Threads")).isValid&&t.id);
// Then, create an itemByID specifier
// ---
var showHideThreadAction = showHideThreadAction||
(acID&&app.menuActions.itemByID(acID));
// Finally, executes the ON/OFF Switch (if available)
// ---
showHideThreadAction &&
showHideThreadAction.enabled &&
showHideThreadAction.invoke();
Hope this helps.
@+
Marc
Hi,
I have a question about "session-persistent" rules:
I placed in my startup folder simple script (script1 is just a path) calling another one (script2) to add menu/submenu items. Each submenu Event Listener onInvoke execute separate scripts (script3...6).
Script1 has #targetengine "menu" directive;
Script2 has #targetengine "scripts" directive;
Scripts 3...6 have #target indesign directive.
Problem is, that if one of my scripts3...6 define dialog, show it, destroy it, do another job - Indesign is forced to quit when script ends.
It works fine if I deactivate "dialog" code lines.
So, the question is: are scripts, running from separated sessions, losing comunication or var Dialog is out of some session or should I run every script in the same session to defend against InDesing quitting?
THX
To be more complex about above:
to deactivate a line:
myDialog.destroy();
is enough to keep Indesign working.