Monday, May 20, 2013

Using Applescript to reposition objects

***
Update (07/20/2013): a new version of this script that prompts for the horizontal and vertical offsets  (rather than having to edit the script) appears after the jump.

***

Omnigraffle gives quite a bit of access to AppleScript for automating just about anything you can do by hand. To execute AppleScript, open the AppleScript editor, create a script in it, and run it.

The following example shows you how to reposition all objects in your document by a certain horizontal/vertical offset - in this case, 20 pixels up, and 20 pixels to the left.

While you may have no need for this particular functionality, the code below shows a general technique for accessing each canvas, each layer within a canvas, and each object on a layer. For instance, you might want to change all canvases in your document to a different size, so knowing how to walk through all canvases in a document is a good starting point.

Here is the Applescript code, and below I'll call out various lines to explain them. This post assumes that you have some familiarity with programming concepts such as variables, loops, classes, etc., but does not assume knowledge of AppleScript in particular.

Notes on the colors below:
  • Green items are variables
  • Blue italicized items are classes
  • Blue bolded items are commands particular to OmniGraffle
  • Black bolded items are AppleScript keywords
  • Black unbolded items are values
  • Purple, unbolded items are class properties (this script contains none)

-- Copyright © 2013, Joseph Brick
-- All rights reserved. 
-- Redistribution, with or without modification, is permitted provided that the copyright notice is retained.

set theOffset to {-20, -20} as point
tell application id "OGfl"
    set allCanvases to canvases of front document
    repeat with currentCanvas in allCanvases
        set allNonsharedLayers to (layers of currentCanvas where class is not shared layer)
        repeat with currentLayer in allNonsharedLayers
            set allGraphics to graphics of currentLayer
            repeat with currentGraphic in allGraphics
                slide currentGraphic by theOffset
            end repeat
        end repeat
    end repeat
end tell

-----

And now, let's break down this script:

set theOffset to {-20, -20} as point

theOffset is a variable. In Applescript, you don't need to define variables ahead of time; the act of setting one for the first time creates it. theOffset is set to a list of two items. Lists are defined by delimiting a set of values by commas within curly braces.

The variable is defined as an instance of the class point, since that is what the slide command expects. (A point must be a list of two real numbers.) Without specifying the class, however, AppleScript would figure it out, so in most cases you don't need to specify the class.

tell application id "OGfl"

The tell statement here has a corresponding end tell, which is the last line in the script. Everything between those two lines will be directed at Omnigraffle, as opposed to at another application.

set allCanvases to canvases of the front document

canvases returns a list of all canvases in the document. This is placed in the variable allCanvases.

repeat with currentCanvas in allCanvases / end repeat

The repeat with statement creates a loop for each item in a list, and at the start of each loop it places the corresponding list item into a variable. Here we are walking through all canvases in the document.

The list of canvases is held by the variable allCanvases, set above, and the variable that receives the corresponding value at the beginning of each loop is currentCanvas. So at the beginning the first loop, currentCanvas receives the first item of allCanvases; in the second loop, it gets the second item, and so on.

set allNonsharedLayers to (layers of currentCanvas where class is not shared layer)

Note: there is a bug in Omnigraffle's Applescript implementation (as of version 5.4.2) that returns an invalid form of the graphics list for shared layers, so this script is limited to non-shared layers. Sadly, you'll have to move the objects in your shared layers manually, or make a temporary non-shared layer copy.

You can filter a list of items using the keyword where. (The keywords whose and that are synonyms of where.) Here, the variable allNonsharedLayers receives a list of layers in the current canvas, but only the layer objects whose class is not shared layer. Thus, the loop nested within this loop will deal only with non-shared layers.

repeat with currentLayer in allNonsharedLayers

Another repeat with loop, this time looping through all non-shared layers in the current canvas and placing each one in the variable currentLayer.

set allGraphics to graphics of currentLayer

graphics returns a list of Omnigraffle objects regardless of type - in this case, all objects on the layer the current loop is dealing with. You could also use shapes or lines if you need to be more specific.

repeat with currentGraphic in allGraphics

Here's the innermost repeat with loop, this time looping through all graphics in the current layer and placing each one in the variable currentGraphic.

slide currentGraphic by theOffset

The slide command moves a graphic to a new location by a horizontal/vertical offset from its previous location. We previously set theOffset at the top of this script to {-20, -20}, so each item will move 20 to the left, and 20 up.

-----

So where does one find documentation on OmniGraffle's AppleScript implementation?

OmniGraffle is a fairly niche product, and AppleScripting with OmniGraffle is a fairly niche activity, so as you can imagine, there's not a ton of information out there.

But in the AppleScript Editor, you can choose File > Open Dictionary, and then choose OmniGraffle Professional from the ensuing dialog. This will bring up a window showing everything OmniGraffle includes in its AppleScript implementation. You can also Google for examples like this to work from.

If you want to learn how to create objects and groups wholesale in AppleScript, you can select any object in OmniGraffle and from the menu choose Edit > Copy As > AppleScript. This will put on the clipboard AppleScript that will create this object from scratch. 

And of course it helps to be familiar with AppleScript, which is documented very well in the Mac Developer Library.



This updated version of the script prompts for the horizontal and vertical offsets, rather than requiring you to edit the script.

-- Copyright © 2013, Joseph Brick
-- All rights reserved. 
-- Redistribution, with or without modification, is permitted provided that the copyright notice is retained.

on run
set theOffset to getPointValueFromUser("Enter the horizontal and vertical distance (in pixels) that you want each object to move,separated by a comma.", "20,20", false)
if theOffset is null then
return
end if
tell application id "OGfl"
set allCanvases to canvases of front document
repeat with currentCanvas in allCanvases
set allNonsharedLayers to (layers of currentCanvas where class is layer)
repeat with currentLayer in allNonsharedLayers
set allGraphics to graphics of currentLayer
repeat with currentGraphic in allGraphics
slide currentGraphic by theOffset
end repeat
end repeat
end repeat
end tell
end run


on getPointValueFromUser(instructionsToDisplay, defaultValue, valuesMustBePositive)
try
set userReply to display dialog instructionsToDisplay default answer defaultValue with title "Enter a value"
on error errorText number errorNumber
if errorNumber is -128 then -- user canceled
return null
end if
end try
set userText to text returned of userReply
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to {","}
try
set pX to first text item of userText as number
set pY to second text item of userText as number
on error
display dialog "You must enter two numeric values separated by a comma." buttons {"OK"} with title "Error" with icon caution
set AppleScript's text item delimiters to astid
return null
end try
set AppleScript's text item delimiters to astid
if (pX > 0 and pY > 0) or valuesMustBePositive = false then
return {pX, pY}
else
display dialog "Both numbers must be greater than zero." buttons {"OK"} with title "Error" with icon caution
return null
end if
end getPointValueFromUser

1 comment:

  1. Thanks a lot. In other to make your post findable by people searching tutorials about omnigraffle AppleScripting, I suggest to change the title of this post emphasizing the real scope, i.e "While you may have no need for this particular functionality, the code below shows a general technique for accessing each canvas, each layer within a canvas, and each object on a layer."

    ReplyDelete