How to Implement a Basic Action Listener in InDesign
January 04, 2011 | Tips | en
While Photoshop has a handy Actions panel that gives you the ability to record and playback simple operations, InDesign provides no way —without scripting!— to automate those daily repetitive tasks. But, hey!, why not use a script to mimic such action manager? This is my very first step in that direction...
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):
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!
Comments
Ouahh, j'avais pas compris la portée du travail sur Twitter, m'en vais remettre une couche ! Bravo ;)
Loic
Merci Loïc.
Ça reste toutefois un prototype ultra rudimentaire dont il ne faut pas surestimer les applications concrètes.
Tant qu'on ne disposera pas d'une solution pour tracer les changements de sélection opérées « à la souris », les saisies manuelles ou les modifications de propriétés via les palettes ou boîtes de dialogue, ça reste un jouet rigolo mais trop lacunaire pour offrir des ouvertures fonctionnelles en production quotidienne.
Le truc intéressant, quand même, c'est que l'enregistreur peut tracer la plupart des déplacements et sélections opérés dans un texte à l'aide du clavier, ainsi que les copier-coller. Il peut également capturer les lancements de scripts s'ils sont associés à un raccourci-clavier. Cela dégage quelques perspectives...
Idéalement, j'aimerais que l'utilisateur puisse sélectionner un objet, enregistrer une séquence d'opérations quelconques sur cet objet, puis reproduire à volonté les mêmes opérations sur d'autres objets. Je pense que ce serait l'utilité fondamentale de ce script. Un système de playback rapide destiné aux non-scripteurs, pour soulager des tâches répétitives définies à la volée.
Mais cela nécessite qu'on enregistre plus d'informations. Un truc tout bête comme l'application d'une nuance ou d'un style n'est pas traçable pour le moment!
Bon, mais j'attends de voir les autres commentaires/idées...
@+
Marc
Another nice experiment from your laboratories! Very educational -- if not for a working action recorder, then certainly for other projects.
Peter
Thanks for your encouragement, Peter!
[You may have noticed that the ScriptUI code contains unusual stuff, including the injection of PNG icons using direct strings (no File). I discovered other fun properties about Image and ScriptUIImage objects. I will email you more specific details on this topic...]
@+
Marc
Great work Marc!
Embedded PNG was first thing I noticed! :-D
It looks like Python encoding to me :-)
Marijan
Thanks,
is a very fine feature.
I hve only one wihes, start to play the checked action on press a key would be fine.
This is awesome!, something which I was always wishing for.
Can we not use this in CS3 version as I tried to do so but it doesn't activate the stop button once I click on Record button & there is no clue whether recording is on or not.