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.


#  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

from Cocoa import NSObject, NSColor, NSMakeRect, NSUnionRect
from Cocoa import NSShadow, NSMakeSize, NSBezierPath
from objc import super
import objc
from math import sin, cos

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 = objc.ivar('xLoc', objc._C_DBL)
    yLoc = objc.ivar('yLoc', objc._C_DBL)

    radius = objc.ivar('radius', objc._C_DBL)
    color  = objc.ivar('color')
    shadowOffset = objc.ivar('shadowOffset', objc._C_DBL)
    shadowAngle  = objc.ivar('shadowAngle', objc._C_DBL) # in radians

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

    def init(self):
        self = super(Circle, self).init()
        if self is 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 "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),
            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

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


    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_("xLoc")
        self.yLoc = coder.decodeFloatForKey_("yLoc")
        self.radius = coder.decodeFloatForKey_("radius")
        self.shadowOffset = coder.decodeFloatForKey_("shadowOffset")
        self.shadowAngle = coder.decodeFloatForKey_("shadowAngle")

        colorData = coder.decodeObjectForKey_("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, "xLoc")
        coder.encodeFloat_forKey_(self.yLoc, "yLoc")
        coder.encodeFloat_forKey_(self.radius, "radius")
        coder.encodeFloat_forKey_(self.shadowOffset, "shadowOffset")
        coder.encodeFloat_forKey_(self.shadowAngle, "shadowAngle")

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

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

#  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

import objc
from objc import super
from Cocoa import NSArrayController, NSColor, NSCalibratedRGBColorSpace
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)):
                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_(
            (0.5 + random() / 2.0),
            (0.333 + random() / 3.0),

        self.newCircle.color = color
        return self.newCircle

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

from PyObjCTools import AppHelper
from Foundation import NSProcessInfo

import GraphicsBindingsDocument
import Circle
import GraphicsArrayController
import JoystickView
import GraphicsView

# start the event loop
import objc

#  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

import objc
from objc import super
from PyObjCTools import AppHelper

from RadiansToDegreesTransformer import RadiansToDegreesTransformer
from Cocoa import NSDocument, NSValueTransformer, NSKeyedArchiver, NSKeyedUnarchiver

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 = [] # 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 = { "NSAllowsEditingMultipleValuesSelection" : True }
        angleOptions = {
            "NSValueTransformerName" :  "RadiansToDegreesTransformer",
            "NSAllowsEditingMultipleValuesSelection" : True,

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

        # "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()
        super(GraphicsBindingsDocument, self).close()

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

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

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

#  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

PropertyObservationContext = 1091
GraphicsObservationContext = 1092
SelectionIndexesObservationContext = 1093

import objc
from objc import super
from Cocoa import NSView, NSKeyValueObservingOptionNew, NSKeyValueObservingOptionOld
from Cocoa import NSKeyValueChangeNewKey, NSKeyValueChangeOldKey, NSUnionRect, NSMakeRect
from Cocoa import NSDrawLightBezel, NSBezierPath, NSNotFound, NSIntersectsRect, NSColor
from Cocoa import NSShiftKeyMask, NSIndexSet, NSInsetRect
from Circle import Circle

class GraphicsView (NSView):
    graphicsContainer = objc.ivar('graphicsContainer')
    graphicsKeyPath   = objc.ivar('graphicsKeyPath')

    selectionIndexesContainer = objc.ivar('selectionIndexesContainer') # GraphicsArrayController
    selectionIndexesKeyPath   = objc.ivar('selectionIndexesKeyPath')

    oldGraphics = objc.ivar('oldGraphics')

    def exposedBindings(self):
        return ["graphics", "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
                self, "drawingBounds", (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld),
            keys = Circle.keysForNonBoundsProperties()
            for key in keys:
                    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, "drawingBounds")

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

        elif bindingName == "selectionIndexes":
            self.selectionIndexesContainer = observableObject
            self.selectionIndexesKeyPath = observableKeyPath
                self, self.selectionIndexesKeyPath, 0, SelectionIndexesObservationContext)

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

    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 or [])

            if self.oldGraphics:
                removed = set(self.oldGraphics) - newGraphics

            self.oldGraphics = newGraphics

            # could check drawingBounds of old and new, but...

        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 == "drawingBounds":
                newBounds = change.objectForKey_(NSKeyValueChangeNewKey)
                oldBounds = change.objectForKey_(NSKeyValueChangeOldKey)
                updateRect = NSUnionRect(newBounds, oldBounds)
                updateRect = object.drawingBounds()
            updateRect = NSMakeRect(updateRect.origin.x-1.0,

        if context == SelectionIndexesObservationContext:

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

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

        # 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):
                index = currentSelectionIndexes.indexGreaterThanIndex_(index)


        # 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
            if aGraphic.hitTest_isSelected_(p, False):
                break; # aGraphic soll spaeter einen Wert haben, falls es getroffene gibt!
            aGraphic = None

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

        # 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 =
        if not event.modifierFlags() & NSShiftKeyMask:
            selection = NSIndexSet.indexSetWithIndex_(graphicIndex)
            if  self.selectionIndexes().containsIndex_(graphicIndex):
                selection = self.selectionIndexes().mutableCopy()
                selection = self.selectionIndexes().mutableCopy()

        self.selectionIndexesContainer.setValue_forKeyPath_(selection, self.selectionIndexesKeyPath)


#  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

import objc
from objc import super
from Cocoa import NSView, NSNumber, NSMakePoint, NSValueTransformer, NSMakeRect
from Cocoa import NSNoSelectionMarker, NSNotApplicableMarker, NSMultipleValuesMarker
from Cocoa import NSShiftKeyMask, NSDrawDarkBezel, NSDrawLightBezel, NSBezierPath
from Cocoa import NSAffineTransform, NSLocalizedStringFromTable, NSInsetRect, NSColor
from math import sin, cos, sqrt, atan2, pi

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

    maxOffset = objc.ivar("maxOffset", objc._C_DBL)
    angle  = objc.ivar("angle")#, 'd') # expect angle in degrees
    offset = objc.ivar("offset")#, 'd')

    observedObjectForAngle    = objc.ivar('observedObjectForAngle')
    observedKeyPathForAngle   = objc.ivar('observedKeyPathForAngle')
    angleValueTransformerName = objc.ivar('angleValueTransformerName')
    badSelectionForAngle      = objc.ivar('badSelectionForAngle')
    multipleSelectionForAngle = objc.ivar('multipleSelectionForAngle')
    allowsMultipleSelectionForAngle = objc.ivar('allowsMultipleSelectionForAngle')

    observedObjectForOffset    = objc.ivar('observedObjectForOffset')
    observedKeyPathForOffset   = objc.ivar('observedKeyPathForOffset')
    offsetValueTransformerName = objc.ivar('offsetValueTransformerName')
    badSelectionForOffset      = objc.ivar('badSelectionForOffset')
    multipleSelectionForOffset = objc.ivar('multipleSelectionForOffset')
    allowsMultipleSelectionForOffset = objc.ivar('allowsMultipleSelectionForOffset')

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

    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 == "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.
                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["NSValueTransformerName"]
            self.allowsMultipleSelectionForAngle = False
            if options["NSAllowsEditingMultipleValuesSelection"]:
                self.allowsMultipleSelectionForAngle = True

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

    def unbind_(self, bindingName):
        if bindingName == "angle":
            if self.observedObjectForAngle is None:
                self, self.observedKeyPathForAngle)
            self.observedObjectForAngle = None
            self.observedKeyPathForAngle = None
            self.angleValueTransformerName = None
        elif bindingName == "offset":
            if self.observedObjectForOffset is None:
                return None
                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

                # 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
                    self.multipleSelectionForAngle = False
                    if self.angleValueTransformerName is not None:
                        vt = NSValueTransformer.valueTransformerForName_(self.angleValueTransformerName)
                        newAngle = vt.transformedValue_(newAngle)
                    self.setValue_forKey_(newAngle, "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
                # 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
                    self.setValue_forKey_(newOffset, "offset")
                    self.multipleSelectionForOffset = False

    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)
                    newControllerAngle = angle
            self.observedObjectForAngle.setValue_forKeyPath_(newControllerAngle, self.observedKeyPathForAngle)

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

    def mouseDragged_(self, event):

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

    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
        # user can do something, so draw white background and
        # clip in anticipation of future drawing
        clipRect = NSBezierPath.bezierPathWithRect_(NSInsetRect(myBounds,2.0,2.0))

        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)
            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))
            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,
                         drawRadius*2, drawRadius*2)
                path = NSBezierPath.bezierPathWithOvalInRect_(offsetBounds)
            # shouldn't get here
        trans = NSAffineTransform.transform()
        trans.translateXBy_yBy_( myBounds.size.width/2 + 0.5, myBounds.size.height/2 + 0.5)
        path = NSBezierPath.bezierPath()

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



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



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


#  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

from Foundation import NSNumber, NSValueTransformer

class RadiansToDegreesTransformer(NSValueTransformer):

    def transformedValueClass(cls):
        return NSNumber

    def allowsReverseTransformation(cls):
        return True

    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)
            # we get a decimalNumber when entering a value in the textfield
            return degrees.doubleValue() * (3.1415927/180.0)

Script for building the example:

    python3 py2app
from setuptools import setup

plist = dict(
    CFBundleDocumentTypes = [
            CFBundleTypeExtensions=["GraphicsBindings", "*"],
            CFBundleTypeName="GraphicsBindings File",