1/ Graphics, Layout, Coordinates
Changing the Crop Options of a Placed PDF
Moving Objects with respect to a Reference Point
How to Create a Mixed Ink
Sorting Images based on their Position on the Page

2/ On the JS/ExtendScript Side
Secrets of the throw Statement
Note on Accessing DOM Object Properties
Clarification on Script Versioning Features

3/ UI and ScriptUI stuff
Dealing with Multiline StaticText Preferred Size
Managing a Palette as a Singleton
Replacing Images on the Fly
Using an IdleEvent Listener to Alert on PageTool Usage

4/ BONUS TRACK: Indiscripts on GitHub


1/ Graphics, Layout, Coordinates

Changing the Crop Options of a Placed PDF

Sandee Cohen challenged us to update a CS2-script written by Dave Saunders in 2006. Its purpose is to “change the Crop options of an already placed PDF (includes AI and INDD) file.” Unfortunately it doesn't work in today's environment.

Although we never received any news from S. Cohen on this challenge, I'm pretty confident that the code below answers her request.

//====================================================================
// PDFImportCropPref Reloaded
// (should work in ID CS/CS2/CS3/CS4/CS5/CS6/CC)
//====================================================================
(function(o,a,z,r,i,x,t,k)
{
    for(k in o) o.hasOwnProperty(k) &&
        (a['_'+z]=t=+o[k]) &&
        (a[z++]=k.substr(5).replace(r,' ')) &&
        (i||(x==t&&(i=z)));
 
    (((k='userInteractionLevel')in(o=app))?o:o.scriptPreferences)
        [k] = +UserInteractionLevels.interactWithAll;
 
    (t=o.dialogs).length && t.everyItem().destroy();
 
    r=(t=t.add({name:"Place PDF Crop Preference"}))
        .dialogColumns.add().dialogRows.add()
        .staticTexts.add({staticLabel:"Choose preference: "})
        .parent.dropdowns.add({stringList:a,selectedIndex:i-1});
 
    o.activate();
 
    t.show() &&
        (o.pdfPlacePreferences.pdfCrop=a['_'+r.selectedIndex]);
 
})(PDFCrop,[],0,/_/g,0,+app.pdfPlacePreferences.pdfCrop);
 

• Original discussion: forums.adobe.com/message/5749506#5749506

Moving Objects with respect to a Reference Point

Ken0207 wrote: “I need to move images on my document using different reference points. But it looks like by default, it's moving them using the AnchorPoint.TOP_LEFT_ANCHOR. I can't seem to change it.”

Indeed, the to parameter in PageItem.move(to, by) is (implicitly!) top-left-relative, whatever the current transformReferencePoint. Therefore we need to write a custom method to get more flexibility.

Suggested code (including bug fix):

function moveToConsideringAnchor(obj, xyDest, anchorPt)
// -------------------------------------
// <obj>      : any singular PageItem specifier
// <xyDest>   : [x,y] location expressed in the current ruler space
// <anchorPt> : the considered AnchorPoint (default is top-left)
{
    if( !(obj && 'transform' in obj) ) return;
 
    var CS_PASTEBOARD = CoordinateSpaces.pasteboardCoordinates;
 
    anchorPt || (anchorPt=AnchorPoint.topLeftAnchor);
 
    var xy0 = obj.resolve(anchorPt,CS_PASTEBOARD)[0],
        xy1 = obj.resolve([xyDest,anchorPt],CS_PASTEBOARD,true)[0],
        dx = xy1[0]-xy0[0],
        dy = xy1[1]-xy0[1];
 
    obj.transform(CS_PASTEBOARD,[0,0],[1,0,0,1,dx,dy]);
}
 
// ---
// SAMPLE CODE
// ---
 
// Your settings:
var CONSIDERED_ANCHOR = AnchorPoint.BOTTOM_RIGHT_ANCHOR,
    XY_DESTINATION = [159,61.3]; // in ruler space
 
var sel = (1==app.selection.length) && app.selection[0];
 
if( sel instanceof Rectangle )
    {
    moveToConsideringAnchor(sel, XY_DESTINATION,
        CONSIDERED_ANCHOR);
    }
 

• Original discussion: forums.adobe.com/message/5021396#5021396

How to Create a Mixed Ink

The MixedInks.add(...) method expects the following arguments, in that order:
• inkList (array of Inks or MixedInkGroup),
• inkPercentages (Array of numbers),
• withProperties (Object w/ additional mixed ink props — optionnal).

