Friday, August 31, 2007

 

Overset Text

The question came up this morning on how to get the overset text in a story. I interpreted that as meaning how to get a reference to it and banged out a quick reply. But this got me musing about how to write a release independent function that would do this for either a story or a cell in a table. I came up with this function (which expects to receive either a story or a cell):
function getOversetText(textFlow) {
if (!textFlow.overflows) return null;
var start = textFlow.characters[0];
if (textFlow instanceof Cell) {
if (textFlow.characters.length > 0) {
start = textFlow.texts[0].characters[textFlow.contents.length];
}
} else {
if (Number(String(app.version.split(".")[0])) > 4) {
var myTFs = textFlow.textContainers;
} else {
var myTFs = textFlow.textFrames;
}
for (var j = myTFs.length - 1; j >= 0; j--) {
if (myTFs[j].characters.length > 0) {
start = textFlow.characters[myTFs[j].characters[-1].index + 1];
break;
}
}
}
return textFlow.texts.itemByRange(start, textFlow.texts[0].characters[-1]);
}
I confess that I haven't tested this in CS, but I have in both CS2 and CS3 where it works. It should work in CS also.

But notice the assymetry between working with cells and working with stories. Even though the contents of a cell returns only the non-overset text, if you access the characters of a cell you get the lot, overset or not. So, to get the index of the first overset character in a cell, I had to use the length of the contents rather than the index of the last character plus 1, which is what I first tried to use.

Sunday, August 26, 2007

 

ScriptUI Dialog with drop-down

I found the documentation for drop-down lists to be less easy to follow than for simpler objects. The script ended up being a lot simpler than I was at first led to believe by the combination of scant information in the Tools Guide and the structure of the object model. Here's what I ended up with:
//DESCRIPTION: Simple ScriptUI Drop-down List

listStrings = ["25%", "50%", "75%", "90%", "100%", "110%", "125%", "140%", "200%", "400%"];
myDlg = new Window('dialog', 'Drop-down List');
myDlg.orientation = 'column';
myDlg.alignment = 'right';
//add drop-down
myDlg.DDgroup = myDlg.add('group');
myDlg.DDgroup.orientation = 'row';
myDlg.DDgroup.add('statictext', undefined, "Zoom Percentage");
myDlg.DDgroup.DD = myDlg.DDgroup.add('dropdownlist', undefined, undefined, {items:listStrings})
myDlg.DDgroup.DD.selection = 4;
myDlg.closeBtn = myDlg.add('button', undefined, 'OK');
// add button functions
myDlg.closeBtn.onClick = function() {
this.parent.close();
}
result = myDlg.show();
alert(myDlg.DDgroup.DD.selection);
I had the following difficulties:
  1. I didn't know how many undefineds to include in the add statement for the list. I arrived at two by trial and error. I imagine that the first one is for the dimension information. I'm not sure what the second one is.
  2. I expected to have to create an array of listItems for the items property of the drop-down, but the array of strings sufficed.
  3. I went through a number of hoops trying to work out how to set the initial value of the selection before stumbling on the simple use of the index to 100%.
  4. As a result of that, I was amazed to discover that the selection returns the selected value and not the index into the list.

Saturday, August 25, 2007

 

Selecting Paragraphs

This is a real-time blog. I don't know the answer as I start to write this. I have a paragraph reference and a number, n. I want to select n paragraphs starting with the referenced paragraph.

The first question anyone ever asks about a problem like this is: why are you selecting text in a script? You don't need to. All you need do is reference it.

Well, I'm selecting it because this is a feature in an interactive script. But of course, in order to select it, I need to create that reference. And once I have the reference, all will be well. Perhaps the quickest solution is to walk through the paragraphs, until I reach the last one and then grab a text reference from the first character of the first paragraph to the last of the last. So, how would that go:
function getTextRef(myPara, n) {
var tf = myPara.parent.texts[0]; // text flow
var s = myPara.index; // start
var e = myPara.characters[-1].index; // end
for (j = n-1; j > 0; j--) {
myPara = myPara.insertionPoints[-1].paragraphs[0];
e = myPara.characters[-1].index; // updated end
}
return tf.characters.itemByRange(s, e).texts[0];
}
And that just about does the trick. There is one possible fly-in-the-ointment I can think of: what if there isn't enough text to serve up the n paragraphs? A simple try/catch will handle that. Here's the final version of the function inside the text script I used to test it:
myPara = app.selection[0].paragraphs[0];
n = 2;
selectIt(getTextRef(myPara, n));

