Buttons to Layer Script

Earlier today Bob Levine wrote in a tweet that he needs a script to move all buttons to a “button layer”. I’m not sure exactly what Bob needed this for, but I can see where this could be useful in digital (magazine) publishing.

This seemed like an easy enough task, so I decided to give it a go. At the same time, Loic Aigon decided the same thing and proposed a first draft. There were not many differences in our code, but mine solved some issues Loic encountered. Looking into the problem proved to be an interesting scripting lesson, so if you’re interested in writing scripts, read on.

Otherwise, you can just download the script from here and start using it!

How to script this

There’s only a couple of steps to move buttons to a specific layer:

  1. Find the buttons
  2. Change their layer

Step 1

Step 1 seems pretty easy. A document has a buttons collection which gives you all buttons, so to get all buttons it should be as simple as:

app.documents.item(0).buttons

.
The only apparent issue with that (more on not so apparent issues later) is it only gets top level buttons. Any buttons nested in groups or inline in text are not picked up. You can get all page items in the document using the Document.allPageItems property, but you then need to filter the results. To get all buttons no matter what the level of nesting I used this function:

1
2
3
4
5
6
7
8
9
10
function GetButtons(doc){
  var retVal = [];
  var pis = doc.allPageItems;
  for(var i=0;i<pis.length;i++){
    if(pis[i] instanceof Button){
      retVal.push(pis[i]);
    }
  }
  return retVal;
}

Step 2

Step 2 is also very easy. There’s actually two ways to go about setting the layer of an object. You can use the Button.move() method or you can set the Button.itemLayer property. The only issue here is that locked items can not be moved. Actually, items in a locked layer can’t be moved either, so to successfully move (or change) objects, you need to first unlock them and their layer. Of course you don’t want to mess up the document state, so you really want to return the locked state when you are done.

I have a canned function for this purpose, and it only works for setting properties and not for methods, so for my purposes here, setting the itemLayer is the easier way to go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ForceSetProp (button,"itemLayer",buttonLayer);
 
function ForceSetProp (obj,prop,value){
  if(obj instanceof Array){
    var lockedLayers = [];
    for(var i=0;i<obj.length;i++){
      if(obj[i].hasOwnProperty("itemLayer")){
        var layer = obj[i].itemLayer;
        if(layer.locked){
          lockedLayers.push(layer);
          layer.locked=false;
        }
      }
      ForceSetProp(obj[i],prop,value);
    }
    for(var i=0;i<lockedLayers;i++){
      lockedLayers[i].locked = true;
    }
  } else {
    var relock = false;
    var objLocked = obj.locked;
    if(objLocked){obj.locked = false}
    if(obj.hasOwnProperty("itemLayer")){
      var layer = obj.itemLayer;
      if(layer.locked){
        relock = true;
      }
    }
    obj[prop] = value;
    if(objLocked){obj.locked = true;}
    if(relock){layer.locked = true}
  }
}

Issues

Issue 1

The first issue with the script is what to do with grouped buttons. InDesign requires that all parts of a group be on the same layer. If you try to change the layer of part of a group you will get an error. This gives us two options: You can either ignore any button that’s part of a group, or move the whole group. I decided to go the second way.

Since groups can be nested in other groups, you need to recursively climb to the top level group. That is done with a while loop:

while(buttons[i].parent instanceof Group){
	buttons[i] = buttons[i].parent;
}

This simply changes the reference to the button to its parent as long as the parent is a Group. Once we have the reference to the top level group, we change the layer of that.

Issue 2

The second issue is how to handle inline and anchored buttons. In this case, I decided to leave them as-is. I’m making the assumption that the user does not want to move all the text that contains the button as well. On the other hand, the user probably wants to know that the inline buttons are not moved, so I note the fact and alert the user at the end of the script. The other point to keep in mind is that this should be done after the previously mentioned while loop because the same issue applied to buttons nested in groups which are then nested in text.

if(buttons[i].parent instanceof Character){
  missedSome = true;
  continue;
}

Issue 3

The last issue is that we need to be sure that our layer exists. That’s done with this simple function:

function GetLayer(doc,name){
  var layer = doc.layers.item(name);
  if(!layer.isValid){
    layer = doc.layers.add({name:name});
  }
  return layer;
}

Great! We’re all done! :-)

