diff options
author | Jesús Eduardo <heckyel@hyperbola.info> | 2017-09-11 17:47:17 -0500 |
---|---|---|
committer | Jesús Eduardo <heckyel@hyperbola.info> | 2017-09-11 17:47:17 -0500 |
commit | 14738704ede6dfa6ac79f362a9c1f7f40f470cdc (patch) | |
tree | 31c83bdd188ae7b64d7169974d6f066ccfe95367 /lvc/widgets/gtk/customcontrols.py | |
parent | eb1896583afbbb622cadcde1a24e17173f61904f (diff) | |
download | librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.tar.lz librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.tar.xz librevideoconverter-14738704ede6dfa6ac79f362a9c1f7f40f470cdc.zip |
rename mvc at lvc
Diffstat (limited to 'lvc/widgets/gtk/customcontrols.py')
-rw-r--r-- | lvc/widgets/gtk/customcontrols.py | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/lvc/widgets/gtk/customcontrols.py b/lvc/widgets/gtk/customcontrols.py new file mode 100644 index 0000000..ff5b068 --- /dev/null +++ b/lvc/widgets/gtk/customcontrols.py @@ -0,0 +1,517 @@ +# @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. + +""".controls -- Contains the ControlBox and +CustomControl classes. These handle the custom buttons/sliders used during +playback. +""" + +from __future__ import division +import math + +import gtk +import gobject + +import wrappermap +from .base import Widget +from .simple import Label, Image +from .drawing import (CustomDrawingMixin, Drawable, + ImageSurface) +from lvc.widgets import widgetconst + +class CustomControlMixin(CustomDrawingMixin): + def do_expose_event(self, event): + CustomDrawingMixin.do_expose_event(self, event) + if self.is_focus(): + style = self.get_style() + style.paint_focus(self.window, self.state, + event.area, self, None, self.allocation.x, + self.allocation.y, self.allocation.width, + self.allocation.height) + +class CustomButtonWidget(CustomControlMixin, gtk.Button): + def draw(self, wrapper, context): + if self.is_active(): + wrapper.state = 'pressed' + elif self.state == gtk.STATE_PRELIGHT: + wrapper.state = 'hover' + else: + wrapper.state = 'normal' + wrapper.draw(context, wrapper.layout_manager) + self.set_focus_on_click(False) + + def is_active(self): + return self.state == gtk.STATE_ACTIVE + +class ContinuousCustomButtonWidget(CustomButtonWidget): + def is_active(self): + return (self.state == gtk.STATE_ACTIVE or + wrappermap.wrapper(self).button_down) + +class DragableCustomButtonWidget(CustomButtonWidget): + def __init__(self): + CustomButtonWidget.__init__(self) + self.button_press_x = None + self.set_events(self.get_events() | gtk.gdk.POINTER_MOTION_MASK) + + def do_button_press_event(self, event): + self.button_press_x = event.x + self.last_drag_event = None + gtk.Button.do_button_press_event(self, event) + + def do_button_release_event(self, event): + self.button_press_x = None + gtk.Button.do_button_release_event(self, event) + + def do_motion_notify_event(self, event): + DRAG_THRESHOLD = 15 + if self.button_press_x is None: + # button not down + return + if (self.last_drag_event != 'right' and + event.x > self.button_press_x + DRAG_THRESHOLD): + wrappermap.wrapper(self).emit('dragged-right') + self.last_drag_event = 'right' + elif (self.last_drag_event != 'left' and + event.x < self.button_press_x - DRAG_THRESHOLD): + wrappermap.wrapper(self).emit('dragged-left') + self.last_drag_event = 'left' + + def do_clicked(self): + # only emit clicked if we didn't emit dragged-left or dragged-right + if self.last_drag_event is None: + wrappermap.wrapper(self).emit('clicked') + +class _DragInfo(object): + """Info about the start of a drag. + + Attributes: + + - button: button that started the drag + - start_pos: position of the slider + - click_pos: position of the click + + Note that start_pos and click_pos will be different if the user clicks + inside the slider. + """ + + def __init__(self, button, start_pos, click_pos): + self.button = button + self.start_pos = start_pos + self.click_pos = click_pos + +class CustomScaleMixin(CustomControlMixin): + def __init__(self): + CustomControlMixin.__init__(self) + self.drag_info = None + self.min = self.max = 0.0 + + def get_range(self): + return self.min, self.max + + def set_range(self, min, max): + self.min = float(min) + self.max = float(max) + gtk.Range.set_range(self, min, max) + + def is_continuous(self): + return wrappermap.wrapper(self).is_continuous() + + def is_horizontal(self): + # this comes from a mixin + pass + + def gtk_scale_class(self): + if self.is_horizontal(): + return gtk.HScale + else: + return gtk.VScale + + def get_slider_pos(self, value=None): + if value is None: + value = self.get_value() + if self.is_horizontal(): + size = self.allocation.width + else: + size = self.allocation.height + ratio = (float(value) - self.min) / (self.max - self.min) + start_pos = self.slider_size() / 2.0 + return start_pos + ratio * (size - self.slider_size()) + + def slider_size(self): + return wrappermap.wrapper(self).slider_size() + + def _event_pos(self, event): + """Get the position of an event. + + If we are horizontal, this will be the x coordinate. If we are + vertical, the y. + """ + if self.is_horizontal(): + return event.x + else: + return event.y + + def do_button_press_event(self, event): + if self.drag_info is not None: + return + current_pos = self.get_slider_pos() + event_pos = self._event_pos(event) + pos_difference = abs(current_pos - event_pos) + # only move the slider if the click was outside its boundaries + # (#18840) + if pos_difference > self.slider_size() / 2.0: + self.move_slider(event_pos) + current_pos = event_pos + self.drag_info = _DragInfo(event.button, current_pos, event_pos) + self.grab_focus() + wrappermap.wrapper(self).emit('pressed') + + def do_motion_notify_event(self, event): + if self.drag_info is not None: + event_pos = self._event_pos(event) + delta = event_pos - self.drag_info.click_pos + self.move_slider(self.drag_info.start_pos + delta) + + def move_slider(self, new_pos): + """Move the slider so that it's centered on new_pos.""" + if self.is_horizontal(): + size = self.allocation.width + else: + size = self.allocation.height + + slider_size = self.slider_size() + new_pos -= slider_size / 2 + size -= slider_size + ratio = max(0, min(1, float(new_pos) / size)) + self.set_value(ratio * (self.max - self.min)) + + wrappermap.wrapper(self).emit('moved', self.get_value()) + if self.is_continuous(): + wrappermap.wrapper(self).emit('changed', self.get_value()) + + def handle_drag_out_of_bounds(self): + if not self.is_continuous(): + self.set_value(self.start_value) + + def do_button_release_event(self, event): + if self.drag_info is None or event.button != self.drag_info.button: + return + self.drag_info = None + if (self.is_continuous and + (0 <= event.x < self.allocation.width) and + (0 <= event.y < self.allocation.height)): + wrappermap.wrapper(self).emit('changed', self.get_value()) + wrappermap.wrapper(self).emit('released') + + def do_scroll_event(self, event): + wrapper = wrappermap.wrapper(self) + if self.is_horizontal(): + if event.direction == gtk.gdk.SCROLL_UP: + event.direction = gtk.gdk.SCROLL_DOWN + elif event.direction == gtk.gdk.SCROLL_DOWN: + event.direction = gtk.gdk.SCROLL_UP + if (wrapper._scroll_step is not None and + event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_DOWN)): + # handle the scroll ourself + if event.direction == gtk.gdk.SCROLL_DOWN: + delta = wrapper._scroll_step + else: + delta = -wrapper._scroll_step + self.set_value(self.get_value() + delta) + else: + # let GTK handle the scroll + self.gtk_scale_class().do_scroll_event(self, event) + # Treat mouse scrolls as if the user clicked on the new position + wrapper.emit('pressed') + wrapper.emit('changed', self.get_value()) + wrapper.emit('released') + + def do_move_slider(self, scroll): + if self.is_horizontal(): + if scroll == gtk.SCROLL_STEP_UP: + scroll = gtk.SCROLL_STEP_DOWN + elif scroll == gtk.SCROLL_STEP_DOWN: + scroll = gtk.SCROLL_STEP_UP + elif scroll == gtk.SCROLL_PAGE_UP: + scroll = gtk.SCROLL_PAGE_DOWN + elif scroll == gtk.SCROLL_PAGE_DOWN: + scroll = gtk.SCROLL_PAGE_UP + elif scroll == gtk.SCROLL_START: + scroll = gtk.SCROLL_END + elif scroll == gtk.SCROLL_END: + scroll = gtk.SCROLL_START + return self.gtk_scale_class().do_move_slider(self, scroll) + +class CustomHScaleWidget(CustomScaleMixin, gtk.HScale): + def __init__(self): + CustomScaleMixin.__init__(self) + gtk.HScale.__init__(self) + + def is_horizontal(self): + return True + +class CustomVScaleWidget(CustomScaleMixin, gtk.VScale): + def __init__(self): + CustomScaleMixin.__init__(self) + gtk.VScale.__init__(self) + + def is_horizontal(self): + return False + +gobject.type_register(CustomButtonWidget) +gobject.type_register(ContinuousCustomButtonWidget) +gobject.type_register(DragableCustomButtonWidget) +gobject.type_register(CustomHScaleWidget) +gobject.type_register(CustomVScaleWidget) + +class CustomControlBase(Drawable, Widget): + def __init__(self): + Widget.__init__(self) + Drawable.__init__(self) + self._gtk_cursor = None + self._entry_handlers = None + + def _connect_enter_notify_handlers(self): + if self._entry_handlers is None: + self._entry_handlers = [ + self.wrapped_widget_connect('enter-notify-event', + self.on_enter_notify), + self.wrapped_widget_connect('leave-notify-event', + self.on_leave_notify), + self.wrapped_widget_connect('button-release-event', + self.on_click) + ] + + def _disconnect_enter_notify_handlers(self): + if self._entry_handlers is not None: + for handle in self._entry_handlers: + self._widget.disconnect(handle) + self._entry_handlers = None + + def set_cursor(self, cursor): + if cursor == widgetconst.CURSOR_NORMAL: + self._gtk_cursor = None + self._disconnect_enter_notify_handlers() + elif cursor == widgetconst.CURSOR_POINTING_HAND: + self._gtk_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2) + self._connect_enter_notify_handlers() + else: + raise ValueError("Unknown cursor: %s" % cursor) + + def on_enter_notify(self, widget, event): + self._widget.window.set_cursor(self._gtk_cursor) + + def on_leave_notify(self, widget, event): + if self._widget.window: + self._widget.window.set_cursor(None) + + def on_click(self, widget, event): + self.emit('clicked') + return True + +class CustomButton(CustomControlBase): + def __init__(self): + """Create a new CustomButton. active_image will be displayed while + the button is pressed. The image must have the same size. + """ + CustomControlBase.__init__(self) + self.set_widget(CustomButtonWidget()) + self.create_signal('clicked') + self.forward_signal('clicked') + +class DragableCustomButton(CustomControlBase): + def __init__(self): + CustomControlBase.__init__(self) + self.set_widget(DragableCustomButtonWidget()) + self.create_signal('clicked') + self.create_signal('dragged-left') + self.create_signal('dragged-right') + +class CustomSlider(CustomControlBase): + def __init__(self): + CustomControlBase.__init__(self) + self.create_signal('pressed') + self.create_signal('released') + self.create_signal('changed') + self.create_signal('moved') + self._scroll_step = None + if self.is_horizontal(): + self.set_widget(CustomHScaleWidget()) + else: + self.set_widget(CustomVScaleWidget()) + self.wrapped_widget_connect('move-slider', self.on_slider_move) + + def on_slider_move(self, widget, scrolltype): + self.emit('changed', widget.get_value()) + self.emit('moved', widget.get_value()) + + def get_value(self): + return self._widget.get_value() + + def set_value(self, value): + self._widget.set_value(value) + + def get_range(self): + return self._widget.get_range() + + def get_slider_pos(self, value=None): + """Get the position for the slider for our current value. + + This will return position that the slider should be centered on to + display the value. It will be the x coordinate if is_horizontal() is + True and the y coordinate otherwise. + + This method takes into acount the size of the slider when calculating + the position. The slider position will start at (slider_size / 2) and + will end (slider_size / 2) px before the end of the widget. + + :param value: value to get the position for. Defaults to the current + value + """ + return self._widget.get_slider_pos(value) + + def set_range(self, min_value, max_value): + self._widget.set_range(min_value, max_value) + # set_digits controls the precision of the scale by limiting changes + # to a certain number of digits. If the range is [0, 1], this code + # will give us 4 digits of precision, which seems reasonable. + range = max_value - min_value + self._widget.set_digits(int(round(math.log10(10000.0 / range)))) + + def set_increments(self, small_step, big_step, scroll_step=None): + """Set the increments to scroll. + + :param small_step: scroll amount for up/down + :param big_step: scroll amount for page up/page down. + :param scroll_step: scroll amount for mouse wheel, or None to make + this 2 times the small step + """ + self._widget.set_increments(small_step, big_step) + self._scroll_step = scroll_step + +def to_miro_volume(value): + """Convert from 0 to 1.0 to 0.0 to MAX_VOLUME. + """ + if value == 0: + return 0.0 + return value * widgetconst.MAX_VOLUME + +def to_gtk_volume(value): + """Convert from 0.0 to MAX_VOLUME to 0 to 1.0. + """ + if value > 0.0: + value = (value / widgetconst.MAX_VOLUME) + return value + +if hasattr(gtk.VolumeButton, "get_popup"): + # FIXME - Miro on Windows has an old version of gtk (2.16) and + # doesn't have the get_popup method. Once we upgrade and + # fix that, we can take out the hasattr check. + + class VolumeMuter(Label): + """Empty space that has a clicked signal so it can be dropped + in place of the VolumeMuter. + """ + def __init__(self): + Label.__init__(self) + self.create_signal("clicked") + + class VolumeSlider(Widget): + """VolumeSlider that uses the gtk.VolumeButton(). + """ + def __init__(self): + Widget.__init__(self) + self.set_widget(gtk.VolumeButton()) + self.wrapped_widget_connect('value-changed', self.on_value_changed) + self._widget.get_popup().connect("hide", self.on_hide) + self.create_signal('changed') + self.create_signal('released') + + def on_value_changed(self, *args): + value = self.get_value() + self.emit('changed', value) + + def on_hide(self, *args): + self.emit('released') + + def get_value(self): + value = self._widget.get_property('value') + return to_miro_volume(value) + + def set_value(self, value): + value = to_gtk_volume(value) + self._widget.set_property('value', value) + +class ClickableImageButton(CustomButton): + """Image that can send clicked events. If max_width and/or max_height are + specified, resizes the image proportionally such that all constraints are + met. + """ + def __init__(self, image_path, max_width=None, max_height=None): + CustomButton.__init__(self) + self.max_width = max_width + self.max_height = max_height + self.image = None + self._width, self._height = None, None + if image_path: + self.set_path(image_path) + self.set_cursor(widgetconst.CURSOR_POINTING_HAND) + + def set_path(self, path): + image = Image(path) + if self.max_width: + image = image.resize_for_space(self.max_width, self.max_height) + self.image = ImageSurface(image) + self._width, self._height = image.width, image.height + + def size_request(self, layout): + w = self._width + h = self._height + if not w: + w = self.max_width + if not h: + h = self.max_height + return w, h + + def draw(self, context, layout): + if self.image: + self.image.draw(context, 0, 0, self._width, self._height) + w = self._width + h = self._height + if not w: + w = self.max_width + if not h: + h = self.max_height + w = min(context.width, w) + h = min(context.height, h) + context.rectangle(0, 0, w, h) + context.set_color((0, 0, 0)) # black + context.set_line_width(1) + context.stroke() |