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