Wednesday, January 24, 2007

 

Making an Array of Objects

Every now and then I find myself having to rethink something I know I dealt with just recently but the details have slipped through my grasp. So this blog entry is an aide-memoire for me!

I'm working on a script where I want a function to read values from a table and return an array of objects that reflect the values in the table. I want each object to have this form:

{srcFnt:, srcStyl:, targFnt:, targStyl:}

So, the the loop looks like this (it starts at row 2 to skip over the heading info in the table):
    var List = new Array();
    for (var j = 2; myTable.bodyRowCount > j; j++) {
      if (myTable.rows[j].cells[0].contents == "") { break }
      
    }
So, if I get to that blank line, what do I do to create the object?

Well, there's nothing like writing a blog to trigger new ideas: previously, it never occurred to me to use a function to create the object directly. Let's see how this works:
    var List = new Array();
    for (var j = 2; myTable.bodyRowCount > j; j++) {
      if (myTable.rows[j].cells[0].contents == "") { break }
      List.push(getObject(myTable.rows[j]))
    }
  
  function getObject(theRow) {
    return {
      srcFnt:theRow.cells[0].texts[0].contents,
      srcStyl:theRow.cells[1].texts[0].contents,
      targFnt:theRow.cells[2].texts[0].contents,
      targStyl:theRow.cells[3].texts[0].contents
    }
  }
Well what do you know! That was easy. Much easier than what I did last time I tried to solve this problem.

Sunday, January 14, 2007

 

Object Specifiers

If you do a search of the JavaScript section of the InDesign CS2 Scripting Reference for the words "object specifier" you'll get 448 hits. Half of them are found in the descriptions of the getElements() method. The other half are in the descriptions of the toSpecifier() method.

So, what are these object specifiers? I confess to having some difficulty explaining what they are in abstract terms. I think of them as dynamic pointers into the object structure of InDesign.

Which begs the question: what is the object structure when it is at home? Isn't the term "object model"? Well, yes, most people speak of the object model all the time, but I see a difference between the model: all the possible ways that objects might related to each other, and the structure: the current set of objects and their interrelationships. The model describes what might happen at some point; the structure reflects what is happening now.

For example, if you have no documents open, the model still includes pageItems, but with no documents for those objects to exist in, the structure includes no pageItems right now. Of course, as you work with InDesign, whether with scripts or with the UI, the structure changes to reflect your actions.

So, an object specifier points at an object in the structure. Well, maybe. It's a little more complicated than that because it might point at an object that doesn't yet exist. Look at this:
myObjSpecifier = app.documents.item("Fred.indd");
if (myObjSpecifier == null) {
  alert("'Fred.indd' doesn't exist, yet.");
}
Assuming you don't have a document named "Fred.indd" open when you run that script, you'll get the alert. Compare that with this:
myObjSpecifier = app.documents.item("Fred.indd");
var myFolder= Folder.selectDialog("Choose a home for Fred.indd");
if (myFolder == null) {exit()}
app.documents.add();
app.documents[0].save(File(myFolder.absoluteURI + "/Fred.indd"));
if (myObjSpecifier == null) {
  alert("'Fred.indd' still doesn't exist");
} else {
  alert("'Fred.indd' exists now.");
}
Run this script, and even though we set the value of myObjSpecifier before the document was created, you get the message that Fred.indd now exists. This is why I call this a dynamic pointer into the object structure. Its value adapts to the moment.

Let's take a look at that toSpecifier() method:
myObjSpecifier = app.documents.item("Fred.indd");
$.writeln(myObjSpecifier.toSpecifier());
Run this in ESTK and you see in the console:
/document[@name="Fred.indd"]
undefined
We can ignore the second line; that's just the result of the script. What's interesting is the first line. That's what an object specifier looks like when it is coerced to text. As you can see, it is a descriptive pointer into the object structure.

If we modify this last script to read:
myObjSpecifier = app.documents.item("Fred.indd");
$.writeln(myObjSpecifier.toSpecifier());
myObjSpecifier
And again run it from ESTK, this time, we see in the Console:
/document[@name="Fred.indd"]
[object Document]
This time, the second line is of interest. The result of our script is an object of class Document. That it doesn't exist (yet) is irrelevant (as far as the ExtendScript engine is concerned).

Let's take this one step further:
myObjSpecifier = app.documents.item("Fred.indd");
myObjSpecifier == null;
$.writeln(myObjSpecifier.toSpecifier());
Hmmm, the result of running this is an eye-opening disappointment. To execute the second statement, the engine has to evaluate the variable myObjSpecifier. And, in this case, because we're running with no document named "Fred.indd", it evaluates to null. Apparently, when that happens, it does more than just evaluate the variable; it resolves it, so it becomes undefined, causing the third line of the script to give the error:
Object is invalid
This leaves me scratching my head a little, making the ability to pre-create object specifiers more than a little dodgy if they are this fragile.

