Whenever the user initiates an action from a menu, or from a keyboard shortcut, InDesign internally notifies several events which reflect the fact that this MenuAction is being or has just been invoked. A script can observe and record this kind of events through an event listener. We have already mentioned this mechanism in the article “How to Create your Own InDesign Menus”.

Events and event handlers (briefly)

An event handler is nothing but a function that executes each time the specified event occurs. Given a MenuAction (myMenuAction), and a function (myEventHandler), it's easy to attach the second to the first:

myMenuAction.addEventListener('beforeInvoke', myEventHandler);
 

Provided that the scripting engine maintains myEventHandler in a persistent scope, the handler will run just before the user invokes the menu action.

Four types of events are available for each MenuAction object: beforeDisplay, beforeInvoke, onInvoke, afterInvoke. In order for a spy code to record an action without interfering with its execution, it is usually best to listen to the beforeInvoke event.

A rudimentary “Action Spy”

There are two interesting points to note about event handlers. First, you can attach the same function to several menu actions, so if you need to perform a generic treatment you don't need to create an event handler for each action. Second, the event handler —your custom function— receives as an argument an Event instance which provides extensive information on when, how, and where it occurred. In particular, the property Event.target refers to the object —namely the menu action— which initiated the event.

Now, let's study the following snippet:

#targetengine 'myPersistentScope'
 
var myEventHandler = function(ev)
    {
    alert( "You did: " + ev.target.name + " at " + ev.timeStamp );
    };
 
app.menuActions.everyItem().
    addEventListener('beforeInvoke', myEventHandler);
 

Funny, isn't it? The script allocates a persistent scope, creates a function that displays some details about an event, then attaches this function to all menu actions —3000+ objects! Thus, whatever you do via the InDesign menu interface, or even through keyboard shortcuts (Cmd C, Cmd D...), each move is spied on by the script.

Let's make a step further. Within myEventHandler, we have access to the MenuAction object the user is now performing: ev.target. So we can silently store this information into an array. In fact, we just need to record the unique ID of the action: ev.target.id. That's it. We just found a way to record a sequence of actions.

Then to playback these actions, we just need to successively invoke each of them in the same order from another routine. Something like:

// pseudocode
for each id in actionIDArray
    {
    app.menuActions.itemByID(id).invoke();
    }
 

Final script

The script ActionListener.js for InDesign CS4/CS5 is a first implementation of the principles outlined above. It wraps the whole logic in two persistent components: ActionManager and UserInterface.

ActionManager contains the core mechanism and exposes a simple interface which speaks for itself: record(), stop(), play(). It allows to start and stop recording actions, and implements the playback mechanism. During the recording stage, it just sends to UserInterface the actions to record and display.

The complete code of ActionManager is pretty short:

var ActionManager = ActionManager||(function()
//================================================
// Action Manager Interface:
//  record() -- Activate the record handler
//  stop() -- Deactivate the record handler
//  play(idStack) -- Play a stack of actions (by IDs)
//================================================
{
    var recordHandler = function(ev)
        {
        UserInterface && UserInterface.addMenuAction(ev.target);
        };
 
    // Interface
    return {
        record: function()
            {
            app.menuActions.everyItem().
                addEventListener('beforeInvoke', recordHandler);
            },
        stop: function()
            {
            app.menuActions.everyItem().
                removeEventListener('beforeInvoke', recordHandler);
            },
        play: function(/*arr&*/idStack)
            {
            var id;
            while( id=idStack.pop() )
                app.menuActions.itemByID(id).invoke();
            }
        };
})();
 

UserInterface is basically a ScriptUI palette which allows the user to conveniently activate the different features (record, stop, playback) and to manage the list of recorded actions (check, uncheck, remove):

User Interface of the Action Listener for InDesign CS4/CS5.

You'll find the whole code of this component in the source file.

Disclaimer and Limitations

ActionListener is an embryonic attempt to simulate the Photoshop Actions paradigm within InDesign. Unfortunately, it suffers from serious limitations:

• You can record a menu action which displays a native modal dialog, but you cannot record what you do in that dialog! So the playback mechanism only recalls the original dialog box, and waits until the user validates before continuing.

• You can only record MenuAction events (or the corresponding keyboard shortcuts). You cannot record direct inputs of text —except for some special characters—, mouse gestures, or other panel-based actions. It's far from possible to do everything in InDesign by only using regular menus!

Note. — InDesign CS5 introduced a number of new events at every DOM object level. Especially, it is now possible to listen to AFTER_SELECTION_CHANGED and AFTER_SELECTION_ATTRIBUTE_CHANGED from the app object. Therefore, it would be theoretically possible to spy on selection changes and attribute changes by extending the ActionManager, and to playback the corresponding operations on relevant targets. In practice, however, it sounds like a mammoth task!