MakeSideHeads: A Complete InDesign CS5 Panel

The MakeSideHeads example (download link below) shows how to create a fully functional InDesign CS5 panel using the Creative Suite Extension Builder and the CSAW and host adapter libraries for InDesign.

This extension demonstrates:

  • How to use CSXS events and event listeners.
  • How to use the host adapter library for InDesign to monitor and respond to application-specific events (i.e., events that are outside of the standard CXSX events)
  • How to work with InDesign’s find text preferences from ActionScript
  • Reading and writing CSXS preferences
  • How to display a modal dialog box from a CS Extension
  • A number of InDesign scripting tricks




Download a Zip archive containing the project here:
makesideheads

Update: This project is now available via Import>Adobe Creative Suite Extension Builder>Remote Creative Suite SDK examples.

A Bit of Background

To create a hanging side heading in InDesign, it’s usually necessary to copy the text of the heading out of the body of a story, then paste the text into an anchored/inline frame, and then adjust the frame size and set the frame properties. Typically, you’ll also need to change the formatting of the paragraph containing the anchored frame (usually to add space before the paragraph). For a long document, this process can quickly become laborious and time-consuming.

If you need to export the text for use in a word processor, you’ll have to get the text out of the anchored frames and back into the body of the story—again, a difficult process.

As you’d expect from the above, this process (or anyting else that drives an InDesign user crazy) is a good candidate for automation. For my own book production work, I’d created a number of ExtendScripts to automate this process—one script to format all hanging side headings, one to remove all hanging side headings, one to create hanging side headings from the selected text, and one to remove selected hanging side headings. These scripts were “hardwired” to use specific paragraph and object style names, and did not include any sort of user interface.

I thought it would be a good exercise to bring these features to a Creative Suite extension, so I wrote the MakeSideHeads example. My new panel contains all of the capabilities of the original scripts, adds a user interface, user interactivity, and other features one would expect in a standard InDesign panel. I didn’t need to make many changes to the logic of my existing scripts, and I didn’t have to use the InDesign C++ SDK. Everything I needed was provided by the Creative Suite Extension Builder and the associated CSAW library for InDesign.

Using Standard CSXS Events with InDesign

The MakeSideHeads example panel needs to populate its pop-up menus with the names of paragraph and object styles in the active (frontmost) document, but it can’t do that when no documents are open. The panel also needs to change the menus whenever the user opens or closes a document, or switches from one document to another.

We’ll use three of the standard CSXS events to respond to changes in the InDesign user interface: documentAfterActivate (fired after a document is opened or otherwise becomes the active document), documentAfterDeactivate (fired after a document is closed or when another document becomes the active document), applicationAfterActivate (fired when the application itself is activated). We’ll also watch for two events related to the panel itself: StateChangeEvent.WINDOW_OPEN and StateChangeEvent.WINDOW_SHOW.

I set up the extension to create event listeners for all of these events immediately after the panel is opened. Here’s the function that creates the event listeners (from the DocumentWatcher.as module). Note that all event listeners use the same event handler function.

public function start():void
{
	var myCSXS:CSXSInterface = CSXSInterface.getInstance();
	myCSXS.addEventListener("documentAfterActivate", myEventHandler);
	myCSXS.addEventListener("documentAfterDeactivate", myEventHandler);
	myCSXS.addEventListener("applicationActivate", myEventHandler);
	myCSXS.addEventListener(StateChangeEvent.WINDOW_OPEN, myEventHandler);
	myCSXS.addEventListener(StateChangeEvent.WINDOW_SHOW, myEventHandler);
	controller.myUpdateControls();
}



The event handler function itself does the work of sorting out which event was triggered. If the event is documentAfterDeactivate, I found I needed to add a short delay to allow the document time to close. If we don’t do this, the menus will not be refreshed properly (i.e., with the paragraph and object style names from the new active document—without the timer, they’ll be filled with the style names fromthe document we just closed). For all other events, we’ll run the myUpdateControls function.

private function myEventHandler(event:Event):void
{
	if(event.type == "documentAfterDeactivate")
{
		//Need a slight delay here.
		var myTimer:Timer = new Timer(1000, 1);
		myTimer.addEventListener("timer", myTimerHandler);
		myTimer.start();
	}
	else
{
		controller.myUpdateControls();
	}
}



