Watching the Detections

Most of the time, Creative Suite extension panels will need to know something about the state of their host application. If the extension, for example, displays a pop-up menu containing a list of the layers in the current document, the extension will need to know when the document closes, or when a new document opens. When the extension detects that the current document has changed, it can do whatever it needs to do to repopulate the menu.

While you could monitor the state of the application using polling—a function in a timing loop that checks the state of the application every so often—it’s much better to use event listeners. Event listeners are triggered whenever a particular event takes place, and run a function that responds to that event in some way. The only trick is that the application you’re interested in working with has to provide some sort of notification that something has happened that’s relevant to your extension. As you’ll see, that’s not always as straightforward as it sounds.

Because CS extensions are built on top of CSXS (Adobe’s Creative Suite Extensible Services framework), they can make use of CSXS “standardized” events.

Event Name Event Triggers:
documentAfterActivate When you activate the document.
documentAfterDeactivate When you deactivate the document
 (i.e., when you bring another document to the front).
applicationBeforeQuit When you quit the application.
applicationActivate When you activate the application.
documentAfterSave Immediately after you save the document.
 

 

Not all Creative Suite applications support the full range of CSXS “standardized” events, but the applications I want to work with—Illustrator, InDesign, and Photoshop—all support the events I’m most interested in. These applications also support other events—later in this post, I’ll show you how to create event listeners for those application-specific events.

We’ll also use two other CSXS events: StateChangeEvent.WINDOW_OPEN and StateChangeEvent.WINDOW_SHOW. These events are not part of the ”standardized” CSXS events—they apply to the state of the panel window itself. For more on general CSXS events, refer to the “com.adobe.csxs.events” section of the CSXS Library API Reference.

To test our event listeners, we’ll get and display the layers in the current document, and we’ll update the list of layers every time a document changes. I’m thinking that this is something that many CS extension developers will want to do.

You can find the project at:

DocumentWatcher

Setting Up the Project

I’ll use the same project organization as I’ve used in my other posts. That means that I’ll create a very simple main.mxml, put shared data in the AppModel class, put common functions in the AppController class, and quarantine application-specific code in separate files (one class per application). Here’s what the project looks like after I’ve created and arranged the files:

The user interface for the example panel will be very simple: one text label and one ComboBox control. I’ll bind the ComboBox control to an ArrayCollection variable defined in the AppModel class.

main.mxml

<?xml version="1.0" encoding="utf-8"?>
<csxs:CSXSWindowedApplication
	xmlns:csxs="com.adobe.csxs.core.*"
	xmlns:mx="http://www.adobe.com/2006/mxml"
	layout="absolute"
	historyManagementEnabled="false"
	creationComplete="onCreationComplete()"
 	close="onClose()"
	>
	<mx:Script>
		<![CDATA[
			import documentWatcher.DocumentWatcherIllustrator;
			import documentWatcher.DocumentWatcherInDesign;
			import documentWatcher.DocumentWatcherPhotoshop;
			import documentWatcher.AppController;
			import documentWatcher.AppModel;
			import documentWatcher.DocumentWatcher;

			[Bindable]
			// init on creationComplete
			private var model:AppModel = null;
			private var controller:AppController = null;
			private var watcher:DocumentWatcher = null;
			private var hostName:String = HostObject.mainExtension;

			private function onCreationComplete():void
			{
				model = AppModel.getInstance();
				controller = AppController.getInstance();
				model.readyState();
				watcher = DocumentWatcher.getInstance();
				controller.handleEvent();
			}
			private function onClose():void{
				controller.detach();
			}

		]]>
	</mx:Script>
	<mx:VBox height="100%" width="100%" verticalAlign="middle" horizontalAlign="center">
		<mx:HBox>
			<mx:Label
				text="Layers:"
				width="100" textAlign="right"/>
			<mx:ComboBox
				width="200"
				id="LayersComboBox"
				dataProvider="{model.layerNames}"
			/>
		</mx:HBox>
	</mx:VBox>
