GraphicsBindings

Shows the use of a custom controller, a value transformer, and two custom bindings-enabled views. One view is a control that allows you to set the angle and offset of a shadow; the other view observes and displays a collection of graphic objects.

Originally from “Cocoa Bindings Examples and Hints”, converted to PyObjC by u.fiedler.

Sources

Circle.py

#
#  Circle.py
#  GraphicsBindings
#
#  Converted by u.fiedler on feb 2005
#  with great help from Bob Ippolito - Thank you Bob!
#
#  The original version was written in Objective-C by Malcolm Crawford
#  http://homepage.mac.com/mmalc/CocoaExamples/controllers.html

from Foundation import *
from AppKit import *
from objc import ivar
from math import sin, cos #, sqrt, atan2


class Circle(NSObject):
    """
    Graphic protocol to define methods all graphics objects must implement

    Circle class, adopts Graphic protocol
    Adds radius and color, and support for drawing a shadow
    """
    xLoc = ivar(u'xLoc', 'd')
    yLoc = ivar(u'yLoc', 'd')

    radius = ivar(u'radius', 'd')
    color  = ivar(u'color')
    shadowOffset = ivar(u'shadowOffset', 'd')
    shadowAngle  = ivar(u'shadowAngle', 'd') # in radians


    def keysForNonBoundsProperties(cls):
        return [u"xLoc", u"yLoc", u"shadowOffset", u"shadowAngle", u"color", u"radius"]
    keysForNonBoundsProperties = classmethod(keysForNonBoundsProperties)


    def init(self):
        self = super(Circle, self).init()
        if self == None: return None
        self.color = NSColor.redColor()
        self.xLoc = 15.0
        self.yLoc = 15.0
        self.radius = 15.0
        return self

    def description(self):
        return u"circle"

    def drawingBounds(self):
        drawingBounds = NSMakeRect(self.xLoc - self.radius-1, self.yLoc - self.radius-1,
                      self.radius*2+2, self.radius*2+2)
        if self.shadowOffset > 0.0:
            shadowXOffset = sin(self.shadowAngle)*self.shadowOffset
            shadowYOffset = cos(self.shadowAngle)*self.shadowOffset
            # allow for blur
            shadowBounds = NSMakeRect(self.xLoc - self.radius + shadowXOffset - (self.shadowOffset/2),
               self.yLoc - self.radius + shadowYOffset - (self.shadowOffset/2),
               (self.radius*2)+self.shadowOffset,
               (self.radius*2)+self.shadowOffset)
            drawingBounds = NSUnionRect(shadowBounds, drawingBounds)
        return drawingBounds

    def drawInView_(self, aView):
        # ignore aView here for simplicity...
        (xLoc, yLoc, radius, shadowOffset, shadowAngle) = (self.xLoc, self.yLoc, self.radius, self.shadowOffset, self.shadowAngle)

        circleBounds = NSMakeRect(xLoc-radius, yLoc-radius, radius*2, radius*2)

        # draw shadow if we'll see it
        shadow = NSShadow.alloc().init()
        if shadowOffset > 0.00001:
            shadowXOffset = sin(shadowAngle)*shadowOffset
            shadowYOffset = cos(shadowAngle)*shadowOffset
            shadow.setShadowOffset_(NSMakeSize(shadowXOffset,shadowYOffset))
            shadow.setShadowBlurRadius_(shadowOffset)
            shadow.set()

        # draw circle
        circle = NSBezierPath.bezierPathWithOvalInRect_(circleBounds)
        myColor = self.color
        if myColor == None: myColor = NSColor.redColor()
        myColor.set()
        circle.fill()

        shadow.setShadowColor_(None)
        shadow.set()

    def hitTest_isSelected_(self, point, isSelected):
        # ignore isSelected here for simplicity...
        # don't count shadow for selection
        hypotenuse2 = pow((self.xLoc - point.x), 2.0) + pow((self.yLoc - point.y), 2.0)
        return hypotenuse2 < (self.radius * self.radius)


    def initWithCoder_(self, coder):
        if not coder.allowsKeyedCoding():
            print "Circle only works with NSKeyedArchiver"
        self.xLoc = coder.decodeFloatForKey_(u"xLoc")
        self.yLoc = coder.decodeFloatForKey_(u"yLoc")
        self.radius = coder.decodeFloatForKey_(u"radius")
        self.shadowOffset = coder.decodeFloatForKey_(u"shadowOffset")
        self.shadowAngle = coder.decodeFloatForKey_(u"shadowAngle")

        colorData = coder.decodeObjectForKey_(u"color")
        self.color = NSUnarchiver.unarchiveObjectWithData_(colorData)
        return self

    def encodeWithCoder_(self, coder):
        if not coder.allowsKeyedCoding():
            print "Circle only works with NSKeyedArchiver"
        coder.encodeFloat_forKey_(self.xLoc, u"xLoc")
        coder.encodeFloat_forKey_(self.yLoc, u"yLoc")
        coder.encodeFloat_forKey_(self.radius, u"radius")
        coder.encodeFloat_forKey_(self.shadowOffset, u"shadowOffset")
        coder.encodeFloat_forKey_(self.shadowAngle, u"shadowAngle")

        colorData = NSArchiver.archivedDataWithRootObject_(self.color)
        coder.encodeObject_forKey_(colorData, u"color")


