Avg. Rating 4.0

Problem

There is an issue with Photoshop CS5 and developing Creative Suite Extensions using Creative Suite Extension Builder and ActionScript. The issue affects Mac computers and Photoshop DOM calls in the context of a system event (such as a timer or network event). As Creative Suite Extensions are typically designed to run on both Mac and Windows, this has been a significant hurdle for developers. Presented here is a viable workaround.

Solution

Including the attached library (PsMacDoc.swc) and it's PsEventScrubber class, you can work around this issue in a way that is not overly invasive to your program design.

Detailed explanation

The Creative Suite SDK along with Extension Builder allow developers to create compelling extensions to Creative Suite applications using ActionScript. For the cases of InDesign, Photoshop, Bridge, and Illlustrator, there are a set of librarys included in the SDK, the Creative Suite ActionScript Wrapper libraries.  These libraries allow developers to directly call the product's internal scripting DOM's directly from ActionScript.

In the case of Photshop on the Mac, however, there was an issue with ActionScript and the DOM when the ActionScript was in the context of an OS generated event (such as a timer or network event). In the case where, say a timer event fires, your ActionScript code could not use Photoshop's DOM calls directly. As extensions are typically cross-platform, this was a significant impediment.

One workaround was to use the ScriptListener. Photoshop includes a ScriptListener plug in that, once installed, writes out action code to a log file for whatever the user does in Photoshop's UI. That action code, ported to ActionScript, could be executed in the context of a system event. But that code is hard to read and relatively inflexible. There are many times a developer will use this method when what's needed is beyond the DOM to provide. But having to use action code for simple things like creating or saving a document, creating a layer was an inconvenience.

I have managed to come up with a code library that allows developers to work around this issue. This cookbook will show you how to do it.

If everything worked right on upload, there should be 2 files attached to this entry. PsMacDom.zip, and PsDomEvents.zip. When unzipped, the first file is the library (swc file). The latter is the sample project I'll be writing about here. Unzip everything, import the project to your Flash Builder workspace, and of course add the library to your imported project. Go ahead, I'll wait...

There are two workaround methods included in the swc, and both are demo'd in the sample project. Here's the code from the first one:

public function run():void
        {
            var timer: Timer = new Timer( 1000, 1 );
            timer.addEventListener( TimerEvent.TIMER, dummyHandler );
            timer.addEventListener( TimerEvent.TIMER_COMPLETE, secondDemo );
            timer.start();
            trace( "timer started for first demo" );
        }
        // first demo shows how to use a dummy handler to call the real handler.
        // PsEventScrubber - cleans up stuff and makes it so the real handler's DOM Calls work.
        public function dummyHandler( evt: Event ): void
        {
            trace( "in the dummy handler" );
            PsEventScrubber.getInstance().scrubEvent( evt, realHandler );
        }
        public function realHandler( evt: TimerEvent ): void
        {
            trace( "in the real handler" );
            try
            {
                var app: Application = Photoshop.app;
                var doc: Document = app.documents.add();
                var layer: ArtLayer = doc.artLayers.add();
                layer.name = "fred";
                doc.close( SaveOptions.DONOTSAVECHANGES );
            } catch ( e: Error )
            {
                trace( e );
            }
        }

When the "run" method is called, we set up a timer, and add a couple of event listeners. The first one calls "dummyHandler" when the timer event fires. The second one simply starts the second demo with the first is over.

Looking at dummyHandler - you can see there's a singleton, PsEventScrubber. That's the class included in the SWC that handles the core of the workaround. All you have to do is call one of it's two workaround methods.
In this case, we're calling scrubEvent, passing it the event and the handler function that actually contains your code. PsEventScrubber then does it's magic to execute the workaround then calls your "real" handler ot do the Photoshop DOM work. It's a pretty simple workaround, but has a limitation - it must be first in line when you receive the system event.

What would you do it you had a function that did some PS magic and wasn't being called in the context of a system event; but, there was no guarantee that it wouldn't be in the future? It'd be nice to be able to write that method so that no matter how it's executed, it will work.

That's why I did the second workaround. Here's the code you need:

public function secondDemo( evt: TimerEvent ): void
        {
            var timer: Timer = new Timer( 1000, 1);
            timer.addEventListener( TimerEvent.TIMER, secondHandler );
            timer.start();
            trace( "timer started for second demo" );
        }
        public function secondHandler( evt: TimerEvent ): void
        {
            trace( "in the second handler" );
            var doc: Document = domStuff();

            // this next call WILL FAIL because it is outside of
            // the scope of domStuff() - 
            var doc1: Document = Photoshop.app.activeDocument;
        }
       
        public function domStuff(): Document
        {
            trace( "in domStuff()" );

            var obj: * = PsEventScrubber.getInstance().forceClean( this, domStuff, arguments );
            if ( obj != null ) {
                return ( obj as Document );
            }
            // down here is the rest of the handler, written as normal
            // up to the return, everything here is safe - even other function calls

            var app: Application = Photoshop.app;
            var doc: Document = app.documents.add();
            var layer: ArtLayer = doc.artLayers.add();
            layer.name = "barney";
            return doc;
        }

 