</csxs:CSXSWindowedApplication>

 

AppModel

In the following listing, I’ve omitted the computeHostName function—it’s a common function that we’ve used in a large number of the other CS SDK examples.

package documentWatcher
{
	import com.adobe.csxs.core.CSXSInterface;
	import com.adobe.csxs.types.*;
	import flash.external.HostObject;
	import mx.collections.ArrayCollection;
	[Bindable]
	public class AppModel
	{
		private static var instance:AppModel;

		public function AppModel()
		{
		}

		public static function getInstance():AppModel
		{
			if ( instance == null )
			{
				instance = new AppModel();
			}
			return instance;
		}

		public var hostName:String = "";
		public var layerNames:ArrayCollection = new ArrayCollection();
		//Called from the extension's onCreationComplete() function.
		public function readyState(): void
		{
			this.hostName = computeHostName();
		}
		//computeHostName function omitted.
	}
}

 

AppController

package documentWatcher
{
	import com.adobe.csxs.types.CSXSWindowType;
	import com.adobe.indesign.Event;
	import flash.events.Event;
	import mx.core.Window;
	public class AppController
	{
		private static var instance:AppController;
		private static var model:AppModel = AppModel.getInstance();
		public function AppController()
		{
		}

		public static function getInstance() : AppController
		{
			if(instance == null)
			{
				instance = new AppController();
			}
			return instance;
		}

		public function handleEvent():void
		{
			switch(model.hostName)
			{
				case "indesign":
					DocumentWatcherInDesign.getLayerNames();
					break;
				case "illustrator":
					DocumentWatcherIllustrator.getLayerNames();
					break;
				case "photoshop":
					DocumentWatcherPhotoshop.getLayerNames();
					break;
			}
		}
	}
}

 

DocumentWatcher

The heart of this example is the DocumentWatcher class, which takes care of creating event listeners and responding to events when they occur. When the DocumentWatcher detects an event, it notifies the AppController, which, in turn, calls a function in the application-specific code for the current host application.

Our DocumentWatcher isn’t too particular about which events it receives—all it knows is that if any of the events fire, it needs to let the rest of the extension know that it’s time to update the ComboBox control in the panel.

package documentWatcher
{
	import com.adobe.csxs.core.CSXSInterface;
	import com.adobe.csxs.events.*;
	import com.adobe.csxs.types.*;
	import documentWatcher.AppController;
	import documentWatcher.AppModel;
	import flash.events.Event;
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	public class DocumentWatcher
	{
		private static var instance:DocumentWatcher;
		private static var controller:AppController = AppController.getInstance();
		private static var model:AppModel = AppModel.getInstance();
		public static function getInstance() : DocumentWatcher
		{
			if ( instance == null )
			{
				instance = new DocumentWatcher();
				instance.start();
			}
			return instance;
		}
		public function start():void
		{
			//Add CSXS "standardized" events.
			var myCSXS:CSXSInterface = CSXSInterface.getInstance();
			myCSXS.addEventListener("documentAfterActivate", eventHandler);
			myCSXS.addEventListener("documentAfterDeactivate", eventHandler);
			myCSXS.addEventListener("applicationActivate", eventHandler);
			//Add CSXS events.
			myCSXS.addEventListener(StateChangeEvent.WINDOW_OPEN, eventHandler);
			myCSXS.addEventListener(StateChangeEvent.WINDOW_SHOW, eventHandler);
			//Run the event handler to set the initial state of the panel.
			controller.handleEvent();
		}
		//Event handler for all monitored events.
		private function eventHandler(event:Event):void
		{
			if(event.type == "documentAfterDeactivate")
			{
				//Need a slight delay here--the document is still open/active when the
				//event fires, and we need to give it time to close/deactivate.
				var myTimer:Timer = new Timer(1000, 1);
				myTimer.addEventListener("timer", timerHandler);
				myTimer.start();
			}
			else
			{
				controller.handleEvent();
			}
		}
		//When the timer expires, handle the event.
		private function timerHandler(event:TimerEvent):void{
			controller.handleEvent();
		}
	}
}

 

