Monday, October 29, 2007

Lightning Brain Podcast: Click here to listen to "InDesign User Interfaces"

Welcome to another episode of the Lightning Brain Podcast. We'll talk a little bit about InDesign and user-interface code.

When it comes to extending InDesign, there are many options available: you could create a C++-based plug-in, you can create an ExtendScript (JavaScript) solution, you can build an AppleScript or VBScript-based solution, you can build a Flash-based UI and then use it in InDesign, you can glue some other development environment 'into' InDesign, and you can also make a hybrid of the aforementioned solutions...

Which approach to choose depends on what your need is, what portion of the project is user-interface functionality as opposed to faceless functionality, what development environments you're familiar with, what your potential users are willing to accept, what budgets (time, money, resources, people, testers,...) are available for the project, what the politics involved are,... As in all things automation, there is no single 'best' solution - it all depends.

In this podcast I want to describe an approach we've very successfully used for a few real-size, real-life projects, and what the advantages and disadvantages are.

I first want to include a little disclosure: Rorohiko resells the Active Page Items Developer product as a commercial solution, and Active Page Items is very much part of the approach described here - so you might think this is podcast is a veiled advertisement for Active Page Items. Well, it is - but you have to keep in mind that Active Page Items has been created and has grown out of our own need for such a tool - Active Page Items was first; commercializing it came later.

One of the things we've learned is that C++ development for InDesign can be quite expensive: simple principles and algorithms often take a surprising amount of code to express. I'd describe C++ development around InDesign as 'fluffy'. High-level concepts and patterns result in lots of classes and source code files and fairly large amounts of C++ code, much of which is often quite repetitive in a number of respects. Especially the development of user interface elements takes a lot of doing.

At present, the only way to currently get a really 'native' InDesign UI look is to use C++ - e.g. if you want to create floating palettes, with all their end-user flexibility (tearing off, parking to the side,...) you need to use C++.

From a UI perspective, C++ might be 'perfect', but often there are other approaches that could be classified as 'good enough'. They might not look as nice, but they might do the job.

For example, using ExtendScript with InDesign CS3 one can develop quite complex user interfaces which often are 'good enough'. ExtendScript development is easily an order of magnitude cheaper than C++ development - so if the parameters of the project at hand don't require an absolute perfect-looking interface with floating palettes, ExtendScript can be the way to go.

Often there are also user-interfaces that need something a little bit more complex than what can be accomplished using ExtendScript, yet don't need a full blown native InDesign user interface. That's where the 'hybrid' approach that we've been using comes in.

The approach we've chosen basically boils down to: REALbasic, Active Page Items, ExtendScript.

We create our more complex user interfaces in REALbasic. However, this is not an absolute requirement - on the whole, we might have used Java instead.

The main reasons for choosing REALbasic over Java are 1) that it allows us to create cross-platform code that looks sligthly more 'native' on Mac as well as on Windows 2) easy access to global floating windows (both on Mac and Windows) and 3) purely personal preference: I find I personally can build and implement user-interfaces faster with REALbasic than with Java.

Easy access to global floating windows is one feature of REALbasic that comes in really handy, for which I don't know whether Java offers an easy alternative.

Global floating windows are windows that remain 'on top', even if the application that owns them is not the foreground application. This is fairly important for the illusion we want to maintain.

Active Page Items is a fairly large C++ plug-in, which we extend as we need with new functionality.

One of its many functions is coordinating InDesign with external applications. Through Active Page Items, we are able to create an illusion that makes the external 'satellite' apps seem to be part of InDesign.

One of the tricks is to 'lock' InDesign in a modal mode while one of our REALbasic satellite apps is running. That creates the illusion that a dialog owned by the satellite app seems to belong to InDesign.

However, the modal mode is not real - it's a simulated modal mode, and while this simulated modal mode is active, InDesign is actually still very much 'alive' and able to execute ExtendScript code - so it is possible to create a 'live' session between the satellite app and InDesign while the user interacts with this 'simulated modal dialog', while 'locking out' the user from any undesirable interactions with InDesign.

The illusion is not perfect: on the Mac's Dock and on the Windows start bar it is fairly apparent some secondary app is running, but that is a cosmetic issue, and it does not really seem to annoy our end-users too much.

