1 //------------------------------------------------------------------------------
  2 // File: jamActions.jsxinc
  3 // Version: 4.5
  4 // Release Date: 2016-09-29
  5 // Copyright: © 2011-2016 Michel MARIANI <http://www.tonton-pixel.com/blog/>
  6 // Licence: GPL <http://www.gnu.org/licenses/gpl.html>
  7 //------------------------------------------------------------------------------
  8 // This program is free software: you can redistribute it and/or modify
  9 // it under the terms of the GNU General Public License as published by
 10 // the Free Software Foundation, either version 3 of the License, or
 11 // (at your option) any later version.
 12 // 
 13 // This program is distributed in the hope that it will be useful,
 14 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16 // GNU General Public License for more details.
 17 // 
 18 // You should have received a copy of the GNU General Public License
 19 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 20 //------------------------------------------------------------------------------
 21 // Version History:
 22 //  4.5:
 23 //  - Incremented version number to keep in sync with other modules.
 24 //  4.4:
 25 //  - Normalized error messages.
 26 //  4.1:
 27 //  - Simplified test in jamActions.isActionsPalette ().
 28 //  4.0:
 29 //  - Removed reference to 'this' for main global object.
 30 //  3.6:
 31 //  - Incremented version number to keep in sync with other modules.
 32 //  3.5:
 33 //  - Added jamActions.readActionDescriptor () to public functions, along with
 34 //    an extra insertVersionPrefix parameter.
 35 //  3.4:
 36 //  - Incremented version number to keep in sync with other modules.
 37 //  3.3:
 38 //  - Initial release.
 39 //------------------------------------------------------------------------------
 40 
 41 /**
 42  * @fileOverview
 43  * @name jamActions.jsxinc
 44  * @author Michel MARIANI
 45  */
 46 
 47 //------------------------------------------------------------------------------
 48 
 49 if (typeof jamActions !== 'object')
 50 {
 51     /**
 52      * Global object (used to simulate a namespace in JavaScript) containing
 53      * a set of functions related to decoding actions files into a format usable by scripts written with the
 54      * <a href="http://www.tonton-pixel.com/blog/json-photoshop-scripting/json-action-manager/">JSON Action Manager</a> engine.<br />
 55      * Uses information found in the document
 56      * <a href="http://www.tonton-pixel.com/Photoshop%20Additional%20File%20Formats/actions-file-format.html">Photoshop Actions File Format</a>.
 57      * @author Michel MARIANI
 58      * @version 4.5
 59      * @namespace
 60      */
 61     var jamActions = { };
 62     //
 63     (function ()
 64     {
 65         /**
 66          * @description Test if a given file is an actions file (*.atn).
 67          * @param {Object} file File object
 68          * @returns {Boolean} true if actions file
 69          * @example
 70          * function actionsFileFilter (f)
 71          * {
 72          *     return (f instanceof Folder) || jamActions.<strong>isActionsFile</strong> (f);
 73          * }
 74          * var select = (File.fs === "Macintosh") ? actionsFileFilter : "Actions Files:*.atn,All Files:*";
 75          * var actionsFile = File.openDialog ("Select an actions file:", select);
 76          * if (actionsFile !== null)
 77          * {
 78          *     alert ("OK!");
 79          * }
 80          */
 81         jamActions.isActionsFile = function (file)
 82         {
 83             return (file.type === '8BAC') || file.name.match (/\.atn$/i);
 84         };
 85         //
 86         /**
 87          * @description Test if a given file is an actions palette file (Actions Palette.psp).
 88          * @param {Object} file File object
 89          * @returns {Boolean} true if actions palette file
 90          * @example
 91          * function actionsPaletteFilter (f)
 92          * {
 93          *     return (f instanceof Folder) || jamActions.<strong>isActionsPalette</strong> (f);
 94          * }
 95          * var select = (File.fs === "Macintosh") ? actionsPaletteFilter : "Actions Palette File:*.psp,All Files:*.*";
 96          * var actionsPaletteFile = File.openDialog ("Select an actions palette file:", select);
 97          * if (actionsPaletteFile !== null)
 98          * {
 99          *     alert ("OK!");
100          * }
101          */
102         jamActions.isActionsPalette = function (file)
103         {
104             return ((file.type === '8BPF') && file.name.match (/^Actions Palette$/i)) || file.name.match (/^Actions Palette.psp$/i);
105         };
106         //
107         function readBEInt (file, byteCount)
108         {
109             var bytes = file.read (byteCount);
110             var intValue = 0;
111             for (var index = 0; index < byteCount; index++)
112             {
113                 intValue = (intValue << 8) + bytes.charCodeAt (index);
114             }
115             return intValue;
116         }
117         //
118         function readBytes (file, byteCount)
119         {
120             return file.read (byteCount);
121         }
122         //
123         function readByteString (file)
124         {
125             var stringLength = readBEInt (file, 4);
126             return readBytes (file, stringLength);
127         }
128         //
129         function readUnicodeString (file)
130         {
131             var unicodeString = "";
132             var unicodeLength = readBEInt (file, 4);    // Includes terminating null
133             for (var index = 0; index < unicodeLength; index++)
134             {
135                 var unicodeChar = readBEInt (file, 2);
136                 if (unicodeChar !== 0)
137                 {
138                     unicodeString += String.fromCharCode (unicodeChar);
139                 }
140             }
141             return unicodeString;
142         }
143         //
144         function readEventId (file)
145         {
146             var eventId = 0;
147             var eventType = readBytes (file, 4);
148             switch (eventType)
149             {
150                 case 'TEXT':
151                     eventId = app.stringIDToTypeID (readByteString (file));
152                     break;
153                 case 'long':
154                     eventId = app.charIDToTypeID (readBytes (file, 4));
155                     break;
156                 default:
157                     throw new Error ("[jamActions readEventId] Unrecognized event type: '" + eventType + "'");
158                     break;
159             }
160             return eventId;
161         }
162         //
163         function skipDouble (file)
164         {
165             file.seek (8, 1);
166         }
167         //
168         function skipDoubles (file, doubleCount)
169         {
170             file.seek (doubleCount * 8, 1);
171         }
172         //
173         function skipInt8 (file)
174         {
175             file.seek (1, 1);
176         }
177         //
178         function skipInt16 (file)
179         {
180             file.seek (2, 1);
181         }
182         //
183         function skipInt32 (file)
184         {
185             file.seek (4, 1);
186         }
187         //
188         function skipInt64 (file)
189         {
190             file.seek (8, 1);
191         }
192         //
193         function skipBytes (file, byteCount)
194         {
195             file.seek (byteCount, 1);
196         }
197         //
198         function skipByteString (file)
199         {
200             var stringLength = readBEInt (file, 4);
201             skipBytes (file, stringLength);
202         }
203         //
204         function skipUnicodeString (file)
205         {
206             var unicodeLength = readBEInt (file, 4);    // Includes terminating null
207             skipBytes (file, unicodeLength * 2);
208         }
209         //
210         function skipId (file)
211         {
212             var idLength = readBEInt (file, 4);
213             if (idLength)
214             {
215                 skipBytes (file, idLength);
216             }
217             else
218             {
219                 skipBytes (file, 4);
220             }
221         }
222         //
223         function skipClass (file)
224         {
225             skipUnicodeString (file);   // Class ID name (usually empty)
226             skipId (file);  // Class ID
227         }
228         //
229         function skipObject (file)
230         {
231             skipClass (file);
232             var itemCount = readBEInt (file, 4);
233             for (var itemIndex = 0; itemIndex < itemCount; itemIndex++)
234             {
235                 skipId (file);  // Key ID
236                 skipItem (file);
237             }
238         }
239         //
240         function skipList (file)
241         {
242             var itemCount = readBEInt (file, 4);
243             for (var itemIndex = 0; itemIndex < itemCount; itemIndex++)
244             {
245                 skipItem (file);
246             }
247         }
248         //
249         function skipItem (file)
250         {
251             var typeId = readBytes (file, 4);
252             switch (typeId)
253             {
254                 // Reference
255                 case 'obj ':
256                     skipReference (file);
257                     break;
258                 // Object descriptor
259                 case 'Objc':
260                 case 'GlbO':
261                     skipObject (file);
262                     break;
263                 // Class
264                 case 'type':
265                 case 'GlbC':
266                     skipClass (file);
267                     break;
268                 // List
269                 case 'VlLs':
270                     skipList (file);
271                     break;
272                 // Double
273                 case 'doub':
274                     skipDouble (file);
275                     break;
276                 // Unit double
277                 case 'UntF':
278                     skipBytes (file, 4);    // Unit
279                     skipDouble (file);      // Double
280                     break;
281                 // String
282                 case 'TEXT':
283                     skipUnicodeString (file);
284                     break;
285                 // Enumerated
286                 case 'enum':
287                     skipId (file);  // Enum type
288                     skipId (file);  // Enum value
289                     break;
290                 // Integer
291                 case 'long':
292                     skipInt32 (file);   // Signed integer
293                     break;
294                 // Large integer
295                 case 'comp':
296                     skipInt64 (file);   // Signed integer
297                     break;
298                 // Boolean
299                 case 'bool':
300                     skipInt8 (file);
301                     break;
302                 // File alias
303                 case 'alis':
304                     skipByteString (file);
305                     break;
306                 // File path
307                 case 'Pth ':
308                     skipByteString (file);
309                     break;
310                 // Raw data
311                 case 'tdta':
312                     skipByteString (file);
313                     break;
314                 // Object array (undocumented; used for polygonal selections...)
315                 case 'ObAr':
316                     var objCount = readBEInt (file, 4);
317                     skipClass (file);
318                     var itemCount = readBEInt (file, 4);
319                     for (var itemIndex = 0; itemIndex < itemCount; itemIndex++)
320                     {
321                         skipId (file);      // Key ID
322                         skipInt32 (file);   // Item type: 'UnFl' (undocumented; assumed to be "Unit floats")...
323                         skipInt32 (file);   // Unit ID
324                         var doublesCount = readBEInt (file, 4); // Should be equal to objCount!
325                         skipDoubles (file, doublesCount);
326                     }
327                     break;
328                 default:
329                     throw new Error ("[jamActions skipItem] Unrecognized item type: '" + typeId + "'");
330                     break;
331             }
332         }
333         //
334         function skipReference (file)
335         {
336             var itemCount = readBEInt (file, 4);
337             for (var itemIndex = 0; itemIndex < itemCount; itemIndex++)
338             {
339                 var formId = readBytes (file, 4);
340                 skipClass (file);   // Desired class
341                 switch (formId)
342                 {
343                     // Class
344                     case 'Clss':
345                         break;
346                     // Property
347                     case 'prop':
348                         skipId (file);
349                         break;
350                     // Enumerated
351                     case 'Enmr':
352                         skipId (file);  // Enum type
353                         skipId (file);  // Enum value
354                         break;
355                     // Offset
356                     case 'rele':
357                         skipInt32 (file);   // Signed integer
358                         break;
359                     // Identifier
360                     case 'Idnt':
361                         skipInt32 (file);   // Unsigned integer
362                         break;
363                     // Index
364                     case 'indx':
365                         skipInt32 (file);   // Unsigned integer
366                         break;
367                     // Name
368                     case 'name':
369                         skipUnicodeString (file);
370                         break;
371                     default:
372                         throw new Error ("[jamActions skipReference] Unrecognized item form: '" + formId + "'");
373                         break;
374                 }
375             }
376         }
377         //
378         /**
379          * @description Read an ActionDescriptor object from the current position of an open file stream [available in CS2 or later].
380          * @param {Object} file File object
381          * @param {Boolean} insertVersionPrefix Insert missing version prefix at the beginning of the file stream
382          * @returns {Object} ActionDescriptor object
383          * @example
384          * var actionDescriptor = jamActions.<strong>readActionDescriptor</strong> (inFile);
385          * var resultDescriptorObj = jamEngine.classIdAndActionDescriptorToJson (0, actionDescriptor);
386          * $.writeln (jamJSON.stringify (resultDescriptorObj["<descriptor>"], '\t'));
387          */
388         jamActions.readActionDescriptor = function (file, insertVersionPrefix)
389         {
390             var versionPrefix = "\x00\x00\x00\x10";
391             var start = file.tell ();
392             if (!insertVersionPrefix)
393             {
394                 if (file.read (4) === versionPrefix)
395                 {
396                     versionPrefix = "";
397                 }
398                 else
399                 {
400                     throw new Error ('[jamActions.readActionDescriptor] Unrecognized version prefix');
401                 }
402             }
403             skipObject (file);
404             var end = file.tell ();
405             file.seek (start, 0);
406             var stream = versionPrefix + file.read (end - start);
407             var actionDescriptor = new ActionDescriptor ();
408             actionDescriptor.fromStream (stream);
409             return actionDescriptor;
410         };
411         //
412         /**
413          * @description Convert an actions file (*.atn) or actions palette file (Actions Palette.psp) into a data structure
414          * in literal object format [available in CS2 or later].
415          * @param {String|Object} actionsFile Actions file (or actions palette file) path string or File object
416          * @param {Boolean} [isPalette] true if actions palette file
417          * @returns {Object|String} Converted actions file data structure in literal object format, or error message string
418          * <p style="margin: 0.5em 0 0 0em;">
419          * The actions file data structure is defined as a literal object with two members: either<br />
420          * <code>{ fileVersion: <em>fileVersion</em>, actionSets: <em>actionSets</em> }</code> for an actions palette file, or <br />
421          * <code>{ fileVersion: <em>fileVersion</em>, actionSet: <em>actionSet</em> }</code> for an actions file.
422          * </p>
423          * <p style="margin: 0.5em 0 0 1.25em;">
424          * <code><em>fileVersion</em></code>: number<br />
425          * <code><em>actionSets</em></code>: literal array [ ] of <code><em>actionSet</em></code><br />
426          * <code><em>actionSet</em></code>: literal object with three members:
427          * <code>{ name: <em>name</em>, expanded: <em>expanded</em>, actions: <em>actions</em> }</code>
428          * </p>
429          * <p style="margin: 0.5em 0 0 2.5em;">
430          * <code><em>name</em></code>: string<br />
431          * <code><em>expanded</em></code>: boolean<br />
432          * <code><em>actions</em></code>: literal array [ ] of <code><em>action</em></code>
433          * </p>
434          * <p style="margin: 0.5em 0 0 3.75em;">
435          * <code><em>action</em></code>: literal object with seven members:<br />
436          * <code>{ functionKey: <em>functionKey</em>, shiftKey: <em>shiftKey</em>, commandKey: <em>commandKey</em>,
437          * colorIndex: <em>colorIndex</em>, name: <em>name</em>, expanded: <em>expanded</em>, commands: <em>commands</em> }</code>
438          * </p>
439          * <p style="margin: 0.5em 0 0 5em;">
440          * <code><em>functionKey</em></code>: number<br />
441          * <code><em>shiftKey</em></code>: boolean<br />
442          * <code><em>commandKey</em></code>: boolean<br />
443          * <code><em>colorIndex</em></code>: number<br />
444          * <code><em>name</em></code>: string<br />
445          * <code><em>expanded</em></code>: boolean<br />
446          * <code><em>commands</em></code>: literal array [ ] of <code><em>command</em></code>
447          * </p>
448          * <p style="margin: 0.5em 0 0 6.25em;">
449          * <code><em>command</em></code>: literal object with six or seven members: either<br />
450          * <code>{ expanded: <em>expanded</em>, enabled: <em>enabled</em>, withDialog: <em>withDialog</em>, dialogOptions: <em>dialogOptions</em>,
451          * eventId: <em>eventId</em>, dictionaryName: <em>dictionaryName</em> }</code> or<br />
452          * <code>{ expanded: <em>expanded</em>, enabled: <em>enabled</em>, withDialog: <em>withDialog</em>, dialogOptions: <em>dialogOptions</em>,
453          * eventId: <em>eventId</em>, dictionaryName: <em>dictionaryName</em>, actionDescriptor: <em>actionDescriptor</em> }</code><br />
454          * </p>
455          * <p style="margin: 0.5em 0 0 7.5em;">
456          * <code><em>expanded</em></code>: boolean<br />
457          * <code><em>enabled</em></code>: boolean<br />
458          * <code><em>withDialog</em></code>: boolean<br />
459          * <code><em>dialogOptions</em></code>: number<br />
460          * <code><em>eventId</em></code>: number<br />
461          * <code><em>dictionaryName</em></code>: string<br />
462          * <code><em>actionDescriptor</em></code>: ActionDescriptor object (optional)
463          * </p>
464          * @example
465          * function actionsFileFilter (f)
466          * {
467          *     return (f instanceof Folder) || jamActions.isActionsFile (f);
468          * }
469          * var select = (File.fs === "Macintosh") ? actionsFileFilter : "Actions Files:*.atn,All Files:*";
470          * var actionsFile = File.openDialog ("Select an actions file:", select);
471          * if (actionsFile !== null)
472          * {
473          *     var fileData = jamActions.<strong>dataFromActionsFile</strong> (actionsFile);
474          *     if (typeof fileData === 'string')
475          *     {
476          *         alert (fileData + "\n" + "Actions file: “" + File.decode (actionsFile.name) + "”");
477          *     }
478          *     else
479          *     {
480          *         alert ("Action set name: “" + fileData.actionSet.name + "”");
481          *         alert ("Number of actions: " + fileData.actionSet.actions.length);
482          *     }
483          * }
484          */
485         jamActions.dataFromActionsFile = function (actionsFile, isPalette)
486         {
487             var that = this;
488             function parseActionSet (file)
489             {
490                 var actionSet = { };
491                 actionSet.name = localize (readUnicodeString (file));
492                 actionSet.expanded = (readBEInt (file, 1) !== 0);
493                 var actionCount = readBEInt (file, 4);
494                 actionSet.actions = [ ];
495                 for (var actionIndex = 0; actionIndex < actionCount; actionIndex++)
496                 {
497                     var action = { };
498                     action.functionKey = readBEInt (file, 2);
499                     action.shiftKey = (readBEInt (file, 1) !== 0);
500                     action.commandKey = (readBEInt (file, 1) !== 0);
501                     action.colorIndex = readBEInt (file, 2);
502                     action.name = localize (readUnicodeString (file));
503                     action.expanded = (readBEInt (file, 1) !== 0);
504                     var commandCount = readBEInt (file, 4);
505                     action.commands = [ ];
506                     for (var commandIndex = 0; commandIndex < commandCount; commandIndex++)
507                     {
508                         var command = { };
509                         command.expanded = (readBEInt (file, 1) !== 0);
510                         command.enabled = (readBEInt (file, 1) !== 0);
511                         command.withDialog = (readBEInt (file, 1) !== 0);
512                         command.dialogOptions = readBEInt (file, 1);
513                         command.eventId = readEventId (file);
514                         command.dictionaryName = readByteString (file);
515                         if (readBEInt (file, 4) !== 0)
516                         {
517                             command.actionDescriptor = that.readActionDescriptor (file, true);
518                         }
519                         action.commands.push (command);
520                     }
521                     actionSet.actions.push (action);
522                 }
523                 return actionSet;
524             }
525             //
526             var file;
527             if (typeof actionsFile === 'string')
528             {
529                 file = new File (actionsFile);
530             }
531             else if (actionsFile instanceof File)
532             {
533                 file = actionsFile;
534             }
535             else
536             {
537                 throw new Error ('[jamActions.dataFromActionsFile] Invalid argument');
538             }
539             //
540             var fileData;
541             if (file.open ("r"))
542             {
543                 try
544                 {
545                     file.encoding = 'BINARY';
546                     var fileVersion = readBEInt (file, 4);
547                     if (fileVersion === 16)
548                     {
549                         fileData = { };
550                         fileData.version = fileVersion;
551                         if (isPalette)
552                         {
553                             fileData.actionSets = [ ];
554                             var actionSetCount = readBEInt (file, 4);
555                             for (var actionSetIndex = 0; actionSetIndex < actionSetCount; actionSetIndex++)
556                             {
557                                 fileData.actionSets.push (parseActionSet (file));
558                             }
559                         }
560                         else
561                         {
562                             fileData.actionSet = parseActionSet (file);
563                         }
564                     }
565                     else
566                     {
567                         fileData = "Unsupported actions file version: " + fileVersion;
568                     }
569                 }
570                 catch (e)
571                 {
572                     fileData = e.message;
573                 }
574                 finally
575                 {
576                     file.close ();
577                 }
578             }
579             else
580             {
581                 fileData = "Cannot open file";
582             }
583             return fileData;
584         };
585         //
586         /**
587          * @description Test if a command is a play of a local action (belonging to the same action set).
588          * @param {Object} command Command data structure
589          * @param {String} actionSetName Action set name
590          * @returns {Array|Null} Array of three items: [ localActionName, localCommandIndex, localContinue ] if true, null if false
591          * @see jamActions.dataFromActionsFile
592          * @example
593          * function executeCommand (command)
594          * {
595          *     if (command.enabled)
596          *     {
597          *         var playCommand = jamActions.<strong>isLocalPlayCommand</strong> (command, actionSet.name);
598          *         if (playCommand !== null)
599          *         {
600          *             jamActions.traverseAction (actionSet, playCommand[0], playCommand[1], playCommand[2]);
601          *         }
602          *         else
603          *         {
604          *             var dialogMode = jamActions.determineDialogMode (command);
605          *             app.executeAction (command.eventId, command.actionDescriptor, dialogMode);
606          *         }
607          *     }
608          * }
609          * jamActions.setCommandHandler (executeCommand);
610          * jamActions.traverseAction (fileData.actionSet, actionName);
611          */
612         jamActions.isLocalPlayCommand = function (command, actionSetName)
613         {
614             var localPlayCommand = null;
615             if (command.eventId === app.stringIDToTypeID ("play"))
616             {
617                 var targetId = app.stringIDToTypeID ("target");
618                 if (command.actionDescriptor.hasKey (targetId))
619                 {
620                     var localReference = command.actionDescriptor.getReference (targetId);
621                     do
622                     {
623                         // var desiredClassId = localReference.getDesiredClass ();
624                         try { var desiredClassId = localReference.getDesiredClass (); } catch (e) { break; }
625                         switch (desiredClassId)
626                         {
627                             case app.stringIDToTypeID ("command"):
628                                 var localCommandIndex = localReference.getIndex () - 1;
629                                 break;
630                             case app.stringIDToTypeID ("action"):
631                                 var localActionName = localReference.getName ();
632                                 break;
633                             case app.stringIDToTypeID ("actionSet"):
634                                 var localActionSetName = localReference.getName ();
635                                 break;
636                         }
637                         localReference = localReference.getContainer ();
638                     }
639                     while (localReference);
640                 }
641                 var continueId = app.stringIDToTypeID ("continue");
642                 if (command.actionDescriptor.hasKey (continueId))
643                 {
644                     var localContinue = command.actionDescriptor.getBoolean (continueId);
645                 }
646                 if ((typeof localActionSetName !== 'undefined') && (localActionSetName === actionSetName))
647                 {
648                     localPlayCommand = [ localActionName, localCommandIndex, localContinue ];
649                 }
650             }
651             return localPlayCommand;
652         };
653         //
654         /**
655          * @description Determine dialog mode depending on command settings
656          * @param {Object} command Command data structure
657          * @returns {Object} Dialog mode: either DialogModes.ALL or DialogModes.NO
658          * @see jamActions.dataFromActionsFile
659          * @example
660          * app.executeAction (command.eventId, command.actionDescriptor, jamActions.<strong>determineDialogMode</strong> (command));
661          */
662         jamActions.determineDialogMode = function (command)
663         {
664             var dialogMode;
665             switch (command.dialogOptions)
666             {
667                 case 0:
668                     dialogMode = command.withDialog ? DialogModes.ALL : DialogModes.NO;
669                     break;
670                 case 2:
671                     dialogMode = DialogModes.NO;
672                     break;
673                 case 1:
674                 case 3:
675                     dialogMode = DialogModes.ALL;
676                     break;
677             }
678             return dialogMode;
679         }
680         //
681         var globalCommandHandler = null;
682         //
683         /**
684          * @description Set the global command handler [available in CS2 or later].<br />
685          * @param {Function} commandHandler Global command handler: function to be called by jamActions.traverseAction () for each command in the action
686          * @see jamActions.traverseAction
687          * @example
688          * function debugCommand (command)
689          * {
690          *     $.writeln ("Dictionary Name: " + command.dictionaryName);
691          *     $.writeln ("Enabled: " + command.enabled);
692          *     $.writeln ("With Dialog: " + command.withDialog);
693          *     $.writeln ();
694          * }
695          * jamActions.<strong>setCommandHandler</strong> (debugCommand);
696          * jamActions.traverseAction (fileData.actionSet, 0);
697          */
698         jamActions.setCommandHandler = function (commandHandler)
699         {
700             globalCommandHandler = commandHandler;
701         };
702         //
703         /**
704          * @description Traverse all the commands of a specific action of an action set data structure obtained from a converted actions file (*.atn)
705          * [available in CS2 or later].<br />
706          * The global command handler set by jamActions.setCommandHandler () is called for each command in the action.
707          * @param {Object} actionSet Action set data structure in literal object format
708          * @param {String|Number} actionLocator Action locator: action name or action index (0-based)
709          * @param {Number} [fromCommandIndex] Index (0-based) of command to start from
710          * @param {Boolean} [continuePlay] Continue to play remaining commands after the fromCommandIndex index
711          * @see jamActions.dataFromActionsFile
712          * @see jamActions.setCommandHandler
713          * @example
714          * Folder.current = new Folder ("~/JSON Action Manager/tests/resources/");
715          * jamEngine.jsonPlay ("open", { "target": { "<path>": "Factory.jpg" } });
716          * var actionsFilePath = "Cross Processing.atn";
717          * var fileData = jamActions.dataFromActionsFile (actionsFilePath);
718          * if (typeof fileData === 'string')
719          * {
720          *     alert (fileData + "\n" + "Actions file: “" + actionsFilePath + "”");
721          * }
722          * else
723          * {
724          *     function executeCommand (command)
725          *     {
726          *         if (command.enabled)
727          *         {
728          *             var dialogMode = jamActions.determineDialogMode (command);
729          *             app.executeAction (command.eventId, command.actionDescriptor, dialogMode);
730          *         }
731          *     }
732          *     jamActions.setCommandHandler (executeCommand);
733          *     jamActions.<strong>traverseAction</strong> (fileData.actionSet, "Cross Process 2");
734          * }
735          */
736         jamActions.traverseAction = function (actionSet, actionLocator, fromCommandIndex, continuePlay)
737         {
738             function handleCommands (commands)
739             {
740                 var commandMax = (continuePlay) ? commands.length : fromCommandIndex + 1;
741                 for (var commandIndex = fromCommandIndex; commandIndex < commandMax; commandIndex++)
742                 {
743                     if (globalCommandHandler !== null)
744                     {
745                         globalCommandHandler (commands[commandIndex]);
746                     }
747                 }
748             }
749             //
750             if (typeof fromCommandIndex === 'undefined')
751             {
752                 fromCommandIndex = 0;
753                 continuePlay = true;
754             }
755             var actions = actionSet.actions;
756             if (typeof actionLocator === 'string')
757             {
758                 var actionName = actionLocator;
759                 for (var actionIndex = 0; actionIndex < actions.length; actionIndex++)
760                 {
761                     var action = actions[actionIndex];
762                     if (action.name === actionName)
763                     {
764                         handleCommands (action.commands);
765                         break;
766                     }
767                 }
768             }
769             else if (typeof actionLocator === 'number')
770             {
771                 var actionIndex = actionLocator;
772                 if ((actionIndex >= 0) && (actionIndex < actions.length))
773                 {
774                     handleCommands (actions[actionIndex].commands);
775                 }
776             }
777         };
778     } ());
779 }
780 
781 //------------------------------------------------------------------------------
782 
783