# if any of these properties changes, the bounds have changed
boundsChangingKeys = [u"xLoc", u"yLoc", u"shadowOffset", u"shadowAngle", u"radius"]
Circle.setKeys_triggerChangeNotificationsForDependentKey_(boundsChangingKeys, u"drawingBounds")

GraphicsArrayController.py

#
#  GraphicsArrayController.py
#  GraphicsBindings
#
#  Converted by u.fiedler on feb 2005
#  with great help from Bob Ippolito - Thank you Bob!
#
#  The original version was written in Objective-C by Malcolm Crawford
#  http://homepage.mac.com/mmalc/CocoaExamples/controllers.html

from sys import maxint
from Foundation import *
from AppKit import *
from random import random
from math import fabs

class GraphicsArrayController (NSArrayController):
    """Allow filtering by color, just for the fun of it"""

    filterColor = objc.IBOutlet()
    newCircle = objc.IBOutlet()
    shouldFilter = objc.ivar.BOOL()
    graphicsView = objc.IBOutlet()

    def arrangeObjects_(self, objects):
        "Filtering is not yet connected in IB!"
        # XXX: This doesn't work yet, so disable
        if self.shouldFilter:
            self.shouldFilter = False

        if not self.shouldFilter:
            return super(GraphicsArrayController, self).arrangeObjects_(objects)

        if self.filterColor is None:
            self.filterColor = NSColor.blackColor().colorUsingColorSpaceName_(NSCalibratedRGBColorSpace)

        filterHue = self.filterColor.hueComponent()
        filteredObjects = []
        for item in objects:
            hue = item.color.hueComponent()
            if ((fabs(hue - filterHue) < 0.05) or
                (fabs(hue - filterHue) > 0.95) or
                (item is self.newCircle)):
                filteredObjects.append(item)
                self.newCircle = None
        return super(GraphicsArrayController, self).arrangeObjects_(filteredObjects)

    def newObject(self):
        "Randomize attributes of new circles so we get a pretty display"
        self.newCircle = super(GraphicsArrayController, self).newObject()
        radius = 5.0 + 15.0 * random()
        self.newCircle.radius = radius

        height = self.graphicsView.bounds().size.height
        width  = self.graphicsView.bounds().size.width

        xOffset = 10.0 + (height - 20.0) * random()
        yOffset = 10.0 + (width - 20.0) * random()

        self.newCircle.xLoc = xOffset
        self.newCircle.yLoc = height - yOffset

        color = NSColor.colorWithCalibratedHue_saturation_brightness_alpha_(
            random(),
            (0.5 + random() / 2.0),
            (0.333 + random() / 3.0),
            1.0)

        self.newCircle.color = color
        return self.newCircle

GraphicsBindings.py

#
#  __main__.py
#  GraphicsBindings
#
#  Created by Fred Flintstone on 11.02.05.
#  Copyright (c) 2005 __MyCompanyName__. All rights reserved.
#

try:
    # scan for pth files that made it into the bundle
    import os, site
    site.addsitedir(os.path.dirname(os.path.realpath(__file__)))
except ImportError:
    pass

from PyObjCTools import AppHelper
from Foundation import NSProcessInfo

# import classes required to start application
#import GraphicsBindingsAppDelegate
import GraphicsBindingsDocument
import Circle
import GraphicsArrayController
import JoystickView
import GraphicsView

# start the event loop
AppHelper.runEventLoop(argv=[])

GraphicsBindingsDocument.py

#
#  GraphicsBindingsDocument.py
#  GraphicsBindings
#
#  Converted by u.fiedler on feb 2005
#  with great help from Bob Ippolito - Thank you Bob!
#
#  The original version was written in Objective-C by Malcolm Crawford
#  http://homepage.mac.com/mmalc/CocoaExamples/controllers.html