- Active Page Items is also used to manage menu items and context menu items. This is mostly because our solutions needed to support CS2 as well as CS3 - InDesign CS3's ExtendScript has all you need to create menu items and context menus, so if you have the luxury of an InDesign-CS3-only setup, you can stick with standard ExtendScript in that respect.

- For communication between the various disparate components, we use temporary files. This is a very low-tech and crude approach, but it works 'well enough'. In a future version of Active Page Items we might add support for a more 'high-tech' information exchange mechanism, but for now temp files do us just fine.

If you want to try things out for yourself, I've created a very small sample of such a hybrid solution; the source code to it comes as part of our Active Page Items Developer Toolkit. If you download the latest demo version of the Toolkit from our web site, you'll find my example code tucked away amongst the other examples.

Some of the scripts can also be viewed at the end of the blog entry.

The sample, which deals with overset text, has no real practical applications as such, but you should be able to see how it can be made into a practical solution for particular problems.

The sample performs the following function: it looks out for overset text. As soon as a text frame gets overset, some ExtendScript code jumps into action, and fires up an external application, which then shows the contents of the text frame in a scrolling text field. The idea is that the user edits the text down to a shorter version to stop the overset. Of course, is not a practical approach at all, but it does allow us to demonstrate the various techniques involved.

In the sample, the active bit of ExtendScript code is currently 'attached' to a 'dummy' page item that sits on the pasteboard. The page item is not meant to be printed or have any sensibly printable content; all it does is hold some script code. We call such a page item a 'controller'. The controller 'watches' one or more page items, and waits for the events to occur.

In a 'real' solution based on Active Page Items, we'd instead 'package' that script code into a so-called 'Scripted Plug-in', in a .spln file.

In this particular case the controller is set to watch all page items, and the 'interesting events' it might watch out for are

- an event called 'subjectModified-recomposed-overset' which occurs when any page item ends up being overset after something happened to it (user typed something, frame resized,...)

- an event called 'idle' which occurs at regular intervals. In this particular solution, we rarely look out for 'idle' events as to not unnecessarily tax the computer's performance. We only do so while InDesign is in 'simulated modal mode' when the satellite application is running.

So, when any page item becomes overset, the subjectModified-recomposed-overset event is captured by the controller.

The controller's ExtendScript then launches the external satellite application using a special Active Page Items method attached to the application object - app.launchWith().

app.launchWith() has a number of functions. The most common use is as an extension to the File.execute() method.

File.execute() is similar to double-clicking an icon in Explorer or in the Finder, and will pick the default application to open a particular document.

app.launchWith() allows you to designate a particular application to open a particular document with - it is more akin to drag-dropping a document file icon onto an application's icon.

On top of that, app.launchWith() has a special feature - it allows us to lock InDesign into simulated modal model for as long as the launched application continues to run.

That makes for a crude, yet effective way to synchronize a satellite app with InDesign: you launch the satellite application using app.launchWith(), and when the user clicks 'OK' in the dialog presented by the satellite application, the application simply exits. Active Page Items is monitoring the satellite app, and as soon as it sees it exit, it will release the simulated modal lock.

So, the controller's ExtendScript first writes the contents of the overset frame into a temporary text file.

It then uses app.launchWith() to tell the satellite app to open this temp text file and pick up the data being communicated.

The satellite app then runs until the user clicks OK in the dialog, after which it writes the new data to the same temporary text file. When the application exits, Active Page Items will release the simulated modal lock automatically.

While the satellite app is running and InDesign is in simulated modal mode, the controller is catching idle events (roughly once per second). During these events, we could perform more communication backwards and forwards with the satellite application (e.g. for live previews or so), but in this case, all we do is check the simulated modal lock: as long as that is not lifted, we know the app is still running and we do nothing.

When we notice that the simulated modal lock has disappeared upon receiving one of the idle events, we know it the user has clicked OK, the app has written the new data to the temp file, and has quit, and we know we can now read the returned data, and then we can stop looking for idle events - things come back to normal, with the controller only watching out for overset events.

This sample should give you a little bit of insight on how we approached some real-live projects with very good results.

The advantages we had were:
- fast development of a good-looking UI that was beyond what can be accomplished with ExtendScript.
- cross-platform (Mac & Win) support: the same code works on both platforms only minute amounts of conditional code.

The disadvantages:
- don't pay attention to the man behind the curtain. Global floating windows and simulated modal mode allow you to get close, but nothing identical to the real thing (an InDesign-generated dialog or palette). The satellite app is visible - we worked around that by giving it a good-looking icon.