The myUpdateControls function in the AppController.as module calls the corresponding function in an application-specific module—and finds the host application by looking at the hostName property of the AppModel.as module. I took this approach to make it possible to add support for another Creative Suite application—such as Illustrator—by simply adding another case and providing an application-specific ActionScript module.

public function myUpdateControls():void
{
	switch(model.hostName){
		case "indesign":
			InDesignSample.myUpdateMenuArrays();
			InDesignSample.myUpdateButtonStatus();
			break;
	}
}



The InDesign application-specific myUpdateMenuArrays function checks the state of the active document and adds or removes menu items to match.

public static function myUpdateMenuArrays():void
{
	if(app.documents.length != 0)
	{
		//Don’t change the menus if the document name hasn’t changed
		//and if object styles/layers have not been added/deleted.
		var myDocument:Document = app.documents.item(0);
		if((myDocument.name != model.myFileName)||(myDocument.objectStyles.length != model.myObjectStyleNames.length))
		{
			model.myObjectStyleNames.removeAll();
			model.myParagraphStyleNames.removeAll();
			model.myFileName = myDocument.name;
			for(var myCounter:int = 0;myCounter < myDocument.objectStyles.length; myCounter++)
			{
				model.myObjectStyleNames.addItem(
				myDocument.objectStyles.item(myCounter).name);
			}
			for(myCounter = 0; myCounter < myDocument.paragraphStyles.length;myCounter++)
			{
				model.myParagraphStyleNames.addItem
				(myDocument.paragraphStyles.item(myCounter).name);
			}
		}
	}
	else
	{
		if(model.myFileName != “”)
		{
			model.myFileName = “”;
			controller.myClearMenuArrays();
		}
	}
}



To manage the state of the buttons in the panel, the myUpdateButtonStatus function checks the state of the selection, enabling buttons when text is selected and disabling buttons when text is not selected.

public static function myUpdateButtonStatus():void
{
	if(app.documents.length != 0){
		//If a document is open, enable the “MakeAll” buttons.
		//You could check for the target paragraph style before
		//enabling the buttons--but that seems a bit like overkill.
		model.myEnableMakeAllButton = true;
		model.myEnableRemoveAllButton = true;
		if(app.documents.item(0).selection.length == 0)
		{
			//No selection exists, so disable the “MakeOne” buttons.
			model.myEnableMakeOneButton = false;
			model.myEnableRemoveOneButton = false;
		}
		else
		{
			//A selection existed. Is it text?
			if(
				(app.selection[0] is InsertionPoint)||
				(app.selection[0] is Character)||
				(app.selection[0] is Word)||
				(app.selection[0] is Line)||
				(app.selection[0] is Paragraph)||
				(app.selection[0] is TextColumn)||
				(app.selection[0] is Text)
			){
				//The selection is text.
				//Again, we could add more rigorous checking here.
				//Does the selection contain inline frames containing
				//the target paragraph style? If so, then enable the
				//”Remove Selected” and “Make Selected” buttons. Does
				//the selection contain instances of the target
				//paragraph style? If so, then
				//enable the “Make Selected” button.
				model.myEnableMakeOneButton = true;
				model.myEnableRemoveOneButton = true;
			}
		}
	}
	else
	{
		//No documents are open, so disable all buttons.
		model.myEnableMakeAllButton = false;
		model.myEnableRemoveAllButton = false;
		model.myEnableMakeOneButton = false;
		model.myEnableRemoveOneButton = false;
	}
}



While we can check the selection when the active document changes, what we really need to do is perform this check when the selection changes. To do that, we’ll have to use InDesign-specific events.

Using InDesign Application-Specific Events

InDesign scripting supports a number of events that aren’t available as CSXS events. For these events, we need to use the InDesign host adapter library. To add the host adapter library to a project, select the project and then choose Properties from the Context menu. Flash Builder displays the Properties panel for the selected project. Select the CS Extension Builder option, then clck the Host Adapters tab. Select the options corresponding to the library or libraries you want to add (InDesign, in this example), then close the Properties panel. The tooling will add the library to your project.