import objc
from PyObjCTools import AppHelper

from RadiansToDegreesTransformer import RadiansToDegreesTransformer
from Cocoa import *

class GraphicsBindingsDocument (NSDocument):
    graphicsView = objc.IBOutlet()
    shadowInspector = objc.IBOutlet()
    graphicsController = objc.IBOutlet()
    graphics = objc.ivar()

    def init(self):
        self = super(GraphicsBindingsDocument, self).init()
        if self is None:
            return None
        self.graphics = [] # NSMutableArray.array()
        self.bindings = []
        return self

    def windowNibName(self):
        return "GraphicsBindingsDocument"

    def makeBinding_fromObject_toObject_withKeyPath_options_(self, key, fromObject, toObject, withKeyPath, options):
        self.bindings.append((fromObject, key))
        fromObject.bind_toObject_withKeyPath_options_(key, toObject, withKeyPath, options)

    def windowControllerDidLoadNib_(self, controller):
        super(GraphicsBindingsDocument, self).windowControllerDidLoadNib_(controller)

        # we can't do these in IB at the moment, as
        # we don't have palette items for them

        # allow the shadow inspector (joystick) to handle multiple selections
        offsetOptions = { u"NSAllowsEditingMultipleValuesSelection" : True }
        angleOptions = {
            u"NSValueTransformerName" :  u"RadiansToDegreesTransformer",
            u"NSAllowsEditingMultipleValuesSelection" : True,
        }

        BINDINGS = [
            (u'graphics',  self.graphicsView, self.graphicsController, u'arrangedObjects', None),
            (u'selectionIndexes', self.graphicsView, self.graphicsController, u'selectionIndexes', None),
            (u'offset', self.shadowInspector, self.graphicsController, u'selection.shadowOffset', offsetOptions),
            (u'angle', self.shadowInspector, self.graphicsController, u'selection.shadowAngle', angleOptions),
        ]
        for binding in BINDINGS:
            self.makeBinding_fromObject_toObject_withKeyPath_options_(*binding)

        # "fake" what should be set in IB if we had a palette...
        self.shadowInspector.maxOffset = 15

    def close(self):
        while self.bindings:
            obj, binding = self.bindings.pop()
            obj.unbind_(binding)
        super(GraphicsBindingsDocument, self).close()

    def dataRepresentationOfType_(self, aType):
        return NSKeyedArchiver.archivedDataWithRootObject_(self.graphics)

    def loadDataRepresentation_ofType_(self, data, aType):
        self.graphics = NSKeyedUnarchiver.unarchiveObjectWithData_(data)
        return True

vt = RadiansToDegreesTransformer.alloc().init()
NSValueTransformer.setValueTransformer_forName_(vt, u"RadiansToDegreesTransformer")

GraphicsView.py

#
#  GraphicsView.py
#  GraphicsBindings
#
#  Converted by u.fiedler on feb 2005
#  with great help from Bob Ippolito - Thank you Bob!
#
#  The original version was written in Objective-C by Malcolm Crawford
#  http://homepage.mac.com/mmalc/CocoaExamples/controllers.html

PropertyObservationContext = 1091
GraphicsObservationContext = 1092
SelectionIndexesObservationContext = 1093


from Foundation import *
from AppKit import *
from objc import ivar
from Circle import Circle
from sets import Set