I'll have more to say on this subject as I explore further.

Tuesday, January 09, 2007

 

Back to the Pasteboard

OK, I've confirmed my suspicion that the current version of the script hits a run-time error if a locked layer holds a pasteboard item. I'm inclined to take the easy way out here and simply exclude any such layers from this game. If the user wants to add it back in, all he need do is unlock the layer and run the script again.

So, now let's get down to the hard part: how to prevent items from colliding with pages?

I think I'm going to assume that only left/right collisions are possible. I don't know enough about the dtptools plug-in that allows different sized pages, but since I don't own it and I'm writing this script as much for me as anyone else (so few people having taken any interest), I might as well not worry about things that aren't going to affect me.

So, for any item, I have three rectangles to worry about:
  1. The geometric bounds of the item
  2. The bounds of the source spread
  3. The bounds of the target spread
The only items I have to worry about are those that are wholly to the left or right of the source spread. I want them to appear the same distance to the left or right of the target spread. Notice that any item on the pasteboard above or below the pages that is not wholly to the left or right will not slide when moved to the target. Being where they are, they can't possibly collide with a page.

Of course, getting the bounds of a spread is non-trivial unless you're using Spread as the ruler origin for your document, so I'm going to have to switch to spread at the start and switch back afterwards. Hmm, that means we'd better reset the zero point too and restore it. That means that for an item to be wholly left, it's right-bounds value must be negative because the spread left bound will always be zero; still, it's probably safer to compare just in case.

Oh wait a cotton-picking minute! Because of the way that the spread-based coordinates work, the left side looks after itself. The coordinates are such that an item to the left of the spread will always be the same distance to the left. It's only the right side we have to worry about. Which means that all I have to worry about is the difference in width between the two spreads. So, perhaps I shouldn't even worry about whether or not an item is wholly right, just if it is right of the zero-X I should move it by the difference in widths.

Well, what do you know? It seems to work!
//DESCRIPTION: Moves all Pasteboard Items to Pasteboard of Current Spread

if (app.documents.length == 0) { exit() }
movePBitems(app.documents[0]);

function movePBitems(myDoc) {
  if (app.activeWindow.constructor.name == "StoryWindow") { return }
  var uO = myDoc.viewPreferences.rulerOrigin;
  var uZP = myDoc.zeroPoint;
  myDoc.viewPreferences.rulerOrigin = RulerOrigin.spreadOrigin;
  myDoc.zeroPoint = [0,0];
  myObjs = myDoc.pageItems.everyItem().parent;
  var moveables = new Array();
  for (var j = myObjs.length - 1; j >= 0; j--) {
    if (myObjs[j].constructor.name == "Page") { continue }
    moveables.push(myDoc.pageItems[j].id);
  }
  var mySpread = app.activeWindow.activeSpread;
  var targSpreadWidth = getSpreadWidth(mySpread);
  for (var j = moveables.length - 1; j >= 0; j--) {
    var myObj = myDoc.pageItems.itemByID(moveables[j]);
    var sourceSpread = myObj.parent;
    if (sourceSpread == mySpread) { continue }
    var sourceWidth = getSpreadWidth(sourceSpread);
    var Shift = 0;
    if (myObj.geometricBounds[3] > 0) {
      Shift = targSpreadWidth - sourceWidth;
    }
    var myBounds = myObj.geometricBounds;
    if (myObj.locked) { continue }
    try {
      myObj.move(mySpread);
    } catch(e) { continue } // myObj must be on locked layer
    myObj.move([myBounds[1] + Shift, myBounds[0]]);
  }
  myDoc.viewPreferences.rulerOrigin = uO;
  myDoc.zeroPoint = uZP;
  
  function getSpreadWidth(spread) {
    var my1stBounds = spread.pages[0].bounds;
    var myLastBounds = spread.pages[-1].bounds;
    return myLastBounds[3] - my1stBounds[1];
  }
} // end function movePBitems

Monday, January 08, 2007

 

Blanking those Cell Labels

With a large table (I was working on one that spanned 18 pages, consisting of 399 rows with 7 columns), having all those cell labels is a blessing while you're looking for the overset text, but once that's out of the way, all they do is slow you down and make your document larger, so
//DESCRIPTION: Clear Labels from Every Cell of Table

if (app.documents.length == 0 || app.selection.length == 0) { exit() }
processSelection(app.selection[0]);