If you don’t see this option in the Properties panel for your project, update to a newer version of the Creative Suite Extension Builder and CSAW libraries. For more on adding host adapter libraries to your project and working with the host adapter, refer to “Creative Suite Host Adapter Librariers”in the Adobe Creative Suite SDK Programmer’s Guide.

InDesign’s afterSelectionChanged event is triggered whenever the selection changes, so we can check the selection and update the panel’s user interface whenever this event fires. Again, this plug-in could be extended to cover multiple applications by simply adding more cases to the switch statement (though it will require that the target application support selection events).

public function attach():void
{
	switch(model.hostName)
	{
		case "indesign":
			// Add a listener for all “after selection changed” event.
			IDScriptingEventAdapter.getInstance().addEventListener(com.adobe.indesign.Event.AFTER_SELECTION_CHANGED, handleEvent);
			model.isAttached = true;
		break;
	}
}



The event handler simply calls the myUpdateButtonStatus function described above.

private function handleEvent(event:com.adobe.indesign.Event):void
{
	InDesignSample.myUpdateButtonStatus();
}


Working With Find Text Preferences

InDesign’s find/change features are very useful, but ExtendScript developers coming to the Creative Suite Extension Builder and ActionScript might find making the transition a little bit tricky, due to differences between the programming languages. In ExtendScript, for example, you can clear all of the settings of the find text preferences object using the following line:

app.findTextPreferences = NothingEnum.NOTHING;



If you attempt to use this line in ActionScript, you’ll get a compilation error (because ActionScript expects the type for the assignment to be FindTextPreferences). To convert the above line to ActionScript, try this:

app.findTextPreferences = NothingEnum.NOTHING as FindTextPreferences;



Next, the result of a findText() operation can be an collection containing almost any of the InDesign text types—Character, Word, Text, TextColumn, Line, etc. In addition, the collection may contain more than a single data type. Therefore, a line of ExtendScript that looks like this:

myFoundItems = myDocument.findText(true);



…you’ll need to cast the result as an Object in ActionScript:

var myFoundItems:Object = myDocument.findText(true);


Reading and Writing CSXS Preferences

The CSXS infrastructure provides a way to save preference settings for specific extensions or for all extensions. The MakeSideHeads extension uses CSXS preferences to store user interface choices from one session to another.

When the panel is first opened, it’ll attempt to load the settings from the last session and set up the user interface. CSXS preferences are simple sets of key/value pairs.

private function myReadPreferences():void
{
	var myString:String;
	var myIndex:int;
	switch(this.TabNavigator.selectedIndex)
	{
		case 0:
			myString = myReadPreference("MakeObjectStyleComboString");
			//Match up string with object style names list in model, then
			//adjust menu selection to match, if it does not.
			myIndex = myFindIndex(model.myObjectStyleNames, myString);
			if(myIndex != -1)
			{
				this.MakeObjectStyleCombo.selectedIndex = myIndex;
				model.myObjectStylesIndex = myIndex;
			}
			else
			{
				this.MakeObjectStyleCombo.selectedIndex = 0;
			}
			myString = myReadPreference("ReplaceParaStyleComboString");
			myIndex = myFindIndex(model.myParagraphStyleNames, myString);
			if(myIndex != -1)
			{
				this.ReplaceParaStyleCombo.selectedIndex = myIndex;
				model.myCleanupStylesIndex = myIndex;
			}
			else
			{
				this.ReplaceParaStyleCombo.selectedIndex = 0;
			}
			myString = myReadPreference("FrameWidthFieldString");
			if(myString != “No value”)
			{
				this.myFrameWidthField.text = myString;
				model.myFrameWidth = Number(myString);
			}
			break;
		case 1:
			myString = myReadPreference("FollowingParaStyleComboString");
			myIndex = myFindIndex(model.myParagraphStyleNames, myString);
			if(myIndex != -1)
			{
				this.FollowingParaStyleCombo.selectedIndex = myIndex;
				model.myFollowingStylesIndex = myIndex;
			}
			else
			{
				this.FollowingParaStyleCombo.selectedIndex = 0;
			}
			break;
	}
	myString = myReadPreference("MakeParaStyleComboString");
	myIndex = myFindIndex(model.myParagraphStyleNames, myString);
	if(myIndex != -1)
	{
		this.MakeParaStyleCombo.selectedIndex = myIndex;
		model.myParagraphStylesIndex = myIndex;
	}
	else
	{
		this.MakeParaStyleCombo.selectedIndex = 0;
	}
}