class GraphicsView(NSView):
    graphicsContainer = ivar(u'graphicsContainer')
    graphicsKeyPath   = ivar(u'graphicsKeyPath')

    selectionIndexesContainer = ivar(u'selectionIndexesContainer') # GraphicsArrayController
    selectionIndexesKeyPath   = ivar(u'selectionIndexesKeyPath')

    oldGraphics = ivar(u'oldGraphics')

    def exposedBindings(self):
        return [u"graphics", u"selectedObjects"]

    def initWithFrame_(self, frameRect):
        return super(GraphicsView, self).initWithFrame_(frameRect)

    def graphics(self):
        if not self.graphicsContainer: return None
        return self.graphicsContainer.valueForKeyPath_(self.graphicsKeyPath)

    def selectionIndexes(self):
        if not self.selectionIndexesContainer: return None
        return self.selectionIndexesContainer.valueForKeyPath_(self.selectionIndexesKeyPath)

    def startObservingGraphics_(self, graphics):
        if not graphics: return
        # Register to observe each of the new graphics, and
        # each of their observable properties -- we need old and new
        # values for drawingBounds to figure out what our dirty rect
        for newGraphic in graphics:
            # Register as observer for all the drawing-related properties
            newGraphic.addObserver_forKeyPath_options_context_(
                self, u"drawingBounds", (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld),
                PropertyObservationContext)
            keys = Circle.keysForNonBoundsProperties()
            for key in keys:
                newGraphic.addObserver_forKeyPath_options_context_(
                    self, key, 0, PropertyObservationContext)

    def stopObservingGraphics_(self, graphics):
        if graphics is None: return
        for graphic in graphics:
            for key in graphic.class__().keysForNonBoundsProperties():
                graphic.removeObserver_forKeyPath_(self, key)
            graphic.removeObserver_forKeyPath_(self, u"drawingBounds")

    def bind_toObject_withKeyPath_options_(self, bindingName, observableObject, observableKeyPath, options):
        if bindingName == u"graphics":
            self.graphicsContainer = observableObject
            self.graphicsKeyPath = observableKeyPath
            self.graphicsContainer.addObserver_forKeyPath_options_context_(
                    self, self.graphicsKeyPath, (NSKeyValueObservingOptionNew |
                    NSKeyValueObservingOptionOld), GraphicsObservationContext)
            self.startObservingGraphics_(self.graphics())

        elif bindingName == u"selectionIndexes":
            self.selectionIndexesContainer = observableObject
            self.selectionIndexesKeyPath = observableKeyPath
            self.selectionIndexesContainer.addObserver_forKeyPath_options_context_(
                self, self.selectionIndexesKeyPath, 0, SelectionIndexesObservationContext)
        self.setNeedsDisplay_(True)

    def unbind_(self, bindingName):
        if bindingName == u"graphics":
            self.graphicsContainer.removeObserver_forKeyPath_(self, self.graphicsKeyPath)
            self.graphicsContainer = None
            self.graphicsKeyPath = None
        if bindingName == u"selectionIndexes":
            self.selectionIndexesContainer.removeObserver_forKeyPath_(self, self.selectionIndexesKeyPath)
            self.seletionIndexesContainer = None
            self.selectionIndexesKeyPath = None
        self.setNeedsDisplay_(True)

    def observeValueForKeyPath_ofObject_change_context_(self, keyPath, object, change, context):
        if context == GraphicsObservationContext:
            # Should be able to use
            # NSArray *oldGraphics = [change objectForKey:NSKeyValueChangeOldKey];
            # etc. but the dictionary doesn't contain old and new arrays...??
            newGraphics = Set(object.valueForKeyPath_(self.graphicsKeyPath))
            onlyNew = newGraphics - Set(self.oldGraphics)
            self.startObservingGraphics_(onlyNew)

            if self.oldGraphics:
                removed = Set(self.oldGraphics) - newGraphics
                self.stopObservingGraphics_(removed)

            self.oldGraphics = newGraphics

            # could check drawingBounds of old and new, but...
            self.setNeedsDisplay_(True)
            return

        if context == PropertyObservationContext:
            updateRect = (0,)
            # Note: for Circle, drawingBounds is a dependent key of all the other
            # property keys except color, so we'll get this anyway...
            if keyPath == u"drawingBounds":
                newBounds = change.objectForKey_(NSKeyValueChangeNewKey)
                oldBounds = change.objectForKey_(NSKeyValueChangeOldKey)
                updateRect = NSUnionRect(newBounds, oldBounds)
            else:
                updateRect = object.drawingBounds()
            updateRect = NSMakeRect(updateRect.origin.x-1.0,
                                updateRect.origin.y-1.0,
                                updateRect.size.width+2.0,
                                updateRect.size.height+2.0)
            self.setNeedsDisplay_(True)
            return

        if context == SelectionIndexesObservationContext:
            self.setNeedsDisplay_(True)
            return

    def drawRect_(self, rect):
        myBounds = self.bounds()
        NSDrawLightBezel(myBounds, myBounds) # AppKit Function
        clipRect = NSBezierPath.bezierPathWithRect_(NSInsetRect(myBounds, 2.0, 2.0))
        clipRect.addClip()

        # Draw graphics
        graphicsArray = self.graphics()
        if graphicsArray:
            for graphic in graphicsArray:
                graphicDrawingBounds = graphic.drawingBounds()
                if NSIntersectsRect(rect, graphicDrawingBounds):
                    graphic.drawInView_(self)

        # Draw a red box around items in the current selection.
        # Selection should be handled by the graphic, but this is a
        # shortcut simply for display.

        currentSelectionIndexes = self.selectionIndexes() # ist das wir ein Array im Indezes?
        if currentSelectionIndexes != None:
            path = NSBezierPath.bezierPath()
            index = currentSelectionIndexes.firstIndex()
            while index != NSNotFound:
                graphicDrawingBounds = graphicsArray[index].drawingBounds()
                if NSIntersectsRect(rect, graphicDrawingBounds):
                    path.appendBezierPathWithRect_(graphicDrawingBounds)
                index = currentSelectionIndexes.indexGreaterThanIndex_(index)

            NSColor.redColor().set()
            path.setLineWidth_(1.5)
            path.stroke()


        # Fairly simple just to illustrate the point
    def mouseDown_(self, event):
        # find out if we hit anything
        p = self.convertPoint_fromView_(event.locationInWindow(), None)
        for aGraphic in self.graphics():
            if aGraphic.hitTest_isSelected_(p, False):
                break; # aGraphic soll spaeter einen Wert haben, falls es getroffene gibt!
        else:
            aGraphic = None

        # if no graphic hit, then if extending selection do nothing
        # else set selection to nil
        if aGraphic == None:
            if not event.modifierFlags() & NSShiftKeyMask:
                self.selectionIndexesContainer.setValue_forKeyPath_(None, self.selectionIndexesKeyPath)
            return

        # graphic hit
        # if not extending selection (Shift key down) then set
        # selection to this graphic
        # if extending selection, then:
        # - if graphic in selection remove it
        # - if not in selection add it
        graphicIndex = self.graphics().index(aGraphic)
        if not event.modifierFlags() & NSShiftKeyMask:
            selection = NSIndexSet.indexSetWithIndex_(graphicIndex)
        else:
            if  self.selectionIndexes().containsIndex_(graphicIndex):
                selection = self.selectionIndexes().mutableCopy()
                selection.removeIndex_(graphicIndex)
            else:
                selection = self.selectionIndexes().mutableCopy()
                selection.addIndex_(graphicIndex)
        self.selectionIndexesContainer.setValue_forKeyPath_(selection, self.selectionIndexesKeyPath)