function getTextRef(myPara, n) {
var tf = myPara.parent.texts[0]; // text flow
var s = myPara.index; // start
var e = myPara.characters[-1].index; // end
for (j = n-1; j > 0; j--) {
try {
myPara = myPara.insertionPoints[-1].paragraphs[0];
} catch (e) { break }
e = myPara.characters[-1].index; // updated end
}
return tf.characters.itemByRange(s, e).texts[0];
}

function selectIt(theObj) {
app.select(theObj,SelectionOptions.replaceWith);
app.activeWindow.zoom(ZoomOptions.fitPage);
app.activeWindow.zoomPercentage = 150
}
Well, that was easy. But I have to believe it would have taken longer if I hadn't chatted to myself here.

 

ScriptUI Dialog with interacting buttons

Here's a simple script I just banged out to show a simple case of two buttons interacting with each other in a ScriptUI modal dialog:
//DESCRIPTION: Sample Dialog

myDlg = new Window('dialog', 'Example');
myDlg.orientation = 'row';

// Add action buttons
myDlg.btn1 = myDlg.add('button', undefined, 'Disable Him');
myDlg.btn2 = myDlg.add('button', undefined, 'Disable Him');
myDlg.closeBtn = myDlg.add('button', undefined, 'Close');

// Add button functions
myDlg.btn1.onClick = function() {
  if (this.text == 'Disable Him') {
    this.text = 'Enable Him';
    myDlg.btn2.enabled = false;
  } else {
    this.text = 'Disable Him';
    myDlg.btn2.enabled = true;
  }
}

myDlg.btn2.onClick = function() {
  if (this.text == 'Disable Him') {
    this.text = 'Enable Him';
    myDlg.btn1.enabled = false;
  } else {
    this.text = 'Disable Him';
    myDlg.btn1.enabled = true;
  }
}

myDlg.closeBtn.onClick = function() {
  this.parent.close(1);
}

result = myDlg.show();
if (result == 1) {
  alert("You used the Close button");
}
Notice that if you close the dialog by hitting the Escape key rather than clicking the Close button, you do not get the alert.

 

Methods hit wall

