aboutsummaryrefslogtreecommitdiffstats
path: root/lvc/widgets/osx/customcontrol.py
diff options
context:
space:
mode:
Diffstat (limited to 'lvc/widgets/osx/customcontrol.py')
-rw-r--r--lvc/widgets/osx/customcontrol.py436
1 files changed, 436 insertions, 0 deletions
diff --git a/lvc/widgets/osx/customcontrol.py b/lvc/widgets/osx/customcontrol.py
new file mode 100644
index 0000000..4a32b8e
--- /dev/null
+++ b/lvc/widgets/osx/customcontrol.py
@@ -0,0 +1,436 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".customcontrol -- CustomControl handlers. """
+
+import collections
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+
+from lvc.widgets import widgetconst
+import wrappermap
+from .base import Widget
+import drawing
+from .layoutmanager import LayoutManager
+
+class DrawableButtonCell(NSButtonCell):
+ def startTrackingAt_inView_(self, point, view):
+ view.setState_(NSOnState)
+ return YES
+
+ def continueTracking_at_inView_(self, lastPoint, at, view):
+ view.setState_(NSOnState)
+ return YES
+
+ def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseIsUp):
+ if not mouseIsUp:
+ view.mouse_inside = False
+ view.setState_(NSOffState)
+
+class DrawableButton(NSButton):
+ def init(self):
+ self = super(DrawableButton, self).init()
+ self.layout_manager = LayoutManager()
+ self.tracking_area = None
+ self.mouse_inside = False
+ self.custom_cursor = None
+ return self
+
+ def resetCursorRects(self):
+ if self.custom_cursor is not None:
+ self.addCursorRect_cursor_(self.visibleRect(), self.custom_cursor)
+ self.custom_cursor.setOnMouseEntered_(YES)
+
+ def updateTrackingAreas(self):
+ # remove existing tracking area if needed
+ if self.tracking_area:
+ self.removeTrackingArea_(self.tracking_area)
+
+ # create a new tracking area for the entire view. This allows us to
+ # get mouseMoved events whenever the mouse is inside our view.
+ self.tracking_area = NSTrackingArea.alloc()
+ self.tracking_area.initWithRect_options_owner_userInfo_(
+ self.visibleRect(),
+ NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
+ NSTrackingActiveInKeyWindow,
+ self,
+ nil)
+ self.addTrackingArea_(self.tracking_area)
+
+ def mouseEntered_(self, event):
+ window = self.window()
+ if window is not nil and window.isMainWindow():
+ self.mouse_inside = True
+ self.setNeedsDisplay_(YES)
+
+ def mouseExited_(self, event):
+ window = self.window()
+ if window is not nil and window.isMainWindow():
+ self.mouse_inside = False
+ self.setNeedsDisplay_(YES)
+
+ def isOpaque(self):
+ return wrappermap.wrapper(self).is_opaque()
+
+ def drawRect_(self, rect):
+ context = drawing.DrawingContext(self, self.bounds(), rect)
+ context.style = drawing.DrawingStyle()
+ wrapper = wrappermap.wrapper(self)
+ wrapper.state = 'normal'
+ disabled = wrapper.get_disabled()
+ if not disabled:
+ if self.state() == NSOnState:
+ wrapper.state = 'pressed'
+ elif self.mouse_inside:
+ wrapper.state = 'hover'
+ else:
+ wrapper.state = 'normal'
+
+ wrapper.draw(context, self.layout_manager)
+ self.layout_manager.reset()
+
+ def sendAction_to_(self, action, to):
+ # We override the Cocoa machinery here and just send it to our wrapper
+ # widget.
+ wrapper = wrappermap.wrapper(self)
+ disabled = wrapper.get_disabled()
+ if not disabled:
+ wrapper.emit('clicked')
+ # Tell Cocoa we handled it anyway, just not emit the actual clicked
+ # event.
+ return YES
+DrawableButton.setCellClass_(DrawableButtonCell)
+
+class ContinousButtonCell(DrawableButtonCell):
+ def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseIsUp):
+ view.onStopTracking(at)
+ NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint, at,
+ view, mouseIsUp)
+
+class ContinuousDrawableButton(DrawableButton):
+ def init(self):
+ self = super(ContinuousDrawableButton, self).init()
+ self.setContinuous_(YES)
+ return self
+
+ def mouseDown_(self, event):
+ self.releaseInbounds = self.stopTracking = self.firedOnce = False
+ self.cell().trackMouse_inRect_ofView_untilMouseUp_(event,
+ self.bounds(), self, YES)
+ wrapper = wrappermap.wrapper(self)
+ if not wrapper.get_disabled():
+ if self.firedOnce:
+ wrapper.emit('released')
+ elif self.releaseInbounds:
+ wrapper.emit('clicked')
+
+ def sendAction_to_(self, action, to):
+ if self.stopTracking:
+ return NO
+ self.firedOnce = True
+ wrapper = wrappermap.wrapper(self)
+ if not wrapper.get_disabled():
+ wrapper.emit('held-down')
+ return YES
+
+ def onStopTracking(self, mouseLocation):
+ self.releaseInbounds = NSPointInRect(mouseLocation, self.bounds())
+ self.stopTracking = True
+ContinuousDrawableButton.setCellClass_(ContinousButtonCell)
+
+class DragableButtonCell(NSButtonCell):
+ def startTrackingAt_inView_(self, point, view):
+ self.start_x = point.x
+ return YES
+
+ def continueTracking_at_inView_(self, lastPoint, at, view):
+ DRAG_THRESHOLD = 15
+ wrapper = wrappermap.wrapper(view)
+ if not wrapper.get_disabled():
+ if (view.last_drag_event != 'right' and
+ at.x > self.start_x + DRAG_THRESHOLD):
+ wrapper.emit("dragged-right")
+ view.last_drag_event = 'right'
+ elif (view.last_drag_event != 'left' and
+ at.x < self.start_x - DRAG_THRESHOLD):
+ view.last_drag_event = 'left'
+ wrapper.emit("dragged-left")
+ return YES
+
+class DragableDrawableButton(DrawableButton):
+ def mouseDown_(self, event):
+ self.last_drag_event = None
+ self.cell().trackMouse_inRect_ofView_untilMouseUp_(event,
+ self.bounds(), self, YES)
+
+ def sendAction_to_(self, action, to):
+ # only send the click event if we didn't send a
+ # dragged-left/dragged-right event
+ wrapper = wrappermap.wrapper(self)
+ if self.last_drag_event is None and not wrapper.get_disabled():
+ wrapper.emit('clicked')
+ return YES
+DragableDrawableButton.setCellClass_(DragableButtonCell)
+
+MouseTrackingInfo = collections.namedtuple("MouseTrackingInfo",
+ "start_pos click_pos")
+
+class CustomSliderCell(NSSliderCell):
+ def calc_slider_amount(self, view, pos, size):
+ slider_size = wrappermap.wrapper(view).slider_size()
+ pos -= slider_size / 2
+ size -= slider_size
+ return max(0, min(1, float(pos) / size))
+
+ def get_slider_pos(self, view, value=None):
+ if value is None:
+ value = view.floatValue()
+ if view.isVertical():
+ size = view.bounds().size.height
+ else:
+ size = view.bounds().size.width
+ slider_size = view.knobThickness()
+ size -= slider_size
+ start_pos = slider_size / 2.0
+ ratio = ((value - view.minValue()) /
+ view.maxValue() - view.minValue())
+ return start_pos + (ratio * size)
+
+ def startTrackingAt_inView_(self, at, view):
+ wrapper = wrappermap.wrapper(view)
+ start_pos = self.get_slider_pos(view)
+ if self.isVertical():
+ click_pos = at.y
+ else:
+ click_pos = at.x
+ # only move the cursor if the click was outside the slider
+ if abs(click_pos - start_pos) > view.knobThickness() / 2:
+ self.moveSliderTo(view, click_pos)
+ start_pos = click_pos
+ view.mouse_tracking_info = MouseTrackingInfo(start_pos, click_pos)
+ if not wrapper.get_disabled():
+ wrapper.emit('pressed')
+ return YES
+
+ def moveSliderTo(self, view, pos):
+ if view.isVertical():
+ size = view.bounds().size.height
+ else:
+ size = view.bounds().size.width
+
+ slider_amount = self.calc_slider_amount(view, pos, size)
+ value = (self.maxValue() - self.minValue()) * slider_amount
+ self.setFloatValue_(value)
+ wrapper = wrappermap.wrapper(view)
+ if not wrapper.get_disabled():
+ wrapper.emit('moved', value)
+ if self.isContinuous():
+ wrapper.emit('changed', value)
+
+ def continueTracking_at_inView_(self, lastPoint, at, view):
+ if view.isVertical():
+ mouse_pos = at.y
+ else:
+ mouse_pos = at.x
+
+ info = view.mouse_tracking_info
+ new_pos = info.start_pos + (mouse_pos - info.click_pos)
+ self.moveSliderTo(view, new_pos)
+ return YES
+
+ def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseUp):
+ wrapper = wrappermap.wrapper(view)
+ if not wrapper.get_disabled():
+ wrapper.emit('released')
+ view.mouse_tracking_info = None
+
+class CustomSliderView(NSSlider):
+ def init(self):
+ self = super(CustomSliderView, self).init()
+ self.layout_manager = LayoutManager()
+ self.custom_cursor = None
+ self.mouse_tracking_info = None
+ return self
+
+ def get_slider_pos(self, value=None):
+ return self.cell().get_slider_pos(self, value)
+
+ def resetCursorRects(self):
+ if self.custom_cursor is not None:
+ self.addCursorRect_cursor_(self.visibleRect(), self.custom_cursor)
+ self.custom_cursor.setOnMouseEntered_(YES)
+
+ def isOpaque(self):
+ return wrappermap.wrapper(self).is_opaque()
+
+ def knobThickness(self):
+ return wrappermap.wrapper(self).slider_size()
+
+ def scrollWheel_(self, event):
+ wrapper = wrappermap.wrapper(self)
+ if wrapper.get_disabled():
+ return
+ # NOTE: we ignore the scroll_step value passed into set_increments()
+ # and calculate the change using deltaY, which is in device
+ # coordinates.
+ slider_size = wrapper.slider_size()
+ if wrapper.is_horizontal():
+ size = self.bounds().size.width
+ else:
+ size = self.bounds().size.height
+ size -= slider_size
+
+ range = self.maxValue() - self.minValue()
+ value_change = (event.deltaY() / size) * range
+ self.setFloatValue_(self.floatValue() + value_change)
+ wrapper.emit('pressed')
+ wrapper.emit('changed', self.floatValue())
+ wrapper.emit('released')
+
+ def isVertical(self):
+ return not wrappermap.wrapper(self).is_horizontal()
+
+ def drawRect_(self, rect):
+ context = drawing.DrawingContext(self, self.bounds(), rect)
+ context.style = drawing.DrawingStyle()
+ wrappermap.wrapper(self).draw(context, self.layout_manager)
+ self.layout_manager.reset()
+
+ def sendAction_to_(self, action, to):
+ # We override the Cocoa machinery here and just send it to our wrapper
+ # widget.
+ wrapper = wrappermap.wrapper(self)
+ disabled = wrapper.get_disabled()
+ if not disabled:
+ wrapper.emit('changed', self.floatValue())
+ # Total Cocoa we handled it anyway to prevent the event passed to
+ # upper layer.
+ return YES
+CustomSliderView.setCellClass_(CustomSliderCell)
+
+class CustomControlBase(drawing.DrawingMixin, Widget):
+ def set_cursor(self, cursor):
+ if cursor == widgetconst.CURSOR_NORMAL:
+ self.view.custom_cursor = None
+ elif cursor == widgetconst.CURSOR_POINTING_HAND:
+ self.view.custom_cursor = NSCursor.pointingHandCursor()
+ else:
+ raise ValueError("Unknown cursor: %s" % cursor)
+ if self.view.window():
+ self.view.window().invalidateCursorRectsForView_(self.view)
+
+class CustomButton(CustomControlBase):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ CustomControlBase.__init__(self)
+ self.create_signal('clicked')
+ self.view = DrawableButton.alloc().init()
+ self.view.setRefusesFirstResponder_(NO)
+ self.view.setEnabled_(True)
+
+ def enable(self):
+ Widget.enable(self)
+ self.view.setNeedsDisplay_(YES)
+
+ def disable(self):
+ Widget.disable(self)
+ self.view.setNeedsDisplay_(YES)
+
+class ContinuousCustomButton(CustomButton):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ CustomButton.__init__(self)
+ self.create_signal('held-down')
+ self.create_signal('released')
+ self.view = ContinuousDrawableButton.alloc().init()
+ self.view.setRefusesFirstResponder_(NO)
+
+ def set_delays(self, initial, repeat):
+ self.view.cell().setPeriodicDelay_interval_(initial, repeat)
+
+class DragableCustomButton(CustomButton):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ CustomButton.__init__(self)
+ self.create_signal('dragged-left')
+ self.create_signal('dragged-right')
+ self.view = DragableDrawableButton.alloc().init()
+
+class CustomSlider(CustomControlBase):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ CustomControlBase.__init__(self)
+ self.create_signal('pressed')
+ self.create_signal('released')
+ self.create_signal('changed')
+ self.create_signal('moved')
+ self.view = CustomSliderView.alloc().init()
+ self.view.setRefusesFirstResponder_(NO)
+ if self.is_continuous():
+ self.view.setContinuous_(YES)
+ else:
+ self.view.setContinuous_(NO)
+ self.view.setEnabled_(True)
+
+ def get_slider_pos(self, value=None):
+ return self.view.get_slider_pos(value)
+
+ def viewport_created(self):
+ self.view.cell().setKnobThickness_(self.slider_size())
+
+ def get_value(self):
+ return self.view.floatValue()
+
+ def set_value(self, value):
+ self.view.setFloatValue_(value)
+
+ def get_range(self):
+ return self.view.minValue(), self.view.maxValue()
+
+ def set_range(self, min_value, max_value):
+ self.view.setMinValue_(min_value)
+ self.view.setMaxValue_(max_value)
+
+ def set_increments(self, small_step, big_step, scroll_step=None):
+ # NOTE: we ignore all of these parameters.
+ #
+ # Cocoa doesn't have a concept of changing the increments for
+ # NSScroller. scroll_step is isn't really compatible with
+ # the event object that's passed to scrollWheel_()
+ pass
+
+ def enable(self):
+ Widget.enable(self)
+ self.view.setNeedsDisplay_(YES)
+
+ def disable(self):
+ Widget.disable(self)
+ self.view.setNeedsDisplay_(YES)