GraphicsView.exposeBinding_(u"graphics")
GraphicsView.exposeBinding_(u"selectionIndexes")

JoystickView.py

#
#  JoystickView.py
#  GraphicsBindings
#
#  Converted by u.fiedler on feb 2005
#  with great help from Bob Ippolito - Thank you Bob!
#
#  The original version was written in Objective-C by Malcolm Crawford
#  http://homepage.mac.com/mmalc/CocoaExamples/controllers.html

from Foundation import *
from AppKit import *
from objc import ivar
from math import sin, cos, sqrt, atan2, pi

class JoystickView(NSView):
    AngleObservationContext = 2091
    OffsetObservationContext = 2092

    maxOffset = ivar(u"maxOffset", 'd')
    angle  = ivar(u"angle")#, 'd') # expect angle in degrees
    offset = ivar(u"offset")#, 'd')

    observedObjectForAngle    = ivar(u'observedObjectForAngle')
    observedKeyPathForAngle   = ivar(u'observedKeyPathForAngle')
    angleValueTransformerName = ivar(u'angleValueTransformerName')
    badSelectionForAngle      = ivar(u'badSelectionForAngle')
    multipleSelectionForAngle = ivar(u'multipleSelectionForAngle')
    allowsMultipleSelectionForAngle = ivar(u'allowsMultipleSelectionForAngle')

    observedObjectForOffset    = ivar(u'observedObjectForOffset')
    observedKeyPathForOffset   = ivar(u'observedKeyPathForOffset')
    offsetValueTransformerName = ivar(u'offsetValueTransformerName')
    badSelectionForOffset      = ivar(u'badSelectionForOffset')
    multipleSelectionForOffset = ivar(u'multipleSelectionForOffset')
    allowsMultipleSelectionForOffset = ivar(u'allowsMultipleSelectionForOffset')


    def valueClassForBinding_(cls, binding):
        # both require numbers
        return NSNumber
    valueClassForBinding_ = classmethod(valueClassForBinding_)


    def initWithFrame_(self, frameRect):
        self = super(JoystickView, self).initWithFrame_(frameRect)
        if self is None: return None
        self.maxOffset = 15.0
        self.offset = 0.0
        self.angle = 28.0
        self.multipleSelectionForAngle = False
        self.multipleSelectionForOffset = False
        return self


    def bind_toObject_withKeyPath_options_(
        self, bindingName, observableController, keyPath, options):

        if bindingName == u"angle":
            # observe the controller for changes -- note, pass binding identifier
            # as the context, so we get that back in observeValueForKeyPath:...
            # that way we can determine what needs to be updated.
            observableController.addObserver_forKeyPath_options_context_(
                self, keyPath, 0, self.AngleObservationContext)
            # register what controller and what keypath are
            # associated with this binding
            self.observedObjectForAngle = observableController
            self.observedKeyPathForAngle = keyPath
            # options
            self.angleValueTransformerName = options[u"NSValueTransformerName"]
            self.allowsMultipleSelectionForAngle = False
            if options[u"NSAllowsEditingMultipleValuesSelection"]:
                self.allowsMultipleSelectionForAngle = True

        if bindingName == u"offset":
            observableController.addObserver_forKeyPath_options_context_(
                self, keyPath, 0, self.OffsetObservationContext)
            self.observedObjectForOffset = observableController
            self.observedKeyPathForOffset = keyPath
            self.allowsMultipleSelectionForOffset = False
            if options[u"NSAllowsEditingMultipleValuesSelection"]:
                self.allowsMultipleSelectionForOffset = True


    def unbind_(self, bindingName):
        if bindingName == u"angle":
            if self.observedObjectForAngle is None:
                return
            self.observedObjectForAngle.removeObserver_forKeyPath_(
                self, self.observedKeyPathForAngle)
            self.observedObjectForAngle = None
            self.observedKeyPathForAngle = None
            self.angleValueTransformerName = None
        elif bindingName == u"offset":
            if self.observedObjectForOffset is None:
                return None
            self.observedObjectForOffset.removeObserver_forKeyPath_(
                self, self.observedKeyPathForOffset)
            self.observedObjectForOffset = None
            self.observedKeyPathForOffset = None


    def observeValueForKeyPath_ofObject_change_context_(self, keyPath, object, change, context):
        # we passed the binding as the context when we added ourselves
        # as an observer -- use that to decide what to update...
        # should ask the dictionary for the value...
        if context == self.AngleObservationContext:
            # angle changed
            # if we got a NSNoSelectionMarker or NSNotApplicableMarker, or
            # if we got a NSMultipleValuesMarker and we don't allow multiple selections
            # then note we have a bad angle
            newAngle = self.observedObjectForAngle.valueForKeyPath_(self.observedKeyPathForAngle)
            if (newAngle == NSNoSelectionMarker or newAngle == NSNotApplicableMarker
                or (newAngle == NSMultipleValuesMarker and not self.allowsMultipleSelectionForAngle)):
                self.badSelectionForAngle = True
            else:
                # note we have a good selection
                # if we got a NSMultipleValuesMarker, note it but don't update value
                self.badSelectionForAngle = False
                if newAngle == NSMultipleValuesMarker:
                    self.multipleSelectionForAngle = True
                else:
                    self.multipleSelectionForAngle = False
                    if self.angleValueTransformerName is not None:
                        vt = NSValueTransformer.valueTransformerForName_(self.angleValueTransformerName)
                        newAngle = vt.transformedValue_(newAngle)
                    self.setValue_forKey_(newAngle, u"angle")

        if context == self.OffsetObservationContext:
            # offset changed
            # if we got a NSNoSelectionMarker or NSNotApplicableMarker, or
            # if we got a NSMultipleValuesMarker and we don't allow multiple selections
            # then note we have a bad selection
            newOffset = self.observedObjectForOffset.valueForKeyPath_(self.observedKeyPathForOffset)
            if (newOffset == NSNoSelectionMarker or newOffset == NSNotApplicableMarker
                or (newOffset == NSMultipleValuesMarker and not self.allowsMultipleSelectionForOffset)):
                self.badSelectionForOffset = True
            else:
                # note we have a good selection
                # if we got a NSMultipleValuesMarker, note it but don't update value
                self.badSelectionForOffset = False
                if newOffset == NSMultipleValuesMarker:
                    self.multipleSelectionForOffset = True
                else:
                    self.setValue_forKey_(newOffset, u"offset")
                    self.multipleSelectionForOffset = False
        self.setNeedsDisplay_(True)


    def updateForMouseEvent_(self, event):
        """
        update based on event location and selection state
        behavior based on modifier key
        """
        if self.badSelectionForAngle or self.badSelectionForOffset:
            return # don't do anything

        # find out where the event is, offset from the view center
        p = self.convertPoint_fromView_(event.locationInWindow(), None)
        myBounds = self.bounds()
        xOffset = (p.x - (myBounds.size.width/2))
        yOffset = (p.y - (myBounds.size.height/2))

        newOffset = sqrt(xOffset*xOffset + yOffset*yOffset)
        if newOffset > self.maxOffset:
            newOffset = self.maxOffset
        elif newOffset < -self.maxOffset:
            newOffset = -self.maxOffset

        # if we have a multiple selection for offset and Shift key is pressed
        # then don't update the offset
        # this allows offsets to remain constant, but change angle
        if not ( self.multipleSelectionForOffset and (event.modifierFlags() & NSShiftKeyMask)):
            self.offset = newOffset
            # update observed controller if set
            if self.observedObjectForOffset is not None:
                self.observedObjectForOffset.setValue_forKeyPath_(newOffset, self.observedKeyPathForOffset)

        # if we have a multiple selection for angle and Shift key is pressed
        # then don't update the angle
        # this allows angles to remain constant, but change offset
        if not ( self.multipleSelectionForAngle and (event.modifierFlags() & NSShiftKeyMask)):
            newAngle = atan2(xOffset, yOffset)
            newAngleDegrees = newAngle / (pi/180.0)
            if newAngleDegrees < 0:
                newAngleDegrees += 360
            self.angle = newAngleDegrees
            # update observed controller if set
            if self.observedObjectForAngle is not None:
                if self.observedObjectForAngle is not None:
                    vt = NSValueTransformer.valueTransformerForName_(self.angleValueTransformerName)
                    newControllerAngle = vt.reverseTransformedValue_(newAngleDegrees)
                else:
                    newControllerAngle = angle
            self.observedObjectForAngle.setValue_forKeyPath_(newControllerAngle, self.observedKeyPathForAngle)
        self.setNeedsDisplay_(True)


    def mouseDown_(self, event):
        self.mouseDown = True
        self.updateForMouseEvent_(event)


    def mouseDragged_(self, event):
        self.updateForMouseEvent_(event)


    def mouseUp_(self, event):
        self.mouseDown = False
        self.updateForMouseEvent_(event)


    def acceptsFirstMouse_(self, event):
        return True

    def acceptsFirstResponder(self):
        return True


    def drawRect_(self, rect):
        """
        Basic goals here:
        If either the angle or the offset has a "bad selection":
        then draw a gray rectangle, and that's it.
        Note: bad selection is set if there's a multiple selection
        but the "allows multiple selection" binding is NO.

        If there's a multiple selection for either angle or offset:
        then what you draw depends on what's multiple.

        - First, draw a white background to show all's OK.

        - If both are multiple, then draw a special symbol.

        - If offset is multiple, draw a line from the center of the view
        - to the edge at the shared angle.

        - If angle is multiple, draw a circle of radius the shared offset
        - centered in the view.

        If neither is multiple, draw a cross at the center of the view
        and a cross at distance 'offset' from the center at angle 'angle'
        """
        myBounds = self.bounds()
        if self.badSelectionForAngle or self.badSelectionForOffset:
            # "disable" and exit
            NSDrawDarkBezel(myBounds,myBounds);
            return;
        # user can do something, so draw white background and
        # clip in anticipation of future drawing
        NSDrawLightBezel(myBounds,myBounds)
        clipRect = NSBezierPath.bezierPathWithRect_( NSInsetRect(myBounds,2.0,2.0) )
        clipRect.addClip()

        if self.multipleSelectionForAngle or self.multipleSelectionForOffset:
            originOffsetX = myBounds.size.width/2 + 0.5
            originOffsetY = myBounds.size.height/2 + 0.5
            if self.multipleSelectionForAngle and self.multipleSelectionForOffset:
                # draw a diagonal line and circle to denote
                # multiple selections for angle and offset
                NSBezierPath.strokeLineFromPoint_toPoint_(NSMakePoint(0,0), NSMakePoint(myBounds.size.width,myBounds.size.height))
                circleBounds = NSMakeRect(originOffsetX-5, originOffsetY-5, 10, 10)
                path = NSBezierPath.bezierPathWithOvalInRect_(circleBounds)
                path.stroke()
                return
            if self.multipleSelectionForOffset:
                # draw a line from center to a point outside
                # bounds in the direction specified by angle
                angleRadians = self.angle * (pi/180.0)
                x = sin(angleRadians) * myBounds.size.width + originOffsetX
                y = cos(angleRadians) * myBounds.size.height + originOffsetX
                NSBezierPath.strokeLineFromPoint_toPoint_(NSMakePoint(originOffsetX, originOffsetY),
                    NSMakePoint(x, y))
                return
            if self.multipleSelectionForAngle:
                # draw a circle with radius the shared offset
                # dont' draw radius < 1.0, else invisible
                drawRadius = self.offset
                if drawRadius < 1.0: drawRadius = 1.0
                offsetBounds = NSMakeRect(originOffsetX-drawRadius,
                         originOffsetY-drawRadius,
                         drawRadius*2, drawRadius*2)
                path = NSBezierPath.bezierPathWithOvalInRect_(offsetBounds)
                path.stroke()
                return
            # shouldn't get here
            return
        trans = NSAffineTransform.transform()
        trans.translateXBy_yBy_( myBounds.size.width/2 + 0.5, myBounds.size.height/2 + 0.5)
        trans.concat()
        path = NSBezierPath.bezierPath()

        # draw + where shadow extends
        angleRadians = self.angle * (pi/180.0)
        xOffset = sin(angleRadians) * self.offset
        yOffset = cos(angleRadians) * self.offset

        path.moveToPoint_( NSMakePoint(xOffset,yOffset-5) )
        path.lineToPoint_( NSMakePoint(xOffset,yOffset+5) )
        path.moveToPoint_( NSMakePoint(xOffset-5,yOffset) )
        path.lineToPoint_( NSMakePoint(xOffset+5,yOffset) )

        NSColor.lightGrayColor().set()
        path.setLineWidth_(1.5)
        path.stroke()

        # draw + in center of view
        path = NSBezierPath.bezierPath()

        path.moveToPoint_( NSMakePoint(0,-5) )
        path.lineToPoint_( NSMakePoint(0,+5) )
        path.moveToPoint_( NSMakePoint(-5,0) )
        path.lineToPoint_( NSMakePoint(+5,0) )

        NSColor.blackColor().set()
        path.setLineWidth_(1.0)
        path.stroke()


    def setNilValueForKey_(self, key):
        "We may get passed nil for angle or offset. Just use 0"
        self.setValue_forKey_(0, key)


    def validateMaxOffset_error(self,ioValue):
        if ioValue == None:
            # trap this in setNilValueForKey
            # alternative might be to create new NSNumber with value 0 here
            return True
        if ioValue <= 0.0:
            errorString = NSLocalizedStringFromTable(u"Maximum Offset must be greater than zero",
                   u"Joystick",
                   u"validation: zero maxOffset error")
            userInfoDict = { NSLocalizedDescriptionKey : errorString }
            error = NSError.alloc().initWithDomain_code_userInfo_(u"JoystickView", 1, userInfoDict)
            outError = error
            return False
        return True


