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.
#
# 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
# 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
#
# __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
# 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
# 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
# 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
# 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)
"""
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,
)),
)