diff options
Diffstat (limited to 'mvc/widgets/gtk')
-rw-r--r-- | mvc/widgets/gtk/__init__.py | 65 | ||||
-rw-r--r-- | mvc/widgets/gtk/base.py | 300 | ||||
-rw-r--r-- | mvc/widgets/gtk/const.py | 44 | ||||
-rw-r--r-- | mvc/widgets/gtk/contextmenu.py | 31 | ||||
-rw-r--r-- | mvc/widgets/gtk/controls.py | 337 | ||||
-rw-r--r-- | mvc/widgets/gtk/customcontrols.py | 517 | ||||
-rw-r--r-- | mvc/widgets/gtk/drawing.py | 268 | ||||
-rw-r--r-- | mvc/widgets/gtk/gtkmenus.py | 404 | ||||
-rw-r--r-- | mvc/widgets/gtk/keymap.py | 94 | ||||
-rw-r--r-- | mvc/widgets/gtk/layout.py | 227 | ||||
-rw-r--r-- | mvc/widgets/gtk/layoutmanager.py | 550 | ||||
-rw-r--r-- | mvc/widgets/gtk/simple.py | 313 | ||||
-rw-r--r-- | mvc/widgets/gtk/tableview.py | 1557 | ||||
-rw-r--r-- | mvc/widgets/gtk/tableviewcells.py | 249 | ||||
-rw-r--r-- | mvc/widgets/gtk/weakconnect.py | 56 | ||||
-rw-r--r-- | mvc/widgets/gtk/widgets.py | 47 | ||||
-rw-r--r-- | mvc/widgets/gtk/widgetset.py | 63 | ||||
-rw-r--r-- | mvc/widgets/gtk/window.py | 708 | ||||
-rw-r--r-- | mvc/widgets/gtk/wrappermap.py | 50 |
19 files changed, 0 insertions, 5880 deletions
diff --git a/mvc/widgets/gtk/__init__.py b/mvc/widgets/gtk/__init__.py deleted file mode 100644 index 8e58700..0000000 --- a/mvc/widgets/gtk/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -import os -import sys -import gtk -import gobject - -def initialize(app): - from gtkmenus import MainWindowMenuBar - app.menubar = MainWindowMenuBar() - app.startup() - app.run() - -def attach_menubar(): - from mvc.widgets import app - app.widgetapp.vbox.pack_start(app.widgetapp.menubar) - -def mainloop_start(): - gobject.threads_init() - gtk.main() - -def mainloop_stop(): - gtk.main_quit() - -def idle_add(callback, periodic=None): - if periodic is not None and periodic < 0: - raise ValueError('periodic cannot be negative') - def wrapper(): - callback() - return periodic is not None - delay = periodic - if delay is not None: - delay *= 1000 # milliseconds - else: - delay = 0 - return gobject.timeout_add(delay, wrapper) - -def idle_remove(id_): - gobject.source_remove(id_) - -def check_kde(): - return os.environ.get("KDE_FULL_SESSION", None) != None - -def open_file_linux(filename): - if check_kde(): - os.spawnlp(os.P_NOWAIT, "kfmclient", "kfmclient", # kfmclient is part of konqueror - "exec", "file://" + filename) - else: - os.spawnlp(os.P_NOWAIT, "gnome-open", "gnome-open", filename) - -def reveal_file(filename): - if hasattr(os, 'startfile'): # Windows - os.startfile(os.path.dirname(filename)) - else: - open_file_linux(filename) - -def get_conversion_directory_windows(): - from mvc.windows import specialfolders - return specialfolders.base_movies_directory - -def get_conversion_directory_linux(): - return os.path.expanduser('~') - -if sys.platform == 'win32': - get_conversion_directory = get_conversion_directory_windows -else: - get_conversion_directory = get_conversion_directory_linux diff --git a/mvc/widgets/gtk/base.py b/mvc/widgets/gtk/base.py deleted file mode 100644 index e02db3f..0000000 --- a/mvc/widgets/gtk/base.py +++ /dev/null @@ -1,300 +0,0 @@ -# @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. - -""".base -- Base classes for GTK Widgets.""" - -import gtk - -from mvc import signals -import wrappermap -from .weakconnect import weak_connect -import keymap - -def make_gdk_color(miro_color): - def convert_value(value): - return int(round(value * 65535)) - - values = tuple(convert_value(c) for c in miro_color) - return gtk.gdk.Color(*values) - -class Widget(signals.SignalEmitter): - """Base class for GTK widgets. - - The actual GTK Widget is stored in '_widget'. - - signals: - - 'size-allocated' (widget, width, height): The widget had it's size - allocated. - """ - def __init__(self, *signal_names): - signals.SignalEmitter.__init__(self, *signal_names) - self.create_signal('size-allocated') - self.create_signal('key-press') - self.create_signal('focus-out') - self.style_mods = {} - self.use_custom_style = False - self._disabled = False - - def wrapped_widget_connect(self, signal, method, *user_args): - """Connect to a signal of the widget we're wrapping. - - We use a weak reference to ensures that we don't have circular - references between the wrapped widget and the wrapper widget. - """ - return weak_connect(self._widget, signal, method, *user_args) - - def set_widget(self, widget): - self._widget = widget - wrappermap.add(self._widget, self) - if self.should_connect_to_hierarchy_changed(): - self.wrapped_widget_connect('hierarchy_changed', - self.on_hierarchy_changed) - self.wrapped_widget_connect('size-allocate', self.on_size_allocate) - self.wrapped_widget_connect('key-press-event', self.on_key_press) - self.wrapped_widget_connect('focus-out-event', self.on_focus_out) - self.use_custom_style_callback = None - - def should_connect_to_hierarchy_changed(self): - # GTK creates windows to handle submenus, which messes with our - # on_hierarchy_changed callback. We don't care about custom styles - # for menus anyways, so just ignore the signal. - return not isinstance(self._widget, gtk.MenuItem) - - def set_can_focus(self, allow): - """Set if we allow the widget to hold keyboard focus. - """ - if allow: - self._widget.set_flags(gtk.CAN_FOCUS) - else: - self._widget.unset_flags(gtk.CAN_FOCUS) - - def on_hierarchy_changed(self, widget, previous_toplevel): - toplevel = widget.get_toplevel() - if not (toplevel.flags() & gtk.TOPLEVEL): - toplevel = None - if previous_toplevel != toplevel: - if self.use_custom_style_callback: - old_window = wrappermap.wrapper(previous_toplevel) - old_window.disconnect(self.use_custom_style_callback) - if toplevel is not None: - window = wrappermap.wrapper(toplevel) - callback_id = window.connect('use-custom-style-changed', - self.on_use_custom_style_changed) - self.use_custom_style_callback = callback_id - else: - self.use_custom_style_callback = None - if previous_toplevel is None: - # Setup our initial state - self.on_use_custom_style_changed(window) - - def on_size_allocate(self, widget, allocation): - self.emit('size-allocated', allocation.width, allocation.height) - - def on_key_press(self, widget, event): - key_modifiers = keymap.translate_gtk_event(event) - if key_modifiers: - key, modifiers = key_modifiers - return self.emit('key-press', key, modifiers) - - def on_focus_out(self, widget, event): - self.emit('focus-out') - - def on_use_custom_style_changed(self, window): - self.use_custom_style = window.use_custom_style - if not self.style_mods: - return # no need to do any work here - if self.use_custom_style: - for (what, state), color in self.style_mods.items(): - self.do_modify_style(what, state, color) - else: - # This should reset the style changes we've made - self._widget.modify_style(gtk.RcStyle()) - self.handle_custom_style_change() - - def handle_custom_style_change(self): - """Called when the user changes a from a theme where we don't want to - use our custom style to one where we do, or vice-versa. The Widget - class handles changes that used modify_style(), but subclasses might - want to do additional work. - """ - pass - - def modify_style(self, what, state, color): - """Change the style of our widget. This method checks to see if we - think the user's theme is compatible with our stylings, and doesn't - change things if not. what is either 'base', 'text', 'bg' or 'fg' - depending on which color is to be changed. - """ - if self.use_custom_style: - self.do_modify_style(what, state, color) - self.style_mods[(what, state)] = color - - def unmodify_style(self, what, state): - if (what, state) in self.style_mods: - del self.style_mods[(what, state)] - default_color = getattr(self.style, what)[state] - self.do_modify_style(what, state, default_color) - - def do_modify_style(self, what, state, color): - if what == 'base': - self._widget.modify_base(state, color) - elif what == 'text': - self._widget.modify_text(state, color) - elif what == 'bg': - self._widget.modify_bg(state, color) - elif what == 'fg': - self._widget.modify_fg(state, color) - else: - raise ValueError("Unknown what in do_modify_style: %s" % what) - - def get_window(self): - gtk_window = self._widget.get_toplevel() - return wrappermap.wrapper(gtk_window) - - def clear_size_request_cache(self): - # This is just an OS X hack - pass - - def get_size_request(self): - return self._widget.size_request() - - def invalidate_size_request(self): - self._widget.queue_resize() - - def set_size_request(self, width, height): - if not width >= -1 and height >= -1: - raise ValueError("invalid dimensions in set_size_request: %s" % - repr((width, height))) - self._widget.set_size_request(width, height) - - def relative_position(self, other_widget): - return other_widget._widget.translate_coordinates(self._widget, 0, 0) - - def convert_gtk_color(self, color): - return (color.red / 65535.0, color.green / 65535.0, - color.blue / 65535.0) - - def get_width(self): - try: - return self._widget.allocation.width - except AttributeError: - return -1 - width = property(get_width) - - def get_height(self): - try: - return self._widget.allocation.height - except AttributeError: - return -1 - height = property(get_height) - - def queue_redraw(self): - if self._widget: - self._widget.queue_draw() - - def redraw_now(self): - if self._widget: - self._widget.queue_draw() - self._widget.window.process_updates(True) - - def forward_signal(self, signal_name, forwarded_signal_name=None): - """Add a callback so that when the GTK widget emits a signal, we emit - signal from the wrapper widget. - """ - if forwarded_signal_name is None: - forwarded_signal_name = signal_name - self.wrapped_widget_connect(signal_name, self.do_forward_signal, - forwarded_signal_name) - - def do_forward_signal(self, widget, *args): - forwarded_signal_name = args[-1] - args = args[:-1] - self.emit(forwarded_signal_name, *args) - - def make_color(self, miro_color): - color = make_gdk_color(miro_color) - self._widget.get_colormap().alloc_color(color) - return color - - def enable(self): - self._disabled = False - self._widget.set_sensitive(True) - - def disable(self): - self._disabled = True - self._widget.set_sensitive(False) - - def set_disabled(self, disabled): - if disabled: - self.disable() - else: - self.enable() - - def get_disabled(self): - return self._disabled - -class Bin(Widget): - def __init__(self): - Widget.__init__(self) - self.child = None - - def add(self, child): - if self.child is not None: - raise ValueError("Already have a child: %s" % self.child) - if child._widget.parent is not None: - raise ValueError("%s already has a parent" % child) - self.child = child - self.add_child_to_widget() - child._widget.show() - - def add_child_to_widget(self): - self._widget.add(self.child._widget) - - def remove_child_from_widget(self): - if self._widget.get_child() is not None: - # otherwise gtkmozembed gets confused - self._widget.get_child().hide() - self._widget.remove(self._widget.get_child()) - - - def remove(self): - if self.child is not None: - self.child = None - self.remove_child_from_widget() - - def set_child(self, new_child): - self.remove() - self.add(new_child) - - def enable(self): - self.child.enable() - - def disable(self): - self.child.disable() diff --git a/mvc/widgets/gtk/const.py b/mvc/widgets/gtk/const.py deleted file mode 100644 index 5e9ec05..0000000 --- a/mvc/widgets/gtk/const.py +++ /dev/null @@ -1,44 +0,0 @@ -# @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. - -""".const -- Constants.""" - -import gtk - -DRAG_ACTION_NONE = 0 -DRAG_ACTION_COPY = gtk.gdk.ACTION_COPY -DRAG_ACTION_MOVE = gtk.gdk.ACTION_MOVE -DRAG_ACTION_LINK = gtk.gdk.ACTION_LINK -DRAG_ACTION_ALL = DRAG_ACTION_COPY | DRAG_ACTION_MOVE | DRAG_ACTION_LINK - -ITEM_TITLE_FONT = "Helvetica" -ITEM_DESC_FONT = "Helvetica" -ITEM_INFO_FONT = "Lucida Grande" - -TOOLBAR_GRAY = (0.2, 0.2, 0.2) diff --git a/mvc/widgets/gtk/contextmenu.py b/mvc/widgets/gtk/contextmenu.py deleted file mode 100644 index cd5b6ba..0000000 --- a/mvc/widgets/gtk/contextmenu.py +++ /dev/null @@ -1,31 +0,0 @@ -import gtk - -from .base import Widget - -class ContextMenu(Widget): - - def __init__(self, options): - super(ContextMenu, self).__init__() - self.set_widget(gtk.Menu()) - for i, item_info in enumerate(options): - if item_info is None: - # separator - item = gtk.SeparatorMenuItem() - else: - label, callback = item_info - item = gtk.MenuItem(label) - if isinstance(callback, list): - submenu = ContextMenu(callback) - item.set_submenu(submenu._widget) - elif callback is not None: - item.connect('activate', self.on_activate, callback, i) - else: - item.set_sensitive(False) - self._widget.append(item) - item.show() - - def popup(self): - self._widget.popup(None, None, None, 0, 0) - - def on_activate(self, widget, callback, i): - callback(self, i) diff --git a/mvc/widgets/gtk/controls.py b/mvc/widgets/gtk/controls.py deleted file mode 100644 index 4367c1f..0000000 --- a/mvc/widgets/gtk/controls.py +++ /dev/null @@ -1,337 +0,0 @@ -# @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 -- Control Widgets.""" - -import gtk -import pango - -from mvc.widgets import widgetconst -import layout -from .base import Widget -from .simple import Label - -class BinBaselineCalculator(object): - """Mixin class that defines the baseline method for gtk.Bin subclasses, - where the child is the label that we are trying to get the baseline for. - """ - - def baseline(self): - my_size = self._widget.size_request() - child_size = self._widget.child.size_request() - ypad = (my_size[1] - child_size[1]) / 2 - - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - -class TextEntry(Widget): - entry_class = gtk.Entry - def __init__(self, initial_text=None): - Widget.__init__(self) - self.create_signal('activate') - self.create_signal('changed') - self.create_signal('validate') - self.set_widget(self.entry_class()) - self.forward_signal('activate') - self.forward_signal('changed') - if initial_text is not None: - self.set_text(initial_text) - - def focus(self): - self._widget.grab_focus() - - def start_editing(self, text): - self.set_text(text) - self.focus() - self._widget.emit('move-cursor', gtk.MOVEMENT_BUFFER_ENDS, 1, False) - - def set_text(self, text): - self._widget.set_text(text) - - def get_text(self): - return self._widget.get_text().decode('utf-8') - - def set_max_length(self, chars): - self._widget.set_max_length(chars) - - def set_width(self, chars): - self._widget.set_width_chars(chars) - - def set_invisible(self, setting): - self._widget.props.visibility = not setting - - def set_activates_default(self, setting): - self._widget.set_activates_default(setting) - - def baseline(self): - layout_height = pango.PIXELS(self._widget.get_layout().get_size()[1]) - ypad = (self._widget.size_request()[1] - layout_height) / 2 - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - -class NumberEntry(TextEntry): - def __init__(self, initial_text=None): - TextEntry.__init__(self, initial_text) - self._widget.connect('changed', self.validate) - self.previous_text = initial_text or "" - - def validate(self, entry): - text = self.get_text() - if text.isdigit() or not text: - self.previous_text = text - else: - self._widget.set_text(self.previous_text) - -class SecureTextEntry(TextEntry): - def __init__(self, initial_text=None): - TextEntry.__init__(self, initial_text) - self.set_invisible(True) - -class MultilineTextEntry(Widget): - entry_class = gtk.TextView - def __init__(self, initial_text=None, border=False): - Widget.__init__(self) - self.set_widget(self.entry_class()) - if initial_text is not None: - self.set_text(initial_text) - self._widget.set_wrap_mode(gtk.WRAP_WORD) - self._widget.set_accepts_tab(False) - self.border = border - - def focus(self): - self._widget.grab_focus() - - def set_text(self, text): - self._widget.get_buffer().set_text(text) - - def get_text(self): - buffer_ = self._widget.get_buffer() - return buffer_.get_text(*(buffer_.get_bounds())).decode('utf-8') - - def baseline(self): - # FIXME - layout_height = pango.PIXELS(self._widget.get_layout().get_size()[1]) - ypad = (self._widget.size_request()[1] - layout_height) / 2 - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - def set_editable(self, editable): - self._widget.set_editable(editable) - -class Checkbox(Widget, BinBaselineCalculator): - """Widget that the user can toggle on or off.""" - - def __init__(self, text=None, bold=False, color=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - if text is None: - text = '' - self.set_widget(gtk.CheckButton()) - self.label = Label(text, color=color) - self._widget.add(self.label._widget) - self.label._widget.show() - self.create_signal('toggled') - self.forward_signal('toggled') - if bold: - self.label.set_bold(True) - - def get_checked(self): - return self._widget.get_active() - - def set_checked(self, value): - self._widget.set_active(value) - - def set_size(self, scale_factor): - self.label.set_size(scale_factor) - - def get_text_padding(self): - """ - Returns the amount of space the checkbox takes up before the label. - """ - indicator_size = self._widget.style_get_property('indicator-size') - indicator_spacing = self._widget.style_get_property( - 'indicator-spacing') - focus_width = self._widget.style_get_property('focus-line-width') - focus_padding = self._widget.style_get_property('focus-padding') - return (indicator_size + 3 * indicator_spacing + 2 * (focus_width + - focus_padding)) - -class RadioButtonGroup(Widget, BinBaselineCalculator): - """RadioButtonGroup. - - Create the group, then create a bunch of RadioButtons passing in the group. - - NB: GTK has built-in radio button grouping functionality, and we should - be using that but we need this widget for portable code. We create - a dummy GTK radio button and make this the "root" button which gets - inherited by all buttons in this radio button group. - """ - def __init__(self): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - self.set_widget(gtk.RadioButton(label="")) - self._widget.set_active(False) - self._buttons = [] - - def add_button(self, button): - self._buttons.append(button) - - def get_buttons(self): - return self._buttons - - def get_selected(self): - for mem in self._buttons: - if mem.get_selected(): - return mem - - def set_selected(self, button): - for mem in self._buttons: - if mem is button: - mem._widget.set_active(True) - else: - mem._widget.set_active(False) - -class RadioButton(Widget, BinBaselineCalculator): - """RadioButton.""" - def __init__(self, label, group=None, color=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - if group: - self.group = group - else: - self.group = RadioButtonGroup() - self.set_widget(gtk.RadioButton(group=self.group._widget)) - self.label = Label(label, color=color) - self._widget.add(self.label._widget) - self.label._widget.show() - self.create_signal('clicked') - self.forward_signal('clicked') - - group.add_button(self) - - def set_size(self, size): - self.label.set_size(size) - - def get_group(self): - return self.group - - def get_selected(self): - return self._widget.get_active() - - def set_selected(self): - self.group.set_selected(self) - -class Button(Widget, BinBaselineCalculator): - def __init__(self, text, style='normal', width=None): - Widget.__init__(self) - BinBaselineCalculator.__init__(self) - # We just ignore style here, GTK users expect their own buttons. - self.set_widget(gtk.Button()) - self.create_signal('clicked') - self.forward_signal('clicked') - self.label = Label(text) - # only honor width if its bigger than the width we need to display the - # label (#18994) - if width and width > self.label.get_width(): - alignment = layout.Alignment(0.5, 0.5, 0, 0) - alignment.set_size_request(width, -1) - alignment.add(self.label) - self._widget.add(alignment._widget) - else: - self._widget.add(self.label._widget) - self.label._widget.show() - - def set_text(self, title): - self.label.set_text(title) - - def set_bold(self, bold): - self.label.set_bold(bold) - - def set_size(self, scale_factor): - self.label.set_size(scale_factor) - - def set_color(self, color): - self.label.set_color(color) - -class OptionMenu(Widget): - def __init__(self, options): - Widget.__init__(self) - self.create_signal('changed') - - self.set_widget(gtk.ComboBox(gtk.ListStore(str, str))) - self.cell = gtk.CellRendererText() - self._widget.pack_start(self.cell, True) - self._widget.add_attribute(self.cell, 'text', 0) - if options: - for option, value in options: - self._widget.get_model().append((option, value)) - self._widget.set_active(0) - self.options = options - self.wrapped_widget_connect('changed', self.on_changed) - - def baseline(self): - my_size = self._widget.size_request() - child_size = self._widget.child.size_request() - ypad = self.cell.props.ypad + (my_size[1] - child_size[1]) / 2 - - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self._widget.style.font_desc) - return pango.PIXELS(metrics.get_descent()) + ypad - - def set_bold(self, bold): - if bold: - self.cell.props.weight = pango.WEIGHT_BOLD - else: - self.cell.props.weight = pango.WEIGHT_NORMAL - - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self.cell.props.scale = 1 - else: - self.cell.props.scale = 0.75 - - def set_color(self, color): - self.cell.props.foreground_gdk = self.make_color(color) - - def set_selected(self, index): - self._widget.set_active(index) - - def get_selected(self): - return self._widget.get_active() - - def on_changed(self, widget): - index = widget.get_active() - self.emit('changed', index) - - def set_width(self, width): - self._widget.set_property('width-request', width) diff --git a/mvc/widgets/gtk/customcontrols.py b/mvc/widgets/gtk/customcontrols.py deleted file mode 100644 index 070cebd..0000000 --- a/mvc/widgets/gtk/customcontrols.py +++ /dev/null @@ -1,517 +0,0 @@ -# @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 mvc.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() diff --git a/mvc/widgets/gtk/drawing.py b/mvc/widgets/gtk/drawing.py deleted file mode 100644 index 5888851..0000000 --- a/mvc/widgets/gtk/drawing.py +++ /dev/null @@ -1,268 +0,0 @@ -# @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. - -""".drawing -- Contains classes used to draw on -widgets. -""" - -import cairo -import gobject -import gtk - -import wrappermap -from .base import Widget, Bin -from .layoutmanager import LayoutManager - -def css_to_color(css_string): - parts = (css_string[1:3], css_string[3:5], css_string[5:7]) - return tuple((int(value, 16) / 255.0) for value in parts) - -class ImageSurface: - def __init__(self, image): - format = cairo.FORMAT_RGB24 - if image.pixbuf.get_has_alpha(): - format = cairo.FORMAT_ARGB32 - self.image = cairo.ImageSurface( - format, int(image.width), int(image.height)) - context = cairo.Context(self.image) - gdkcontext = gtk.gdk.CairoContext(context) - gdkcontext.set_source_pixbuf(image.pixbuf, 0, 0) - gdkcontext.paint() - self.pattern = cairo.SurfacePattern(self.image) - self.pattern.set_extend(cairo.EXTEND_REPEAT) - self.width = image.width - self.height = image.height - - def get_size(self): - return self.width, self.height - - def _align_pattern(self, x, y): - """Line up our image pattern so that it's top-left corner is x, y.""" - m = cairo.Matrix() - m.translate(-x, -y) - self.pattern.set_matrix(m) - - def draw(self, context, x, y, width, height, fraction=1.0): - self._align_pattern(x, y) - cairo_context = context.context - cairo_context.save() - cairo_context.set_source(self.pattern) - cairo_context.new_path() - cairo_context.rectangle(x, y, width, height) - if fraction >= 1.0: - cairo_context.fill() - else: - cairo_context.clip() - cairo_context.paint_with_alpha(fraction) - cairo_context.restore() - - def draw_rect(self, context, dest_x, dest_y, source_x, source_y, - width, height, fraction=1.0): - - self._align_pattern(dest_x-source_x, dest_y-source_y) - cairo_context = context.context - cairo_context.save() - cairo_context.set_source(self.pattern) - cairo_context.new_path() - cairo_context.rectangle(dest_x, dest_y, width, height) - if fraction >= 1.0: - cairo_context.fill() - else: - cairo_context.clip() - cairo_context.paint_with_alpha(fraction) - cairo_context.restore() - -class DrawingStyle(object): - def __init__(self, widget, use_base_color=False, state=None): - if state is None: - state = widget._widget.state - self.use_custom_style = widget.use_custom_style - self.style = widget._widget.style - self.text_color = widget.convert_gtk_color(self.style.text[state]) - if use_base_color: - self.bg_color = widget.convert_gtk_color(self.style.base[state]) - else: - self.bg_color = widget.convert_gtk_color(self.style.bg[state]) - -class DrawingContext(object): - """DrawingContext. This basically just wraps a Cairo context and adds a - couple convenience methods. - """ - - def __init__(self, window, drawing_area, expose_area): - self.window = window - self.context = window.cairo_create() - self.context.rectangle(expose_area.x, expose_area.y, - expose_area.width, expose_area.height) - self.context.clip() - self.width = drawing_area.width - self.height = drawing_area.height - self.context.translate(drawing_area.x, drawing_area.y) - - def __getattr__(self, name): - return getattr(self.context, name) - - def set_color(self, (red, green, blue), alpha=1.0): - self.context.set_source_rgba(red, green, blue, alpha) - - def set_shadow(self, color, opacity, offset, blur_radius): - pass - - def gradient_fill(self, gradient): - old_source = self.context.get_source() - self.context.set_source(gradient.pattern) - self.context.fill() - self.context.set_source(old_source) - - def gradient_fill_preserve(self, gradient): - old_source = self.context.get_source() - self.context.set_source(gradient.pattern) - self.context.fill_preserve() - self.context.set_source(old_source) - -class Gradient(object): - def __init__(self, x1, y1, x2, y2): - self.pattern = cairo.LinearGradient(x1, y1, x2, y2) - - def set_start_color(self, (red, green, blue)): - self.pattern.add_color_stop_rgb(0, red, green, blue) - - def set_end_color(self, (red, green, blue)): - self.pattern.add_color_stop_rgb(1, red, green, blue) - -class CustomDrawingMixin(object): - def do_expose_event(self, event): - wrapper = wrappermap.wrapper(self) - if self.flags() & gtk.NO_WINDOW: - drawing_area = self.allocation - else: - drawing_area = gtk.gdk.Rectangle(0, 0, - self.allocation.width, self.allocation.height) - context = DrawingContext(event.window, drawing_area, event.area) - context.style = DrawingStyle(wrapper) - if self.flags() & gtk.CAN_FOCUS: - focus_space = (self.style_get_property('focus-padding') + - self.style_get_property('focus-line-width')) - if not wrapper.squish_width: - context.width -= focus_space * 2 - translate_x = focus_space - else: - translate_x = 0 - if not wrapper.squish_height: - context.height -= focus_space * 2 - translate_y = focus_space - else: - translate_y = 0 - context.translate(translate_x, translate_y) - wrapper.layout_manager.update_cairo_context(context.context) - self.draw(wrapper, context) - - def draw(self, wrapper, context): - wrapper.layout_manager.reset() - wrapper.draw(context, wrapper.layout_manager) - - def do_size_request(self, requesition): - wrapper = wrappermap.wrapper(self) - width, height = wrapper.size_request(wrapper.layout_manager) - requesition.width = width - requesition.height = height - if self.flags() & gtk.CAN_FOCUS: - focus_space = (self.style_get_property('focus-padding') + - self.style_get_property('focus-line-width')) - if not wrapper.squish_width: - requesition.width += focus_space * 2 - if not wrapper.squish_height: - requesition.height += focus_space * 2 - -class MiroDrawingArea(CustomDrawingMixin, gtk.Widget): - def __init__(self): - gtk.Widget.__init__(self) - CustomDrawingMixin.__init__(self) - self.set_flags(gtk.NO_WINDOW) - -class BackgroundWidget(CustomDrawingMixin, gtk.Bin): - def do_size_request(self, requesition): - CustomDrawingMixin.do_size_request(self, requesition) - if self.get_child(): - child_width, child_height = self.get_child().size_request() - requesition.width = max(child_width, requesition.width) - requesition.height = max(child_height, requesition.height) - - def do_expose_event(self, event): - CustomDrawingMixin.do_expose_event(self, event) - if self.get_child(): - self.propagate_expose(self.get_child(), event) - - def do_size_allocate(self, allocation): - gtk.Bin.do_size_allocate(self, allocation) - if self.get_child(): - self.get_child().size_allocate(allocation) - -gobject.type_register(MiroDrawingArea) -gobject.type_register(BackgroundWidget) - -class Drawable: - def __init__(self): - self.squish_width = self.squish_height = False - - def set_squish_width(self, setting): - self.squish_width = setting - - def set_squish_height(self, setting): - self.squish_height = setting - - def set_widget(self, drawing_widget): - if self.is_opaque() and 0: - box = gtk.EventBox() - box.add(drawing_widget) - Widget.set_widget(self, box) - else: - Widget.set_widget(self, drawing_widget) - self.layout_manager = LayoutManager(self._widget) - - def size_request(self, layout_manager): - return 0, 0 - - def draw(self, context, layout_manager): - pass - - def is_opaque(self): - return False - -class DrawingArea(Drawable, Widget): - def __init__(self): - Widget.__init__(self) - Drawable.__init__(self) - self.set_widget(MiroDrawingArea()) - -class Background(Drawable, Bin): - def __init__(self): - Bin.__init__(self) - Drawable.__init__(self) - self.set_widget(BackgroundWidget()) diff --git a/mvc/widgets/gtk/gtkmenus.py b/mvc/widgets/gtk/gtkmenus.py deleted file mode 100644 index 926ba15..0000000 --- a/mvc/widgets/gtk/gtkmenus.py +++ /dev/null @@ -1,404 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 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. - -"""gtkmenus.py -- Manage menu layout.""" - -import gtk - -from mvc.widgets import app - -import base -import keymap -import wrappermap - -def _setup_accel(widget, name, shortcut=None): - """Setup accelerators for a menu item. - - This method sets an accel path for the widget and optionally connects a - shortcut to that accel path. - """ - # The GTK docs say that we should set the path using this form: - # <Window-Name>/Menu/Submenu/MenuItem - # ...but this is hard to do because we don't yet know what window/menu - # this menu item is going to be added to. gtk.Action and gtk.ActionGroup - # don't follow the above suggestion, so we don't need to either. - path = "<MiroActions>/MenuBar/%s" % name - widget.set_accel_path(path) - if shortcut is not None: - accel_string = keymap.get_accel_string(shortcut) - key, mods = gtk.accelerator_parse(accel_string) - if gtk.accel_map_lookup_entry(path) is None: - gtk.accel_map_add_entry(path, key, mods) - else: - gtk.accel_map_change_entry(path, key, mods, True) - -# map menu names to GTK stock ids. -_STOCK_IDS = { - "SaveItem": gtk.STOCK_SAVE, - "CopyItemURL": gtk.STOCK_COPY, - "RemoveItems": gtk.STOCK_REMOVE, - "StopItem": gtk.STOCK_MEDIA_STOP, - "NextItem": gtk.STOCK_MEDIA_NEXT, - "PreviousItem": gtk.STOCK_MEDIA_PREVIOUS, - "PlayPauseItem": gtk.STOCK_MEDIA_PLAY, - "Open": gtk.STOCK_OPEN, - "EditPreferences": gtk.STOCK_PREFERENCES, - "Quit": gtk.STOCK_QUIT, - "Help": gtk.STOCK_HELP, - "About": gtk.STOCK_ABOUT, - "Translate": gtk.STOCK_EDIT -} -try: - _STOCK_IDS['Fullscreen'] = gtk.STOCK_FULLSCREEN -except AttributeError: - # fullscreen not available on all GTK versions - pass - -class MenuItemBase(base.Widget): - """Base class for MenuItem and Separator.""" - - def show(self): - """Show this menu item.""" - self._widget.show() - - def hide(self): - """Hide and disable this menu item.""" - self._widget.hide() - - def remove_from_parent(self): - """Remove this menu item from it's parent Menu.""" - parent_menu = self._widget.get_parent() - if parent_menu is None: - return - parent_menu_item = parent_menu.get_attach_widget() - if parent_menu_item is None: - return - parent_menu_item.remove(self._widget) - - def _set_accel_group(self, accel_group): - # menu items don't care about the accel group, their parent Menu - # handles it for them - pass - -class MenuItem(MenuItemBase): - """Single item in the menu that can be clicked - - :param label: The label it has (must be internationalized) - :param name: String identifier for this item - :param shortcut: Shortcut object to use - - Signals: - - activate: menu item was clicked - - Example: - - >>> MenuItem(_("Preferences"), "EditPreferences") - >>> MenuItem(_("Cu_t"), "ClipboardCut", Shortcut("x", MOD)) - >>> MenuItem(_("_Update Podcasts and Library"), "UpdatePodcasts", - ... (Shortcut("r", MOD), Shortcut(F5))) - >>> MenuItem(_("_Play"), "PlayPauseItem", - ... play=_("_Play"), pause=_("_Pause")) - """ - - def __init__(self, label, name, shortcut=None): - MenuItemBase.__init__(self) - self.name = name - self.set_widget(self.make_widget(label)) - self.activate_id = self.wrapped_widget_connect('activate', - self._on_activate) - self._widget.show() - self.create_signal('activate') - _setup_accel(self._widget, self.name, shortcut) - - def _on_activate(self, menu_item): - self.emit('activate') - gtk_menubar = self._find_menubar() - if gtk_menubar is not None: - try: - menubar = wrappermap.wrapper(gtk_menubar) - except KeyError: - logging.exception('menubar activate: ' - 'no wrapper for gtbbk.MenuBar') - else: - menubar.emit('activate', self.name) - - def _find_menubar(self): - """Find the MenuBar that this menu item is attached to.""" - menu_item = self._widget - while True: - parent_menu = menu_item.get_parent() - if isinstance(parent_menu, gtk.MenuBar): - return parent_menu - elif parent_menu is None: - return None - menu_item = parent_menu.get_attach_widget() - if menu_item is None: - return None - - def make_widget(self, label): - """Create the menu item to use for this widget. - - Subclasses will probably want to override this. - """ - if self.name in _STOCK_IDS: - mi = gtk.ImageMenuItem(stock_id=_STOCK_IDS[self.name]) - mi.set_label(label) - return mi - else: - return gtk.MenuItem(label) - - def set_label(self, new_label): - self._widget.set_label(new_label) - - def get_label(self): - self._widget.get_label() - -class CheckMenuItem(MenuItem): - """MenuItem that toggles on/off""" - - def make_widget(self, label): - return gtk.CheckMenuItem(label) - - def set_state(self, active): - # prevent the activate signal from fireing in response to us manually - # changing a value - self._widget.handler_block(self.activate_id) - if active is not None: - self._widget.set_inconsistent(False) - self._widget.set_active(active) - else: - self._widget.set_inconsistent(True) - self._widget.set_active(False) - self._widget.handler_unblock(self.activate_id) - - def get_state(self): - return self._widget.get_active() - -class RadioMenuItem(CheckMenuItem): - """MenuItem that toggles on/off and is grouped with other RadioMenuItems. - """ - - def make_widget(self, label): - widget = gtk.RadioMenuItem() - widget.set_label(label) - return widget - - def set_group(self, group_item): - self._widget.set_group(group_item._widget) - - def remove_from_group(self): - """Remove this RadioMenuItem from its current group.""" - self._widget.set_group(None) - - def _on_activate(self, menu_item): - # GTK sends the activate signal for both the radio button that's - # toggled on and the one that gets turned off. Just emit our signal - # for the active radio button. - if self.get_state(): - MenuItem._on_activate(self, menu_item) - -class Separator(MenuItemBase): - """Separator item for menus""" - - def __init__(self): - MenuItemBase.__init__(self) - self.set_widget(gtk.SeparatorMenuItem()) - self._widget.show() - # Set name to be None just so that it has a similar API to other menu - # items. - self.name = None - -class MenuShell(base.Widget): - """Common code shared between Menu and MenuBar. - - Subclasses must define a _menu attribute that's a gtk.MenuShell subclass. - """ - - def __init__(self): - base.Widget.__init__(self) - self._accel_group = None - self.children = [] - - def append(self, menu_item): - """Add a menu item to the end of this menu.""" - self.children.append(menu_item) - menu_item._set_accel_group(self._accel_group) - self._menu.append(menu_item._widget) - - def insert(self, index, menu_item): - """Insert a menu item in the middle of this menu.""" - self.children.insert(index, menu_item) - menu_item._set_accel_group(self._accel_group) - self._menu.insert(menu_item._widget, index) - - def remove(self, menu_item): - """Remove a child menu item. - - :raises ValueError: menu_item is not a child of this menu - """ - self.children.remove(menu_item) - self._menu.remove(menu_item._widget) - menu_item._set_accel_group(None) - - def index(self, name): - """Get the position of a menu item in this list. - - :param name: name of the menu - :returns: index of the menu item, or -1 if not found. - """ - for i, menu_item in enumerate(self.children): - if menu_item.name == name: - return i - return -1 - - def get_children(self): - """Get the child menu items in order.""" - return list(self.children) - - def find(self, name): - """Search for a menu or menu item - - This method recursively searches the entire menu structure for a Menu - or MenuItem object with a given name. - - :raises KeyError: name not found - """ - found = self._find(name) - if found is None: - raise KeyError(name) - else: - return found - - def _find(self, name): - """Low-level helper-method for find(). - - :returns: found menu item or None. - """ - for menu_item in self.get_children(): - if menu_item.name == name: - return menu_item - if isinstance(menu_item, MenuShell): - submenu_find = menu_item._find(name) - if submenu_find is not None: - return submenu_find - return None - -class Menu(MenuShell): - """A Menu holds a list of MenuItems and Menus. - - Example: - >>> Menu(_("P_layback"), "Playback", [ - ... MenuItem(_("_Foo"), "Foo"), - ... MenuItem(_("_Bar"), "Bar") - ... ]) - >>> Menu("", "toplevel", [ - ... Menu(_("_File"), "File", [ ... ]) - ... ]) - """ - - def __init__(self, label, name, child_items): - MenuShell.__init__(self) - self.set_widget(gtk.MenuItem(label)) - self._widget.show() - self.name = name - # set up _menu for the MenuShell code - self._menu = gtk.Menu() - _setup_accel(self._menu, self.name) - self._widget.set_submenu(self._menu) - for item in child_items: - self.append(item) - - def show(self): - """Show this menu.""" - self._widget.show() - - def hide(self): - """Hide this menu.""" - self._widget.hide() - - def _set_accel_group(self, accel_group): - """Set the accel group for this widget. - - Accel groups get created by the MenuBar. Whenever a menu or menu item - is added to that menu bar, the parent calls _set_accel_group() to give - the accel group to the child. - """ - if accel_group == self._accel_group: - return - self._menu.set_accel_group(accel_group) - self._accel_group = accel_group - for child in self.children: - child._set_accel_group(accel_group) - -class MenuBar(MenuShell): - """Displays a list of Menu items. - - Signals: - - - activate(menu_bar, name): a menu item was activated - """ - - def __init__(self): - """Create a new MenuBar - - :param name: string id to use for our action group - """ - MenuShell.__init__(self) - self.create_signal('activate') - self.set_widget(gtk.MenuBar()) - self._widget.show() - self._accel_group = gtk.AccelGroup() - # set up _menu for the MenuShell code - self._menu = self._widget - - def get_accel_group(self): - return self._accel_group - -class MainWindowMenuBar(MenuBar): - """MenuBar for the main window. - - This gets installed into app.widgetapp.menubar on GTK. - """ - def add_initial_menus(self, menus): - """Add the initial set of menus. - - We modify the menu structure slightly for GTK. - """ - for menu in menus: - self.append(menu) - self._modify_initial_menus() - - def _modify_initial_menus(self): - """Update the portable root menu with GTK-specific stuff.""" - # on linux, we don't have a CheckVersion option because - # we update with the package system. - #this_platform = app.config.get(prefs.APP_PLATFORM) - #if this_platform == 'linux': - # self.find("CheckVersion").remove_from_parent() - #app.video_renderer.setup_subtitle_encoding_menu() diff --git a/mvc/widgets/gtk/keymap.py b/mvc/widgets/gtk/keymap.py deleted file mode 100644 index cf341ff..0000000 --- a/mvc/widgets/gtk/keymap.py +++ /dev/null @@ -1,94 +0,0 @@ -# @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. - -"""keymap.py -- Map portable key values to GTK ones. -""" - -import gtk - -from mvc.widgets import keyboard - -menubar_mod_map = { - keyboard.MOD: '<Ctrl>', - keyboard.CTRL: '<Ctrl>', - keyboard.ALT: '<Alt>', - keyboard.SHIFT: '<Shift>', -} - -menubar_key_map = { - keyboard.RIGHT_ARROW: 'Right', - keyboard.LEFT_ARROW: 'Left', - keyboard.UP_ARROW: 'Up', - keyboard.DOWN_ARROW: 'Down', - keyboard.SPACE: 'space', - keyboard.ENTER: 'Return', - keyboard.DELETE: 'Delete', - keyboard.BKSPACE: 'BackSpace', - keyboard.ESCAPE: 'Escape', - '>': 'greater', - '<': 'less' -} -for i in range(1, 13): - name = 'F%d' % i - menubar_key_map[getattr(keyboard, name)] = name - -# These are reversed versions of menubar_key_map and menubar_mod_map -gtk_key_map = dict((i[1], i[0]) for i in menubar_key_map.items()) - -def get_accel_string(shortcut): - mod_str = ''.join(menubar_mod_map[mod] for mod in shortcut.modifiers) - key_str = menubar_key_map.get(shortcut.shortcut, shortcut.shortcut) - return mod_str + key_str - -def translate_gtk_modifiers(event): - """Convert a keypress event to a set of modifiers from the shortcut - module. - """ - modifiers = set() - if event.state & gtk.gdk.CONTROL_MASK: - modifiers.add(keyboard.CTRL) - if event.state & gtk.gdk.MOD1_MASK: - modifiers.add(keyboard.ALT) - if event.state & gtk.gdk.SHIFT_MASK: - modifiers.add(keyboard.SHIFT) - return modifiers - -def translate_gtk_event(event): - """Convert a GTK key event into the tuple (key, modifiers) where - key and modifiers are from the shortcut module. - """ - gtk_keyval = gtk.gdk.keyval_name(event.keyval) - if gtk_keyval == None: - return None - if len(gtk_keyval) == 1: - key = gtk_keyval - else: - key = gtk_key_map.get(gtk_keyval) - modifiers = translate_gtk_modifiers(event) - return key, modifiers diff --git a/mvc/widgets/gtk/layout.py b/mvc/widgets/gtk/layout.py deleted file mode 100644 index d887fcb..0000000 --- a/mvc/widgets/gtk/layout.py +++ /dev/null @@ -1,227 +0,0 @@ -# @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. - -""".layout -- Layout widgets. """ - -import gtk - -from mvc.utils import Matrix -from .base import Widget, Bin - -class Box(Widget): - def __init__(self, spacing=0): - Widget.__init__(self) - self.children = set() - self.set_widget(self.WIDGET_CLASS(spacing=spacing)) - - def pack_start(self, widget, expand=False, padding=0): - self._widget.pack_start(widget._widget, expand, fill=True, - padding=padding) - widget._widget.show() - self.children.add(widget) - - def pack_end(self, widget, expand=False, padding=0): - self._widget.pack_end(widget._widget, expand, fill=True, - padding=padding) - widget._widget.show() - self.children.add(widget) - - def remove(self, widget): - widget._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(widget._widget) - self.children.remove(widget) - - def enable(self): - for mem in self.children: - mem.enable() - - def disable(self): - for mem in self.children: - mem.disable() - -class HBox(Box): - WIDGET_CLASS = gtk.HBox - -class VBox(Box): - WIDGET_CLASS = gtk.VBox - -class Alignment(Bin): - def __init__(self, xalign=0, yalign=0, xscale=0, yscale=0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - Bin.__init__(self) - self.set_widget(gtk.Alignment(xalign, yalign, xscale, yscale)) - self.set_padding(top_pad, bottom_pad, left_pad, right_pad) - - def set(self, xalign=0, yalign=0, xscale=0, yscale=0): - self._widget.set(xalign, yalign, xscale, yscale) - - def set_padding(self, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - self._widget.set_padding(top_pad, bottom_pad, left_pad, right_pad) - -class DetachedWindowHolder(Alignment): - def __init__(self): - Alignment.__init__(self, xscale=1, yscale=1) - -class Splitter(Widget): - def __init__(self): - """Create a new splitter.""" - Widget.__init__(self) - self.set_widget(gtk.HPaned()) - - def set_left(self, widget): - """Set the left child widget.""" - self.left = widget - self._widget.pack1(widget._widget, resize=False, shrink=False) - widget._widget.show() - - def set_right(self, widget): - """Set the right child widget. """ - self.right = widget - self._widget.pack2(widget._widget, resize=True, shrink=False) - widget._widget.show() - - def remove_left(self): - """Remove the left child widget.""" - if self.left is not None: - self.left._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(self.left._widget) - self.left = None - - def remove_right(self): - """Remove the right child widget.""" - if self.right is not None: - self.right._widget.hide() # otherwise gtkmozembed gets confused - self._widget.remove(self.right._widget) - self.right = None - - def set_left_width(self, width): - self._widget.set_position(width) - - def get_left_width(self): - return self._widget.get_position() - - def set_right_width(self, width): - self._widget.set_position(self.width - width) - # We should take into account the width of the bar, but this seems - # good enough. - -class Table(Widget): - """Lays out widgets in a table. It works very similar to the GTK Table - widget, or an HTML table. - """ - def __init__(self, columns, rows): - Widget.__init__(self) - self.set_widget(gtk.Table(rows, columns, homogeneous=False)) - self.children = Matrix(columns, rows) - - def pack(self, widget, column, row, column_span=1, row_span=1): - """Add a widget to the table. - """ - self.children[column, row] = widget - self._widget.attach(widget._widget, column, column + column_span, - row, row + row_span) - widget._widget.show() - - def remove(self, widget): - widget._widget.hide() # otherwise gtkmozembed gets confused - self.children.remove(widget) - self._widget.remove(widget._widget) - - def set_column_spacing(self, spacing): - self._widget.set_col_spacings(spacing) - - def set_row_spacing(self, spacing): - self._widget.set_row_spacings(spacing) - - def enable(self, row=None, column=None): - if row != None and column != None: - if self.children[column, row]: - self.children[column, row].enable() - elif row != None: - for mem in self.children.row(row): - if mem: mem.enable() - elif column != None: - for mem in self.children.column(column): - if mem: mem.enable() - else: - for mem in self.children: - if mem: mem.enable() - - def disable(self, row=None, column=None): - if row != None and column != None: - if self.children[column, row]: - self.children[column, row].disable() - elif row != None: - for mem in self.children.row(row): - if mem: mem.disable() - elif column != None: - for mem in self.children.column(column): - if mem: mem.disable() - else: - for mem in self.children: - if mem: mem.disable() - -class TabContainer(Widget): - def __init__(self, xalign=0, yalign=0, xscale=0, yscale=0, - top_pad=0, bottom_pad=0, left_pad=0, right_pad=0): - Widget.__init__(self) - self.set_widget(gtk.Notebook()) - self._widget.set_tab_pos(gtk.POS_TOP) - self.children = [] - self._page_to_select = None - self.wrapped_widget_connect('realize', self._on_realize) - - def _on_realize(self, widget): - if self._page_to_select is not None: - self._widget.set_current_page(self._page_to_select) - self._page_to_select = None - - def append_tab(self, child_widget, text, image=None): - if image is not None: - label_widget = gtk.VBox(spacing=2) - image_widget = gtk.Image() - image_widget.set_from_pixbuf(image.pixbuf) - label_widget.pack_start(image_widget) - label_widget.pack_start(gtk.Label(text)) - label_widget.show_all() - else: - label_widget = gtk.Label(text) - - # switch from a center align to a top align - child_widget.set(0, 0, 1, 0) - child_widget.set_padding(10, 10, 10, 10) - - self._widget.append_page(child_widget._widget, label_widget) - self.children.append(child_widget) - - def select_tab(self, index): - if self._widget.flags() & gtk.REALIZED: - self._widget.set_current_page(index) - else: - self._page_to_select = index diff --git a/mvc/widgets/gtk/layoutmanager.py b/mvc/widgets/gtk/layoutmanager.py deleted file mode 100644 index fb60049..0000000 --- a/mvc/widgets/gtk/layoutmanager.py +++ /dev/null @@ -1,550 +0,0 @@ -# @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. - -"""drawing.py -- Contains the LayoutManager class. LayoutManager is -handles laying out complex objects for the custom drawing code like -text blocks and buttons. -""" - -import math - -import cairo -import gtk -import pango - -from mvc import utils - -use_native_buttons = False # not implemented in MVC - -class FontCache(utils.Cache): - def get(self, context, description, scale_factor, bold, italic): - key = (context, description, scale_factor, bold, italic) - return utils.Cache.get(self, key) - - def create_new_value(self, key, invalidator=None): - (context, description, scale_factor, bold, italic) = key - return Font(context, description, scale_factor, bold, italic) - -_font_cache = FontCache(512) - -class LayoutManager(object): - def __init__(self, widget): - self.pango_context = widget.get_pango_context() - self.update_style(widget.style) - self.update_direction(widget.get_direction()) - widget.connect('style-set', self.on_style_set) - widget.connect('direction-changed', self.on_direction_changed) - self.widget = widget - self.reset() - - def reset(self): - self.current_font = self.font(1.0) - self.text_color = (0, 0, 0) - self.text_shadow = None - - def on_style_set(self, widget, previous_style): - old_font_desc = self.style_font_desc - self.update_style(widget.style) - if self.style_font_desc != old_font_desc: - # bug #17423 font changed, so the widget's width might have changed - widget.queue_resize() - - def on_direction_changed(self, widget, previous_direction): - self.update_direction(widget.get_direction()) - - def update_style(self, style): - self.style_font_desc = style.font_desc - self.style = style - - def update_direction(self, direction): - if direction == gtk.TEXT_DIR_RTL: - self.pango_context.set_base_dir(pango.DIRECTION_RTL) - else: - self.pango_context.set_base_dir(pango.DIRECTION_LTR) - - def font(self, scale_factor, bold=False, italic=False, family=None): - return _font_cache.get(self.pango_context, self.style_font_desc, - scale_factor, bold, italic) - - def set_font(self, scale_factor, bold=False, italic=False, family=None): - self.current_font = self.font(scale_factor, bold, italic) - - def set_text_color(self, color): - self.text_color = color - - def set_text_shadow(self, shadow): - self.text_shadow = shadow - - def textbox(self, text, underline=False): - textbox = TextBox(self.pango_context, self.current_font, - self.text_color, self.text_shadow) - textbox.set_text(text, underline=underline) - return textbox - - def button(self, text, pressed=False, disabled=False, style='normal'): - if style == 'webby': - return StyledButton(text, self.pango_context, self.current_font, - pressed, disabled) - elif use_native_buttons: - return NativeButton(text, self.pango_context, self.current_font, - pressed, self.style, self.widget) - else: - return StyledButton(text, self.pango_context, self.current_font, - pressed) - - def update_cairo_context(self, cairo_context): - cairo_context.update_context(self.pango_context) - -class Font(object): - def __init__(self, context, style_font_desc, scale, bold, italic): - self.context = context - self.description = style_font_desc.copy() - self.description.set_size(int(scale * style_font_desc.get_size())) - if bold: - self.description.set_weight(pango.WEIGHT_BOLD) - if italic: - self.description.set_style(pango.STYLE_ITALIC) - self.font_metrics = None - - def get_font_metrics(self): - if self.font_metrics is None: - self.font_metrics = self.context.get_metrics(self.description) - return self.font_metrics - - def ascent(self): - return pango.PIXELS(self.get_font_metrics().get_ascent()) - - def descent(self): - return pango.PIXELS(self.get_font_metrics().get_descent()) - - def line_height(self): - metrics = self.get_font_metrics() - # the +1: some glyphs can be slightly taller than ascent+descent - # (#17329) - return (pango.PIXELS(metrics.get_ascent()) + - pango.PIXELS(metrics.get_descent()) + 1) - -class TextBox(object): - def __init__(self, context, font, color, shadow): - self.layout = pango.Layout(context) - self.layout.set_wrap(pango.WRAP_WORD_CHAR) - self.font = font - self.color = color - self.layout.set_font_description(font.description.copy()) - self.width = self.height = None - self.shadow = shadow - - def set_text(self, text, font=None, color=None, underline=False): - self.text_chunks = [] - self.attributes = [] - self.text_length = 0 - self.underlines = [] - self.append_text(text, font, color, underline) - - def append_text(self, text, font=None, color=None, underline=False): - if text == None: - text = u"" - startpos = self.text_length - self.text_chunks.append(text) - endpos = self.text_length = self.text_length + len(text) - if font is not None: - attr = pango.AttrFontDesc(font.description, startpos, endpos) - self.attributes.append(attr) - if underline: - self.underlines.append((startpos, endpos)) - if color: - def convert(value): - return int(round(value * 65535)) - attr = pango.AttrForeground(convert(color[0]), convert(color[1]), - convert(color[2]), startpos, endpos) - self.attributes.append(attr) - self.text_set = False - - def set_width(self, width): - if width is not None: - self.layout.set_width(int(width * pango.SCALE)) - else: - self.layout.set_width(-1) - self.width = width - - def set_height(self, height): - # if height is not None: - # # not sure why set_height isn't in the python bindings, but it - # # isn't - # pygtkhacks.set_pango_layout_height(self.layout, - # int(height * pango.SCALE)) - self.height = height - - def set_wrap_style(self, wrap): - if wrap == 'word': - self.layout.set_wrap(pango.WRAP_WORD_CHAR) - elif wrap == 'char' or wrap == 'truncated-char': - self.layout.set_wrap(pango.WRAP_CHAR) - else: - raise ValueError("Unknown wrap value: %s" % wrap) - if wrap == 'truncated-char': - self.layout.set_ellipsize(pango.ELLIPSIZE_END) - else: - self.layout.set_ellipsize(pango.ELLIPSIZE_NONE) - - def set_alignment(self, align): - if align == 'left': - self.layout.set_alignment(pango.ALIGN_LEFT) - elif align == 'right': - self.layout.set_alignment(pango.ALIGN_RIGHT) - elif align == 'center': - self.layout.set_alignment(pango.ALIGN_CENTER) - else: - raise ValueError("Unknown align value: %s" % align) - - def ensure_layout(self): - if not self.text_set: - text = ''.join(self.text_chunks) - if len(text) > 100: - text = text[:self._calc_text_cutoff()] - self.layout.set_text(text) - attr_list = pango.AttrList() - for attr in self.attributes: - attr_list.insert(attr) - self.layout.set_attributes(attr_list) - self.text_set = True - - def _calc_text_cutoff(self): - """This method is a bit of a hack... GTK slows down if we pass too - much text to the layout. Even text that falls below our height has a - performance penalty. Try not to have too much more than is necessary. - """ - if None in (self.width, self.height): - return -1 - - chars_per_line = (self.width * pango.SCALE // - self.font.get_font_metrics().get_approximate_char_width()) - lines_available = self.height // self.font.line_height() - # overestimate these because it's better to have too many characters - # than too little. - return int(chars_per_line * lines_available * 1.2) - - def line_count(self): - self.ensure_layout() - return self.layout.get_line_count() - - def get_size(self): - self.ensure_layout() - return self.layout.get_pixel_size() - - def char_at(self, x, y): - self.ensure_layout() - x *= pango.SCALE - y *= pango.SCALE - width, height = self.layout.get_size() - if 0 <= x < width and 0 <= y < height: - index, leading = self.layout.xy_to_index(x, y) - # xy_to_index returns the nearest character, but that - # doesn't mean the user actually clicked on it. Double - # check that (x, y) is actually inside that char's - # bounding box - char_x, char_y, char_w, char_h = self.layout.index_to_pos(index) - if char_w > 0: # the glyph is LTR - left = char_x - right = char_x + char_w - else: # the glyph is RTL - left = char_x + char_w - right = char_x - if left <= x < right: - return index - return None - - - def draw(self, context, x, y, width, height): - self.set_width(width) - self.set_height(height) - self.ensure_layout() - cairo_context = context.context - cairo_context.save() - underline_drawer = UnderlineDrawer(self.underlines) - if self.shadow: - # draw shadow first so that it's underneath the regular text - # FIXME: we don't use the blur_radius setting - cairo_context.set_source_rgba(self.shadow.color[0], - self.shadow.color[1], self.shadow.color[2], - self.shadow.opacity) - self._draw_layout(context, x + self.shadow.offset[0], - y + self.shadow.offset[1], width, height, - underline_drawer) - cairo_context.set_source_rgb(*self.color) - self._draw_layout(context, x, y, width, height, underline_drawer) - cairo_context.restore() - cairo_context.new_path() - - def _draw_layout(self, context, x, y, width, height, underline_drawer): - line_height = 0 - alignment = self.layout.get_alignment() - for i in xrange(self.layout.get_line_count()): - line = self.layout.get_line_readonly(i) - extents = line.get_pixel_extents()[1] - next_line_height = line_height + extents[3] - if next_line_height > height: - break - if alignment == pango.ALIGN_CENTER: - line_x = max(x, x + (width - extents[2]) / 2.0) - elif alignment == pango.ALIGN_RIGHT: - line_x = max(x, x + width - extents[2]) - else: - line_x = x - baseline = y + line_height + pango.ASCENT(extents) - context.move_to(line_x, baseline) - context.context.show_layout_line(line) - underline_drawer.draw(context, line_x, baseline, line) - line_height = next_line_height - -class UnderlineDrawer(object): - """Class to draw our own underlines because cairo's don't look - that great at small fonts. We make sure that the underline is - always drawn at a pixel boundary and that there always is space - between the text and the baseline. - - This class makes a couple assumptions that might not be that - great. It assumes that the correct underline size is 1 pixel and - that the text color doesn't change in the middle of an underline. - """ - def __init__(self, underlines): - self.underline_iter = iter(underlines) - self.finished = False - self.next_underline() - - def next_underline(self): - try: - self.startpos, self.endpos = self.underline_iter.next() - except StopIteration: - self.finished = True - else: - # endpos is the char to stop underlining at - self.endpos -= 1 - - def draw(self, context, x, baseline, line): - baseline = round(baseline) + 0.5 - context.set_line_width(1) - while not self.finished and line.start_index <= self.startpos: - startpos = max(line.start_index, self.startpos) - endpos = min(self.endpos, line.start_index + line.length) - x1 = x + pango.PIXELS(line.index_to_x(startpos, 0)) - x2 = x + pango.PIXELS(line.index_to_x(endpos, 1)) - context.move_to(x1, baseline + 1) - context.line_to(x2, baseline + 1) - context.stroke() - if endpos < self.endpos: - break - else: - self.next_underline() - -class NativeButton(object): - ICON_PAD = 4 - - def __init__(self, text, context, font, pressed, style, widget): - self.layout = pango.Layout(context) - self.font = font - self.pressed = pressed - self.layout.set_font_description(font.description.copy()) - self.layout.set_text(text) - self.pad_x = style.xthickness + 11 - self.pad_y = style.ythickness + 1 - self.style = style - self.widget = widget - # The above code assumes an "inner-border" style property of - # 1. PyGTK doesn't seem to support Border objects very well, - # so can't get it from the widget style. - self.min_width = 0 - self.icon = None - - def set_min_width(self, width): - self.min_width = width - - def set_icon(self, icon): - self.icon = icon - - def get_size(self): - width, height = self.layout.get_pixel_size() - if self.icon: - width += self.icon.width + self.ICON_PAD - height = max(height, self.icon.height) - width += self.pad_x * 2 - height += self.pad_y * 2 - return max(self.min_width, width), height - - def draw(self, context, x, y, width, height): - text_width, text_height = self.layout.get_pixel_size() - if self.icon: - inner_width = text_width + self.icon.width + self.ICON_PAD - # calculate the icon position x and y are still in cairo - # coordinates - icon_x = x + (width - inner_width) / 2.0 - icon_y = y + (height - self.icon.height) / 2.0 - text_x = icon_x + self.icon.width + self.ICON_PAD - else: - text_x = x + (width - text_width) / 2.0 - text_y = y + (height - text_height) / 2.0 - - x, y = context.context.user_to_device(x, y) - text_x, text_y = context.context.user_to_device(text_x, text_y) - # Hmm, maybe we should somehow support floating point numbers - # here, but I don't know how to. - x, y, width, height = (int(f) for f in (x, y, width, height)) - context.context.get_target().flush() - self.draw_box(context.window, x, y, width, height) - self.draw_text(context.window, text_x, text_y) - if self.icon: - self.icon.draw(context, icon_x, icon_y, self.icon.width, - self.icon.height) - - def draw_box(self, window, x, y, width, height): - if self.pressed: - shadow = gtk.SHADOW_IN - state = gtk.STATE_ACTIVE - else: - shadow = gtk.SHADOW_OUT - state = gtk.STATE_NORMAL - if 'QtCurveStyle' in str(self.style): - # This is a horrible hack for the libqtcurve library. See - # http://bugzilla.pculture.org/show_bug.cgi?id=10380 for - # details - widget = window.get_user_data() - else: - widget = self.widget - - self.style.paint_box(window, state, shadow, None, widget, "button", - int(x), int(y), int(width), int(height)) - - def draw_text(self, window, x, y): - if self.pressed: - state = gtk.STATE_ACTIVE - else: - state = gtk.STATE_NORMAL - self.style.paint_layout(window, state, True, None, None, None, - int(x), int(y), self.layout) - -class StyledButton(object): - PAD_HORIZONTAL = 4 - PAD_VERTICAL = 3 - TOP_COLOR = (1, 1, 1) - BOTTOM_COLOR = (0.86, 0.86, 0.86) - LINE_COLOR_TOP = (0.71, 0.71, 0.71) - LINE_COLOR_BOTTOM = (0.45, 0.45, 0.45) - TEXT_COLOR = (0.184, 0.184, 0.184) - DISABLED_COLOR = (0.86, 0.86, 0.86) - DISABLED_TEXT_COLOR = (0.5, 0.5, 0.5) - ICON_PAD = 8 - - def __init__(self, text, context, font, pressed, disabled=False): - self.layout = pango.Layout(context) - self.font = font - self.layout.set_font_description(font.description.copy()) - self.layout.set_text(text) - self.min_width = 0 - self.pressed = pressed - self.disabled = disabled - self.icon = None - - def set_icon(self, icon): - self.icon = icon - - def set_min_width(self, width): - self.min_width = width - - def get_size(self): - width, height = self.layout.get_pixel_size() - if self.icon: - width += self.icon.width + self.ICON_PAD - height = max(height, self.icon.height) - height += self.PAD_VERTICAL * 2 - if height % 2 == 1: - # make height even so that the radius of our circle is - # whole - height += 1 - width += self.PAD_HORIZONTAL * 2 + height - return max(self.min_width, width), height - - def draw_path(self, context, x, y, width, height, radius): - inner_width = width - radius * 2 - context.move_to(x + radius, y) - context.rel_line_to(inner_width, 0) - context.arc(x + width - radius, y+radius, radius, -math.pi/2, - math.pi/2) - context.rel_line_to(-inner_width, 0) - context.arc(x + radius, y+radius, radius, math.pi/2, -math.pi/2) - - def draw_button(self, context, x, y, width, height, radius): - context.context.save() - self.draw_path(context, x, y, width, height, radius) - if self.disabled: - end_color = self.DISABLED_COLOR - start_color = self.DISABLED_COLOR - elif self.pressed: - end_color = self.TOP_COLOR - start_color = self.BOTTOM_COLOR - else: - context.set_line_width(1) - start_color = self.TOP_COLOR - end_color = self.BOTTOM_COLOR - gradient = cairo.LinearGradient(x, y, x, y + height) - gradient.add_color_stop_rgb(0, *start_color) - gradient.add_color_stop_rgb(1, *end_color) - context.context.set_source(gradient) - context.fill() - context.set_line_width(1) - self.draw_path(context, x+0.5, y+0.5, width, height, radius) - gradient = cairo.LinearGradient(x, y, x, y + height) - gradient.add_color_stop_rgb(0, *self.LINE_COLOR_TOP) - gradient.add_color_stop_rgb(1, *self.LINE_COLOR_BOTTOM) - context.context.set_source(gradient) - context.stroke() - context.context.restore() - - def draw(self, context, x, y, width, height): - radius = height / 2 - self.draw_button(context, x, y, width, height, radius) - - text_width, text_height = self.layout.get_pixel_size() - # draw the text in the center of the button - text_x = x + (width - text_width) / 2 - text_y = y + (height - text_height) / 2 - if self.icon: - icon_x = text_x - (self.icon.width + self.ICON_PAD) / 2 - text_x += (self.icon.width + self.ICON_PAD) / 2 - icon_y = y + (height - self.icon.height) / 2 - self.icon.draw(context, icon_x, icon_y, self.icon.width, - self.icon.height) - self.draw_text(context, text_x, text_y, width, height, radius) - - def draw_text(self, context, x, y, width, height, radius): - if self.disabled: - context.set_color(self.DISABLED_TEXT_COLOR) - else: - context.set_color(self.TEXT_COLOR) - context.move_to(x, y) - context.context.show_layout(self.layout) diff --git a/mvc/widgets/gtk/simple.py b/mvc/widgets/gtk/simple.py deleted file mode 100644 index f0921e0..0000000 --- a/mvc/widgets/gtk/simple.py +++ /dev/null @@ -1,313 +0,0 @@ -# @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. - -"""simple.py -- Collection of simple widgets.""" - -import gtk -import gobject -import pango - -from mvc.widgets import widgetconst -from .base import Widget, Bin - -class Image(object): - def __init__(self, path): - try: - self._set_pixbuf(gtk.gdk.pixbuf_new_from_file(path)) - except gobject.GError, ge: - raise ValueError("%s" % ge) - self.width = self.pixbuf.get_width() - self.height = self.pixbuf.get_height() - - def _set_pixbuf(self, pixbuf): - self.pixbuf = pixbuf - self.width = self.pixbuf.get_width() - self.height = self.pixbuf.get_height() - - def resize(self, width, height): - width = int(round(width)) - height = int(round(height)) - resized_pixbuf = self.pixbuf.scale_simple(width, height, - gtk.gdk.INTERP_BILINEAR) - return TransformedImage(resized_pixbuf) - - def resize_for_space(self, width, height): - """Returns an image scaled to fit into the specified space at the - correct height/width ratio. - """ - ratio = min(1.0 * width / self.width, 1.0 * height / self.height) - return self.resize(ratio * self.width, ratio * self.height) - - def crop_and_scale(self, src_x, src_y, src_width, src_height, dest_width, - dest_height): - """Crop an image then scale it. - - The image will be cropped to the rectangle (src_x, src_y, src_width, - src_height), that rectangle will be scaled to a new Image with tisez - (dest_width, dest_height) - """ - dest = gtk.gdk.Pixbuf(self.pixbuf.get_colorspace(), - self.pixbuf.get_has_alpha(), - self.pixbuf.get_bits_per_sample(), dest_width, dest_height) - - scale_x = dest_width / float(src_width) - scale_y = dest_height / float(src_height) - - self.pixbuf.scale(dest, 0, 0, dest_width, dest_height, - -src_x * scale_x, -src_y * scale_y, scale_x, scale_y, - gtk.gdk.INTERP_BILINEAR) - return TransformedImage(dest) - -class TransformedImage(Image): - def __init__(self, pixbuf): - # XXX intentionally not calling direct super's __init__; we should do - # this differently - self._set_pixbuf(pixbuf) - -class ImageDisplay(Widget): - def __init__(self, image=None): - Widget.__init__(self) - self.set_widget(gtk.Image()) - self.set_image(image) - - def set_image(self, image): - self.image = image - if image is not None: - self._widget.set_from_pixbuf(image.pixbuf) - else: - self._widget.clear() - -class AnimatedImageDisplay(Widget): - def __init__(self, path): - Widget.__init__(self) - self.set_widget(gtk.Image()) - self._animation = gtk.gdk.PixbufAnimation(path) - # Set to animate before we are shown and stop animating after - # we disappear. - self._widget.connect('map', lambda w: self._set_animate(True)) - self._widget.connect('unmap-event', - lambda w, a: self._set_animate(False)) - - def _set_animate(self, enabled): - if enabled: - self._widget.set_from_animation(self._animation) - else: - self._widget.clear() - -class Label(Widget): - """Widget that displays simple text.""" - def __init__(self, text="", color=None): - Widget.__init__(self) - self.set_widget(gtk.Label()) - if text: - self.set_text(text) - self.attr_list = pango.AttrList() - self.font_description = self._widget.style.font_desc.copy() - self.scale_factor = 1.0 - if color is not None: - self.set_color(color) - self.wrapped_widget_connect('style-set', self.on_style_set) - - def set_bold(self, bold): - if bold: - weight = pango.WEIGHT_BOLD - else: - weight = pango.WEIGHT_NORMAL - self.font_description.set_weight(weight) - self.set_attr(pango.AttrFontDesc(self.font_description)) - - def set_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self.scale_factor = 1 - elif size == widgetconst.SIZE_SMALL: - self.scale_factor = 0.75 - else: - self.scale_factor = size - baseline = self._widget.style.font_desc.get_size() - self.font_description.set_size(int(baseline * self.scale_factor)) - self.set_attr(pango.AttrFontDesc(self.font_description)) - - def get_preferred_width(self): - return self._widget.size_request()[0] - - def on_style_set(self, widget, old_style): - self.set_size(self.scale_factor) - - def set_wrap(self, wrap): - self._widget.set_line_wrap(wrap) - - def set_alignment(self, alignment): - # default to left. - gtkalignment = gtk.JUSTIFY_LEFT - if alignment == widgetconst.TEXT_JUSTIFY_LEFT: - gtkalignment = gtk.JUSTIFY_LEFT - elif alignment == widgetconst.TEXT_JUSTIFY_RIGHT: - gtkalignment = gtk.JUSTIFY_RIGHT - elif alignment == widgetconst.TEXT_JUSTIFY_CENTER: - gtkalignment = gtk.JUSTIFY_CENTER - self._widget.set_justify(gtkalignment) - - def get_alignment(self): - return self._widget.get_justify() - - def get_width(self): - return self._widget.get_layout().get_pixel_size()[0] - - def set_text(self, text): - self._widget.set_text(text) - - def get_text(self): - return self._widget.get_text().decode('utf-8') - - def set_selectable(self, val): - self._widget.set_selectable(val) - - def set_attr(self, attr): - attr.end_index = 65535 - self.attr_list.change(attr) - self._widget.set_attributes(self.attr_list) - - def set_color(self, color): - color_as_int = (int(65535 * c) for c in color) - self.set_attr(pango.AttrForeground(*color_as_int)) - - def baseline(self): - pango_context = self._widget.get_pango_context() - metrics = pango_context.get_metrics(self.font_description) - return pango.PIXELS(metrics.get_descent()) - - def hide(self): - self._widget.hide() - - def show(self): - self._widget.show() - -class Scroller(Bin): - def __init__(self, horizontal, vertical): - Bin.__init__(self) - self.set_widget(gtk.ScrolledWindow()) - if horizontal: - h_policy = gtk.POLICY_AUTOMATIC - else: - h_policy = gtk.POLICY_NEVER - if vertical: - v_policy = gtk.POLICY_AUTOMATIC - else: - v_policy = gtk.POLICY_NEVER - self._widget.set_policy(h_policy, v_policy) - - def set_has_borders(self, has_border): - pass - - def set_background_color(self, color): - pass - - def add_child_to_widget(self): - if (isinstance(self.child._widget, gtk.TreeView) or - isinstance(self.child._widget, gtk.TextView)): - # child has native scroller - self._widget.add(self.child._widget) - else: - self._widget.add_with_viewport(self.child._widget) - self._widget.get_child().set_shadow_type(gtk.SHADOW_NONE) - if isinstance(self.child._widget, gtk.TextView): - self._widget.set_shadow_type(gtk.SHADOW_IN) - else: - self._widget.set_shadow_type(gtk.SHADOW_NONE) - - def prepare_for_dark_content(self): - # this is just a hack for cocoa - pass - - -class SolidBackground(Bin): - def __init__(self, color=None): - Bin.__init__(self) - self.set_widget(gtk.EventBox()) - if color is not None: - self.set_background_color(color) - - def set_background_color(self, color): - self.modify_style('base', gtk.STATE_NORMAL, self.make_color(color)) - self.modify_style('bg', gtk.STATE_NORMAL, self.make_color(color)) - -class Expander(Bin): - def __init__(self, child=None): - Bin.__init__(self) - self.set_widget(gtk.Expander()) - if child is not None: - self.add(child) - self.label = None - # This is a complete hack. GTK expanders have a transparent - # background most of the time, except when they are prelighted. So we - # just set the background to white there because that's what should - # happen in the item list. - self.modify_style('bg', gtk.STATE_PRELIGHT, - gtk.gdk.color_parse('white')) - - def set_spacing(self, spacing): - self._widget.set_spacing(spacing) - - def set_label(self, widget): - self.label = widget - self._widget.set_label_widget(widget._widget) - widget._widget.show() - - def set_expanded(self, expanded): - self._widget.set_expanded(expanded) - -class ProgressBar(Widget): - def __init__(self): - Widget.__init__(self) - self.set_widget(gtk.ProgressBar()) - self._timer = None - - def set_progress(self, fraction): - self._widget.set_fraction(fraction) - - def start_pulsing(self): - if self._timer is None: - self._timer = gobject.timeout_add(100, self._do_pulse) - - def stop_pulsing(self): - if self._timer: - gobject.source_remove(self._timer) - self._timer = None - - def _do_pulse(self): - self._widget.pulse() - return True - -class HLine(Widget): - """A horizontal separator. Not to be confused with HSeparator, which is is - a DrawingArea, not a Widget. - """ - def __init__(self): - Widget.__init__(self) - self.set_widget(gtk.HSeparator()) diff --git a/mvc/widgets/gtk/tableview.py b/mvc/widgets/gtk/tableview.py deleted file mode 100644 index 930270c..0000000 --- a/mvc/widgets/gtk/tableview.py +++ /dev/null @@ -1,1557 +0,0 @@ -# @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. - -"""tableview.py -- Wrapper for the GTKTreeView widget. It's used for the tab -list and the item list (AKA almost all of the miro). -""" - -import logging - -import itertools -import gobject -import gtk -from collections import namedtuple - -# These are probably wrong, and are placeholders for now, until custom headers -# are also implemented for GTK. -CUSTOM_HEADER_HEIGHT = 25 -HEADER_HEIGHT = 25 - -from mvc import signals -from mvc.errors import (WidgetActionError, WidgetDomainError, - WidgetRangeError, WidgetNotReadyError) -from mvc.widgets.tableselection import SelectionOwnerMixin -from mvc.widgets.tablescroll import ScrollbarOwnerMixin -import drawing -import wrappermap -from .base import Widget -from .simple import Image -from .layoutmanager import LayoutManager -from .weakconnect import weak_connect -from .tableviewcells import GTKCustomCellRenderer - - -PathInfo = namedtuple('PathInfo', 'path column x y') -Rect = namedtuple('Rect', 'x y width height') -_album_view_gtkrc_installed = False - -def _install_album_view_gtkrc(): - """Hack for styling GTKTreeView for the album view widget. - - We do a couple things: - - Remove the focus ring - - Remove any separator space. - - We do this so that we don't draw a box through the album view column for - selected rows. - """ - global _album_view_gtkrc_installed - if _album_view_gtkrc_installed: - return - rc_string = ('style "album-view-style"\n' - '{ \n' - ' GtkTreeView::vertical-separator = 0\n' - ' GtkTreeView::horizontal-separator = 0\n' - ' GtkWidget::focus-line-width = 0 \n' - '}\n' - 'widget "*.miro-album-view" style "album-view-style"\n') - gtk.rc_parse_string(rc_string) - _album_view_gtkrc_installed = True - -def rect_contains_point(rect, x, y): - return ((rect.x <= x < rect.x + rect.width) and - (rect.y <= y < rect.y + rect.height)) - -class TreeViewScrolling(object): - def __init__(self): - self.scrollbars = [] - self.scroll_positions = None, None - self.restoring_scroll = None - self.connect('parent-set', self.on_parent_set) - self.scroller = None - # hack necessary because of our weird widget hierarchy (GTK doesn't deal - # well with the Scroller's widget not being the direct parent of the - # TableView's widget.) - self._coords_working = False - - def scroll_range_changed(self): - """Faux-signal; this should all be integrated into - GTKScrollbarOwnerMixin, making this unnecessary. - """ - - @property - def manually_scrolled(self): - """Return whether the view has been scrolled explicitly by the user - since the last time it was set automatically. - """ - auto_pos = self.scroll_positions[1] - if auto_pos is None: - # if we don't have any position yet, user can't have manually - # scrolled - return False - real_pos = self.scrollbars[1].get_value() - return abs(auto_pos - real_pos) > 5 # allowing some fuzziness - - @property - def position_set(self): - """Return whether the scroll position has been set in any way.""" - return any(x is not None for x in self.scroll_positions) - - def on_parent_set(self, widget, old_parent): - """We have parent window now; we need to control its scrollbars.""" - self.set_scroller(widget.get_parent()) - - def set_scroller(self, window): - """Take control of the scrollbars of window.""" - if not isinstance(window, gtk.ScrolledWindow): - return - self.scroller = window - scrollbars = tuple(bar.get_adjustment() - for bar in (window.get_hscrollbar(), window.get_vscrollbar())) - self.scrollbars = scrollbars - for i, bar in enumerate(scrollbars): - weak_connect(bar, 'changed', self.on_scroll_range_changed, i) - if self.restoring_scroll: - self.set_scroll_position(self.restoring_scroll) - - def on_scroll_range_changed(self, adjustment, bar): - """The scrollbar might have a range now. Set its initial position if - we haven't already. - """ - self._coords_working = True - if self.restoring_scroll: - self.set_scroll_position(self.restoring_scroll) - # our wrapper handles the same thing for iters - self.scroll_range_changed() - - def set_scroll_position(self, scroll_position): - """Restore the scrollbars to a remembered state.""" - try: - self.scroll_positions = tuple(self._clip_pos(adj, x) - for adj, x in zip(self.scrollbars, scroll_position)) - except WidgetActionError, error: - logging.debug("can't scroll yet: %s", error.reason) - # try again later - self.restoring_scroll = scroll_position - else: - for adj, pos in zip(self.scrollbars, self.scroll_positions): - adj.set_value(pos) - self.restoring_scroll = None - - def _clip_pos(self, adj, pos): - lower = adj.get_lower() - upper = adj.get_upper() - adj.get_page_size() - # currently, StandardView gets an upper of 2.0 when it's not ready - # FIXME: don't count on that - if pos > upper and upper < 5: - raise WidgetRangeError("scrollable area", pos, lower, upper) - return min(max(pos, lower), upper) - - def get_path_rect(self, path): - """Return the Rect for the given item, in tree coords.""" - if not self._coords_working: - # part of solution to #17405; widget_to_tree_coords tends to return - # y=8 before the first scroll-range-changed signal. ugh. - raise WidgetNotReadyError('_coords_working') - rect = self.get_background_area(path, self.get_columns()[0]) - x, y = self.widget_to_tree_coords(rect.x, rect.y) - return Rect(x, y, rect.width, rect.height) - - @property - def _scrollbars(self): - if not self.scrollbars: - raise WidgetNotReadyError - return self.scrollbars - - def scroll_ancestor(self, newly_selected, down): - # Try to figure out what just became selected. If multiple things - # somehow became selected, select the outermost one - if len(newly_selected) == 0: - raise WidgetActionError("need at an item to scroll to") - if down: - path_to_show = max(newly_selected) - else: - path_to_show = min(newly_selected) - - if not self.scrollbars: - return - vadjustment = self.scrollbars[1] - - rect = self.get_background_area(path_to_show, self.get_columns()[0]) - _, top = self.translate_coordinates(self.scroller, 0, rect.y) - top += vadjustment.value - bottom = top + rect.height - if down: - if bottom > vadjustment.value + vadjustment.page_size: - bottom_value = min(bottom, vadjustment.upper) - vadjustment.set_value(bottom_value - vadjustment.page_size) - else: - if top < vadjustment.value: - vadjustment.set_value(max(vadjustment.lower, top)) - -class MiroTreeView(gtk.TreeView, TreeViewScrolling): - """Extends the GTK TreeView widget to help implement TableView - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - # Add a tiny bit of padding so that the user can drag feeds below - # the table, i.e. to the bottom row, as a top-level - PAD_BOTTOM = 3 - def __init__(self): - gtk.TreeView.__init__(self) - TreeViewScrolling.__init__(self) - self.height_without_pad_bottom = -1 - self.set_enable_search(False) - self.horizontal_separator = self.style_get_property("horizontal-separator") - self.expander_size = self.style_get_property("expander-size") - self.group_lines_enabled = False - self.group_line_color = (0, 0, 0) - self.group_line_width = 1 - - def do_size_request(self, req): - gtk.TreeView.do_size_request(self, req) - self.height_without_pad_bottom = req.height - req.height += self.PAD_BOTTOM - - def do_move_cursor(self, step, count): - if step == gtk.MOVEMENT_VISUAL_POSITIONS: - # GTK is asking us to move left/right. Since our TableViews don't - # support this, return False to let the key press propagate. See - # #15646 for more info. - return False - if isinstance(self.get_parent(), gtk.ScrolledWindow): - # If our parent is a ScrolledWindow, let GTK take care of this - handled = gtk.TreeView.do_move_cursor(self, step, count) - return handled - else: - # Otherwise, we have to search up the widget tree for a - # ScrolledWindow to take care of it - selection = self.get_selection() - model, start_selection = selection.get_selected_rows() - gtk.TreeView.do_move_cursor(self, step, count) - - model, end_selection = selection.get_selected_rows() - newly_selected = set(end_selection) - set(start_selection) - down = (count > 0) - - try: - self.scroll_ancestor(newly_selected, down) - except WidgetActionError: - # not possible - return False - return True - - def get_position_info(self, x, y): - """Wrapper for get_path_at_pos that converts the path_info to a named - tuple and handles rounding the coordinates. - """ - path_info = self.get_path_at_pos(int(round(x)), int(round(y))) - if path_info: - return PathInfo(*path_info) - -gobject.type_register(MiroTreeView) - -class HotspotTracker(object): - """Handles tracking hotspots. - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def __init__(self, treeview, event): - self.treeview = treeview - self.treeview_wrapper = wrappermap.wrapper(treeview) - self.hit = False - self.button = event.button - path_info = treeview.get_position_info(event.x, event.y) - if path_info is None: - return - self.path, self.column, background_x, background_y = path_info - # We always pack 1 renderer for each column - gtk_renderer = self.column.get_cell_renderers()[0] - if not isinstance(gtk_renderer, GTKCustomCellRenderer): - return - self.renderer = wrappermap.wrapper(gtk_renderer) - self.attr_map = self.treeview_wrapper.attr_map_for_column[self.column] - if not rect_contains_point(self.calc_cell_area(), event.x, event.y): - # Mouse is in the padding around the actual cell area - return - self.update_position(event) - self.iter = treeview.get_model().get_iter(self.path) - self.name = self.calc_hotspot() - if self.name is not None: - self.hit = True - - def is_for_context_menu(self): - return self.name == "#show-context-menu" - - def calc_cell_area(self): - cell_area = self.treeview.get_cell_area(self.path, self.column) - xpad = self.renderer._renderer.props.xpad - ypad = self.renderer._renderer.props.ypad - cell_area.x += xpad - cell_area.y += ypad - cell_area.width -= xpad * 2 - cell_area.height -= ypad * 2 - return cell_area - - def update_position(self, event): - self.x, self.y = int(event.x), int(event.y) - - def calc_cell_state(self): - if self.treeview.get_selection().path_is_selected(self.path): - if self.treeview.flags() & gtk.HAS_FOCUS: - return gtk.STATE_SELECTED - else: - return gtk.STATE_ACTIVE - else: - return gtk.STATE_NORMAL - - def calc_hotspot(self): - cell_area = self.calc_cell_area() - if rect_contains_point(cell_area, self.x, self.y): - model = self.treeview.get_model() - self.renderer.cell_data_func(self.column, self.renderer._renderer, - model, self.iter, self.attr_map) - style = drawing.DrawingStyle(self.treeview_wrapper, - use_base_color=True, state=self.calc_cell_state()) - x = self.x - cell_area.x - y = self.y - cell_area.y - return self.renderer.hotspot_test(style, - self.treeview_wrapper.layout_manager, - x, y, cell_area.width, cell_area.height) - else: - return None - - def update_hit(self): - if self.is_for_context_menu(): - return # we always keep hit = True for this one - old_hit = self.hit - self.hit = (self.calc_hotspot() == self.name) - if self.hit != old_hit: - self.redraw_cell() - - def redraw_cell(self): - # Check that the treeview is still around. We might have switched - # views in response to a hotspot being clicked. - if self.treeview.flags() & gtk.REALIZED: - cell_area = self.treeview.get_cell_area(self.path, self.column) - x, y = self.treeview.tree_to_widget_coords(cell_area.x, - cell_area.y) - self.treeview.queue_draw_area(x, y, - cell_area.width, cell_area.height) - -class TableColumn(signals.SignalEmitter): - """A single column of a TableView. - - Signals: - - clicked (table_column) -- The header for this column was clicked. - """ - # GTK hard-codes 4px of padding for each column - FIXED_PADDING = 4 - def __init__(self, title, renderer, header=None, **attrs): - # header widget not used yet in GTK (#15800) - signals.SignalEmitter.__init__(self) - self.create_signal('clicked') - self._column = gtk.TreeViewColumn(title, renderer._renderer) - self._column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self._column.set_clickable(True) - self.attrs = attrs - renderer.setup_attributes(self._column, attrs) - self.renderer = renderer - weak_connect(self._column, 'clicked', self._header_clicked) - self.do_horizontal_padding = True - - def set_right_aligned(self, right_aligned): - """Horizontal alignment of the header label.""" - if right_aligned: - self._column.set_alignment(1.0) - else: - self._column.set_alignment(0.0) - - def set_min_width(self, width): - self._column.props.min_width = width + TableColumn.FIXED_PADDING - - def set_max_width(self, width): - self._column.props.max_width = width - - def set_width(self, width): - self._column.set_fixed_width(width + TableColumn.FIXED_PADDING) - - def get_width(self): - return self._column.get_width() - - def _header_clicked(self, tablecolumn): - self.emit('clicked') - - def set_resizable(self, resizable): - """Set if the user can resize the column.""" - self._column.set_resizable(resizable) - - def set_do_horizontal_padding(self, horizontal_padding): - self.do_horizontal_padding = False - - def set_sort_indicator_visible(self, visible): - """Show/Hide the sort indicator for this column.""" - self._column.set_sort_indicator(visible) - - def get_sort_indicator_visible(self): - return self._column.get_sort_indicator() - - def set_sort_order(self, ascending): - """Display a sort indicator on the column header. Ascending can be - either True or False which affects the direction of the indicator. - """ - if ascending: - self._column.set_sort_order(gtk.SORT_ASCENDING) - else: - self._column.set_sort_order(gtk.SORT_DESCENDING) - - def get_sort_order_ascending(self): - """Returns if the sort indicator is displaying that the sort is - ascending. - """ - return self._column.get_sort_order() == gtk.SORT_ASCENDING - -class GTKSelectionOwnerMixin(SelectionOwnerMixin): - """GTK-specific methods for selection management. - - This subclass should not define any behavior. Methods that cannot be - completed in this widget state should raise WidgetActionError. - """ - def __init__(self): - SelectionOwnerMixin.__init__(self) - self.selection = self._widget.get_selection() - weak_connect(self.selection, 'changed', self.on_selection_changed) - - def _set_allow_multiple_select(self, allow): - if allow: - mode = gtk.SELECTION_MULTIPLE - else: - mode = gtk.SELECTION_SINGLE - self.selection.set_mode(mode) - - def _get_allow_multiple_select(self): - return self.selection.get_mode() == gtk.SELECTION_MULTIPLE - - def _get_selected_iters(self): - iters = [] - def collect(treemodel, path, iter_): - iters.append(iter_) - self.selection.selected_foreach(collect) - return iters - - def _get_selected_iter(self): - model, iter_ = self.selection.get_selected() - return iter_ - - @property - def num_rows_selected(self): - return self.selection.count_selected_rows() - - def _is_selected(self, iter_): - return self.selection.iter_is_selected(iter_) - - def _select(self, iter_): - self.selection.select_iter(iter_) - - def _unselect(self, iter_): - self.selection.unselect_iter(iter_) - - def _unselect_all(self): - self.selection.unselect_all() - - def _iter_to_string(self, iter_): - return self._model.get_string_from_iter(iter_) - - def _iter_from_string(self, string): - try: - return self._model.get_iter_from_string(string) - except ValueError: - raise WidgetDomainError( - "model iters", string, "%s other iters" % len(self.model)) - - def select_path(self, path): - self.selection.select_path(path) - - def _validate_iter(self, iter_): - if self.get_path(iter_) is None: - raise WidgetDomainError( - "model iters", iter_, "%s other iters" % len(self.model)) - real_model = self._widget.get_model() - if not real_model: - raise WidgetActionError("no model") - elif real_model != self._model: - raise WidgetActionError("wrong model?") - - def get_cursor(self): - """Return the path of the 'focused' item.""" - path, column = self._widget.get_cursor() - return path - - def set_cursor(self, path): - """Set the path of the 'focused' item.""" - if path is None: - # XXX: is there a way to clear the cursor? - return - path_as_string = ':'.join(str(component) for component in path) - with self.preserving_selection(): # set_cursor() messes up the selection - self._widget.set_cursor(path_as_string) - -class DNDHandlerMixin(object): - """TableView row DnD. - - Depends on arbitrary TableView methods; otherwise self-contained except: - on_button_press: may call start_drag - on_button_release: may unset drag_button_down - on_motion_notify: may call potential_drag_motion - """ - def __init__(self): - self.drag_button_down = False - self.drag_data = {} - self.drag_source = self.drag_dest = None - self.drag_start_x, self.drag_start_y = None, None - self.wrapped_widget_connect('drag-data-get', self.on_drag_data_get) - self.wrapped_widget_connect('drag-end', self.on_drag_end) - self.wrapped_widget_connect('drag-motion', self.on_drag_motion) - self.wrapped_widget_connect('drag-leave', self.on_drag_leave) - self.wrapped_widget_connect('drag-drop', self.on_drag_drop) - self.wrapped_widget_connect('drag-data-received', - self.on_drag_data_received) - self.wrapped_widget_connect('unrealize', self.on_drag_unrealize) - - def set_drag_source(self, drag_source): - self.drag_source = drag_source - # XXX: the following note no longer seems accurate: - # No need to call enable_model_drag_source() here, we handle it - # ourselves in on_motion_notify() - - def set_drag_dest(self, drag_dest): - """Set the drop handler.""" - self.drag_dest = drag_dest - if drag_dest is not None: - targets = self._gtk_target_list(drag_dest.allowed_types()) - self._widget.enable_model_drag_dest(targets, - drag_dest.allowed_actions()) - self._widget.drag_dest_set(0, targets, - drag_dest.allowed_actions()) - else: - self._widget.unset_rows_drag_dest() - self._widget.drag_dest_unset() - - def start_drag(self, treeview, event, path_info): - """Check whether the event is a drag event; return whether handled - here. - """ - if event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK): - return False - model, row_paths = treeview.get_selection().get_selected_rows() - - if path_info.path not in row_paths: - # something outside the selection is being dragged. - # make it the new selection. - self.unselect_all(signal=False) - self.select_path(path_info.path) - row_paths = [path_info.path] - rows = self.model.get_rows(row_paths) - self.drag_data = rows and self.drag_source.begin_drag(self, rows) - self.drag_button_down = bool(self.drag_data) - if self.drag_button_down: - self.drag_start_x = int(event.x) - self.drag_start_y = int(event.y) - - if len(row_paths) > 1 and path_info.path in row_paths: - # handle multiple selection. If the current row is already - # selected, stop propagating the signal. We will only change - # the selection if the user doesn't start a DnD operation. - # This makes it more natural for the user to drag a block of - # selected items. - renderer = path_info.column.get_cell_renderers()[0] - if (not self._x_coord_in_expander(treeview, path_info) - and not isinstance(renderer, GTKCheckboxCellRenderer)): - self.delaying_press = True - # grab keyboard focus since we handled the event - self.focus() - return True - - def on_drag_data_get(self, treeview, context, selection, info, timestamp): - for typ, data in self.drag_data.items(): - selection.set(typ, 8, repr(data)) - - def on_drag_end(self, treeview, context): - self.drag_data = {} - - def find_type(self, drag_context): - return self._widget.drag_dest_find_target(drag_context, - self._widget.drag_dest_get_target_list()) - - def calc_positions(self, x, y): - """Given x and y coordinates, generate a list of drop positions to - try. The values are tuples in the form of (parent_path, position, - gtk_path, gtk_position), where parent_path and position is the - position to send to the Miro code, and gtk_path and gtk_position is an - equivalent position to send to the GTK code if the drag_dest validates - the drop. - """ - model = self._model - try: - gtk_path, gtk_position = self._widget.get_dest_row_at_pos(x, y) - except TypeError: - # Below the last row - yield (None, len(model), None, None) - return - - iter_ = model.get_iter(gtk_path) - if gtk_position in (gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, - gtk.TREE_VIEW_DROP_INTO_OR_AFTER): - yield (iter_, -1, gtk_path, gtk_position) - - if hasattr(model, 'iter_is_valid'): - # tablist has this; item list does not - assert model.iter_is_valid(iter_) - parent_iter = model.iter_parent(iter_) - position = gtk_path[-1] - if gtk_position in (gtk.TREE_VIEW_DROP_BEFORE, - gtk.TREE_VIEW_DROP_INTO_OR_BEFORE): - # gtk gave us a "before" position, no need to change it - yield (parent_iter, position, gtk_path, gtk.TREE_VIEW_DROP_BEFORE) - else: - # gtk gave us an "after" position, translate that to before the - # next row for miro. - if (self._widget.row_expanded(gtk_path) and - model.iter_has_child(iter_)): - child_path = gtk_path + (0,) - yield (iter_, 0, child_path, gtk.TREE_VIEW_DROP_BEFORE) - else: - yield (parent_iter, position+1, gtk_path, - gtk.TREE_VIEW_DROP_AFTER) - - def on_drag_motion(self, treeview, drag_context, x, y, timestamp): - if not self.drag_dest: - return True - type = self.find_type(drag_context) - if type == "NONE": - drag_context.drag_status(0, timestamp) - return True - drop_action = 0 - for pos_info in self.calc_positions(x, y): - drop_action = self.drag_dest.validate_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1]) - if isinstance(drop_action, (list, tuple)): - drop_action, iter = drop_action - path = self.model.get_path(iter) - pos = gtk.TREE_VIEW_DROP_INTO_OR_BEFORE - else: - path, pos = pos_info[2:4] - - if drop_action: - self.set_drag_dest_row(path, pos) - break - else: - self.unset_drag_dest_row() - drag_context.drag_status(drop_action, timestamp) - return True - - def set_drag_dest_row(self, path, position): - self._widget.set_drag_dest_row(path, position) - - def unset_drag_dest_row(self): - self._widget.unset_drag_dest_row() - - def on_drag_leave(self, treeview, drag_context, timestamp): - treeview.unset_drag_dest_row() - - def on_drag_drop(self, treeview, drag_context, x, y, timestamp): - # prevent the default handler - treeview.emit_stop_by_name('drag-drop') - target = self.find_type(drag_context) - if target == "NONE": - return False - treeview.drag_get_data(drag_context, target, timestamp) - treeview.unset_drag_dest_row() - - def on_drag_data_received(self, - treeview, drag_context, x, y, selection, info, timestamp): - # prevent the default handler - treeview.emit_stop_by_name('drag-data-received') - if not self.drag_dest: - return - type = self.find_type(drag_context) - if type == "NONE": - return - if selection.data is None: - return - drop_action = 0 - for pos_info in self.calc_positions(x, y): - drop_action = self.drag_dest.validate_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1]) - if drop_action: - self.drag_dest.accept_drop(self, self.model, type, - drag_context.actions, pos_info[0], pos_info[1], - eval(selection.data)) - return True - return False - - def on_drag_unrealize(self, treeview): - self.drag_button_down = False - - def potential_drag_motion(self, treeview, event): - """A motion event has occurred and did not hit a hotspot; start a drag - if applicable. - """ - if (self.drag_data and self.drag_button_down and - treeview.drag_check_threshold(self.drag_start_x, - self.drag_start_y, int(event.x), int(event.y))): - self.delaying_press = False - treeview.drag_begin(self._gtk_target_list(self.drag_data.keys()), - self.drag_source.allowed_actions(), 1, event) - - @staticmethod - def _gtk_target_list(types): - count = itertools.count() - return [(type, gtk.TARGET_SAME_APP, count.next()) for type in types] - -class HotspotTrackingMixin(object): - def __init__(self): - self.hotspot_tracker = None - self.create_signal('hotspot-clicked') - self._hotspot_callback_handles = [] - self._connect_hotspot_signals() - self.wrapped_widget_connect('unrealize', self.on_hotspot_unrealize) - - def _connect_hotspot_signals(self): - SIGNALS = { - 'row-inserted': self.on_row_inserted, - 'row-deleted': self.on_row_deleted, - 'row-changed': self.on_row_changed, - } - self._hotspot_callback_handles.extend( - weak_connect(self._model, signal, handler) - for signal, handler in SIGNALS.iteritems()) - - def _disconnect_hotspot_signals(self): - for handle in self._hotspot_callback_handles: - self._model.disconnect(handle) - self._hotspot_callback_handles = [] - - def on_row_inserted(self, model, path, iter_): - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_row_deleted(self, model, path): - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_row_changed(self, model, path, iter_): - if self.hotspot_tracker: - self.hotspot_tracker.update_hit() - - def handle_hotspot_hit(self, treeview, event): - """Check whether the event is a hotspot event; return whether handled - here. - """ - if self.hotspot_tracker: - return - hotspot_tracker = HotspotTracker(treeview, event) - if hotspot_tracker.hit: - self.hotspot_tracker = hotspot_tracker - hotspot_tracker.redraw_cell() - if hotspot_tracker.is_for_context_menu(): - menu = self._popup_context_menu(self.hotspot_tracker.path, event) - if menu: - menu.connect('selection-done', - self._on_hotspot_context_menu_selection_done) - # grab keyboard focus since we handled the event - self.focus() - return True - - def _on_hotspot_context_menu_selection_done(self, menu): - # context menu is closed, we won't get the button-release-event in - # this case, but we can unset hotspot tracker here. - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - - def on_hotspot_unrealize(self, treeview): - self.hotspot_tracker = None - - def release_on_hotspot(self, event): - """A button_release occurred; return whether it has been handled as a - hotspot hit. - """ - hotspot_tracker = self.hotspot_tracker - if hotspot_tracker and event.button == hotspot_tracker.button: - hotspot_tracker.update_position(event) - hotspot_tracker.update_hit() - if (hotspot_tracker.hit and - not hotspot_tracker.is_for_context_menu()): - self.emit('hotspot-clicked', hotspot_tracker.name, - hotspot_tracker.iter) - hotspot_tracker.redraw_cell() - self.hotspot_tracker = None - return True - - def hotspot_model_changed(self): - """A bulk change has ended; reconnect signals and update hotspots.""" - self._connect_hotspot_signals() - if self.hotspot_tracker: - self.hotspot_tracker.redraw_cell() - self.hotspot_tracker.update_hit() - -class ColumnOwnerMixin(object): - """Keeps track of the table's columns - including the list of columns, and - properties that we set for a table but need to apply to each column. - - This manages: - columns - attr_map_for_column - gtk_column_to_wrapper - for use throughout tableview. - """ - def __init__(self): - self._columns_draggable = False - self._renderer_xpad = self._renderer_ypad = 0 - self.columns = [] - self.attr_map_for_column = {} - self.gtk_column_to_wrapper = {} - self.create_signal('reallocate-columns') # not emitted on GTK - - def remove_column(self, index): - """Remove a column from the display and forget it from the column lists. - """ - column = self.columns.pop(index) - del self.attr_map_for_column[column._column] - del self.gtk_column_to_wrapper[column._column] - self._widget.remove_column(column._column) - - def get_columns(self): - """Returns the current columns, in order, by title.""" - # FIXME: this should probably return column objects, and really should - # not be keeping track of columns by title at all - titles = [column.get_title().decode('utf-8') - for column in self._widget.get_columns()] - return titles - - def add_column(self, column): - """Append a column to this table; setup all necessary mappings, and - setup the new column's properties to match the table's settings. - """ - self.model.check_new_column(column) - self._widget.append_column(column._column) - self.columns.append(column) - self.attr_map_for_column[column._column] = column.attrs - self.gtk_column_to_wrapper[column._column] = column - self.setup_new_column(column) - - def setup_new_column(self, column): - """Apply properties that we keep track of at the table level to a - newly-created column. - """ - if self.background_color: - column.renderer._renderer.set_property('cell-background-gdk', - self.background_color) - column._column.set_reorderable(self._columns_draggable) - if column.do_horizontal_padding: - column.renderer._renderer.set_property('xpad', self._renderer_xpad) - column.renderer._renderer.set_property('ypad', self._renderer_ypad) - - def set_column_spacing(self, space): - """Set the amount of space between columns.""" - self._renderer_xpad = space / 2 - for column in self.columns: - if column.do_horizontal_padding: - column.renderer._renderer.set_property('xpad', - self._renderer_xpad) - - def set_row_spacing(self, space): - """Set the amount of space between columns.""" - self._renderer_ypad = space / 2 - for column in self.columns: - column.renderer._renderer.set_property('ypad', self._renderer_ypad) - - def set_columns_draggable(self, setting): - """Set the draggability of existing and future columns.""" - self._columns_draggable = setting - for column in self.columns: - column._column.set_reorderable(setting) - - def set_column_background_color(self): - """Set the background color of existing columns to the table's - background_color. - """ - for column in self.columns: - column.renderer._renderer.set_property('cell-background-gdk', - self.background_color) - - def set_auto_resizes(self, setting): - # FIXME: to be implemented. - # At this point, GTK somehow does the right thing anyway in terms of - # auto-resizing. I'm not sure exactly what's happening, but I believe - # that if the column widths don't add up to the total width, - # gtk.TreeView allocates extra width for the last column. This works - # well enough for the tab list and item list, since there's only one - # column. - pass - -class HoverTrackingMixin(object): - """Handle mouse hover events - tooltips for some cells and hover events for - renderers which support them. - """ - def __init__(self): - self.hover_info = None - self.hover_pos = None - if hasattr(self, 'get_tooltip'): - # this should probably be something like self.set_tooltip_source - self._widget.set_property('has-tooltip', True) - self.wrapped_widget_connect('query-tooltip', self.on_tooltip) - self._last_tooltip_place = None - - def on_tooltip(self, treeview, x, y, keyboard_mode, tooltip): - # x, y are relative to the entire widget, but we want them to be - # relative to our bin window. The bin window doesn't include things - # like the column headers. - origin = treeview.window.get_origin() - bin_origin = treeview.get_bin_window().get_origin() - x += origin[0] - bin_origin[0] - y += origin[1] - bin_origin[1] - path_info = treeview.get_position_info(x, y) - if path_info is None: - self._last_tooltip_place = None - return False - if (self._last_tooltip_place is not None and - path_info[:2] != self._last_tooltip_place): - # the default GTK behavior is to keep the tooltip in the same - # position, but this is looks bad when we move to a different row. - # So return False once to stop this. - self._last_tooltip_place = None - return False - self._last_tooltip_place = path_info[:2] - iter_ = treeview.get_model().get_iter(path_info.path) - column = self.gtk_column_to_wrapper[path_info.column] - text = self.get_tooltip(iter_, column) - if text is None: - return False - pygtkhacks.set_tooltip_text(tooltip, text) - return True - - def _update_hover(self, treeview, event): - old_hover_info, old_hover_pos = self.hover_info, self.hover_pos - path_info = treeview.get_position_info(event.x, event.y) - if (path_info and - self.gtk_column_to_wrapper[path_info.column].renderer.want_hover): - self.hover_info = path_info.path, path_info.column - self.hover_pos = path_info.x, path_info.y - else: - self.hover_info = None - self.hover_pos = None - if (old_hover_info != self.hover_info or - old_hover_pos != self.hover_pos): - if (old_hover_info != self.hover_info and - old_hover_info is not None): - self._redraw_cell(treeview, *old_hover_info) - if self.hover_info is not None: - self._redraw_cell(treeview, *self.hover_info) - -class GTKScrollbarOwnerMixin(ScrollbarOwnerMixin): - # XXX this is half a wrapper for TreeViewScrolling. A lot of things will - # become much simpler when we integrate TVS into this - def __init__(self): - ScrollbarOwnerMixin.__init__(self) - # super uses this for postponed scroll_to_iter - # it's a faux-signal from our _widget; this hack is only necessary until - # we integrate TVS - self._widget.scroll_range_changed = (lambda *a: - self.emit('scroll-range-changed')) - - def set_scroller(self, scroller): - """Set the Scroller object for this widget, if its ScrolledWindow is - not a direct ancestor of the object. Standard View needs this. - """ - self._widget.set_scroller(scroller._widget) - - def _set_scroll_position(self, scroll_pos): - self._widget.set_scroll_position(scroll_pos) - - def _get_item_area(self, iter_): - return self._widget.get_path_rect(self.get_path(iter_)) - - @property - def _manually_scrolled(self): - return self._widget.manually_scrolled - - @property - def _position_set(self): - return self._widget.position_set - - def _get_visible_area(self): - """Return the Rect of the visible area, in tree coords. - - get_visible_rect gets this wrong for StandardView, always returning an - origin of (0, 0) - this is because our ScrolledWindow is not our direct - parent. - """ - bars = self._widget._scrollbars - x, y = (int(adj.get_value()) for adj in bars) - width, height = (int(adj.get_page_size()) for adj in bars) - if height == 0: - # this happens even after _widget._coords_working - raise WidgetNotReadyError('visible height') - return Rect(x, y, width, height) - - def _get_scroll_position(self): - """Get the current position of both scrollbars, to restore later.""" - try: - return tuple(int(bar.get_value()) for bar in self._widget._scrollbars) - except WidgetNotReadyError: - return None - -class TableView(Widget, GTKSelectionOwnerMixin, DNDHandlerMixin, - HotspotTrackingMixin, ColumnOwnerMixin, HoverTrackingMixin, - GTKScrollbarOwnerMixin): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - draws_selection = True - - def __init__(self, model, custom_headers=False): - Widget.__init__(self) - self.set_widget(MiroTreeView()) - self.model = model - self.model.add_to_tableview(self._widget) - self._model = self._widget.get_model() - wrappermap.add(self._model, model) - self._setup_colors() - self.background_color = None - self.context_menu_callback = None - self.in_bulk_change = False - self.delaying_press = False - self._use_custom_headers = False - self.layout_manager = LayoutManager(self._widget) - self.height_changed = None # 17178 hack - self._connect_signals() - # setting up mixins after general TableView init - GTKSelectionOwnerMixin.__init__(self) - DNDHandlerMixin.__init__(self) - HotspotTrackingMixin.__init__(self) - ColumnOwnerMixin.__init__(self) - HoverTrackingMixin.__init__(self) - GTKScrollbarOwnerMixin.__init__(self) - if custom_headers: - self._enable_custom_headers() - - # FIXME: should implement set_model() and make None a special case. - def unset_model(self): - """Disconnect our model from this table view. - - This should be called when you want to destroy a TableView and - there's a new TableView sharing its model. - """ - self._widget.set_model(None) - self.model = None - - def _connect_signals(self): - self.create_signal('row-expanded') - self.create_signal('row-collapsed') - self.create_signal('row-clicked') - self.create_signal('row-activated') - self.wrapped_widget_connect('row-activated', self.on_row_activated) - self.wrapped_widget_connect('row-expanded', self.on_row_expanded) - self.wrapped_widget_connect('row-collapsed', self.on_row_collapsed) - self.wrapped_widget_connect('button-press-event', self.on_button_press) - self.wrapped_widget_connect('button-release-event', - self.on_button_release) - self.wrapped_widget_connect('motion-notify-event', - self.on_motion_notify) - - def set_gradient_highlight(self, gradient): - # This is just an OS X thing. - pass - - def set_background_color(self, color): - self.background_color = self.make_color(color) - self.modify_style('base', gtk.STATE_NORMAL, self.background_color) - if not self.draws_selection: - self.modify_style('base', gtk.STATE_SELECTED, - self.background_color) - self.modify_style('base', gtk.STATE_ACTIVE, self.background_color) - if self.use_custom_style: - self.set_column_background_color() - - def set_group_lines_enabled(self, enabled): - """Enable/Disable group lines. - - This only has an effect if our model is an InfoListModel and it has a - grouping set. - - If group lines are enabled, we will draw a line below the last item in - the group. Use set_group_line_style() to change the look of the line. - """ - self._widget.group_lines_enabled = enabled - self.queue_redraw() - - def set_group_line_style(self, color, width): - self._widget.group_line_color = color - self._widget.group_line_width = width - self.queue_redraw() - - def handle_custom_style_change(self): - if self.background_color is not None: - if self.use_custom_style: - self.set_column_background_color() - else: - for column in self.columns: - column.renderer._renderer.set_property( - 'cell-background-set', False) - - def set_alternate_row_backgrounds(self, setting): - self._widget.set_rules_hint(setting) - - def set_grid_lines(self, horizontal, vertical): - if horizontal and vertical: - setting = gtk.TREE_VIEW_GRID_LINES_BOTH - elif horizontal: - setting = gtk.TREE_VIEW_GRID_LINES_HORIZONTAL - elif vertical: - setting = gtk.TREE_VIEW_GRID_LINES_VERTICAL - else: - setting = gtk.TREE_VIEW_GRID_LINES_NONE - self._widget.set_grid_lines(setting) - - def width_for_columns(self, total_width): - """Given the width allocated for the TableView, return how much of that - is available to column contents. Note that this depends on the number of - columns. - """ - column_spacing = TableColumn.FIXED_PADDING * len(self.columns) - return total_width - column_spacing - - def enable_album_view_focus_hack(self): - _install_album_view_gtkrc() - self._widget.set_name("miro-album-view") - - def focus(self): - self._widget.grab_focus() - - def _enable_custom_headers(self): - # NB: this is currently not used because the GTK tableview does not - # support custom headers. - self._use_custom_headers = True - - def set_show_headers(self, show): - self._widget.set_headers_visible(show) - self._widget.set_headers_clickable(show) - - def _setup_colors(self): - style = self._widget.style - if not self.draws_selection: - # if we don't want to draw selection, make the selected/active - # colors the same as the normal ones - self.modify_style('base', gtk.STATE_SELECTED, - style.base[gtk.STATE_NORMAL]) - self.modify_style('base', gtk.STATE_ACTIVE, - style.base[gtk.STATE_NORMAL]) - - def set_search_column(self, model_index): - self._widget.set_search_column(model_index) - - def set_fixed_height(self, fixed_height): - self._widget.set_fixed_height_mode(fixed_height) - - def set_row_expanded(self, iter_, expanded): - """Expand or collapse the row specified by iter_. Succeeds or raises - WidgetActionError. Causes row-expanded or row-collapsed to be emitted - when successful. - """ - path = self.get_path(iter_) - if expanded: - self._widget.expand_row(path, False) - else: - self._widget.collapse_row(path) - if bool(self._widget.row_expanded(path)) != bool(expanded): - raise WidgetActionError("cannot expand the given item - it " - "probably has no children.") - - def is_row_expanded(self, iter_): - path = self.get_path(iter_) - return self._widget.row_expanded(path) - - def set_context_menu_callback(self, callback): - self.context_menu_callback = callback - - # GTK is really good and it is safe to operate on table even when - # cells may be constantly changing in flux. - def set_volatile(self, volatile): - return - - def on_row_expanded(self, _widget, iter_, path): - self.emit('row-expanded', iter_, path) - - def on_row_collapsed(self, _widget, iter_, path): - self.emit('row-collapsed', iter_, path) - - def on_button_press(self, treeview, event): - """Handle a mouse button press""" - if event.type == gtk.gdk._2BUTTON_PRESS: - # already handled as row-activated - return False - - path_info = treeview.get_position_info(event.x, event.y) - if not path_info: - # no item was clicked, so it's not going to be a hotspot, drag, or - # context menu - return False - if event.type == gtk.gdk.BUTTON_PRESS: - # single click; emit the event but keep on running so we can handle - # stuff like drag and drop. - if not self._x_coord_in_expander(treeview, path_info): - iter_ = treeview.get_model().get_iter(path_info.path) - self.emit('row-clicked', iter_) - - if (event.button == 1 and self.handle_hotspot_hit(treeview, event)): - return True - if event.window != treeview.get_bin_window(): - # click is outside the content area, don't try to handle this. - # In particular, our DnD code messes up resizing table columns. - return False - if (event.button == 1 and self.drag_source and - not self._x_coord_in_expander(treeview, path_info)): - return self.start_drag(treeview, event, path_info) - elif event.button == 3 and self.context_menu_callback: - self.show_context_menu(treeview, event, path_info) - return True - - # FALLTHROUGH - return False - - def show_context_menu(self, treeview, event, path_info): - """Pop up a context menu for the given click event (which is a - right-click on a row). - """ - # hack for album view - if (treeview.group_lines_enabled and - path_info.column == treeview.get_columns()[0]): - self._select_all_rows_in_group(treeview, path_info.path) - self._popup_context_menu(path_info.path, event) - # grab keyboard focus since we handled the event - self.focus() - - def _select_all_rows_in_group(self, treeview, path): - """Select all items in the group """ - - # FIXME: this is very tightly coupled with the portable code. - - infolist = self.model - gtk_model = treeview.get_model() - if (not isinstance(infolist, InfoListModel) or - infolist.get_grouping() is None): - return - it = gtk_model.get_iter(path) - info, attrs, group_info = infolist.row_for_iter(it) - start_row = path[0] - group_info[0] - total_rows = group_info[1] - - with self._ignoring_changes(): - self.unselect_all() - for row in xrange(start_row, start_row + total_rows): - self.select_path((row,)) - self.emit('selection-changed') - - def _popup_context_menu(self, path, event): - if not self.selection.path_is_selected(path): - self.unselect_all(signal=False) - self.select_path(path) - menu = self.make_context_menu() - if menu: - menu.popup(None, None, None, event.button, event.time) - return menu - else: - return None - - # XXX treeview.get_cell_area handles what we're trying to use this for - def _x_coord_in_expander(self, treeview, path_info): - """Calculate if an x coordinate is over the expander triangle - - :param treeview: Gtk.TreeView - :param path_info: PathInfo( - tree path for the cell, - Gtk.TreeColumn, - x coordinate relative to column's cell area, - y coordinate relative to column's cell area (ignored), - ) - """ - if path_info.column != treeview.get_expander_column(): - return False - model = treeview.get_model() - if not model.iter_has_child(model.get_iter(path_info.path)): - return False - # GTK allocateds an extra 4px to the right of the expanders. This - # seems to be hardcoded as EXPANDER_EXTRA_PADDING in the source code. - total_exander_size = treeview.expander_size + 4 - # include horizontal_separator - # XXX: should this value be included in total_exander_size ? - offset = treeview.horizontal_separator / 2 - # allocate space for expanders for parent nodes - expander_start = total_exander_size * (len(path_info.path) - 1) + offset - expander_end = expander_start + total_exander_size + offset - return expander_start <= path_info.x < expander_end - - def on_row_activated(self, treeview, path, view_column): - iter_ = treeview.get_model().get_iter(path) - self.emit('row-activated', iter_) - - def make_context_menu(self): - def gen_menu(menu_items): - menu = gtk.Menu() - for menu_item_info in menu_items: - if menu_item_info is None: - item = gtk.SeparatorMenuItem() - else: - label, callback = menu_item_info - - if isinstance(label, tuple) and len(label) == 2: - text_label, icon_path = label - pixbuf = gtk.gdk.pixbuf_new_from_file(icon_path) - image = gtk.Image() - image.set_from_pixbuf(pixbuf) - item = gtk.ImageMenuItem(text_label) - item.set_image(image) - else: - item = gtk.MenuItem(label) - - if callback is None: - item.set_sensitive(False) - elif isinstance(callback, list): - item.set_submenu(gen_menu(callback)) - else: - item.connect('activate', self.on_context_menu_activate, - callback) - menu.append(item) - item.show() - return menu - - items = self.context_menu_callback(self) - if items: - return gen_menu(items) - else: - return None - - def on_context_menu_activate(self, item, callback): - callback() - - def on_button_release(self, treeview, event): - if self.release_on_hotspot(event): - return True - if event.button == 1: - self.drag_button_down = False - - if self.delaying_press: - # if dragging did not happen, unselect other rows and - # select current row - path_info = treeview.get_position_info(event.x, event.y) - if path_info is not None: - self.unselect_all(signal=False) - self.select_path(path_info.path) - self.delaying_press = False - - def _redraw_cell(self, treeview, path, column): - cell_area = treeview.get_cell_area(path, column) - x, y = treeview.convert_bin_window_to_widget_coords(cell_area.x, - cell_area.y) - treeview.queue_draw_area(x, y, cell_area.width, cell_area.height) - - def on_motion_notify(self, treeview, event): - self._update_hover(treeview, event) - - if self.hotspot_tracker: - self.hotspot_tracker.update_position(event) - self.hotspot_tracker.update_hit() - return True - - self.potential_drag_motion(treeview, event) - return None # XXX: used to fall through; not sure what retval does here - - def start_bulk_change(self): - self._widget.freeze_child_notify() - self._widget.set_model(None) - self._disconnect_hotspot_signals() - self.in_bulk_change = True - - def model_changed(self): - if self.in_bulk_change: - self._widget.set_model(self._model) - self._widget.thaw_child_notify() - self.hotspot_model_changed() - self.in_bulk_change = False - - def get_path(self, iter_): - """Always use this rather than the model's get_path directly - - if the iter isn't valid, a GTK assertion causes us to exit - without warning; this wrapper changes that to a much more useful - AssertionError. Example related bug: #17362. - """ - assert self.model.iter_is_valid(iter_) - return self._model.get_path(iter_) - -class TableModel(object): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - MODEL_CLASS = gtk.ListStore - - def __init__(self, *column_types): - self._model = self.MODEL_CLASS(*self.map_types(column_types)) - self._column_types = column_types - if 'image' in self._column_types: - self.convert_row_for_gtk = self.convert_row_for_gtk_slow - self.convert_value_for_gtk = self.convert_value_for_gtk_slow - else: - self.convert_row_for_gtk = self.convert_row_for_gtk_fast - self.convert_value_for_gtk = self.convert_value_for_gtk_fast - - def add_to_tableview(self, widget): - widget.set_model(self._model) - - def map_types(self, miro_column_types): - type_map = { - 'boolean': bool, - 'numeric': float, - 'integer': int, - 'text': str, - 'image': gtk.gdk.Pixbuf, - 'datetime': object, - 'object': object, - } - try: - return [type_map[type] for type in miro_column_types] - except KeyError, e: - raise ValueError("Unknown column type: %s" % e[0]) - - # If we store image data, we need to do some work to convert row data to - # send to GTK - def convert_value_for_gtk_slow(self, column_value): - if isinstance(column_value, Image): - return column_value.pixbuf - else: - return column_value - - def convert_row_for_gtk_slow(self, column_values): - return tuple(self.convert_value_for_gtk(c) for c in column_values) - - def check_new_column(self, column): - for value in column.attrs.values(): - if not isinstance(value, int): - msg = "Attribute values must be integers, not %r" % value - raise TypeError(msg) - if value < 0 or value >= len(self._column_types): - raise ValueError("Attribute index out of range: %s" % value) - - # If we don't store image data, we can don't need to do any work to - # convert row data to gtk - def convert_value_for_gtk_fast(self, value): - return value - - def convert_row_for_gtk_fast(self, column_values): - return column_values - - def append(self, *column_values): - return self._model.append(self.convert_row_for_gtk(column_values)) - - def update_value(self, iter_, index, value): - assert self._model.iter_is_valid(iter_) - self._model.set(iter_, index, self.convert_value_for_gtk(value)) - - def update(self, iter_, *column_values): - self._model[iter_] = self.convert_value_for_gtk(column_values) - - def remove(self, iter_): - if self._model.remove(iter_): - return iter_ - else: - return None - - def insert_before(self, iter_, *column_values): - row = self.convert_row_for_gtk(column_values) - return self._model.insert_before(iter_, row) - - def first_iter(self): - return self._model.get_iter_first() - - def next_iter(self, iter_): - return self._model.iter_next(iter_) - - def nth_iter(self, index): - assert index >= 0 - return self._model.iter_nth_child(None, index) - - def __iter__(self): - return iter(self._model) - - def __len__(self): - return len(self._model) - - def __getitem__(self, iter_): - return self._model[iter_] - - def get_rows(self, row_paths): - return [self._model[path] for path in row_paths] - - def get_path(self, iter_): - return self._model.get_path(iter_) - - def iter_is_valid(self, iter_): - return self._model.iter_is_valid(iter_) - -class TreeTableModel(TableModel): - """https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - MODEL_CLASS = gtk.TreeStore - - def append(self, *column_values): - return self._model.append(None, self.convert_row_for_gtk( - column_values)) - - def insert_before(self, iter_, *column_values): - parent = self.parent_iter(iter_) - row = self.convert_row_for_gtk(column_values) - return self._model.insert_before(parent, iter_, row) - - def append_child(self, iter_, *column_values): - return self._model.append(iter_, self.convert_row_for_gtk( - column_values)) - - def child_iter(self, iter_): - return self._model.iter_children(iter_) - - def nth_child_iter(self, iter_, index): - assert index >= 0 - return self._model.iter_nth_child(iter_, index) - - def has_child(self, iter_): - return self._model.iter_has_child(iter_) - - def children_count(self, iter_): - return self._model.iter_n_children(iter_) - - def parent_iter(self, iter_): - assert self._model.iter_is_valid(iter_) - return self._model.iter_parent(iter_) diff --git a/mvc/widgets/gtk/tableviewcells.py b/mvc/widgets/gtk/tableviewcells.py deleted file mode 100644 index 33ac6f8..0000000 --- a/mvc/widgets/gtk/tableviewcells.py +++ /dev/null @@ -1,249 +0,0 @@ -# @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. - -"""tableviewcells.py - Cell renderers for TableView.""" - -import gobject -import gtk -import pango - -from mvc import signals -from mvc.widgets import widgetconst -import drawing -import wrappermap -from .base import make_gdk_color - -class CellRenderer(object): - """Simple Cell Renderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - self._renderer = gtk.CellRendererText() - self.want_hover = False - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'text', attr_map['value']) - - def set_align(self, align): - if align == 'left': - self._renderer.props.xalign = 0.0 - elif align == 'center': - self._renderer.props.xalign = 0.5 - elif align == 'right': - self._renderer.props.xalign = 1.0 - else: - raise ValueError("unknown alignment: %s" % align) - - def set_color(self, color): - self._renderer.props.foreground_gdk = make_gdk_color(color) - - def set_bold(self, bold): - font_desc = self._renderer.props.font_desc - if bold: - font_desc.set_weight(pango.WEIGHT_BOLD) - else: - font_desc.set_weight(pango.WEIGHT_NORMAL) - self._renderer.props.font_desc = font_desc - - def set_text_size(self, size): - if size == widgetconst.SIZE_NORMAL: - self._renderer.props.scale = 1.0 - elif size == widgetconst.SIZE_SMALL: - # FIXME: on 3.5 we just ignored the call. Always setting scale to - # 1.0 basically replicates that behavior, but should we actually - # try to implement the semantics of SIZE_SMALL? - self._renderer.props.scale = 1.0 - else: - raise ValueError("unknown size: %s" % size) - - def set_font_scale(self, scale_factor): - self._renderer.props.scale = scale_factor - -class ImageCellRenderer(object): - """Cell Renderer for images - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - self._renderer = gtk.CellRendererPixbuf() - self.want_hover = False - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'pixbuf', attr_map['image']) - -class GTKCheckboxCellRenderer(gtk.CellRendererToggle): - def do_activate(self, event, treeview, path, background_area, cell_area, - flags): - iter = treeview.get_model().get_iter(path) - self.set_active(not self.get_active()) - wrappermap.wrapper(self).emit('clicked', iter) - -gobject.type_register(GTKCheckboxCellRenderer) - -class CheckboxCellRenderer(signals.SignalEmitter): - """Cell Renderer for booleans - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - def __init__(self): - signals.SignalEmitter.__init__(self) - self.create_signal("clicked") - self._renderer = GTKCheckboxCellRenderer() - wrappermap.add(self._renderer, self) - self.want_hover = False - - def set_control_size(self, size): - pass - - def setup_attributes(self, column, attr_map): - column.add_attribute(self._renderer, 'active', attr_map['value']) - -class GTKCustomCellRenderer(gtk.GenericCellRenderer): - """Handles the GTK hide of CustomCellRenderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def on_get_size(self, widget, cell_area=None): - wrapper = wrappermap.wrapper(self) - widget_wrapper = wrappermap.wrapper(widget) - style = drawing.DrawingStyle(widget_wrapper, use_base_color=True) - # NOTE: CustomCellRenderer.cell_data_func() sets up its attributes - # from the model itself, so we don't have to worry about setting them - # here. - width, height = wrapper.get_size(style, widget_wrapper.layout_manager) - x_offset = self.props.xpad - y_offset = self.props.ypad - width += self.props.xpad * 2 - height += self.props.ypad * 2 - if cell_area: - x_offset += cell_area.x - y_offset += cell_area.x - extra_width = max(0, cell_area.width - width) - extra_height = max(0, cell_area.height - height) - x_offset += int(round(self.props.xalign * extra_width)) - y_offset += int(round(self.props.yalign * extra_height)) - return x_offset, y_offset, width, height - - def on_render(self, window, widget, background_area, cell_area, expose_area, - flags): - widget_wrapper = wrappermap.wrapper(widget) - cell_wrapper = wrappermap.wrapper(self) - - selected = (flags & gtk.CELL_RENDERER_SELECTED) - if selected: - if widget.flags() & gtk.HAS_FOCUS: - state = gtk.STATE_SELECTED - else: - state = gtk.STATE_ACTIVE - else: - state = gtk.STATE_NORMAL - if cell_wrapper.IGNORE_PADDING: - area = background_area - else: - xpad = self.props.xpad - ypad = self.props.ypad - area = gtk.gdk.Rectangle(cell_area.x + xpad, cell_area.y + ypad, - cell_area.width - xpad * 2, cell_area.height - ypad * 2) - context = drawing.DrawingContext(window, area, expose_area) - if (selected and not widget_wrapper.draws_selection and - widget_wrapper.use_custom_style): - # Draw the base color as our background. This erases the gradient - # that GTK draws for selected items. - window.draw_rectangle(widget.style.base_gc[state], True, - background_area.x, background_area.y, - background_area.width, background_area.height) - context.style = drawing.DrawingStyle(widget_wrapper, - use_base_color=True, state=state) - widget_wrapper.layout_manager.update_cairo_context(context.context) - hotspot_tracker = widget_wrapper.hotspot_tracker - if (hotspot_tracker and hotspot_tracker.hit and - hotspot_tracker.column == self.column and - hotspot_tracker.path == self.path): - hotspot = hotspot_tracker.name - else: - hotspot = None - if (self.path, self.column) == widget_wrapper.hover_info: - hover = widget_wrapper.hover_pos - hover = (hover[0] - xpad, hover[1] - ypad) - else: - hover = None - # NOTE: CustomCellRenderer.cell_data_func() sets up its attributes - # from the model itself, so we don't have to worry about setting them - # here. - widget_wrapper.layout_manager.reset() - cell_wrapper.render(context, widget_wrapper.layout_manager, selected, - hotspot, hover) - - def on_activate(self, event, widget, path, background_area, cell_area, - flags): - pass - - def on_start_editing(self, event, widget, path, background_area, - cell_area, flags): - pass -gobject.type_register(GTKCustomCellRenderer) - -class CustomCellRenderer(object): - """Customizable Cell Renderer - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - IGNORE_PADDING = False - - def __init__(self): - self._renderer = GTKCustomCellRenderer() - self.want_hover = False - wrappermap.add(self._renderer, self) - - def setup_attributes(self, column, attr_map): - column.set_cell_data_func(self._renderer, self.cell_data_func, - attr_map) - - def cell_data_func(self, column, cell, model, iter, attr_map): - cell.column = column - cell.path = model.get_path(iter) - row = model[iter] - # Set attributes on self instead cell This works because cell is just - # going to turn around and call our methods to do the rendering. - for name, index in attr_map.items(): - setattr(self, name, row[index]) - - def hotspot_test(self, style, layout, x, y, width, height): - return None - -class InfoListRenderer(CustomCellRenderer): - """Custom Renderer for InfoListModels - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def cell_data_func(self, column, cell, model, iter, attr_map): - self.info, self.attrs, self.group_info = \ - wrappermap.wrapper(model).row_for_iter(iter) - cell.column = column - cell.path = model.get_path(iter) - -class InfoListRendererText(CellRenderer): - """Renderer for InfoListModels that only display text - https://develop.participatoryculture.org/index.php/WidgetAPITableView""" - - def setup_attributes(self, column, attr_map): - infolist.gtk.setup_text_cell_data_func(column, self._renderer, - self.get_value) diff --git a/mvc/widgets/gtk/weakconnect.py b/mvc/widgets/gtk/weakconnect.py deleted file mode 100644 index b8b9526..0000000 --- a/mvc/widgets/gtk/weakconnect.py +++ /dev/null @@ -1,56 +0,0 @@ -# @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. - -"""weakconnect.py -- Connect to a signal of a GObject using a weak method -reference. This means that this connection will not keep the object alive. -This is a good thing because it prevents circular references between wrapper -widgets and the wrapped GTK widget. -""" - -from mvc import signals - -class WeakSignalHandler(object): - def __init__(self, method): - self.method = signals.WeakMethodReference(method) - - def connect(self, obj, signal, *user_args): - self.user_args = user_args - self.signal_handle = obj.connect(signal, self.handle_callback) - return self.signal_handle - - def handle_callback(self, obj, *args): - real_method = self.method() - if real_method is not None: - return real_method(obj, *(args + self.user_args)) - else: - obj.disconnect(self.signal_handle) - -def weak_connect(gobject, signal, method, *user_args): - handler = WeakSignalHandler(method) - return handler.connect(gobject, signal, *user_args) diff --git a/mvc/widgets/gtk/widgets.py b/mvc/widgets/gtk/widgets.py deleted file mode 100644 index 6c4280d..0000000 --- a/mvc/widgets/gtk/widgets.py +++ /dev/null @@ -1,47 +0,0 @@ -# @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. - -""".widgets -- Contains portable implementations of -the GTK Widgets. These are shared between the windows port and the x11 port. -""" - -import gtk - -# Just use the GDK Rectangle class -class Rect(gtk.gdk.Rectangle): - @classmethod - def from_string(cls, rect_string): - x, y, width, height = [int(i) for i in rect_string.split(',')] - return Rect(x, y, width, height) - - def __str__(self): - return "%d,%d,%d,%d" % (self.x, self.y, self.width, self.height) - - def get_width(self): - return self.width diff --git a/mvc/widgets/gtk/widgetset.py b/mvc/widgets/gtk/widgetset.py deleted file mode 100644 index c63855c..0000000 --- a/mvc/widgets/gtk/widgetset.py +++ /dev/null @@ -1,63 +0,0 @@ -# @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. - -from .base import Widget, Bin -from .const import * -from .controls import TextEntry, NumberEntry, \ - SecureTextEntry, MultilineTextEntry, Checkbox, RadioButton, \ - RadioButtonGroup, OptionMenu, Button -from .customcontrols import ( - CustomButton, DragableCustomButton, CustomSlider, - ClickableImageButton) -# VolumeSlider and VolumeMuter aren't defined if gtk.VolumeButton -# doesn't have get_popup. -try: - from .customcontrols import ( - VolumeSlider, VolumeMuter) -except ImportError: - pass -from .contextmenu import ContextMenu -from .drawing import ImageSurface, DrawingContext, \ - DrawingArea, Background, Gradient -from .layout import HBox, VBox, Alignment, \ - Splitter, Table, TabContainer, DetachedWindowHolder -from .window import Window, MainWindow, Dialog, \ - FileOpenDialog, FileSaveDialog, DirectorySelectDialog, AboutDialog, \ - AlertDialog, DialogWindow -from .tableview import (TableView, TableModel, - TableColumn, TreeTableModel, CUSTOM_HEADER_HEIGHT) -from .tableviewcells import (CellRenderer, - ImageCellRenderer, CheckboxCellRenderer, CustomCellRenderer, - InfoListRenderer, InfoListRendererText) -from .simple import (Image, ImageDisplay, - AnimatedImageDisplay, Label, Scroller, Expander, SolidBackground, - ProgressBar, HLine) -from .widgets import Rect -from .gtkmenus import (MenuItem, RadioMenuItem, CheckMenuItem, Separator, - Menu, MenuBar, MainWindowMenuBar) diff --git a/mvc/widgets/gtk/window.py b/mvc/widgets/gtk/window.py deleted file mode 100644 index 3859a1a..0000000 --- a/mvc/widgets/gtk/window.py +++ /dev/null @@ -1,708 +0,0 @@ -# @Base: Miro - an RSS based video player application -# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011 -# Jesus Eduardo (Heckyel) | 2017 -# -# 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. - -""".window -- GTK Window widget.""" - -import gobject -import gtk -import os - -from mvc import resources -from mvc import signals - -import keymap -import layout -import widgets -import wrappermap - -# keeps the objects alive until destroy() is called -alive_windows = set() -running_dialogs = set() - -class WrappedWindow(gtk.Window): - def do_map(self): - gtk.Window.do_map(self) - wrappermap.wrapper(self).emit('show') - - def do_unmap(self): - gtk.Window.do_unmap(self) - wrappermap.wrapper(self).emit('hide') - def do_focus_in_event(self, event): - gtk.Window.do_focus_in_event(self, event) - wrappermap.wrapper(self).emit('active-change') - def do_focus_out_event(self, event): - gtk.Window.do_focus_out_event(self, event) - wrappermap.wrapper(self).emit('active-change') - - def do_key_press_event(self, event): - if self.activate_key(event): # event activated a menu item - return - - if self.propagate_key_event(event): # event handled by widget - return - - ret = keymap.translate_gtk_event(event) - if ret is not None: - key, modifiers = ret - rv = wrappermap.wrapper(self).emit('key-press', key, modifiers) - if not rv: - gtk.Window.do_key_press_event(self, event) - - def _get_focused_wrapper(self): - """Get the wrapper of the widget with keyboard focus""" - focused = self.get_focus() - # some of our widgets created children for their use - # (GtkSearchTextEntry). If we don't find a wrapper for - # focused, try it's parents - while focused is not None: - try: - wrapper = wrappermap.wrapper(focused) - except KeyError: - focused = focused.get_parent() - else: - return wrapper - return None - - def change_focus_using_wrapper(self, direction): - my_wrapper = wrappermap.wrapper(self) - focused_wrapper = self._get_focused_wrapper() - if direction == gtk.DIR_TAB_FORWARD: - to_focus = my_wrapper.get_next_tab_focus(focused_wrapper, True) - elif direction == gtk.DIR_TAB_BACKWARD: - to_focus = my_wrapper.get_next_tab_focus(focused_wrapper, False) - else: - return False - if to_focus is not None: - to_focus.focus() - return True - return False - - def do_focus(self, direction): - if not self.change_focus_using_wrapper(direction): - gtk.Window.do_focus(self, direction) - -gobject.type_register(WrappedWindow) - -class WindowBase(signals.SignalEmitter): - def __init__(self): - signals.SignalEmitter.__init__(self) - self.create_signal('use-custom-style-changed') - self.create_signal('key-press') - self.create_signal('show') - self.create_signal('hide') - - def set_window(self, window): - self._window = window - window.connect('style-set', self.on_style_set) - wrappermap.add(window, self) - self.calc_use_custom_style() - - def on_style_set(self, widget, old_style): - old_use_custom_style = self.use_custom_style - self.calc_use_custom_style() - if old_use_custom_style != self.use_custom_style: - self.emit('use-custom-style-changed') - - def calc_use_custom_style(self): - if self._window is not None: - base = self._window.style.base[gtk.STATE_NORMAL] - # Decide if we should use a custom style. Right now the - # formula is the base color is a very light shade of - # gray/white (lighter than #f0f0f0). - self.use_custom_style = ((base.red == base.green == base.blue) and - base.red >= 61680) - - -class Window(WindowBase): - """The main Libre window. """ - - def __init__(self, title, rect=None): - """Create the Libre Main Window. Title is the name to give the - window, rect specifies the position it should have on screen. - """ - WindowBase.__init__(self) - self.set_window(self._make_gtk_window()) - self._window.set_title(title) - self.setup_icon() - if rect: - self._window.set_default_size(rect.width, rect.height) - self._window.set_default_size(rect.width, rect.height) - self._window.set_gravity(gtk.gdk.GRAVITY_CENTER) - self._window.move(rect.x, rect.y) - - self.create_signal('active-change') - self.create_signal('will-close') - self.create_signal('did-move') - self.create_signal('file-drag-motion') - self.create_signal('file-drag-received') - self.create_signal('file-drag-leave') - self.create_signal('on-shown') - self.drag_signals = [] - alive_windows.add(self) - - self._window.connect('delete-event', self.on_delete_window) - self._window.connect('map-event', lambda w, a: self.emit('on-shown')) - # XXX: Define MVCWindow/MiroWindow style not hard code this - self._window.set_resizable(False) - - def setup_icon(self): - icon_pixbuf = gtk.gdk.pixbuf_new_from_file( - resources.image_path("mvc-logo.png")) - self._window.set_icon(icon_pixbuf) - - - def accept_file_drag(self, val): - if not val: - self._window.drag_dest_set(0, [], 0) - for handle in self.drag_signals: - self.disconnect(handle) - self.drag_signals = [] - else: - self._window.drag_dest_set( - gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_DROP, - [('text/uri-list', 0, 0)], - gtk.gdk.ACTION_COPY) - for signal, callback in ( - ('drag-motion', self.on_drag_motion), - ('drag-data-received', self.on_drag_data_received), - ('drag-leave', self.on_drag_leave)): - self.drag_signals.append( - self._window.connect(signal, callback)) - - def on_drag_motion(self, widget, context, x, y, time): - self.emit('file-drag-motion') - - def on_drag_data_received(self, widget, context, x, y, selection_data, - info, time): - self.emit('file-drag-received', selection_data.get_uris()) - - def on_drag_leave(self, widget, context, time): - self.emit('file-drag-leave') - - def on_delete_window(self, widget, event): - # when the user clicks on the X in the corner of the window we - # want that to close the window, but also trigger our - # will-close signal and all that machinery unless the window - # is currently hidden--then we don't do anything. - if not self._window.window.is_visible(): - return - self.close() - return True - - def _make_gtk_window(self): - return WrappedWindow() - - def set_title(self, title): - self._window.set_title(title) - - def get_title(self): - self._window.get_title() - - def center(self): - self._window.set_position(gtk.WIN_POS_CENTER) - - def show(self): - if self not in alive_windows: - raise ValueError("Window destroyed") - self._window.show() - - def close(self): - if hasattr(self, "_closing"): - return - self._closing = True - # Keep a reference to the widget in case will-close signal handler - # calls destroy() - old_window = self._window - self.emit('will-close') - old_window.hide() - del self._closing - - def destroy(self): - self.close() - self._window = None - alive_windows.discard(self) - - def is_active(self): - return self._window.is_active() - - def is_visible(self): - return self._window.props.visible - - def get_next_tab_focus(self, current, is_forward): - return None - - def set_content_widget(self, widget): - """Set the widget that will be drawn in the content area for this - window. - - It will be allocated the entire area of the widget, except the - space needed for the titlebar, frame and other decorations. - When the window is resized, content should also be resized. - """ - self._add_content_widget(widget) - widget._widget.show() - self.content_widget = widget - - def _add_content_widget(self, widget): - self._window.add(widget._widget) - - def get_content_widget(self, widget): - """Get the current content widget.""" - return self.content_widget - - def get_frame(self): - pos = self._window.get_position() - size = self._window.get_size() - return widgets.Rect(pos[0], pos[1], size[0], size[1]) - - def set_frame(self, x=None, y=None, width=None, height=None): - if x is not None or y is not None: - pos = self._window.get_position() - x = x if x is not None else pos[0] - y = y if y is not None else pos[1] - self._window.move(x, y) - - if width is not None or height is not None: - size = self._window.get_size() - width = width if width is not None else size[0] - height = height if height is not None else size[1] - self._window.resize(width, height) - - def get_monitor_geometry(self): - """Returns a Rect of the geometry of the monitor that this - window is currently on. - - :returns: Rect - """ - gtkwindow = self._window - gdkwindow = gtkwindow.window - screen = gtkwindow.get_screen() - - monitor = screen.get_monitor_at_window(gdkwindow) - return screen.get_monitor_geometry(monitor) - - def check_position_and_fix(self): - """This pulls the geometry of the monitor of the screen this - window is on as well as the position of the window. - - It then makes sure that the position y is greater than the - monitor geometry y. This makes sure that the titlebar of - the window is showing. - """ - gtkwindow = self._window - gdkwindow = gtkwindow.window - monitor_geom = self.get_monitor_geometry() - - frame_extents = gdkwindow.get_frame_extents() - position = gtkwindow.get_position() - - # if the frame is not visible, then we move the window so that - # it is - if frame_extents.y < monitor_geom.y: - gtkwindow.move(position[0], - monitor_geom.y + (position[1] - frame_extents.y)) - - - -class DialogWindow(Window): - def __init__(self, title, rect=None): - Window.__init__(self, title, rect) - self._window.set_resizable(False) - -class MainWindow(Window): - def __init__(self, title, rect): - Window.__init__(self, title, rect) - self.vbox = gtk.VBox() - self._window.add(self.vbox) - self.vbox.show() - self._add_app_menubar() - self.create_signal('save-dimensions') - self.create_signal('save-maximized') - self._window.connect('key-release-event', self.on_key_release) - self._window.connect('window-state-event', self.on_window_state_event) - self._window.connect('configure-event', self.on_configure_event) - - def _make_gtk_window(self): - return WrappedWindow() - - def on_delete_window(self, widget, event): - return True - - def on_configure_event(self, widget, event): - (x, y) = self._window.get_position() - (width, height) = self._window.get_size() - self.emit('save-dimensions', x, y, width, height) - - def on_window_state_event(self, widget, event): - maximized = bool( - event.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED) - self.emit('save-maximized', maximized) - - def on_key_release(self, widget, event): - if app.playback_manager.is_playing: - if gtk.gdk.keyval_name(event.keyval) in ('Right', 'Left', - 'Up', 'Down'): - return True - - def _add_app_menubar(self): - self.menubar = app.widgetapp.menubar - self.vbox.pack_start(self.menubar._widget, expand=False) - self.connect_menu_keyboard_shortcuts() - - def _add_content_widget(self, widget): - self.vbox.pack_start(widget._widget, expand=True) - - -class DialogBase(WindowBase): - def set_transient_for(self, window): - self._window.set_transient_for(window._window) - - def run(self): - running_dialogs.add(self) - try: - return self._run() - finally: - running_dialogs.remove(self) - self._window = None - - def _run(self): - """Run the dialog. Must be implemented by subclasses.""" - raise NotImplementedError() - - def destroy(self): - if self._window is not None: - self._window.response(gtk.RESPONSE_NONE) - # don't set self._window to None yet. We will unset it when we - # return from the _run() method - -class Dialog(DialogBase): - def __init__(self, title, description=None): - """Create a dialog.""" - DialogBase.__init__(self) - self.create_signal('open') - self.create_signal('close') - self.set_window(gtk.Dialog(title)) - self._window.set_default_size(425, -1) - self.extra_widget = None - self.buttons_to_add = [] - wrappermap.add(self._window, self) - self.description = description - - def build_content(self): - packing_vbox = layout.VBox(spacing=20) - packing_vbox._widget.set_border_width(6) - if self.description is not None: - label = gtk.Label(self.description) - label.set_line_wrap(True) - label.set_size_request(390, -1) - label.set_selectable(True) - packing_vbox._widget.pack_start(label) - if self.extra_widget: - packing_vbox._widget.pack_start(self.extra_widget._widget) - return packing_vbox - - def add_button(self, text): - from mvc.widgets import dialogs - _stock = { - dialogs.BUTTON_OK.text: gtk.STOCK_OK, - dialogs.BUTTON_CANCEL.text: gtk.STOCK_CANCEL, - dialogs.BUTTON_YES.text: gtk.STOCK_YES, - dialogs.BUTTON_NO.text: gtk.STOCK_NO, - dialogs.BUTTON_QUIT.text: gtk.STOCK_QUIT, - dialogs.BUTTON_REMOVE.text: gtk.STOCK_REMOVE, - dialogs.BUTTON_DELETE.text: gtk.STOCK_DELETE, - } - if text in _stock: - # store both the text and the stock ID - text = _stock[text], text - self.buttons_to_add.append(text) - - def pack_buttons(self): - # There's a couple tricky things here: - # 1) We need to add them in the reversed order we got them, since GTK - # lays them out left-to-right - # - # 2) We can't use 0 as a response-id. GTK only reserves positive - # response_ids for the user. - response_id = len(self.buttons_to_add) - for text in reversed(self.buttons_to_add): - label = None - if isinstance(text, tuple): # stock ID, text - text, label = text - button = self._window.add_button(text, response_id) - if label is not None: - button.set_label(label) - response_id -= 1 - self.buttons_to_add = [] - self._window.set_default_response(1) - - def _run(self): - self.pack_buttons() - packing_vbox = self.build_content() - self._window.vbox.pack_start(packing_vbox._widget, True, True) - self._window.show_all() - response = self._window.run() - self._window.hide() - if response == gtk.RESPONSE_DELETE_EVENT: - return -1 - else: - return response - 1 # response IDs started at 1 - - def set_extra_widget(self, widget): - self.extra_widget = widget - - def get_extra_widget(self): - return self.extra_widget - -class FileDialogBase(DialogBase): - def _run(self): - ret = self._window.run() - self._window.hide() - if ret == gtk.RESPONSE_OK: - self._files = self._window.get_filenames() - return 0 - -class FileOpenDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - fcd = gtk.FileChooserDialog(title, - action=gtk.FILE_CHOOSER_ACTION_OPEN, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - gtk.STOCK_OPEN, - gtk.RESPONSE_OK)) - - self.set_window(fcd) - - def set_filename(self, text): - self._window.set_filename(text) - - def set_select_multiple(self, value): - self._window.set_select_multiple(value) - - def add_filters(self, filters): - for name, ext_list in filters: - f = gtk.FileFilter() - f.set_name(name) - for mem in ext_list: - f.add_pattern('*.%s' % mem) - self._window.add_filter(f) - - f = gtk.FileFilter() - f.set_name(_('All files')) - f.add_pattern('*') - self._window.add_filter(f) - - def get_filenames(self): - return [unicode(f) for f in self._files] - - def get_filename(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_filename - def set_path(self, path): - # set_filename puts the whole path in the filename field - self._window.set_current_folder(os.path.dirname(path)) - self._window.set_current_name(os.path.basename(path)) - -class FileSaveDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - fcd = gtk.FileChooserDialog(title, - action=gtk.FILE_CHOOSER_ACTION_SAVE, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, - gtk.RESPONSE_OK)) - self.set_window(fcd) - - def set_filename(self, text): - self._window.set_current_name(text) - - def get_filename(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_filename - def set_path(self, path): - # set_filename puts the whole path in the filename field - self._window.set_current_folder(os.path.dirname(path)) - self._window.set_current_name(os.path.basename(path)) - -class DirectorySelectDialog(FileDialogBase): - def __init__(self, title): - FileDialogBase.__init__(self) - self._files = None - choose_str = 'Choose' - fcd = gtk.FileChooserDialog( - title, - action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, - buttons=(gtk.STOCK_CANCEL, - gtk.RESPONSE_CANCEL, - choose_str, gtk.RESPONSE_OK)) - self.set_window(fcd) - - def set_directory(self, text): - self._window.set_filename(text) - - def get_directory(self): - if self._files is None: - # clicked Cancel - return None - else: - return unicode(self._files[0]) - - # provide a common interface for file chooser dialogs - get_path = get_directory - set_path = set_directory - -class AboutDialog(Dialog): - def __init__(self): - Dialog.__init__(self, "Libre Video Converter") -# _("About %(appname)s", -# {'appname': app.config.get(prefs.SHORT_APP_NAME)})) -# self.add_button(_("Close")) - self.add_button("Close") - self._window.set_has_separator(False) - - def build_content(self): - packing_vbox = layout.VBox(spacing=20) - #icon_pixbuf = gtk.gdk.pixbuf_new_from_file_at_size( - # resources.share_path('icons/hicolor/128x128/apps/miro.png'), - # 48, 48) - #packing_vbox._widget.pack_start(gtk.image_new_from_pixbuf(icon_pixbuf)) - #if app.config.get(prefs.APP_REVISION_NUM): - # version = "%s (%s)" % ( - # app.config.get(prefs.APP_VERSION), - # app.config.get(prefs.APP_REVISION_NUM)) - #else: - # version = "%s" % app.config.get(prefs.APP_VERSION) - version = '3.0' - #name_label = gtk.Label( - # '<span size="xx-large" weight="bold">%s %s</span>' % ( - # app.config.get(prefs.SHORT_APP_NAME), version)) - name_label = gtk.Label( - '<span size="xx-large" weight="bold">%s %s</span>' % ( - 'Libre Video Converter', version)) - name_label.set_use_markup(True) - packing_vbox._widget.pack_start(name_label) - copyright_text = 'Copyright (c) Jesus Eduardo (Heckyel) | 2017' - copyright_label = gtk.Label('<small>%s</small>' % copyright_text) - copyright_label.set_use_markup(True) - copyright_label.set_justify(gtk.JUSTIFY_CENTER) - packing_vbox._widget.pack_start(copyright_label) - - # FIXME - make the project url clickable - #packing_vbox._widget.pack_start( - # gtk.Label(app.config.get(prefs.PROJECT_URL))) - - #contributor_label = gtk.Label( - # _("Thank you to all the people who contributed to %(appname)s " - # "%(version)s:", - # {"appname": app.config.get(prefs.SHORT_APP_NAME), - # "version": app.config.get(prefs.APP_VERSION)})) - #contributor_label.set_justify(gtk.JUSTIFY_CENTER) - #packing_vbox._widget.pack_start(contributor_label) - - # get contributors, remove newlines and wrap it - #contributors = open(resources.path('CREDITS'), 'r').readlines() - #contributors = [c[2:].strip() - # for c in contributors if c.startswith("* ")] - #contributors = ", ".join(contributors) - - # show contributors - #contrib_buffer = gtk.TextBuffer() - #contrib_buffer.set_text(contributors) - - #contrib_view = gtk.TextView(contrib_buffer) - #contrib_view.set_editable(False) - #contrib_view.set_cursor_visible(False) - #contrib_view.set_wrap_mode(gtk.WRAP_WORD) - #contrib_window = gtk.ScrolledWindow() - #contrib_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) - #contrib_window.add(contrib_view) - #contrib_window.set_size_request(-1, 100) - #packing_vbox._widget.pack_start(contrib_window) - - # FIXME - make the project url clickable - #donate_label = gtk.Label( - # _("To help fund continued %(appname)s development, visit the " - # "donation page at:", - # {"appname": app.config.get(prefs.SHORT_APP_NAME)})) - #donate_label.set_justify(gtk.JUSTIFY_CENTER) - #packing_vbox._widget.pack_start(donate_label) - - #packing_vbox._widget.pack_start( - # gtk.Label(app.config.get(prefs.DONATE_URL))) - return packing_vbox - - def on_contrib_link_event(self, texttag, widget, event, iter_): - if event.type == gtk.gdk.BUTTON_PRESS: - resources.open_url('https://notabug.org/heckyel/librevideoconverter') - -type_map = { - 0: gtk.MESSAGE_WARNING, - 1: gtk.MESSAGE_INFO, - 2: gtk.MESSAGE_ERROR -} - -class AlertDialog(DialogBase): - def __init__(self, title, description, alert_type): - DialogBase.__init__(self) - message_type = type_map.get(alert_type, gtk.MESSAGE_INFO) - self.set_window(gtk.MessageDialog(type=message_type, - message_format=description)) - self._window.set_title(title) - self.description = description - - def add_button(self, text): - self._window.add_button(_stock.get(text, text), 1) - self._window.set_default_response(1) - - def _run(self): - self._window.set_modal(False) - self._window.show_all() - response = self._window.run() - self._window.hide() - if response == gtk.RESPONSE_DELETE_EVENT: - return -1 - else: - # response IDs start at 1 - return response - 1 diff --git a/mvc/widgets/gtk/wrappermap.py b/mvc/widgets/gtk/wrappermap.py deleted file mode 100644 index c2b2aad..0000000 --- a/mvc/widgets/gtk/wrappermap.py +++ /dev/null @@ -1,50 +0,0 @@ -# @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. - -""".wrappermap -- Map GTK Widgets to the Libre Widget -that wraps them. -""" - -import weakref - -# Maps gtk windows -> wrapper objects. We use a weak references to prevent -# circular references between the GTK widget and it's wrapper. (Keeping a -# reference to the GTK widget is fine, since if the wrapper is alive, the GTK -# widget should be). -widget_mapping = weakref.WeakValueDictionary() - -def wrapper(gtk_widget): - """Find the wrapper widget for a GTK widget.""" - try: - return widget_mapping[gtk_widget] - except KeyError: - raise KeyError("Widget wrapper no longer exists") - -def add(gtk_widget, wrapper): - widget_mapping[gtk_widget] = wrapper |