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

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


    @classmethod
    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.
            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, 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 = 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, 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")