Application-Specific Code

Now we get to the fun part—the application specific code that will be called whenever one of our monitored events occurs (i.e., when the panel opens, or when a document opens, becomes the active document, or closes).

Illustrator

package documentWatcher
{
	import com.adobe.csawlib.illustrator.Illustrator;
	import com.adobe.illustrator.*;
	import com.adobe.cshostadapter.AIEvent;
	import com.adobe.cshostadapter.AIEventAdapter;

	public class DocumentWatcherIllustrator
	{
		private static var app:Application = Illustrator.app;
		public static var instance:DocumentWatcherIllustrator = null;
		private static var adapter:AIEventAdapter;
		private static var model:AppModel = AppModel.getInstance();
		private static var controller:AppController = AppController.getInstance();
		public function DocumentWatcherIllustrator()
		{
		}
		public static function getInstance():DocumentWatcherIllustrator{
			if ( instance == null )
			{
				instance = new DocumentWatcherIllustrator;
			}
			return instance;
		}
		public static function getLayerNames():void
		{
			//Clear the list of layers.
			model.layerNames.removeAll();
			if(app.documents.length != 0)
			{
				var document:Document = app.documents.index(0);
				for(var counter:int = 0; counter < document.layers.length; counter++){
					model.layerNames.addItem(document.layers.index(counter).name);
				}
			}
			else
			{
				model.layerNames.addItem("No documents are open");
			}
		}
	}
}

 

InDesign

package documentWatcher
{
	import com.adobe.csawlib.indesign.InDesign;
	import com.adobe.cshostadapter.IDScriptingEventAdapter;
	import com.adobe.indesign.*;
	public class DocumentWatcherInDesign
	{
		private static var app:Application = InDesign.app;
		private static var model:AppModel = AppModel.getInstance();
		private static var controller:AppController = AppController.getInstance();
		private static var adapter:IDScriptingEventAdapter;
		public static var instance:DocumentWatcherInDesign = null;
		public function DocumentWatcherInDesign()
		{
		}
		public static function getInstance():DocumentWatcherInDesign{
			if ( instance == null )
			{
				instance = new DocumentWatcherInDesign;
			}
			return instance;
		}
		public static function getLayerNames():void
		{
			//Clear the list of layers.
			model.layerNames.removeAll();
			if(app.documents.length != 0)
			{
				var document:Document = app.documents.item(0);
				for(var counter:int = 0; counter < document.layers.length; counter++){
					model.layerNames.addItem(document.layers.item(counter).name);
				}
			}
			else
			{
				model.layerNames.addItem("No documents are open");
			}
		}
	}
}

 

Photoshop

package documentWatcher
{
	import com.adobe.csawlib.photoshop.Photoshop;
	import com.adobe.cshostadapter.PSEvent;
	import com.adobe.cshostadapter.PSEventAdapter;
	import com.adobe.photoshop.*;
	public class DocumentWatcherPhotoshop
	{
		private static var app:Application = Photoshop.app;
		private static var adapter:PSEventAdapter;
		private static var model:AppModel = AppModel.getInstance();
		private static var controller:AppController = AppController.getInstance();
		public static var instance:DocumentWatcherPhotoshop = null;
		public function DocumentWatcherPhotoshop()
		{
		}
		public static function getInstance():DocumentWatcherPhotoshop{
			if ( instance == null )
			{
				instance = new DocumentWatcherPhotoshop;
			}
			return instance;
		}
		public static function getLayerNames():void
		{
			//Clear the list of layers.
			model.layerNames.removeAll();
			if(app.documents.length != 0)
			{
				var document:Document = app.documents.index(0);
				for(var counter:int = 0; counter < document.layers.length; counter++){
					model.layerNames.addItem(document.layers.index(counter).name);
				}
			}
			else
			{
				model.layerNames.addItem("No documents are open");
			}
		}
	}
}

 

Going Deeper

