Divide It: Old is New Again

Many times when revisiting a script that you may have written awhile ago, you may wonder why you decided to use the logic that you did. That’s the beauty of working with InDesign: There’s so many different ways you can solve a problem.

One script that I have considered essential over the years is one that divides a selected page item into a grid of same-styled items. It does this by duplicating the selected item. One optional functionality that I often take advantage of is the ability to name and incrementally number the items created. The changes I have made to my original DivideIt script are minor but makes the code a little more organized. It’s a fairly complex script so we will take it a step at a time.

STEP 1

Determine if a document is open and a page item is selected.

set selItem to getSelectItem()
(*returns reference to item selected and its parent page. 
Throws error if no document or no valid page selection*)
on getSelectItem()
  set errStr to "Requires document with selected page item to run this script"
  tell application "Adobe InDesign CC 2019"
    if (exists document 1) then
      set docRef to document 1
    else
      error errStr
    end if
    set objTypes to {rectangle, oval, polygon, text frame}
    set selList to selection
    if selList is not {} and class of item 1 of selList is in objTypes then
	set selItem to item 1 of selList
    else
	error errStr
    end if
  end tell
  return selItem
end getSelectItem

Notice that although there are error faults written into the getSelection handler, there is no trap set to catch the error. We will wait to add this later when most of the script is written. We will start with calculations and perform the first duplication.

STEP 2

Start Main Functionality

Place values for variables at the top of the script. These will be replaced when the main functionality (divideItem) is completed and tested.

--measurements are in points
set numRows to 3
set numCols to 4
set gut to 12
set gap to 6
set doInc to true
set nameBase to "Img"
set nameList to {nameBase, doInc}
set selItem to getSelectItem()
divideItem (selItem, numRows, numCols, gut, gap, nameList)
(*Gets measurements for dividing selected item using duplicate. *)
on divideItem(pageRef, selItem, numRows, numCols, gut, gap, nameList)
  set cntr to 1
  tell application "Adobe InDesign CC 2019"
    set measurement unit of script preferences to points
    copy geometric bounds of selItem to {y0, x0, y1, x1}
    set itemWid to x1 - x0
    set itemHgt to y1 - y0
    set colWid to (itemWid - (numCols - 1) * gut) / numCols
    set rowHgt to (itemHgt - (numRows - 1) * gap) / numRows
    set ny0 to y0 
    set nx0 to x0
    set lastItem to selItem
    set thisItem to duplicate lastItem
    set nx1 to nx0 + colWid
    set geometric bounds of selItem to {ny0 + colWid, nx0, ny1, nx1}
  end tell
end divideItem

Notice that the measurement unit for script preferences is set to points. This makes sure that all calclations in the script will be in that unit without disturbing the measurement units set for the document.

The column width (colWid) and row height (rowHgt) for the grid are calculated by multiplying the number of items in the column/row minus 1 by the gutter/gap measurement. This is then subtracted from the column/row width/height. This is then divided by the number of units in the column/row.

Make sure to add the getSelectItem() handler to your script. Save your document before testing your script so you can revert it (File > Revert) to its original after testing . You can now run the script. The reference to the selected item (selItem) is preserved so we can delete it when the script finishes. (We will leave it intact at this point.)

Add a Nested Repeat Loop for Rows and Columns

With the script working as anticipated, add the following repeat loop after the line that sets lastItem to selItem:

      repeat with i from 1 to numRows
	set ny1 to ny0 + rowHgt
	repeat with j from 1 to numCols
	  set thisItem to duplicate lastItem
	  set nx1 to nx0 + colWid
	  set geometric bounds of thisItem to {ny0, nx0, ny1, nx1}
	  set lastItem to thisItem
	  set nx0 to nx1 + gut
	end repeat
	set nx0 to x0
	set ny0 to ny1 + gap
      end repeat

When you test the script with this addition you should have 3 rows and 4 columns of page items. Notice how lastItem becomes the reference to what was thisItem each time through the loop. Lastly, value for the lest coordinate is set to the original x0 value and the value for the top y coordinate (ny0) is set to what was the bottom coordinate of the previous item (ny1) plus the value for gap.

Name the Items

If the value for nameList is not an empty list, the script will need to name each item as part of the loop. This will be done with the following if statements.

      if length of nameList > 0 then
	set baseName to item 1 of nameList
	if item 2 of nameList is true then
	  set baseName to baseName & "_" & cntr
	end if
	set name of thisItem to baseName
	set cntr to cntr + 1
      end if

Add the if statements to the script right after the geometric bounds of thisItem are set. (Now you see why the variable cntr was established at the beginning of the handler.)

Add the getSelectItem() handler to the bottom of the script, The rest should now read as follows :

set numRows to 3
set numCols to 4
set gut to 12
set gap to 18
set doInc to true
set nameBase to "Img"
set nameList to {nameBase, doInc}
try
 set selItem to getSelectItem()
 divideItem(pageRef, selItem, numRows, numCols, gut, gap, nameList)
 tell application "Adobe InDesign CC 2019"
   delete selItem
 end tell