Note that inkList, if supplied as an array, must contains actual existing (i.e., valid) specifiers, rather than just ink names. So it's a good idea to provide a generic validation routine—see validateInkData() below.

Suggested code:

// Your settings
// -------------------------------------
var MY_MIX_INKS = {
    // Valid ink names are required
    // At least 2 process inks are required
    'Process Cyan'    :   0,      // Process1 : %
    'Process Magenta' :   0,      // Process2 : %
    'MyCustomInk'     : 100,      // Custom1  : %
    'PANTONE 871 C'   : 100       // Custom2  : %
    },
    MY_MIX_NAME = 'DoubleColor';
 
// Ink Validator
// -------------------------------------
var validateInkData = function(o, c, ll, pp)
    {
    var k, t, z=0, r=1;
 
    ll.length = pp.length = 0;
 
    for( k in o )
        {
        if( !o.hasOwnProperty(k) ) continue;
        if( !(t=c.itemByName(k)).isValid ){ r=0; break; }
        ll[z] = t.getElements()[0];
        pp[z++] = +o[k];
        }
 
    return r;
    };
 
// Main Code
// -------------------------------------
var doc = app.activeDocument,
    inkList,    // will receive validated ink list (array)
    inkPerc;    // corresponding percentages (array)
 
if( !doc.mixedInks.itemByName(MY_MIX_NAME).isValid )
    {
    if( validateInkData(MY_MIX_INKS,
        doc.inks, inkList=[], inkPerc=[]) )
        {
        doc.mixedInks.add(inkList, inkPerc,
            {
            name: MY_MIX_NAME,
            model: ColorModel.MIXEDINKMODEL,
            space: ColorSpace.MIXEDINK
            });
 
        alert( "The mixed ink '" + MY_MIX_NAME +
            "' has been successfully created." );
        }
    else
        {
        alert( "Unable to create mixed ink. " +
            "Some inner inks are missing." );
        }
    }
else
    {
    alert( "The mixed ink '" + MY_MIX_NAME +
        "' already exists." );
    }
 

• Original discussion: forums.adobe.com/message/5709318#5709318

Sorting Images based on their Position on the Page

Tushar wrote: “How would I go about sorting images based on their position on the page? I've read a few articles that mention using array sort method and then checking their x and y values to sort the array. While that works on page items that are perfectly aligned on their x and y axis, it fails on a layout [that contains sparsed items].”

The main issue here is that no “natural order” can be directly derived from just considering (x,y) coordinates as they are. Studying the disposition below:

Sparse rectangles.

our eyes instantly detect that there should be 3 columns and 5 rows, but this underlying order isn't reached from just sorting the set of coordinates. We need a better algorithm.

What I suggest is to speculate on the gaps that occur on the ordered sequence of x-coordinates and y-coordinates, respectively. To reveal these gaps, let's sort data along the x-axis first:

Sorting along the x-axis.

The figure above only shows the widths of the rectangles. Each arrow represents a rectangle, and I've ordered the items by increasing x-centers. One can estimate that the element labelled #1 belongs to a new group from this simple fact: its left coordinate (red guide) is higher than the right coordinate (blue guide) of the element #0. This observation will give us a strategy to detect column gaps.

Then, the same method is applied to detect rows. (Sorting by y-values, identifying gaps based on min-max progression.)

At the end of this process, every object has a (column, row) coordinate pair instead of sparse (x,y) coordinates. So we can compute the final order, i.e. the weights used in the comparison function.

Here is my implementation of this algorithm:

// ========================================================
// Up2Bottom and Left2Right Sorting Algorithm
//   addressing (weakly) sparse rectangles
// ---
// Usage:  Select the objects, then run the script
// Target: InDesign CS4/CS5/CS6/CC
// ========================================================
 
const CS = +CoordinateSpaces.SPREAD_COORDINATES,
      AP_MIN = +AnchorPoint.TOP_LEFT_ANCHOR,
      AP_CENTER = +AnchorPoint.CENTER_ANCHOR,
      AP_MAX = +AnchorPoint.BOTTOM_RIGHT_ANCHOR;
 
var sel = app.properties.selection || null,
    data = [],
    r, i, j, k, t, n, w, vMax;
 