At this point, the extension works. If you open or create a document, or switch in and out of the application, the extension will get the list of layers of the frontmost document and display them in the ComboBox control in the panel. If you’ve experimented with the extension, you’ve probably noticed that the layer list doesn’t get updated when you add or remove layers. To add this capability, we’ll need to go a little bit deeper—specifically, we’ll need to use the host adapter libraries to monitor other events.

The host adapter libraries provide a way to monitor and respond to application events that are not supported by the CSXS standardized events. For Illustrator and Photoshop, you’ll need to install plug-ins to use the host adapter; for InDesign, no additional plug-in is required (the host adapter creates event listeners in the ExtendScript engine running inside InDesign). Apart from that, all you need to do to use the host adapter is to add it to your CS extension project. Since we’re creating an extension for Illustrator, InDesign, and Photoshop, I’ll add the single host adapter library cs_host_adapter.swc (if you’re not using all three applications, you’ll want to use one or more of the application-specific host adapter libraries: ai_host_adapter.swc, id_host_adapter.swc, and/or ps_host_adapter.swc).

If you’re using the Creative Suite Extension Builder, you can add the libraries from the Host Adapters panel.

I won’t go into too much detail about the host adapter libraries, because it’s all very well documented in the “Creative Suite Host Adapter Libraries” section of the Adobe Creative Suite Programmer’s Guide.

To add the application-specific events, I’ll go back to the DocumentWatcher class and make a few changes. I’ll continue to use my “blunt instrument” approach to the problem of updating the list of layers—if the extension receives any of the events specified in the DocumentWatcher class, perform an update.

public function start():void
{
	var myCSXS:CSXSInterface = CSXSInterface.getInstance();
	myCSXS.addEventListener("documentAfterActivate", eventHandler);
	myCSXS.addEventListener("documentAfterDeactivate", eventHandler);
	myCSXS.addEventListener("applicationActivate", eventHandler);
	myCSXS.addEventListener(StateChangeEvent.WINDOW_OPEN, eventHandler);
	myCSXS.addEventListener(StateChangeEvent.WINDOW_SHOW, eventHandler);
	//Add the following line:
	controller.addSpecialCases();
	controller.handleEvent();
}

 

AppController

Add the following function:

public function addSpecialCases():void
{
	switch(model.hostName)
	{
		case "indesign":
			DocumentWatcherInDesign.addSpecialCases();
			break;
		case "illustrator":
			DocumentWatcherIllustrator.addSpecialCases();
			break;
		case "photoshop":
			DocumentWatcherPhotoshop.addSpecialCases();
			break;
	}
}

 

Illustrator

Add the following function:

public static function addSpecialCases():void{
	adapter = AIEventAdapter.getInstance();
	adapter.addEventListener(AIEvent.LAYER_LIST_CHANGED, getLayerNames);
}

 

As you can see, Illustrator has a very handy event—AIEvent.LAYER_LIST_CHANGED—that does exactly what we want, but I had to come up with workarounds for InDesign and Photoshop.

InDesign

Add the following function:

public static function addSpecialCases():void{
	adapter = IDScriptingEventAdapter.getInstance();
	adapter.addEventListener(MutationEvent.AFTER_ATTRIBUTE_CHANGED, getLayerNames);
}

 

For InDesign, I added an event listener for the afterAttributeChanged event. This event fires whenever the active layer changes, which happens whenever you add, remove, or merge layers.

Photoshop

Add the following functions:

public static function addSpecialCases():void
{
	var adapter:PSEventAdapter = PSEventAdapter.getInstance();
	adapter.addEventListener(PSEvent.MAKE, compareLayerLists);
	adapter.addEventListener(PSEvent.DELETE, compareLayerLists);
}
public static function compareLayerLists(event:PSEvent):void
{
	if(app.documents.length > 0){
		if(app.documents.index(0).layers.length != model.layerNames.length){
			getLayerNames();
		}
	}
}

 