on error errStr
 activate
 display alert "Error: " & errStr
end try
pageRef
(*Divides selected item using duplicate. *)
on divideItem(selItem, numRows, numCols, gut, gap, nameList)
  set cntr to 0
  tell application "Adobe InDesign CC 2019"
    set measurement unit of script preferences to points
    copy geometric bounds of selItem to {y0, x0, y1, x1}
    set itemWid to x1 - x0
    set itemHgt to y1 - y0
    set colWid to (itemWid - (numCols - 1) * gut) / numCols
    set rowHgt to (itemHgt - (numRows - 1) * gap) / numRows
    set ny0 to y0
    set nx0 to x0
    set lastItem to selItem
    repeat with i from 1 to numRows
      set ny1 to ny0 + rowHgt
	repeat with j from 1 to numCols
          set thisItem to duplicate lastItem
	  set nx1 to nx0 + colWid
	  set geometric bounds of thisItem to {ny0, nx0, ny1, nx1}
	  if length of nameList > 0 then
	    set baseName to item 1 of nameList
	    if item 2 of nameList is true then
	      set baseName to baseName & "_" & cntr
	    end if
	    set name of thisItem to baseName
	    set cntr to cntr + 1
	  end if
          set lastItem to thisItem
	  set nx0 to nx1 + gut
        end repeat
      set nx0 to x0
      set ny0 to ny1 + gap
    end repeat
  end tell
end divideItem

Hopefully, you should be getting a somewhat comfortable in writing scripts as modules (making them easier to write and certainly much easier to test and debug).

ONWARD AND UPWARD

Of course you will want to add a custom dialog to get the values for creating the grid from the user. You will want to create this as a separate script and add it once thoroughly tested. To call the handler and set values change the lines between try and on error at the top of the script to read:

set userResponse to userDialog("NameOfDialog")

The handler for creating the dialog is a little wordy but should be easy to follow:

(*Returns user-entered values for rows, columns, gutter, gap, and optional name and increment*)
on userDialog(dlgName)
  tell application "Adobe InDesign CC 2019"
    activate
    set origLevel to user interaction level of script preferences
    set user interaction level of script preferences to interact with all
    set labelWid to 72
    set myDlg to make dialog with properties {name:dlgName}
    tell myDlg
      set colRef to make dialog column
      tell colRef
	 tell (make dialog row)
	   make static text with properties {static label:"Enter values for grid using point units", min width:272, static alignment:center align}
	 end tell
	 set myBdr to make border panel
	 tell myBdr
	   tell (make dialog column)
	     make static text with properties {static label:"Number Columns: ", min width:labelWid}
	     make static text with properties {static label:"Number Rows: ", min width:labelWid}
	     make static text with properties {static label:"Gutter Width: ", min width:labelWid}
	     make static text with properties {static label:"Gap Height: ", min width:labelWid}
	   end tell
	 tell (make dialog column)
	   set colField to make integer editbox with properties {edit value:2}
	   set rowField to make integer editbox with properties {edit value:2}
	   set gutterField to make measurement editbox with properties {edit value:12, edit units:points}
	   set gapField to make measurement editbox with properties {edit value:12, edit units:points}
	end tell
      end tell
      set enable1 to make enabling group with properties {static label:"Add Names", checked state:false}
	tell enable1
	  tell (make dialog column)
	    tell (make dialog row)
	      make static text with properties {static label:"Name Base:", min width:labelWid}
	      set nameField to make text editbox with properties {min width:200, edit contents:"Image"}
	    end tell
	    tell (make dialog row)
	      set checkField to make checkbox control with properties {static label:"Add Increment", checked state:false}
	    end tell
	end tell
      end tell
    end tell
  end tell
  set userResponse to show myDlg
  set valueList to {}
  set nameList to {}
  if userResponse then
    set valueList to {edit value of colField, edit value of rowField, edit value of gutterField, edit value of gapField}
    if checked state of enable1 then
	set nameList to {edit contents of nameField, checked state of checkField}
    end if
    set end of valueList to nameList
  end if
  destroy myDlg
  set user interaction level of script preferences to origLevel
    return valueList
  end tell
end userDialog

The script packages the values entered by the user into a list and passes it back to the main script, eliminating the need for setting variables at the top.

Once tested, you can add it to the DivideIt script by changing the statements inside the try/on error statements to the following:

   set selItem to getSelectItem()
   set userResponse to userDialog("NameOfDialog")
   if userResponse is not {} then
     copy userResponse to {numRows, numCols, gut, gap, nameList}
     divideItem(selItem, numRows, numCols, gut, gap, nameList)
     tell application "Adobe InDesign CC 2019"
       delete selItem
     end tell
   else
     error ("User Cancelled dialog")
   end if

Now that you have added the custom dialog you no longer need to have the values for variables defined at the top of the script.

Disclaimer:
Scripts provided are for demonstration and educational purposes. No representation is made as to their accuracy or completeness. Readers are advised to use the code at their own risk.