When the user interface selections change, the panel writes the changes to the preferences.

private function mySavePreference(myKey:String, myValue:String):void
{
	var myResult:SyncRequestResult = CSXSInterface.instance.storePreference(myKey, myValue);
	if(SyncRequestResult.COMPLETE != myResult.status)
	{
		showAlert("Failed to save preference.");
	}
}
private function mySavePreferences():void
{
	//Current menu selections --> saved prefs.
	var myString:String;
	myString = this.MakeObjectStyleCombo.selectedLabel;
	mySavePreference("MakeObjectStyleComboString", myString);
	myString = this.MakeParaStyleCombo.selectedLabel;
	mySavePreference("MakeParaStyleComboString", myString);
	myString = this.ReplaceParaStyleCombo.selectedLabel;
	mySavePreference("ReplaceParaStyleComboString", myString);
	myString = this.FollowingParaStyleCombo.selectedLabel;
	mySavePreference("FollowingParaStyleComboString", myString);
	myString = this.myFrameWidthField.text;
	mySavePreference("FrameWidthFieldString", myString);
}



If the preferences cannot be read, or if the style name specified by a preference cannot be found in the list of strings for a given menu, the extension does not use the preference value and simply sets the menu selection to the first name in the list of paragraph or object style names.

Displaying a Modal Dialog from a CSXS Panel

The MakeSideHeads example shows how to use a simple modal dialog box as an “About” box. When the user selects the About option from the panel menu, the extension calls the myDoAbout function in the AppController.as module.

public function myDoAbout():void{
	var window:Window = new about();
	if(window)
	{
		window.type = CSXSWindowType.MODAL_DIALOG;
		window.resizable = false;
		window.open();
	}
}


A Few InDesign Scripting Tips

Inside the application-specific side of the MakeSideHeads extension (InDesignSample.as), you‘ll find a grab bag of InDesign automation techniques. Most of these are fairly well known to existing InDesign ExtendScript developers, but some might be new to developers coming to InDesign from the Flash/Flex/ActionScript domain. We’ve already covered a couple of issues related to finding text (see “Working With Find Text Preferences”, above), but here are a few more techniques that might come in handy.

Use Reverse Iteration When Working with Text Objects

Note that I set the reverseOrder parameter of the findText() method to true when I searched for text:

myFoundItems = myDocument.findText(true);



I did this to avoid invalidating text references as I iterate through the results of the findText(). InDesign collection objects (such as Characters, Words, or Paragraphs in a stream of text) are dynamic, and text objects are referred to relative to their location relative to the start of the story (a Story object is the container object for a continuous stream of text in an InDesign document).

When you add or remove text that appears before a text reference, you run the risk of having that text reference becoming an invalid reference (i.e., pointing to the wrong text, or, worse, pointing to null). Here’s an example: suppose your findText() operation returns two text references–let’s call them characters A and B. If character A comes before character B in a story, deleting character A will change the character that character B refers to. If you work with character B before working with character A, on the other hand, the references will remain valid.

Setting the reverseOrder parameter to true means that the text object references returned by findText() will be in reverse order, so we won’t need to do anything special when we iterate through the collection; in other cases you’ll need to use reverse iteration to process text object collections.

Getting the Type of the Selection

When text is selected, the Selection property (of the application or the document) will have a length of 1, because you can’t create non-contiguous text selections in InDesign. The myUpdateButtonStatus function in the InDesignSample.as module shows a fairly typical “selection sorter” for InDesign.

//A selection existed. Is it text?
if(
	(app.selection[0] is InsertionPoint)||
	(app.selection[0] is Character)||
	(app.selection[0] is Word)||
	(app.selection[0] is Line)||
	(app.selection[0] is Paragraph)||
	(app.selection[0] is TextColumn)||
	(app.selection[0] is Text)
)
{
	//The selection is text. Do something here.
}


