CEP Guide Part 6: Events

This is part of the CEP Mega Guide series. See also: sample CEP extension source

Now that we’ve covered calling APIs in the host tool, “all” that remains for building CEP extensions is knowing how to pass events back and forth. This is another epically long one, buckle up!

Overview

  • Passing events within and between extension panels is simple and easy. There’s also a very straightforward library for sending messages between extensions in different tools.
  • Several useful CEP or tool features are invoked by dispatching special events.
  • Sending events from the host tool VM to the panel VM works, but the details vary somewhat in different tools:
    • For ExtendScript tools, arbitrary events can be passed using a plugin (which is built into most, but not all, of the tools)
    • Flash and Photoshop each have a way to directly register for native tool events (“fileClosed” and so on) from the panel VM.
    • Outside of Flash and PS, it’s possible to register for a few native events; for others you might need to listen for them in the tool VM and then dispatch your own custom event.

Event passing within the Panel VM

All handling of events inside the panel VM is done with CSInterface.js (covered in Part 3 of this guide).

var cs = new CSInterface();
cs.addEventListener( "foo.bar", function(e) {
alert("event: "+e);
} );

// elsewhere...
var event = new CSEvent("foo.bar", "APPLICATION");
event.data = "Hello world!";
new CSInterface().dispatchEvent(event);

As simple as that. The CSEvent class referenced above is defined in CSInterface.js, so for further details it’s easiest to check the source.

The namespace for event IDs (i.e. "foo.bar"  in the code sample above) is global, so you can pass events between two different extensions just by matching up the IDs. To avoid unintentionally getting events from other extensions, it’s best to prefix event IDs with your extension ID:

var event = new CSEvent();
event.type = "com.fenomas.myExt.myEvent";

Using Panel VM events

Besides defining your own custom events, there are a few useful built-in things you can do with events inside the panel VM. One is to catch UI theme changes. Many extension panels match their look and feel to the host tool, in order to look like part of the UI, but nowadays most Adobe tools let the user change the lightness/darkness of the interface. Catching this event lets you update your panel’s style if the UI theme changes.

Note that the theme colors aren’t part of the event object, you query them from CSInterface. (You’d probably want to do that at init time as well, to match the tool’s initial theme.) Here’s an outline of the code you’d need:

var cs = new CSInterface();
cs.addEventListener(
CSInterface.THEME_COLOR_CHANGED_EVENT,
setAppTheme
);

function setAppTheme(event) {
var hostEnv = window.__adobe_cep__.getHostEnvironment();
var skinInfo = JSON.parse(hostEnv).appSkinInfo;
var color = skinInfo.panelBackgroundColor.color;
alert( "Tool background color: " +
"r="+color.red +
", g="+color.green +
", b="+color.blue
);
// ...
}

Incidentally, one thing I’ve been asked several times is whether Adobe has a piece of CSS that HTML panels can use to closely match the various tool UIs. There isn’t anything like this so far, but for my panels I’ve found that just using Topcoat‘s light and dark desktop themes gets close enough for horseshoes. I just choose which theme to use based on whether the tool’s background color is above or below 50% gray, and then style the tag’s bgcolor to the tool’s, which winds up looking quite natural. Here’s the theme handling in my sample CEP extension panel.

The other useful thing you can do with CS events is, there are a few special event IDs which CEP will watch for, and handle if you dispatch them. For example, there’s one that lets you set whether Photoshop panels should be persistent:

// set PS panel persistence
var event = new CSEvent();
event.type = "com.adobe.PhotoshopPersistent";
event.scope = "APPLICATION";
event.extensionId = window.__adobe_cep__.getExtensionId();
new CSInterface().dispatchEvent(event);

Dispatching that event will cause Photoshop panels to persist (i.e. they won’t reset when closed and reopened). CEP’s illustrious product manager Hallgrimur has posted a list of the other CEP events that Photoshop supports.

Catching native events from the host tool

Each of the tools has a way to catch events from the tool, but the specifics vary among the following three cases:

In Flash Pro:

For Flash you can register for native tool events directly from the panel VM. The supported events are:

com.adobe.events.flash.documentChanged
com.adobe.events.flash.timelineChanged
com.adobe.events.flash.documentSaved
com.adobe.events.flash.documentOpened
com.adobe.events.flash.documentClosed
com.adobe.events.flash.documentNew
com.adobe.events.flash.layerChanged
com.adobe.events.flash.frameChanged
com.adobe.events.flash.selectionChanged
com.adobe.events.flash.mouseMove

So to listen for when new FLA files are created, you’d do this:

new CSInterface().addEventListener(
"com.adobe.events.flash.documentOpened",
function(e) {
alert("doc Opened");
}
);

That’s all in the panel VM - no JSFL is necessary. Docs for these events can be found in the Flash CC reference, around page 384.

For ExtendScript tools:

The ExtendScript tools (i.e. all but Flash) also let you register directly, but the supported events vary by tool:

// This works in Illustrator, InCopy, and InDesign
new CSInterface().addEventListener(
"documentAfterActivate",
function(event) {
alert("Event type:" + event.type +
"\nData: " + event.data );
}
)

The list of events you can catch this way is in the CEP 5 docs, page 41-42. Note though that not very many events are supported. In some cases it’s possible to listen for other events from inside the tool VM, and when you catch one, dispatch a custom event to the panel VM. (Custom events are detailed further below.)

For Photoshop only:

Photoshop has a slightly more roundabout mechanism that allows you to catch every possible native event, as long as you know its internal code number. To do this, you dispatch a special event with ID com.adobe.PhotoshopRegisterEvent , with the code numbers in the event.data property, and then get callbacks via CSInterface.

For example, to catch events for “copy” and “paste” actions:

var event = new CSEvent();
event.type = "com.adobe.PhotoshopRegisterEvent";
event.scope = "APPLICATION";
event.data = "1668247673, 1885434740";

var cs = new CSInterface();
cs.dispatchEvent(event);

cs.addEventListener(
"PhotoshopCallback",
function(e) {
alert("event: " + e.data);
}
);

With code like this you can catch basically anything. Of course, the mystery is where the "1668247673, 1885434740"  came from. The answer gets into Photoshop’s “other API” that I touched on in part 5: in short, Photoshop actions and events are internally codified into special string codes (char IDs),  and passing those codes to Photoshop’s charIDToTypeID  command gets you the event numbers (type IDs) to listen for:

// JSX inside Photoshop's tool VM:
charIDToTypeID( "copy" ) // 1668247673
charIDToTypeID( "past" ) // 1885434740

The various charIDs are documented in Photoshop Javascript reference, Appendix A.

That’s not all though - newer features seem to use string IDs of more than four characters. These IDs can be converted to numeric codes with the stringIDtoTypeID command, and listened for as above. The catch is that they don’t seem to be documented. When I was working on my Photoshop extension I found these string IDs by running the Script Listener plugin (see previous article), doing whatever action I wanted to listen for, and then playing around with the generated scripts. Also, in the case I tested the numerical code was different on Mac and Windows, so I did the string-to-type conversion at runtime, rather than hard coding the numbers.

So there’s a fair bit of trial and error here, but the punchline is that you can catch events for almost anything in Photoshop, if you’re willing to wade around in internals.

Sending custom events from the tool VM

Besides catching events created natively by the tool, ExtendScript also has a way to send custom events from the tool VM to panels. (There’s currently no way to do this in JSFL, you have to rely on native events.)

This is done with a plugin called PlugPlugExternalObject . Currently, this plugin comes built into Photoshop, Illustrator, Premiere Pro, and After Effects - so for those tools there’s no setup, the sample code below will just work. For InDesign and InCopy, you’ll need to include the plugin with your extension and reference the extension object with the code on page 41 of the CEP docs.

Sending events works like this:

var xLib;
try {
xLib = new ExternalObject("lib:\PlugPlugExternalObject");
} catch(e) { alert("Missing ExternalObject: "+e); }

// send an event from the tool VM
$.sendEvent = function(type) {
if (xLib) {
var eventObj = new CSXSEvent();
eventObj.type = type;
eventObj.data = app.toString();
eventObj.dispatch();
}
}

Note that the  new ExternalObject()  created at first is never referenced, but that line is necessary to load the plugin and create the CSXSEvent  class.  This only needs to be done once. After that, if you create a  CSXSEvent  and dispatch it, any listeners in your panel listening for the same event ID (the type  property) will get the event.

Communication between tools

Finally, it’s also possible to send messages between extensions in different tools (i.e. from an Illustrator panel to a Photoshop panel), to check if other tools are installed,and running, and even to launch them. This is all done with the Vulcan.js library, found in the CEP Resources repo, which is used very similarly to CSInterface.js.

Here are the highlights of what you can do with Vulcan.js:

// register for messages
VulcanInterface.addMessageListener(
VulcanMessage.TYPE_PREFIX + "com.foo.bar",
function(message) {
var str = VulcanInterface.getPayload(message);
alert("Inter-tool message: " + str );
}
);

// checking if tools are installed/running:
var installCheck = VulcanInterface.isAppInstalled( "flash" )
var versionCheck = VulcanInterface.isAppInstalled( "flash-14.0" )
var runningCheck = VulcanInterface.isAppRunning( "flash" )

// launching other tools:
VulcanInterface.launchApp( "illustrator-18" )

// sending messages
var msg = new VulcanMessage (
VulcanMessage.TYPE_PREFIX + "com.foo.bar" );
msg.setPayload( "Hello, other tools" );
VulcanInterface.dispatchMessage(msg);

For full details see the IPC message handling on p39 of the docs.

And with that, we’ve covered everything I know so far about building, running, and testing HTML panel extensions. The last article will cover packaging and distribution.