if( sel && 1 < (n=sel.length) )
    {
    // Collect coordinates and IDs
    // ---
    for(i=0 ; i < n && (t=sel[i]) ; ++i )
        {
        data[i] = {
            min:    t.resolve(AP_MIN,CS)[0],
            weight: t.resolve(AP_CENTER,CS)[0],
            max:    t.resolve(AP_MAX,CS)[0],
            id:      t.id
            };
        }
 
    // Find rows and columns [i.e. y-weights and x-weights]
    // ---
    for( j=0 ; j < 2 ; ++j )
        {
        // Sort by center coordinate
        // ---
        data.sort(function(a,b){return a.weight[j]-b.weight[j]});
 
        // min > max ==> w++
        // ---
        for( vMax=(t=data[0]).max[j], t.weight[j]=(w=0), i=1 ;
            (i < n)&&(t=data[i]) ; ++i )
            {
            if( t.min[j] > vMax ){ ++w; vMax=t.max[j]; }
            t.weight[j] = w;
            }
        }
 
    // Compute weights, clean up data, create ID-to-weight access
    // ---
    for( i=0 ; (i < n)&&(t=data[i]) ; ++i )
        {
        w = n*t.weight[1] + t.weight[0];  // final weight (y 1st)
        k = '_'+t.id;                     // ID key
 
        (t.min.length=0)||(t.weight.length=0)||(t.max.length=0);
        delete t.min; delete t.weight; delete t.max; delete t.id;
        delete data[i];
 
        data[k] = w;                      // ID-to-weight
        }
 
    // Apply sort --> r
    // ---
    r = sel.sort(function(a,b)
        {
        return data['_'+a.id]-data['_'+b.id];
        });
 
    // Show the resulting order
    // ---
    for( i=0 ; i < n ; ++i )
        {
        app.select(r[i]);
        $.sleep(1000);
        }
    }
 

• Original discussion: forums.adobe.com/message/5711651#5711651

2/ On the JS/ExtendScript Side

Secrets of the throw Statement

We incidently discovered that the throw statement always triggers the toString() method of the throwed object, which might be very surprising:

// Throwing obj causes obj.toString() invocation
//--------------------------------------
 
var obj = {
    toString: function(){ alert("Hello guys!"); }
    };
 
try{ throw obj; } // => alerts Hello guys!
catch(_){}
 

As far as we know this behavior does not comply with ECMA-262 standard, that is, it's a pure ExtendScript “feature.” Another fact we noticed is that throwing anything binds the caught reference to anything, whatever it refers to:

// What you throw is what you catch!
//--------------------------------------
 
var anything = {}; // works with literals too
                   // e.g. "foobar", or even 5!
 
try{ throw anything; }
catch(e){ alert( e===anything ); }  // true!
 

For these reasons the actual Error(...) constructor—which basically acts as a simple {message, name, etc.} object factory—doesn't seem essential to the try/throw/catch trio.

Therefore, as long as you need to handle custom exceptions, you could as well entirely bypass the native Error object and create your own exception tracker, as shown below:

// No need to use the native Error class
//--------------------------------------
 
function MyException(msg,s)
{
    this.message = msg || '';
    this.name = s || 'Error';
    this.stack = null;
};
 
MyException.prototype.toString = function()
{
    // Hook $.stack *on first invocation*
    // ---
    this.stack || (this.stack = $.stack.
                replace(/[\r\n]toString\(\)[\r\n]$/,'').
                split(/[\r\n]/)
                );
 
    // Make toString() ECMA-262 compliant
    // ---
    return this.name + ': ' + this.message;
};
 
 
// =================
// Example code
// =================
 
function inner()
{
    throw new MyException("Something's gone wrong!");
}
 
function outer()
{
    inner();
}
 
function main()
{
    try{ outer(); }
    catch(e)
        {
        alert( e );        // => Error: Something's gone wrong!
        alert( e.stack );  // => [myScript.jsx],main(),outer(),inner()
        }
}
 
main();
 

However, built-in Error types (Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError) cannot be tracked this way. Indeed, ExtendScript does not invoke Error.prototype.toString() when a native error occurs. (The opposite would be somewhat embarrassing!)

// Pure errors (not explicitly thrown) do not involve toString
//--------------------------------------
 
// !!!!!!!!!!!!!!
// Disclaimer: overriding Error.prototype
// is not recommended at all!
// !!!!!!!!!!!!!!
 
Error.prototype.toString = function(){ alert( "Hello guys!" ); };
 
// Let's cause a RangeError...
try {
    new Array(-1); // silent!
    }
catch(e)
    {
    alert( e.name ); // => RangeError
    }
 
// By contrast:
try {
    throw new Error("My error"); // => Hello guys!
    }
catch(e)
    {
    alert( e.name ); // => Error
    }
 