Here’s the complete script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
var doc = app.documents[0];
var buttonLayer = GetLayer(doc,"Button Layer");
var buttons = GetButtons(doc);
var missedSome = false;
for(var i=0;i<buttons.length;i++){
  while(buttons[i].parent instanceof Group){
    buttons[i] = buttons[i].parent;
  }
  if(buttons[i].parent instanceof Character){
    missedSome = true;
    continue;
  }
  ForceSetProp (buttons[i],"itemLayer",buttonLayer);
}
if(missedSome){
  alert("One or more buttons are contained in text and could not be moved.");
}
function GetLayer(doc,name){
  var layer = doc.layers.item(name);
  if(!layer.isValid){
    layer = doc.layers.add({name:name});
  }
  return layer;
}
function GetButtons(doc){
  var retVal = [];
  var pis = doc.allPageItems;
  for(var i=0;i<pis.length;i++){
    if(pis[i] instanceof Button){
      retVal.push(pis[i]);
    }
  }
  return retVal;
}
function ForceSetProp (obj,prop,value){
  if(obj instanceof Array){
    var lockedLayers = [];
    for(var i=0;i<obj.length;i++){
      if(obj[i].hasOwnProperty("itemLayer")){
        var layer = obj[i].itemLayer;
        if(layer.locked){
          lockedLayers.push(layer);
          layer.locked=false;
        }
      }
      ForceSetProp(obj[i],prop,value);
    }
    for(var i=0;i<lockedLayers;i++){
      lockedLayers[i].locked = true;
    }
  } else {
    var relock = false;
    var objLocked = obj.locked;
    if(objLocked){obj.locked = false}
    if(obj.hasOwnProperty("itemLayer")){
      var layer = obj.itemLayer;
      if(layer.locked){
        relock = true;
      }
    }
    obj[prop] = value;
    if(objLocked){obj.locked = true;}
    if(relock){layer.locked = true}
  }
}

Loic’s Script

Loic’s script was not so different:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function main(){
  if(app.documents.length==0) return false;
  var doc=app.activeDocument,
    btns=doc.buttons,
    btnLayer=doc.layers.itemByName("Buttons"),
    btnMax=btns.length-1,
    lock,
    hidden,
    l;
  if(!btnLayer.isValid){
    btnLayer = doc.layers.add({name:"Buttons"});
  }
  for(var i=btnMax; i>=0; i--){
    l=btns[i].itemLayer;
    lock=l.locked;
    l.locked=false;
    btns[i].move(btnLayer);
    l.locked=lock;
  }
}
main();

The coding style is slightly different, and it doesn’t deal with nested objects (for good or bad), but it’s very close to what I wrote. For some reason, when his script was run it was hit-or-miss. Some buttons were moved, and other not. I was very puzzled when I saw the results! The only differences between his script and mine were the fact that he used Document.buttons and he used Button.move() instead of Button.itemLayer. I checked and both seemed to work correctly.

As much as I hate working in the ESTK, I actually stepped through Loic’s script in the ESTK to see what was going on.

Arrays & Collections

To explain what happened, we will digress slightly to talk about collections. Collections are very central to InDesign scripting. Almost everything in the InDesign DOM is part of a collection: The application has a collection of documents. Documents have collections of Spreads, Pages, PageItems, Stories, and just about everything else that a document can contain. Collections have special methods to get individual objects and objects can also be referred to using square bracket notation like arrays. In fact collections act a lot like arrays to the point that many people can confuse the difference between collections and arrays.

The major difference between collections and arrays are that collections are dynamic, while arrays are static. In other words, every time you access a collection, InDesign checks the structure of the items in the collection and rebuilds the collection on the fly. It’s important to remember that references to items in a collection does not actually store the item in memory. Rather it points to an index within the collection object. When you ask for that object, InDesign looks for the object at the index specified and hands you a reference to that object. Arrays on the other hand are static. Each placement within the array points to the specifier of a specific object and will always hold that specifier. (You have to be careful about exactly what specifier you store, but that’s a lesson for another time.)

Collection Shuffling

In our case, something interesting was happening. When items were moved from a layer in the back to a layer in the front, the index of the item within the document buttons collection changed. In other words. When we moved item #2 to the front-most layer, it became item #1, and item #1 became item #2. The next iteration then moves item #1, but that was the item we just moved! Item #1 which became item #2 was never moved by the script, and item #2 which became item #1 was moved twice!

Turning Collections Into Arrays

Once we know what the problem is, the solution is straight-forward: Change the collection into an array. Another advantage of working on arrays as opposed to collections is the fact that InDesign doesn’t need to reconstruct it every time you access it, so that results in a much more efficient script. So how do we turn a collection into an array? InDesign provides a method for that: Collection.everyItem().getElements(). So to make Loic’s script work correctly all we needed to do was edit line #4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function main(){
  if(app.documents.length==0) return false;
  var doc=app.activeDocument,
    btns=doc.buttons.everyItem().getElements(),
    btnLayer=doc.layers.itemByName("Buttons"),
    btnMax=btns.length-1,
    lock,
    hidden,
    l;
  if(!btnLayer.isValid){
    btnLayer = doc.layers.add({name:"Buttons"});
  }
  for(var i=btnMax; i>=0; i--){
    l=btns[i].itemLayer;
    lock=l.locked;
    l.locked=false;
    btns[i].move(btnLayer);
    l.locked=lock;
  }
}
main();
Tags: , , , ,

3 Comments

  1. Zev says:

    Very informative! Thanks!

  2. […] a script to move all your buttons to a single layer? (Scripters definitely need to check out his […]

  3. Jean-Claude Tremblay says:

    Nice script and really useful. CS6 add a bunch of Forms buttons. Would be great to have an updated script that collect and move all the interactive & forms buttons.

    JC

Leave feedback