aboutsummaryrefslogtreecommitdiffstats
path: root/mvc/widgets/gtk/tableview.py
diff options
context:
space:
mode:
Diffstat (limited to 'mvc/widgets/gtk/tableview.py')
-rw-r--r--mvc/widgets/gtk/tableview.py1557
1 files changed, 0 insertions, 1557 deletions
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_)