I’m writing a tool in Python + ScriptingBridge to automate some graph generation. So far, I’ve been able to decode enough of the Objective-C interfaces to accomplish the following:
object creation (from properties)
object selection
object modification (for shapes)
What’s eluded me so far is how to take a Line object and modify it’s “source” and “destination” attributes to point to some shapes.
In AppleScript, I use syntax like this …
set source of line1 to object2
set destination of line1 to object1
I’d expect the ScriptingBridge + Python equivalent to look something like this …
The solution relies on the recognition that a handle to the line object is fetched from the lines of the canvas and not from the graphics of the canvas.
Here are the steps to resolve:
line1 = activeCanvas.lines()[idx]
line1.setSource_(object2)
line1.setDestination_(object1)
In my case, the index of the lines array is retrieved via a pair of functions that I wrote named “findLineByName” and “fetchLineObject”.
Bottom line: my original code was “correct” in the sense that the methods used to manipulate the line were right. Where I messed up was in where I was fetching my Line object from.
Lines are part of the canvases()[idx].lines() array and not part of canvases()[idx].graphics() !
#!/usr/bin/python
#
# OmniGraffleGraphs.py
#
# Description: A python module useful for drawing directed graphs in OmniGraffle
# Author: Sharif Abdallah (sharif dot abdallah at me dot com)
#
from Foundation import *
from ScriptingBridge import SBApplication, SBElementArray
class OmniGraffle:
# Set some sensible defaults
_defaultObjectWidth = 75.0
_defaultObjectHeight = 75.0
_defaultObjectShadow = False
def __init__(self):
"""Instantiate an Omnigraffle object (start OG if it's not already running
"""
# Instantiate the OG Application
self._appInstance = SBApplication.applicationWithBundleIdentifier_("com.omnigroup.OmniGraffle7")
# Fetch the active Document form the OG object stack ---- WE DON'T NEED THIS ----
#self._activeDocument = self._appInstance.documents().objectAtLocation_("1")
# Fetch the active Canvas from the OG object stack
self._activeCanvas = self._appInstance.documents()[0].canvases()[0]#_activeDocument.canvases().objectAtLocation_("1")
# Prevent the diagram from performing an auto-layout until we tell it to
self._activeCanvas.layoutInfo().setAutomaticLayout_(False)
def CurrentDocument(self):
"""Fetch the current / active document
"""
return self._activeDocument
def CurrentCanvas(self):
"""Fetch the current / active canvas
"""
return self._activeCanvas
def layout(self):
"""Perform a layout action
"""
self._activeCanvas.layout()
def drawVertex(self, vName, vAbbrev):
"""Draw a Vertex object
"""
# Define the base properties for a Vertex
# Size defaults to 50x50
# Origin defaults to 50,50
self._props = {'name' : 'Circle',
'text' : vName,
'size' : [50,50],
'drawsShadow' : False,
'origin' : [50,50],
'userName' : vAbbrev}
# Instantiate a new OG shape
self._vertex = self._appInstance.classForScriptingClass_("shape").alloc().initWithProperties_(self._props)
# Add the OG shape (ie Vertex) to the active canvas' Graphics collection
self._activeCanvas.graphics().addObject_(self._vertex)
def findGraphicByName(self, objName):
"""Search for a graphic object by name
"""
self._result = False
# Fetch an array of OG Graphic objects that have a 'userName' value and then search
# for the matching graphic name
#
# NOTE: I chose to use object properties to help identify and maniuplate graphs
self._knownGraphicNames = SBElementArray(self._activeCanvas.graphics()).valueForKey_('userName')
for GraphicName in self._knownGraphicNames:
if GraphicName == objName:
self._result = True
break
return self._result
def findLineByName(self, objName):
"""Search for a line object in the canvas by name
"""
self._result = False
# Fetch an array of OG Line objects that have a 'userName' value and then search
# for the matching line name
#
# NOTE: I chose to use object properties to help identify and maniuplate graphs
self._knownLineNames = SBElementArray(self._activeCanvas.lines()).valueForKey_('userName')
for LineName in self._knownLineNames:
if LineName == objName:
self._result = True
break
return self._result
def fetchGraphicObject(self, objName):
"""Return an OG Graphic object from the current canvas' graphics collection
"""
# Fetch an array of all OG Graphic objects in the active canvas
self._allGraphics = self._activeCanvas.graphics()
# Search the returned array for the desired 'userName' value
return self._allGraphics.filteredArrayUsingPredicate_(NSPredicate.predicateWithFormat_('userName == %@', objName))[0]
def fetchLineObject(self, objName):
"""Return an OG Line object from the current canvas' Lines collection
"""
# Fetch an array of all OG Line objects in the active canvas
self._allLines = self._activeCanvas.lines()
# Search the returned array for the desired 'userName' value
return self._allLines.filteredArrayUsingPredicate_(NSPredicate.predicateWithFormat_('userName == %@', objName))[0]
def drawEdge(self, origin, destination):
"""Try to fetch the origin and destination object.
If both are found, create the Edge and add it to the diagram
"""
if self.findGraphicByName(origin) and self.findGraphicByName(destination):
self._originVertex = self.fetchGraphicObject(origin)
self._destinationVertex = self.fetchGraphicObject(destination)
# We have the source and destination Vertices so create the new edge
# so create the Edge
self._edgeName = str(origin+"->"+destination)
# Just as with Graphic objects, we use a Property list to help
# define some sane defaults
# Line Endpoints 0,0 and 100,100
# Line Thickness 2
# Line Head FilledArrow
self._props = {'lineType' : 'straight',
'pointList' : [[0,0],[100,100]],
'thickness' : 2,
'drawsShadow' : False,
'userName' : self._edgeName,
'headType' : "FilledArrow",
'userData' : {'src' : origin, 'dst' : destination}}
# Initialize a new OG Line object using the props
self._edge = self._appInstance.classForScriptingClass_("line").alloc().initWithProperties_(self._props)
# Add the OG Line object to the active canvas
self._activeCanvas.graphics().addObject_(self._edge)
# Finally, connect the new Edge to its source and destination Vertices
self._allLines = self._activeCanvas.lines()
self._thisEdge = self.fetchLineObject(self._edgeName)
self._thisEdge.setSource_(self._originVertex)
self._thisEdge.setDestination_(self._destinationVertex)
#### Some crappy test code ####
f = OmniGraffle()
c = f.CurrentCanvas()
f.drawVertex('A','A')
f.drawVertex('B','B')
f.drawVertex('C','C')
f.drawVertex('D','D')
f.drawEdge('A','B')
f.drawEdge('C','A')
f.drawEdge('C','B')
f.drawEdge('D','A')
### This stuff could probably be put in the initializer for the OmniGraffle class
c.layoutInfo().setType_(1330080819)
c.layoutInfo().setCircularLineLength_(50)
c.layoutInfo().setAutomaticLayout_(True)
c.layout()