(SOLVED) Using ScriptingBridge + Python to manipulate Line objects

Hi all, my first post here.

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 …


I can’t find either the “source” or “destination” properties via the Objective-C translation layer (aka ScriptingBridge).

Has anyone ever run into this issue before and managed to find a solution?


Sharif Abdallah

Solved my own issue.

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:

  1. line1 = activeCanvas.lines()[idx]
  2. line1.setSource_(object2)
  3. 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() !

I hope this helps someone out there.

Any chance you can share your python code?

Sorry it took so long. Here’re the goods …

# 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

    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

    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

    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
        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
        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

            # Finally, connect the new Edge to its source and destination Vertices
            self._allLines = self._activeCanvas.lines()
            self._thisEdge = self.fetchLineObject(self._edgeName)

####  Some crappy test code ####
f = OmniGraffle()
c = f.CurrentCanvas()

### This stuff could probably be put in the initializer for the OmniGraffle class
1 Like