diff options
author | Jesús Eduardo <heckyel@hyperbola.info> | 2017-05-31 18:08:31 -0500 |
---|---|---|
committer | Jesús Eduardo <heckyel@hyperbola.info> | 2017-05-31 18:08:31 -0500 |
commit | e1180428ed3e7634fe1596103511fbb1da05f228 (patch) | |
tree | 13de9592bcde7050b089b9644839668024c518b3 /mvc/widgets/osx/customcontrol.py | |
download | librevideoconverter-e1180428ed3e7634fe1596103511fbb1da05f228.tar.lz librevideoconverter-e1180428ed3e7634fe1596103511fbb1da05f228.tar.xz librevideoconverter-e1180428ed3e7634fe1596103511fbb1da05f228.zip |
first commit
Diffstat (limited to 'mvc/widgets/osx/customcontrol.py')
-rw-r--r-- | mvc/widgets/osx/customcontrol.py | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/mvc/widgets/osx/customcontrol.py b/mvc/widgets/osx/customcontrol.py new file mode 100644 index 0000000..d100f33 --- /dev/null +++ b/mvc/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 mvc.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) |