Recompose after Adding an Inline Frame

When you insert an inline frame in text, recompose the text before attempting to change the size of the frame. If you don’t do this, the values returned for the GeometricBounds of the frame will be invalid.

//Given a reference to a paragraph "myParagraph" and a story "myStory"...
myInlineFrame = myParagraph.insertionPoints.item(-1).textFrames.add();
myStory.recompose();



Note, in addition, that you can create an inline frame by telling an InsertionPoint object to make one—there’s no need to use copy/paste.

Testing MakeSideHeads

To test this extension, open the MakeSideHeadsTest.indd InDesign document included in the project.

To create hanging side heads:

  1. Select the paragraph style you want to convert to a hanging side head using the Paragraph Style menu in the Find section of the panel. With the example document, select “heading 3.”
  2. Select the object style you want to apply to the frame containing the hanging side head from the Object Style menu in the Replace section of the panel. With the example document, select “sidehead.”
  3. Select a paragraph style for the paragraph following the hanging side head from the Following Style menu in the Replace section of the panel. With the example document, select “heading 3 para1.”
  4. Enter the width of the anchored frame in the Frame Width field in the Replace section of the panel. With the example document, enter 104.
  5. Click the Make All or Make Selected button.

To remove hanging side heads:

  1. Select the paragraph style for the hanging side head you want to remove using the Paragraph Style menu in the Remove section of the panel. With the example document, select “heading 3.”
  2. Select a paragraph style for the paragraph following the hanging side head from the Following Style menu in the Remove section of the panel. With the example document, select “para1.”
  3. Click the Remove All or Remove Selected button.

Save Preferences

Choose Save Preferences to save the current panel selections to CSXS preferences.

Load Preferences

Choose Load Preferences to load the saved panel settings. If the styles named in the saved settings are not present in the current document, the corresponding controls in the panel will not change.

More Samples to Come

I hope you enjoyed this sample! I have to say that I’m pretty new to ActionScript/Flex–coming to it from the ExtendScript side of the world. While I’m able to get things to work, I’d be happy to hear about ways that I could improve the samples (in particular, I’m learning Flex UI widgets as I go, and I’m never quite sure I’m doing it right). I’ve got more CS SDK examples that I’ll be posting as time permits.



Download a Zip archive containing the project here:
makesideheads