On the whole, the disadvantages were acceptable for the particular projects, and as a result we were able to offer very high efficiency in realizing these projects.

Thanks for your attention!

---

//
// Example of using an external program for dialogs.
//
// This document needs a file called "ExampleInDesignSatellite.app" (on Mac)
// or "ExampleInDesignSatellite.exe" (on Windows) in the same folder
// as the document.
//
// This event handler handles subjectModified-recomposed-overset and
// idle events.
//
// In normal circumstances, the event filter is set to just
// subjectModified-recomposed-overset - i.e. the handler only
// activates when there is a text frame that has just recomposed,
// and shows overset
//
// When that happens, the handler below will launch an external
// program to edit the text frame contents, and also change the event
// filter to "idle" - causing repeated calls to this handler while
// the user is editing the text in the external program. The
// external program is launched using launchWith and a mode equal
// to 4 - meaning: InDesign is modal locked for the user until the
// external program terminates.
//
// So, what happens is that we repeatedly receive and handle
// idle events, until app.callExtension(0x90b6C,10003) returns
// false (meaning: not modal locked), which only happens when
// the external program has terminated.
//
// As soon as the external program terminates, we restore the
// normal event filter, and read the output of the external
// program to stuff into the text frame
//
do
{
//
// tempFile is used to communicate data to and from the
// external program
//
var tempFile = File(Folder.temp + "/tempText.txt");

//
// Check if we're in the "idle" phase - waiting for the external
// program to finish
//
if (theItem.eventCode == "idle")
{
//
// We check whether InDesign is still modal locked. If so, then
// the external program has not finished yet - bail out of the event
// handler. In a second or so, on the next idle event, we'll give
// it another go
//
var indesignModalLocked = app.callExtension(0x90b6C,10003);
if (indesignModalLocked)
break;

//
// The modal lock is gone - so the external program is finished.
// We restore the event filter to what it was before it all started
//
theItem.eventFilter = "subjectModified-recomposed-overset";

//
// Did the external program communicate some data back to us?
// If so, then it is in the temporary file
//
if (! tempFile.exists)
break;

//
// Read the edited text and stuff it back into the story being edited
// We've stored a reference to the story in the data store associated to
// theItem
//
tempFile.open("r");
var editedStory = theItem.getDataStore("editedStory");
editedStory.contents = tempFile.read();
tempFile.close();

//
// And we're done for now!
//
break;
}

//
// Ok, we're handling a subjectModified-recomposed-overset event here.
//
var theDocument = GetParentDocument(theItem);

//
// We need the document's path to find the satellite app. If the document
// has not been saved yet, there is no path - so bail out
//
if (! theDocument.saved)
break;

//
// Is this a Mac or a PC? The Mac uses .app files, the PC uses .exe
//
var isMac = $.os.charAt(0) == "M";
if (isMac)
{
var theSatelliteApp = File(theDocument.fullName.parent + "/ExampleInDesignSatellite.app");
}
else
{
var theSatelliteApp = File(theDocument.fullName.parent + "/ExampleInDesignSatellite.exe");
}

//
// If we cannot find the satellite app, bail out
//
if (! theSatelliteApp.exists)
break;

//
// Write the overset story to a temporary text file
//
var theStory = theItem.eventSource.parentStory;
tempFile.open("w");
tempFile.write(theStory.contents);
tempFile.close();

//
// If we cannot find the temp file we just created, bail out
//
if (! tempFile.exists)
break;

//
// Open the temp file with the satellite app, and use flag "4"
// which means: lock InDesign into a user-modal mode until
// the satellite app terminates
//
app.launchWith(tempFile.fsName,theSatelliteApp.fsName,4);

//
// Change the event filter to process idle events - so we
// can regularly check whether the satellite app has
// terminated or not
//
theItem.eventFilter = "idle";

//
// We need to remember the story so we can put the edited
// text somewhere later on
//
theItem.setDataStore("editedStory",theStory);
}
while (false);

// End of event handler. Utility functions below

function GetParentDocument(pageItem)
{
var document = null;
do
{
var err;
try
{
document = pageItem.parent;
}
catch(err)
{
document = null;
}

if (document == null)
{
break;
}

if (document instanceof Document)
{
break;
}

if (document == pageItem)
{
document = null;
break;
}

pageItem = document;
}
while (true);

return document;
}