JoystickView.exposeBinding_(u"offset")
JoystickView.exposeBinding_(u"angle")

RadiansToDegreesTransformer.py

#
#  RadiansToDegreesTransformer.py
#  GraphicsBindings
#
#  Converted by u.fiedler on feb 2005
#  with great help from Bob Ippolito - Thank you Bob!
#
#  The original version was written in Objective-C by Malcolm Crawford
#  http://homepage.mac.com/mmalc/CocoaExamples/controllers.html

from Foundation import *

class RadiansToDegreesTransformer(NSValueTransformer):

    def transformedValueClass(cls):
        return NSNumber
    transformedValueClass = classmethod(transformedValueClass)

    def allowsReverseTransformation(cls):
        return True
    allowsReverseTransformation = classmethod(allowsReverseTransformation)

    def transformedValue_(self, radians):
        return radians / (3.1415927/180.0)

    def reverseTransformedValue_(self, degrees):
        if type(degrees) == type(1.2):
            # when using jostickview we get a value of type float()
            return degrees * (3.1415927/180.0)
        else:
            # we get a decimalNumber when entering a value in the textfield
            return degrees.doubleValue() * (3.1415927/180.0)

setup.py

"""
Script for building the example:

Usage:
    python setup.py py2app
"""
from distutils.core import setup
import py2app

plist = dict(
    CFBundleDocumentTypes = [
        dict(
            CFBundleTypeExtensions=[u'GraphicsBindings', u'*'],
            CFBundleTypeName=u'GraphicsBindings File',
            CFBundleTypeRole=u'Editor',
            NSDocumentClass=u'GraphicsBindingsDocument',
        ),
    ],
)

setup(
    name="GraphicsBinding",
    app=["GraphicsBindings.py"],
    data_files=["English.lproj"],
    options=dict(py2app=dict(
        plist=plist,
    )),
)

Resources

Table Of Contents

Resources

Support development