function processSelection(sel) {
  var myTable = findTable(sel);
  myTable.cells.everyItem().label = "";
  
  function findTable(obj) {
    while (obj.constructor.name != "Table") {
      obj = obj.parent;
      if (obj.constructor.name == "Application") {
        throw "Can't find table"
      }
    }
    return obj
  }
}
Which of course raises the issue: is a blank label still present? From a script, you can't tell the difference between a blank label and an absent one, so functionally it makes no difference, but what about internal resources? All I can say is that my document became more responsive after I'd deleted the contents of all the cell labels.

 

Aggregate Pasteboard

For those of you who provided feedback, many thanks. Here's how far I've gotten after a few minutes of scripting. This version does nothing to deal with the issue of facing pages spreads and the possibility that an item that started on the pasteboard will be moved to a page if the source spread has just one page but the target has two (or more).

However, for single-sided documents, it works like a charm (I think -- if anyone wants to give it a spin, let me know if you find anything wrong.

Note the following functional decisions:

1. Use only with single-sided documents (for now).
2. If the active window is a story editor window, the script does nothing.
3. If a pasteboard item is locked, the script leaves it where it is. This allows the user to lock an item to a particular spread, for whatever reason.
4. If a layer is invisible, any pasteboard item on that layer is nonetheless moved to the new pasteboard.

Ooh, I bet there's a bug if the layer is locked. Well, I'll have to fix that the next time around. Here's the current state of the script:
//DESCRIPTION: Moves all Pasteboard Items to Pasteboard of Current Spread

if (app.documents.length == 0) { exit() }
movePBitems(app.documents[0]);

function movePBitems(myDoc) {
  if (app.activeWindow.constructor.name == "StoryWindow") { return }
  myObjs = myDoc.pageItems.everyItem().parent;
  var moveables = new Array();
  for (var j = myObjs.length - 1; j >= 0; j--) {
    if (myObjs[j].constructor.name == "Page") { continue }
    moveables.push(myDoc.pageItems[j].id);
  }
  var mySpread = app.activeWindow.activeSpread;
  for (var j = moveables.length - 1; j >= 0; j--) {
    var myObj = myDoc.pageItems.itemByID(moveables[j]);
    if (myObj.parent == mySpread) { continue }
    var myBounds = myObj.geometricBounds;
    if (myObj.locked) { continue }
    myObj.move(mySpread);
    myObj.move([myBounds[1], myBounds[0]]);
  }
} // end function movePBitems
Enjoy,

Dave

 

Story Editor for Tables?

One of the frustrations of working with large tables is that if you have an overset cell, it is hard to find out what's in it from the UI. Would that you could summon the story editor to look at the contents of a cell! But you can't. So, it occurred to me to write a script to label all the cells of a table with their contents. This might be overkill -- perhaps a script to label only the selected cell would be sufficient, but by labeling them all I get the chance to have the Script Label palette open and immediately see the contents of an overset cell without having to do anything more than look. I suppose, later, I should run a script to clear out the labels just because of the amount of extra text my document will be carrying around.

Here's the script:
//DESCRIPTION: Label Every Cell of Table

if (app.documents.length == 0 || app.selection.length == 0) { exit() }
processSelection(app.selection[0]);

function processSelection(sel) {
  var myTable = findTable(sel);
  var myCellLabels = myTable.cells.everyItem().texts[0].contents;
  for (var j = myTable.cells.length - 1; j >= 0; j--) {
    myTable.cells[j].label = myCellLabels[j];
  }
  
  function findTable(obj) {
    while (obj.constructor.name != "Table") {
      obj = obj.parent;
      if (obj.constructor.name == "Application") {
        throw "Can't find table"
      }
    }
    return obj
  }
}
Ah, it's not true that I have to do nothing to see the label. I have to hit Command-/ to select the cell.

Dave

Friday, January 05, 2007

 

Universal Pasteboard

One disappointment that PageMaker users experience is that InDesign doesn't have a universal pasteboard. Instead, each spread has its own pasteboard. In a discussion this morning on the U2U forum, the idea was voiced that a feature in InDesign to move all pasteboard items to the current spread would be a good substitute.

Well, that's what scripts are for.

But before I leap in and write a script, I thought I'd post some immediate thoughts about what the script should do about certain situations.

First, to find if an item is on the pasteboard, particularly if you know in advance that the item in question is know not to be inline or part of group (for example, the item is a member of the collection returned by document.pageItems).

Something along these lines works:
myDoc = app.activeDocument;
myObjs = myDoc.pageItems.everyItem().parent;
for (j = myObjs.length - 1; j >= 0; j--) {
  if (myObjs[j].constructor.name == "Page") { continue }
  moveToCurrentPB(myDoc.pageItems[j]);
}
Of course, we still have to write the function, but some issues have to be resolved:
  1. What if the item is on a hidden layer?
  2. What if the item is locked?
  3. What if the location of the item on its source spread would put it on a page of the current spread?
  4. Conversely, what if, when moving from a two-page spread to a single-page spread, the item will be more than a page width away from the live page?
  5. While we're thinking about that, what about multi-page spreads or even dragged apart single-page spreads?
Clearly, these questions need to be answered before I can write the function. I welcome any input.

Dave

Wednesday, January 03, 2007

 

Multi-column Text Frames

Suddenly, I've found myself doing a lot of work with multi-column text frames, so when the issue came up on the U2U forum of which column within a text frame contained some text, it was fairly short work to bang out this function:
function getColumnNum(mysearch) {
  myFrame = mysearch.parentTextFrames[0];
  myColumnStarts = myFrame.textColumns.everyItem().index;
  mySpot = mysearch.index;
  for (var j = myColumnStarts.length - 1; j >= 0; j--) {
    if (mySpot >= myColumnStarts[j]) { return j }
  }
  throw "What a revolting development this is"
}
I wasn't quite sure what to do if the loop dropped through. It's one of those theoretically impossible happenings that nonetheless is syntactically possible. In theory, if myFrame is the parent text frame of the text reference contained in mysearch then the loop will never terminate. The error will never be thrown.

So, I borrowed a leaf from the immortal William Bendix (start of "A Life of Riley") and threw an amusing, albeit unhelpful, error message.

Monday, January 01, 2007

 

Installing and Running a Script

It is a surprise for me to discover that I've never actually written a blog entry on installing scripts. I guess I took it for granted that visitors here already knew how to do that! But just in case, and also because I need to have a description in a central location I can point people to, here is a capsule description of the process for InDesign CS2.

For InDesign CS2 to even be aware of a script, it must be located within the Scripts folder of the Presets folder of the Adobe InDesign CS2 application folder. Any scripts located in that Scripts folder or its subfolders are listed in InDesign CS2's Scripts Palette.

That's it in a nutshell, but there are some subtleties to watch out for and some common mistakes that people make.

Locating the Scripts Palette

Like most all of InDesign CS2's palettes, the Scripts palette is accessible from the Window menu, but it is not direcly in that menu. It is in the Automation sub-menu along with two other palettes, Data Merge and Script Label. When you activate the Scripts palette, you'll find that it shares space with the Script Label palette. I find this inconvenient and so I've separated the two into different groups. I have Scripts sharing space with the Hyperlinks, Bookmarks and Tags palettes, while the Script Label palette shares with the Pathfinder and Story palettes.

Installing a zipped script

If you download a script contained in a zipped (or stuffed) archive, the installation process is very simple. First, unzip (or unstuff) the archive. You'll find yourself either with the script file itself, by itself, or a folder containing the script and associated files. In the latter case, if any of those associated files is a read-me file of some kind or a user guide, be sure to read it and follow the instructions it gives—some scripts, for example, depend on other support files being located in particular places. But, if all you have is the single script file, then move or copy it to an appropriate subfolder of the Scripts folder, as described above.

Copying and Pasting the Source of a Script

Many scripts are posted on forums in the form of listings of their original source texts. To install such a script on to your computer, simply copy the text to a text editor of some kind or even better to ExtendScript Toolkit and then save the script file into a subfolder of the Scripts folder with a name of the form: ScriptName.jsx where "ScriptName" is a name you choose that reflects the purpose of the script. If you choose to use an application like Apple's TextEdit or Microsoft's Word, be sure to save as Plain Text (in the case of TextEdit, you have to use the Format menu to convert to Plain Text before Saving). Should you inadvertently save in some other format, your attempts to run the script will be frustrated by a syntax error message.

Beware the Plug-Ins/Script Folder

Do not put your scripts into the folder named Script within the Plug-Ins folder of the Adobe InDesign CS2 application folder. That's where the plug-ins that provide scripting support live. Any scripts you put into that folder will not appear in your Scripts palette.

Consider Using Aliased Sub-folders

Having scripts located physically inside the application folder can be somewhat inconvenient, particularly if you are a scripter, working on the scripts themselves, rather than just using them as tools. But even script users might be frustrated if they forget that the scripts are there and for some reason re-install InDesign, obliterating the Scripts folder in the process.

While you could use an alias of the whole Scripts folder to work around this issue, I have found it better to work with aliased sub-folders. This allows me to keep my scripts in folders inside my Documents folder, while for some projects, I have a Scripts folder associated with the project itself for custom scripts that apply to just that project.

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