Again, we're simply setting up a timer that calls "secondHandler" which in turn calls "domStuff". You can see that domStuff is a regular function that returns a PS Document. It's typical of the situation described above. As a developer you have no idea if one of your colleagues will call this function in the context of an event listener.

You can see the call to PsEventScrubber at the top of the function. That's kind of a weird construct, so I'll try to describe why it's done that way.

What forceClean does is do it's workaround magic, then call domStuff again (yes, recursively). We're passing "this", the domStuff function, and arguments. "this" and domStuff are what make it possible for me to call this function a second time. "arguments" is the arguments array of whatever was originally passed to domStuff.

OK, so when the event fires, secondDemo calls domStuff. Then it's first real line of code - PsEventScrubber.forceClean(...), does the workaround and calls domStuff again.

This second time through, PsEventScrubber.forceClean gets called again. This time, it detects that it's being called for the second time, bypasses the internal workaround magic, and returns null.  The condition just below that call detects the null, and allows the rest of the domStuff function to execute.

So now domStuff #2 does what it does and returns a PS Document. If you were to look at a stack trace here, you would see that it's returning the document to the first execution of PsEventScrubber.forceClean(). Of course, it returns the Document. Now our condition detects that obj is not null, so we cast obj to Document and return it.

No matter how, when, or on what platform the domStuff method is called, it will work and return a Document.

Essentially, all you need are those 4 lines:

var obj: * = PsEventScrubber.getInstance().forceClean( this, domStuff, arguments );
     if ( obj != null ) {
         return ( obj as Document );
     }

"this" will almost always be the first argument (the exception is a static function, in which case use the class itself), "domStuff" needs to be your function name, and "arguments" will never change.

So what if your method doesn't return anything? Here's domStuff redone to not return anything:

public function domStuff(): void
     {
        trace( "in domStuff()" );
        var obj: * = PsEventScrubber.getInstance().forceClean( this, domStuff, arguments );
        if ( obj != null ) {
             return;
        }
        // down here is the rest of the handler, written as normal
        var app: Application = Photoshop.app;
        var doc: Document = app.documents.add();
        var layer: ArtLayer = doc.artLayers.add();
        layer.name = "barney"
      }

If your method doesn't return anything, PsEventScrubber.forceClean will still return a blank object ( {} ) when the time is right, and your method will terminate correctly.

There are a few of important caveats on this workaround.

  1. This has not been tested very much at all. It has not been stress tested at all. Use it at your own risk. While I really want it to work for you, I can not guarantee that it will under all conditions.
  2. I would not endorse the idea of using this method on a function that is designed to be recursive. Theoretically it should work. I haven't tried it. I gave myself a headache trying to figure out the possible ramificantions of calling a recursive function twice for each iteration. Regardless of whether it works or not, using this workaround on a recursive function would likely slow things down to a crawl.
  3. And finally, the workaround does take time. About 0.02 seconds to be precise. Those 0.02 seconds can add up, especially in a recursive function or in the middle of a long iteration. Write your code accordingly.

That's about it for this one! Hope you find this workaround useful.

If I left some questions unanswered, post them on the SDK. I'll get to them as quickly as I can.

Aug 23 2010

I'm back! After playing with the code in a couple of intensive PS projects, I found a subtle bug in the forceClean method. That has been fixed in the current materials. Moreover, I added one more method:

PsEventScrubber.getInstance().macSafeAddEventListener( timer, TimerEvent.TIMER, secondHandler );

It takes all the normal arguments for addEventListener. What it does is create an anonymous function to act as the proxy listener. This will likely be the most handy and least intrusive method of using the workaround.

One last change - the workaround methods are all now platform aware. If your project is run on Windows, the workaround code is bypassed. If on Mac, the workaround is executed.

 

(Author's note: on Aug 11, 2010, I updated the PsMacDom.zip attachment. Please download it again)

(Authors note: on Aug 23, 2010, I updated the cookbook post including new zips, please read it and download the materials again)

PsMacDomswc.zip
[PS mac-safe workaround swc file]
PsDomEvents.zip
[Sample Project]

+
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. Permissions beyond the scope of this license, pertaining to the examples of code included within this work are available at Adobe.

Report abuse

Related recipes