GraphicsBindings¶
A PyObjC Example without documentation
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 math import cos, sin
import objc
from Cocoa import (
NSArchiver,
NSUnarchiver,
NSBezierPath,
NSColor,
NSMakeRect,
NSMakeSize,
NSObject,
NSShadow,
NSUnionRect,
)
from objc import super # noqa: A004
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
@classmethod
def keysForNonBoundsProperties(cls):
return ["xLoc", "yLoc", "shadowOffset", "shadowAngle", "color", "radius"]
def init(self):
self = super().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),
(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 is 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_("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, "color")
# if any of these properties changes, the bounds have changed
boundsChangingKeys = ["xLoc", "yLoc", "shadowOffset", "shadowAngle", "radius"]
Circle.setKeys_triggerChangeNotificationsForDependentKey_(
boundsChangingKeys, "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 math import fabs
from random import random
import objc
from Cocoa import NSArrayController, NSCalibratedRGBColorSpace, NSColor
from objc import super # noqa: A004
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!"""
if self.shouldFilter:
self.shouldFilter = False
if not self.shouldFilter:
return super().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().arrangeObjects_(filteredObjects)
def newObject(self):
"""Randomize attributes of new circles so we get a pretty display"""
self.newCircle = super().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.
#
import Circle # noqa: F401
import GraphicsArrayController # noqa: F401
import GraphicsBindingsDocument # noqa: F401
import GraphicsView # noqa: F401
import JoystickView # noqa: F401
# start the event loop
import objc
from PyObjCTools import AppHelper
objc.setVerbose(1)
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 Cocoa import NSDocument, NSKeyedArchiver, NSKeyedUnarchiver, NSValueTransformer
from objc import super # noqa: A004
from RadiansToDegreesTransformer import RadiansToDegreesTransformer
class GraphicsBindingsDocument(NSDocument):
graphicsView = objc.IBOutlet()
shadowInspector = objc.IBOutlet()
graphicsController = objc.IBOutlet()
graphics = objc.ivar()
def init(self):
self = super().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().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:
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().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, "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
import objc
from Circle import Circle
from Cocoa import (
NSBezierPath,
NSColor,
NSDrawLightBezel,
NSIndexSet,
NSInsetRect,
NSIntersectsRect,
NSKeyValueChangeNewKey,
NSKeyValueChangeOldKey,
NSKeyValueObservingOptionNew,
NSKeyValueObservingOptionOld,
NSMakeRect,
NSNotFound,
NSShiftKeyMask,
NSUnionRect,
NSView,
)
from objc import super # noqa: A004
PropertyObservationContext = 1091
GraphicsObservationContext = 1092
SelectionIndexesObservationContext = 1093
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().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,
"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, "drawingBounds")
def bind_toObject_withKeyPath_options_(
self, bindingName, observableObject, observableKeyPath, options
):
if bindingName == "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 == "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 == "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
self.setNeedsDisplay_(True)
def observeValueForKeyPath_ofObject_change_context_(
self, keyPath, an_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(an_object.valueForKeyPath_(self.graphicsKeyPath))
onlyNew = newGraphics - set(self.oldGraphics or [])
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 == "drawingBounds":
newBounds = change.objectForKey_(NSKeyValueChangeNewKey)
oldBounds = change.objectForKey_(NSKeyValueChangeOldKey)
updateRect = NSUnionRect(newBounds, oldBounds)
else:
updateRect = an_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()
if currentSelectionIndexes is not 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
else:
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
)
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_("graphics")
GraphicsView.exposeBinding_("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 math import atan2, cos, pi, sin, sqrt
import objc
from Cocoa import (
NSError,
NSLocalizedDescriptionKey,
NSAffineTransform,
NSBezierPath,
NSColor,
NSDrawDarkBezel,
NSDrawLightBezel,
NSInsetRect,
NSLocalizedStringFromTable,
NSMakePoint,
NSMakeRect,
NSMultipleValuesMarker,
NSNoSelectionMarker,
NSNotApplicableMarker,
NSNumber,
NSShiftKeyMask,
NSValueTransformer,
NSView,
)
from objc import super # noqa: A004
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")
@classmethod
def valueClassForBinding_(cls, binding):
# both require numbers
return NSNumber
def initWithFrame_(self, frameRect):
self = super().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.
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["NSValueTransformerName"]
self.allowsMultipleSelectionForAngle = False
if options["NSAllowsEditingMultipleValuesSelection"]:
self.allowsMultipleSelectionForAngle = True
if bindingName == "offset":
observableController.addObserver_forKeyPath_options_context_(
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:
return
self.observedObjectForAngle.removeObserver_forKeyPath_(
self, self.observedKeyPathForAngle
)
self.observedObjectForAngle = None
self.observedKeyPathForAngle = None
self.angleValueTransformerName = None
elif bindingName == "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, an_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, "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, "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 = newAngle
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
# don't 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, error):
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",
"Joystick",
"validation: zero maxOffset error",
)
userInfoDict = {NSLocalizedDescriptionKey: errorString}
error = NSError.alloc().initWithDomain_code_userInfo_(
"JoystickView", 1, userInfoDict
)
return False, error
return True, None
JoystickView.exposeBinding_("offset")
JoystickView.exposeBinding_("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 NSNumber, NSValueTransformer
class RadiansToDegreesTransformer(NSValueTransformer):
@classmethod
def transformedValueClass(cls):
return NSNumber
@classmethod
def allowsReverseTransformation(cls):
return True
def transformedValue_(self, radians):
return radians / (3.141_592_7 / 180.0)
def reverseTransformedValue_(self, degrees):
if isinstance(degrees, float):
# when using jostickview we get a value of type float()
return degrees * (3.141_592_7 / 180.0)
else:
# we get a decimalNumber when entering a value in the textfield
return degrees.doubleValue() * (3.141_592_7 / 180.0)
setup.py¶
"""
Script for building the example:
Usage:
python3 setup.py py2app
"""
from setuptools import setup
plist = {
"CFBundleDocumentTypes": [
{
"CFBundleTypeExtensions": ["GraphicsBindings", "*"],
"CFBundleTypeName": "GraphicsBindings File",
"CFBundleTypeRole": "Editor",
"NSDocumentClass": "GraphicsBindingsDocument",
}
]
}
setup(
name="GraphicsBinding",
app=["GraphicsBindings.py"],
data_files=["English.lproj"],
options={"py2app": {"plist": plist}},
setup_requires=["py2app", "pyobjc-framework-Cocoa"],
)