• Original discussion: forums.adobe.com/message/5302989#5302989

Note on Accessing DOM Object Properties

We should keep in mind that InDesign DOM objects do not behave as regular JavaScript objects. A regular object would evaluate obj.fakePropMeth to undefined, but DOM objects involve a kind of runtime pre-check, so that the usual shortcut does not work.

The syntax 'fakePropMeth' in obj is generally safe in that it does not send a hidden command to the receiver. However, 'fakeProp' in obj may still be misleading! It only checks whether the property is syntactically available.

For instance, 'activeDocument' in app always evaluates to true, even while app.activeDocument leads to a runtime error in the case no document is present. This shows that the dot operator causes the program to send an actual command to the subsystem, rather than just checking that a property is available. That's the reason why we often have to use the obj.properties object, which collects only the properties that are actually available.

So, in my example, the correct test would be:

// Testing the actual availability of a property, the right way:
 
if( 'activeDocument' in app.properties ){ /*ok*/ }
 

• Note inserted at: forums.adobe.com/message/5681326#5681326

Clarification on Script Versioning Features

InDesign scripters use app.scriptPreferences.version to target a specific Scripting DOM version. This looks straightforward at first glance, but there are actually many issues in that scope.

Here are just a few key rules you should be aware of:

1) The Cross-DOM Rule. — As long as a script only invokes DOM methods and features that are common to all InDesign versions under consideration—say app.selection or myDocument.pageItems—the code remains univoque and you don't need to set app.scriptPreferences.version to get it properly working (whatever the version from which you run it). The script is cross-version safe.

2) The Time Arrow Rule. — CS5 may have knowledge about CS4 specific features, but CS4 cannot have knowledge about CS5 specific features! Therefore, no matter how your code is interpreted or translated from one DOM version to another, there is no way to make InDesign CS4 understand and manage a topLeftCornerOption. The CS4 Scripting DOM only knows the uniform cornerOption property:

// topLeftCornerOption is definitely not available in CS4
 
// has no effet, seen from CS4:
app.scriptPreferences.version = 7.0;
 
// CS4: Game over! -- CS5: OK
alert( myRectangle.topLeftCornerOption );
 

3) The Retro-Versioning Rule. — To some respect, you can tell to CS5 that your code (or part of your code) is CS4 DOM-based, using the app.scriptPreferences.version = 6.0 directive. This allows to switch to a previous scripting DOM, provided that the underlying objects (in the CS5 document) can be mapped to a CS4 feature:

// topLeftCornerOption is a CS5 specific feature
// cornerOption is a CS4 feature
// (not available in CS5 but 'mappable')
 
// CS5: OK -- CS4: Game over!
alert( myRectangle.topLeftCornerOption );
 
app.scriptPreferences.version = 6.0; // Go back to CS4
alert( myRectangle.cornerOption );   // OK
 
app.scriptPreferences.version = 7.0; // Go back to CS5
alert( myRectangle.topLeftCornerOption ); // OK
 

A crucial consequence of the “Retro-Versioning Rule” is that it not only makes previous DOM tokens available, it also may change the meaning, or the behavior, of an existing method/property. For instance:

alert( typeof SpecialCharacters.BULLET_CHARACTER );
// => CS4: Number -- CS6: Object
 
app.scriptPreferences.version = 6.0; // Go back to CS4
 
alert( typeof SpecialCharacters.BULLET_CHARACTER );
// => CS4: Number -- CS6: Number
 

• Original discussion: forums.adobe.com/message/5692242#5692242

3/ UI and ScriptUI Stuff

Dealing with Multiline StaticText Preferred Size

Multiline StaticText widgets reveal weird things in the way preferredSize is computed within the layout manager.

Usually the characters property (if supplied before you hit preferredSize) will do a good job, provided that you already know how many characters a line is supposed to support:

var w = new Window('dialog'),
    gp = w.add('group'),
    st = gp.add('statictext', undefined, "Nonserita demquat issimus ex eatiust, occatur, ut ommod "+
        "expliquodi ratusam, que vendest dolorepro coreium volorempere doluptat as exceaquam " +
        "quo qui\r\rBus reped mos exere od quodior porum etc. etc.", {multiline:true}
        );
 
st.characters = 50; // WARNING: do not hit st.preferredSize before you set the characters property!
 
with( gp.graphics ){ backgroundColor = newBrush(w.graphics.BrushType.SOLID_COLOR,[1,0,0],1); }
 
w.show();
 