For Photoshop, I used a particularly hideous workaround—I created an event listener for the PSEvent.MAKE event, which fires every time you make something (anything) new. I also added an event listener for the PSEvent.DELETE event, which fires whenever you delete something. Both event listeners trigger the compareLayers function, which compares the length of the layers collection in the document with the length of the layers list in the AppModel. If the two lists differ in length, the extension updates the number of layers. I don’t like this approach very much, but it’s better than setting up a timer and polling the application every so often.

For both InDesign and Photoshop, note that my workarounds mean that the layers list in the panel may get updated more than once following a particular event, but that seems a small price to pay for (relative) simplicity. A more complete extension (such as the MakeSideHeads extension for InDesign) would include code to maintain the selection in the ComboBox and add code to reduce the number of times that the menu items get updated for a particular event.

Finishing Touches

When you add an event listener using a CS host adapter library, it’s a good idea to remove the event listener when the panel closes. If you don’t do this, you run the risk of having an ever-growing number of event listeners in memory—one for each time you open the extension panel. I’m not actually sure if this can happen in Illustrator and Photoshop, but it can definitely happen in InDesign. I’ve added a detach function to the AppController class and to each application-specific class to take care of this potential problem.

main.mxml

Add the following to the attrributes of the CSXSDocumentWindow:

close="onClose()"

 

Add the onClose function:

private function onClose():void{
	controller.detach();
}

 

AppController

Add the following function:

public function detach():void
{
	switch(model.hostName)
	{
		case "indesign":
			DocumentWatcherInDesign.detach();
			break;
		case "illustrator":
			DocumentWatcherIllustrator.detach();
			break;
		case "photoshop":
			DocumentWatcherPhotoshop.detach();
			break;
	}
}

 

Illustrator

public static function detach():void
{
	adapter = AIEventAdapter.getInstance();
	adapter.removeEventListener(AIEvent.LAYER_LIST_CHANGED, getLayerNames);
}

 

InDesign

public static function detach():void
{
	adapter = IDScriptingEventAdapter.getInstance();
	adapter.removeEventListener(MutationEvent.AFTER_ATTRIBUTE_CHANGED, getLayerNames);
}

 

Photoshop

public static function detach():void
{
	adapter = PSEventAdapter.getInstance();
	adapter.removeEventListener(PSEvent.MAKE, compareLayerLists);
	adapter.removeEventListener(PSEvent.DELETE, compareLayerLists);
}

 

The Quest for Perfection

Our event-watching extension is far from perfect—there are still some actions that can change the layer list without triggering an update for the ComboBox in the extension panel. But our DocumentWatcher class does a pretty good job of updating the panel when you open, close, or activate documents, and our layer-watching additions cover the most common cases in which the layers collection of a document might change.

That said, there’s plenty of room for improvement. If your extension really needs to keep a close watch on the layers in a document, you’ll probably need to come up with a more complete system for keeping track of them. If you’re using InDesign, for example, you could attach event listeners to the menu action for merging layers and thereby close that loophole. You might also want to display sub-layers, and convert the ComboBox to a more complex control.

I’ve used CSXS events whenever possible, and have used application-specific events provided by the host adapter libraries only when I needed events not supported by CSXS. I think that this is the correct approach, as it cuts down on the amount of application-specific code.

One change I’d like to make is to fix a user interface problem—when the menu of the ComboBox gets tall enough, it gets clipped to the size of the panel.

You can still select items from the menu, so it’s not a big deal for this very simple example, but I’d still like to fix it. My colleague Bob Stucky has written a cookbook article that shows how to get around this problem, and my next post will show how to apply his solution to this extension. Until then, please let me know what you think of this post, and tell me about any topics you’d like to see me cover in the future.

3 Responses to Watching the Detections

  1. Pingback: More Basics: Importing and Exporting Files « Creative Suite SDK Team

  2. Pingback: Working with Layers « Creative Suite SDK Team

  3. Ilya says:

    Olav, what abour artboard list changed event? How to update artboardslist in my pluggin when artboard added or removed?