I've gone off attaching methods to core JavaScript classes. This week, I've had two scripts fail dismally because of it, so even though the format of working with:
Object.prototype.isInArray = function(myArray){
  for (var i=0; myArray.length > i; i++) {
    if(myArray[i] == this){
      return true;
    }
  }
  return false;
}
is so convenient
if (myObj.isInArray(myArray)) {
or with:
Array.prototype.contains = function(myString){
  for (var i=0; this.length > i; i++) {
    if(myString == this[i]){
      return true;
    }
  }
  return false;
}

if (myArray.contains(myObj)) {
They can really backfire and slap you in the face if you're doing work that involves creating objects or arrays and examining their contents. So, I'm back to the prosaic but safe:
function arrayContains(anArray, anItem) {
  for (var i = 0; anArray.length > i; i++) {
    if (anItem == anArray[i]) return true;
  }
  return false;
}

if (arrayContains(myArray, myObj)) {
Which most people would have been using all along anyway.

Monday, August 20, 2007

 

Open a Copy

In the UI, you get the choice to open a copy of a document in the File > Open dialog. This option is missing from scripting, so how to achieve the same goal?
//DESCRIPTION: Open a copy of a document
openCopy();
function openCopy() {
  var numDocs = app.documents.length;
  if (File.fs == "Windows") {
    var Filter = "InDesign documents: *.indd";
  } else {
    var inddFilter = function(file) {
      while(file.alias){
        file = file.resolve();
        if (file == null) return false;
      }
      if (file instanceof Folder) return true;
      return (file.name.slice(file.name.lastIndexOf(".")).toLowerCase() == ".indd");
    }
    var Filter = inddFilter
  }
  var myFile = File.openDialog("Choose an InDesign document", Filter);
  if (myFile == null) { return } // user canceled
  app.scriptPreferences.userInteractionLevel = UserInteractionLevels.neverInteract;
  var myDoc = app.open(myFile, false);
  app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
  if (numDocs == app.documents.length) {
    alert("Unable to open document; perhaps it is already open by you or another user.");
    return
  }
  var name = "temp";
  var inc = 0;
  do {
    var myTemplate = File("~/Desktop/" + name + inc + ".indt");
    inc++;
  } while (myTemplate.exists);
  myDoc.save(myTemplate, true); // save as template
  app.documents[0].close();
  app.scriptPreferences.userInteractionLevel = UserInteractionLevels.neverInteract;
  app.open(myTemplate);
  app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
  myTemplate.remove();
}
This gets the job done. It's a little more convoluted than just selecting an option. Perhaps some explanation is in order.

The numDocs variable is used to make sure we actually succeed in opening the selected document. The first time I tested the script, I happened to choose the document I already had open. But I got no warning about this because I had switched off interaction. I ended up with an untitled copy of the document, but that was more luck than judgment.

The next few lines show how easy Windows users have when it comes to filtering file types. While they get to use a simple string, Macintosh users get to write a function that not only has to deal with the files in question but also folders and aliases. As a result, we're halfway through the script before we even ask the user to locate the file.

Notice that I open the file without a window. There's no point in letting the user see this step--it just causes unpleasant flashing of the screen. We need somewhere to save the document as a temporary template so we can then open that and get our copy. The desktop seems the obvious place. But we need to make sure that the file name we choose isn't already in use by an existing file. That's what the do/while loop is all about.

Once we have a unique name, all we have to do is save as a template, then open the template (this time with a visible window), and having opened it, we delete it so it doesn't clutter up the user's desktop.

And we have achieved our goal: we've opened a copy of the document.

Sunday, August 19, 2007

 

Tables and keystrokes

The question was asked in the U2U forum this week: how to use the keyboard to move the insertion point out of a table to the story containing it.

The answer is to navigate to the start of the text in the top-left cell and then hit Left Arrow on the keyboard. The quick way to navigate there from anywhere in a table is:

Command-Option-A (or Ctrl-Alt-A)
Escape
Left Arrow

If the top-left cell is empty, hitting the Left Arrow key is unnecessary. So, depending on the state of the top-left cell, to get out of the table takes three or four keystrokes. None of which has much to do with scripting. But that raised the question, how do you use the keyboard to get into a table?

And that requires a script (to which you could attache a shortcut). I composed this script to get the cursor into the first cell of the next table in the current text flow -- bear in mind that a table could be inside the text in a cell in another table:
//DESCRIPTION: Move cursor to next table
if (app.documents.length > 0 && app.selection.length > 0) {
  jumpToNextTable(app.selection[0]);
}

function jumpToNextTable(sel) {
  if (sel.hasOwnProperty("baseline")) {
    try {
      app.select(findTable(sel).cells[0].insertionPoints[0]);
    } catch(e) {};
  }

  function findTable(sel) {
    var story = sel.parent;
    if (sel.index < story.length - 1) {
      var tables = story.characters.itemByRange(sel.index, story.length - 1).texts[0].tables;
      if (tables.length > 0) return tables[0];
    }
  }
}

Sunday, August 05, 2007

 

Active Document Gotcha.

If you have two documents open with the same name, InDesign scripting gets confused about which is which when you try to use app.activeDocument. For example, I had two documents called sampledocument.indd open at the same time this morning. One was being accessed over the network on a remote volume on my G5 while the other was local on my desktop. [I was doing this for testing purposes only--I don't think I've ever actually needed to have two documents open at the same time with the same name.]

For example, when I had the desktop version of my document at front, this script:
myDoc = app.documents[0];
$.writeln(myDoc.fullName);
app.activeDocument = app.documents[-1];
$.writeln(myDoc.fullName);
displayed this:
~/Desktop/sampledocument.indd
/davesaundersG5/Documents/Work/TestFolder/sampledocument.indd
and, when I ran it a second time, it returned:
/davesaundersG5/Documents/Work/TestFolder/sampledocument.indd
~/Desktop/sampledocument.indd
These are right both times. But this script:
myDoc = app.activeDocument;
$.writeln(myDoc.fullName);
app.activeDocument = app.documents[-1];
$.writeln(myDoc.fullName);
returned:
/davesaundersG5/Documents/Work/TestFolder/sampledocument.indd
/davesaundersG5/Documents/Work/TestFolder/sampledocument.indd
no matter which of the two documents I had at the front.

Perhaps the root of the issue is that this:
$.writeln(app.activeDocument.toSource());
returns:
resolve("/document[@name=\"sampledocument.indd\"]")
which contains an inadequate specifier for the document because the two names are the same. Perhaps this indicates that somewhere the documents are maintained is some fixed order so that the specifier always finds the same one no matter where it might be in the collection of documents.

The moral to the story is: never use activeDocument if you have two documents with the same name open at once.

Thursday, August 02, 2007

 

Last shall be First versus NextItem

I have to search through some paragraphs in a story looking for the first one whose applied paragraph style name isn't in a particular list. This kind of search cannot be done using the features of find/change (in either CS2 or CS3) so I have to step through the paragraphs in order looking at each. This would seem to be what the nextItem() method was created for. But I've always been a bit nervous about its performance, so I thought I'd conduct a timing test. I clicked in a story with 83 paragraphs and ran this:
var myStartTime = new Date();
// Do something
var myStory = app.selection[0].parent;
var myPara = myStory.paragraphs[0];
var nextPara = myStory.paragraphs.nextItem(myPara);
try {
  while (myPara != nextPara) {
    myPara = nextPara;
    nextPara = myStory.paragraphs.nextItem(myPara);
  }
} catch(e) {}
var myEndTime = new Date();
var myDuration = (myEndTime - myStartTime)/1000; // Times are in milliseconds
alert(myDuration)
The first time I ran it, it gave me an error trying to get to the nextItem after quite a long time -- I realized that this might be happening at the end of the story (where there is no nextItem) so I added the try/catch and indeed, that was the problem. The script took: 28.575 seconds to run.

So, then I tried this approach which relies on the contained paragraph (i.e, paragraphs[0]) of the last insertionPoint of a paragraph being the next paragraph (hence: "last shall be first"):
var myStartTime = new Date();
// Do something
var myStory = app.selection[0].parent;
var myPara = myStory.paragraphs[0];
var nextPara = myPara.insertionPoints[-1].paragraphs[0];
try {
  while (myPara != nextPara) {
    myPara = nextPara;
    nextPara = myPara.insertionPoints[-1].paragraphs[0];
  }
} catch(e) {beep()}
var myEndTime = new Date();
var myDuration = (myEndTime - myStartTime)/1000; // Times are in milliseconds
alert(myDuration)
And this took: 3.848 seconds. Over seven times faster. No contest!

Notice that I inserted a beep in the catch in this version. As I expected, the while loop exited in this case without the beep, so the try/catch was not actually necessary in this case.

Thinks: these timings were for CS2 on my G5. How about CS3? Because of plug-in issues, I couldn't run exactly the same document, so I picked a story in another that happened to have 141 paragraphs. The first script took 211 seconds on CS3 on my MacIntel iMac/20. The second script took just under 9 seconds. About 23 times faster.

The length of the story is a definite issue with the speed disadvantage of the nextItem approach. Inserting:
$.writeln(nextPara.index);
into the loop allows you to watch progress in ESTK 2's Console and you can see that the further into the story the script gets the slower it gets.

So, the "Last shall be First" technique is the one I shall continue to use.

This page is powered by Blogger. Isn't yours?