However, if you need to specify a preferred width instead, then things become complex because st.preferredSize=[MY_PREFERRED_WIDTH,-1] does not lead the layout manager to properly anticipate the height of the widget. I suspect the preferred size is always computed relative to an explicit or implicit characters number. So you have to provide the optimal number of characters that match the constraint.

As far as I know, the internal ScriptUI processing is based on the metrics of the 'X' character. So my workaround is to proceed as follows:

var w = new Window('dialog'),
    gp = w.add('group'),
    st = gp.add('statictext', undefined, 'X', {multiline:true}),
    X_WIDTH = st.preferredSize[0], // Here I grab the X width
    PREFERRED_WIDTH = 600;     // Your preferred width, in px
 
with( st )
    {
    // Reset the preferred size so that the
    // characters property will cause an update
    // ---
    preferredSize = [-1,-1];
 
    // Estimate the optimal character number
    // considering PREFERRED_WIDTH and X_WIDTH
    // ---
    characters = ~~(PREFERRED_WIDTH/X_WIDTH);
 
    // Will force the preferred height to be computed
    // again given the implied width and characters prop.
    // ---
    preferredSize[1] = -1;
    }
 
// Finally, set the actual text
// ---
st.text = "Nonserita demquat issimus ex eatiust, occatur, ut ommod expliquodi "+
        "ratusam, que vendest dolorepro coreium volorempere doluptat as exceaquam " +
        "quo qui\r\rBus reped mos exere od quodior porum etc. etc.";
 
with( gp.graphics ){ backgroundColor = newBrush(w.graphics.BrushType.SOLID_COLOR,[1,0,0],1); }
 
w.show();
 

Not tested on all platforms though.

• Original discussion: forums.adobe.com/message/5528134#5528134

Managing a Palette as a Singleton

When you create a palette in a persistent #targetengine (and provided that you want it to behave as a singleton), there is usually no need to kill its reference in memory. Just set and manage that instance in a compact location, and make sure you never needlessly rebuild the object.

This can be done through the following template:

#targetengine ManageSingletonPalette
 
$.UserInterface || ($.UserInterface = function F(FLAG)
//--------------------------------------
// FLAG == -1  => destroy
// FLAG ==  0  => restore
// FLAG ==  1  => rebuild
{
    F.W || (F.W=Window.find("palette", "My Palette"));
 
    // Destroy?
    // ---
    if( F.W && FLAG )
        {
        F.W.visible && F.W.close();
        F.W = null;
        delete F.W;
        }
 
    if( -1===FLAG ) return;
 
    // Create?
    // ---
    if( !F.W )
        {
        F.W = new Window('palette', "My Palette",
            [50,50,300,300], {resizeable:true});
        F.W.add('edittext',[50,50,200,200]);
        // etc.
        }
 
    F.W.visible || F.W.show();
});
 
// use -1 to KILL the palette, 1 to REBUILD it from scratch
$.UserInterface();
 

Using a persistent container, $.UserInterface, offers the option to safely and programatically close the window when some condition is reached. Since the code above does not use closures—that is, ExtendScript workspaces—there is no residual reference to the palette once we have successively done:

F.W.close();
F.W = null;
delete F.W;
 

From that point the automatic garbage collection should work the regular way, and $.summary() should never report more than one Window reference.

Note. — If your purpose is only to free up memory in your engine when the user closes the palette, this is not that critical. As long as your code handles at most a single instance of the object and does not introduce memory leaks—as guaranteed by $.UserInterface—there is no serious risk in this matter.

• Original discussion: forums.adobe.com/message/5450852#5450852

Replacing Images on the Fly

Peter Kahrel wrote: “When you use graphic files (such as PNGs) for images, you can change the contents of an image control. The following script sets an image (iconA) but immediately replaces it with iconB, so that by the time the window is displayed, iconB is the one you see:”

iconA = File ('. . . ..png');
iconB = File ('. . . ..png');
 
w = new Window ('dialog');
    icon = w.add ('image', undefined, iconA);
    icon.image = iconB;
w.show();
 

But that trick does not work “when you use strings for icons”, that is, stringified PNGs in the form "\u0089PNG\r\n\x1A\ etc." In other words, ScriptUI does not offer a way to change a ScriptUIImage at runtime through a PNG-string.

A possible workaround is to provide an onDraw method that will invoke graphics.drawImage() in order to override the existing image. This approach involves some black magic, as already shown in our “Sprite Buttons” article.

As the general issue is to trigger onDraw on demand, here is a kind of template that solves the problem by prototyping an Image.refresh() method:

// Trigger onDraw on demand
// [CS4/CS5/CS6/CC compatibility routine]
// ---
const CC_FLAG = +(9 <= parseFloat(app.version));
 
Image.prototype.refresh = CC_FLAG ?
    function()
    {
        var wh = this.size;
        this.size = [1+wh[0],1+wh[1]];
        this.size = [wh[0],wh[1]];
        wh = null;
    }:
    function()
    {
        this.size = [this.size[0],this.size[1]];
    };
 
// Your code
// ---
var PNGS = [
    /*on*/    "\x89PNG . . . \x82",
    /*off*/   "\x89PNG . . . \x82",
    /*mixed*/ "\x89PNG . . . \x82"
    ];
 
var w = new Window('dialog'),
    icon = w.add('image', undefined, PNGS[0], {id:0});
 
icon.onDraw = function()
    {
    this.graphics.drawImage(ScriptUI.
       newImage(PNGS[this.properties.id]),0,0);
    };
 
icon.addEventListener('click', function()
    {
    this.properties.id = (1 + this.properties.id) % 3;
    this.refresh();
    });
 
w.show();
 

However, I do not recommend that approach, because it leads to an expensive use of ScriptUI.newImage(). This is, every onDraw has to compute a new ScriptUIImage.

One option—not tested—is to pre-convert each PNG string into a ScriptUIImage using ScriptUI.newImage(PNGS[0..3]), then to access these ScriptUIImages through an array (from onDraw, as done above).

Another option (my preferred way) is to create a single PNG, that is, a single ScriptUIImage, then to apply a shift accordingly, as shown in the “Sprite Buttons” technique.

• Original discussion: forums.adobe.com/message/5661259#5661259

• See also: “Sprite Buttons” in ScriptUI

Using an IdleEvent Listener to Alert on PageTool Usage

The PageTool selection event can be listened to via afterAttributeChanged, but that event needs some time to complete. Thus, if your script attempts to interact at the wrong time, you may encounter odd effects.

For example, alert() or confirm() might give the focus to a new modal window before the PageTool selection event life-cycle is finished, so the GUI becomes instable and the PageTool icon state is not properly restored, as experienced by a user.

My workaround here consists in queuing a temporary IdleEvent listener as soon as the Page Tool selection event is caught. Then, we do not initiate any modal dialog as long as the GUI is refreshing.

Suggested code:

//====================================================
// PageToolDisclaimer.jsx
//====================================================
 
// Should be useable as a startup script, no #targetengine required
// NB - MutationEvent is known to create a global 'evt' variable
// so we don't seem to need a persistent session engine here :-)
// That's why the active script File is used as the event handler
 
(function(EVENT_HANDLER, TASK_NAME, TASK_TIME, PAGE_TOOL_NAME)
{
    var t;
 
    // Installer
    // ---
    if( !(t=app.toolBoxTools.eventListeners).length )
        {
        t.add(MutationEvent.AFTER_ATTRIBUTE_CHANGED,EVENT_HANDLER);
        return;
        }
 
    // IdleEvent handler (--> confirm)
    // ---
    if( (t=app.idleTasks.itemByName(TASK_NAME)).isValid )
        {
        t.eventListeners.everyItem().remove();
        t.remove();
        if( !confirm("***WARNING***\r"+
            "Do you really want to activate the page tool?",
            true) )
            {
            app.toolBoxTools.currentTool = UITools.SELECTION_TOOL;
            }
        return;
        }
 
    // PageTool event handler
    // ---
    if( ('evt' in $.global) &&
        'currentToolName'==evt.attributeName &&
        PAGE_TOOL_NAME==evt.attributeValue )
        {
        evt.stopPropagation();
        app.idleTasks.add({name:TASK_NAME, sleep:TASK_TIME})
              .addEventListener(IdleEvent.ON_IDLE, EVENT_HANDLER);
        }
})( app.activeScript, 'WaitPageTool', 400,
app.translateKeyString('$ID/Page Tool') );
 

• Original discussion: forums.adobe.com/message/5715676#5715676

4/ BONUS TRACK: Indiscripts on GitHub

Yeah! We finally decided to create our account on GitHub, which is undoubtedly the safest way to share with you long-term codes, libraries, etc.

By way of introduction, the muZ repository contains a micro-parser that shows what can be done using operator overloading in ExtendScript. This project is probably not intended to newbies, but it might be of some interest to experienced scripters.

github.com/indiscripts/extendscript/tree/master/muZ