18 Responses to MakeSideHeads: A Complete InDesign CS5 Panel

  1. Harbs says:

    Wow!

    Very nicely done, and very thoroughly explained!

    Great work as usual Ole!

    Harbs

  2. Jeremy says:

    hi Ole,

    I’m a veteran air/flex developer currently looking into indesign dev with extension builder, is there any chance to grab a trial version so I could have a try before diving in ? any help much appreciated.

    Jeremy.

    • ipaterso says:

      Please contact Roger Risdal (rrisdal (at) adobe.com) about this.

      best wishes
      Ian

    • Jen says:

      Looking for a CS5 Expert with Flex Builder for a long term project in NYC. Top dollar for Top Talent. Please email me your resume and URLs for immediate consideration.

  3. tomaxxi says:

    Great article!

    I have one question. How to attach AFTER_SELECTION_CHANGED on InDesign app without Extension Builder? I’m using CS SDK/Flash Builder Trial.

    Thanks!

    tomaxxi

  4. okvern says:

    Hi tomaxxi,

    The CS Extension builder adds the id_host_adapter.swc (library) to your project–you can do this manually. Go to the Properties panel for your project, then choose Flex Build Path and add the path ${CSAR}/release/id_host_adapter.swc to your project. The sample project should show you how to use it–if you have any questions, please don’t hesitate to ask!

    Thanks,

    Ole

  5. Lele says:

    Well done Olav!
    I used to develop Flex/Air applications to handle InDesign contents with Switchborad and PatchPanel and the good old ExtendToolkit.
    The good thing about that way of developing was that you could create JavaScript files and use Flex to send and execute them to InDesign.
    Now I’m upgrading to Flash Builder and its Extensions Builder but I’m not sure it’s a good thing for me because in this way I have to write all my code within Flash application.
    From what I’ve seen syntax is really similar to JavaScript and that’s a good thing, but when it comes to debugging single portions of code or developing with other people it looks a little complex to handle.
    Is it really time to abandon the old way or is it possible to generate an extension using external .jsx files?
    Thanks.

  6. Olav Kvern says:

    Hi Lele,

    Glad you liked the post!

    While you can run ExtendScript code from within a CS Extension, you can’t debug it there, and you can’t easily share variables/values with the user interface you create for the extension. To me, at least, this means that I need to write as ActionScript or convert existing ExtendScript files to ActionScript.

    I’ll admit, however, that I still test code in the ExtendScript Toolkit–for exploration and improvisation, Flash Builder debugging just can’t touch the immediacy of the JavaScript Console. I’ll often write and debug fragments of code in the ESTK when I’m exploring an idea, and then port the code to AcdtionScript when it’s time to build the extension. At that point–once you have some idea of what you’re doing–debugging in Flash Builder is as good or better than the ESTK.

    I think it’s quite a bit easier to convert ExtendScript to ActionScript (usually a matter of simply adding type declarations) than it is to maintain “escaped” ExtendScript code as strings inside an ActionScript file (as we often had to do with PatchPanel/Switchboard).

    If you need to have your extension run an ExtendScript file, however, I’d suggesting using InDesign’s app.doScript() method to run the file. With doScript(), you get the ability to control Undo, which can speed things up quite a bit.

    Thanks,

    Ole

  7. John Nolan says:

    Okay:
    Total lurker and non-coder here. Is it possible to download the completed extension to install; it looks great.

  8. Olav Kvern says:

    Hi John,

    Right off hand, I’m not sure (I’d have to create a signed version, and I’m not very smart about the legal issues involving doing so and distributing such an extension).My guess is that I probably can’t. But I also have a version in ExtendScript that works quite well–it uses a dialog box instead of a panel, but it’s otherwise identical. I know I can send you that one. If you’d like it, just drop me a line directly.

    Thanks,

    Ole

  9. Matt Newell says:

    Great bag of snippets. Thanks for the help getting started with Extension Builder. I have one question tho… from a scripting side finding object / page items by their script label was very easy. I finding it really had to find an item just using it’s scripting label.

    Any chance that would be the next example ;-)

  10. Olav Kvern says:

    Hi Matt,

    The special case for getting a page item by its label has been replaced by the getItemByName method you can find on page item collection objects (PageItems, Rectangles, TextFrames, etc.). The name of the object is the name that you can view and edit using the Layers panel. The only trick is that it’ll only return one item at a time–to get all of the items with a given name, you’ll have to iterate through the collection. I’m hoping that we can restore the previous behavior–getting multiple items–while still working with itemByName.

    Thanks,

    Ole

  11. Matt Newell says:

    Thanks Olav good to hear from you again.

    I thought that was the case. ;-( I tried

    var myItem:TextFrame = document.pageItems.itemByName(“Test”);

    and it seems to work but if you stop and debug myItem it’s just a blank pageItem reference and not the actual pageItem with that label. One other thing I can find getItemByName but I did find itemByName is this the same?

  12. Lele says:

    Hi Olav.
    I’m developing an InDesign Extension and I’m experiencing some problems trying to get a character.contents value when I have a SpecialCharacter. It looks like it doesn’t return anything. I tried to define caharacter variable as a Text and as an object but it doesn’t work.
    Is it a bug?
    Sorry but I don’t know where to ask it…
    Thanks.

    • Olav Kvern says:

      Hi Lele,

      In most cases, it should be possible to coerce that value into a Unicode character. Let me poke at it for a bit–and sorry it’s taken me a few days to respond.

      Thanks,

      Ole

  13. jh says:

    As a long-time Frame user, I’ve been bugging Adobe to come up with built in side heads in InDesign. Guess I’ll have to upgrade to CS5 in order to use your app.
    Thanks for writing it: something I could never do. I’ll respond again after I upgrade.

  14. Olav Kvern says:

    Hi jh,

    I have ExtendScript versions that should work in just about any version of InDesign (higher than 2.0, anyway). These scripts use a modal dialog box instead of a panel, but do pretty much the same thing. Drop me a line directly if you’d like me to send you a copy.

    Thanks,

    Ole