summaryrefslogtreecommitdiff
path: root/pygtkcompat/generictreemodel.py
diff options
context:
space:
mode:
Diffstat (limited to 'pygtkcompat/generictreemodel.py')
-rw-r--r--pygtkcompat/generictreemodel.py420
1 files changed, 420 insertions, 0 deletions
diff --git a/pygtkcompat/generictreemodel.py b/pygtkcompat/generictreemodel.py
new file mode 100644
index 0000000..ad67d90
--- /dev/null
+++ b/pygtkcompat/generictreemodel.py
@@ -0,0 +1,420 @@
+# -*- Mode: Python; py-indent-offset: 4 -*-
+# generictreemodel - GenericTreeModel implementation for pygtk compatibility.
+# Copyright (C) 2013 Simon Feltman
+#
+# generictreemodel.py: GenericTreeModel implementation for pygtk compatibility
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+# USA
+
+
+# System
+import sys
+import random
+import collections
+import ctypes
+
+# GObject
+from gi.repository import GObject
+from gi.repository import Gtk
+
+
+class _CTreeIter(ctypes.Structure):
+ _fields_ = [('stamp', ctypes.c_int),
+ ('user_data', ctypes.c_void_p),
+ ('user_data2', ctypes.c_void_p),
+ ('user_data3', ctypes.c_void_p)]
+
+ @classmethod
+ def from_iter(cls, iter):
+ offset = sys.getsizeof(object()) # size of PyObject_HEAD
+ return ctypes.POINTER(cls).from_address(id(iter) + offset)
+
+
+def _get_user_data_as_pyobject(iter):
+ citer = _CTreeIter.from_iter(iter)
+ return ctypes.cast(citer.contents.user_data, ctypes.py_object).value
+
+
+def handle_exception(default_return):
+ """Returns a function which can act as a decorator for wrapping exceptions and
+ returning "default_return" upon an exception being thrown.
+
+ This is used to wrap Gtk.TreeModel "do_" method implementations so we can return
+ a proper value from the override upon an exception occurring with client code
+ implemented by the "on_" methods.
+ """
+ def decorator(func):
+ def wrapped_func(*args, **kargs):
+ try:
+ return func(*args, **kargs)
+ except:
+ # Use excepthook directly to avoid any printing to the screen
+ # if someone installed an except hook.
+ sys.excepthook(*sys.exc_info())
+ return default_return
+ return wrapped_func
+ return decorator
+
+
+class GenericTreeModel(GObject.GObject, Gtk.TreeModel):
+ """A base implementation of a Gtk.TreeModel for python.
+
+ The GenericTreeModel eases implementing the Gtk.TreeModel interface in Python.
+ The class can be subclassed to provide a TreeModel implementation which works
+ directly with Python objects instead of iterators.
+
+ All of the on_* methods should be overridden by subclasses to provide the
+ underlying implementation a way to access custom model data. For the purposes of
+ this API, all custom model data supplied or handed back through the overridable
+ API will use the argument names: node, parent, and child in regards to user data
+ python objects.
+
+ The create_tree_iter, set_user_data, invalidate_iters, iter_is_valid methods are
+ available to help manage Gtk.TreeIter objects and their Python object references.
+
+ GenericTreeModel manages a pool of user data nodes that have been used with iters.
+ This pool stores a references to user data nodes as a dictionary value with the
+ key being the integer id of the data. This id is what the Gtk.TreeIter objects
+ use to reference data in the pool.
+ References will be removed from the pool when the model is deleted or explicitly
+ by using the optional "node" argument to the "row_deleted" method when notifying
+ the model of row deletion.
+ """
+
+ leak_references = GObject.Property(default=True, type=bool,
+ blurb="If True, strong references to user data attached to iters are "
+ "stored in a dictionary pool (default). Otherwise the user data is "
+ "stored as a raw pointer to a python object without a reference.")
+
+ #
+ # Methods
+ #
+ def __init__(self):
+ """Initialize. Make sure to call this from derived classes if overridden."""
+ super(GenericTreeModel, self).__init__()
+ self.stamp = 0
+
+ #: Dictionary of (id(user_data): user_data), used when leak-refernces=False
+ self._held_refs = dict()
+
+ # Set initial stamp
+ self.invalidate_iters()
+
+ def iter_depth_first(self):
+ """Depth-first iteration of the entire TreeModel yielding the python nodes."""
+ stack = collections.deque([None])
+ while stack:
+ it = stack.popleft()
+ if it is not None:
+ yield self.get_user_data(it)
+ children = [self.iter_nth_child(it, i) for i in range(self.iter_n_children(it))]
+ stack.extendleft(reversed(children))
+
+ def invalidate_iter(self, iter):
+ """Clear user data and its reference from the iter and this model."""
+ iter.stamp = 0
+ if iter.user_data:
+ if iter.user_data in self._held_refs:
+ del self._held_refs[iter.user_data]
+ iter.user_data = None
+
+ def invalidate_iters(self):
+ """
+ This method invalidates all TreeIter objects associated with this custom tree model
+ and frees their locally pooled references.
+ """
+ self.stamp = random.randint(-2147483648, 2147483647)
+ self._held_refs.clear()
+
+ def iter_is_valid(self, iter):
+ """
+ :Returns:
+ True if the gtk.TreeIter specified by iter is valid for the custom tree model.
+ """
+ return iter.stamp == self.stamp
+
+ def get_user_data(self, iter):
+ """Get the user_data associated with the given TreeIter.
+
+ GenericTreeModel stores arbitrary Python objects mapped to instances of Gtk.TreeIter.
+ This method allows to retrieve the Python object held by the given iterator.
+ """
+ if self.leak_references:
+ return self._held_refs[iter.user_data]
+ else:
+ return _get_user_data_as_pyobject(iter)
+
+ def set_user_data(self, iter, user_data):
+ """Applies user_data and stamp to the given iter.
+
+ If the models "leak_references" property is set, a reference to the
+ user_data is stored with the model to ensure we don't run into bad
+ memory problems with the TreeIter.
+ """
+ iter.user_data = id(user_data)
+
+ if user_data is None:
+ self.invalidate_iter(iter)
+ else:
+ iter.stamp = self.stamp
+ if self.leak_references:
+ self._held_refs[iter.user_data] = user_data
+
+ def create_tree_iter(self, user_data):
+ """Create a Gtk.TreeIter instance with the given user_data specific for this model.
+
+ Use this method to create Gtk.TreeIter instance instead of directly calling
+ Gtk.Treeiter(), this will ensure proper reference managment of wrapped used_data.
+ """
+ iter = Gtk.TreeIter()
+ self.set_user_data(iter, user_data)
+ return iter
+
+ def _create_tree_iter(self, data):
+ """Internal creation of a (bool, TreeIter) pair for returning directly
+ back to the view interfacing with this model."""
+ if data is None:
+ return (False, None)
+ else:
+ it = self.create_tree_iter(data)
+ return (True, it)
+
+ def row_deleted(self, path, node=None):
+ """Notify the model a row has been deleted.
+
+ Use the node parameter to ensure the user_data reference associated
+ with the path is properly freed by this model.
+
+ :Parameters:
+ path : Gtk.TreePath
+ Path to the row that has been deleted.
+ node : object
+ Python object used as the node returned from "on_get_iter". This is
+ optional but ensures the model will not leak references to this object.
+ """
+ super(GenericTreeModel, self).row_deleted(path)
+ node_id = id(node)
+ if node_id in self._held_refs:
+ del self._held_refs[node_id]
+
+ #
+ # GtkTreeModel Interface Implementation
+ #
+ @handle_exception(0)
+ def do_get_flags(self):
+ """Internal method."""
+ return self.on_get_flags()
+
+ @handle_exception(0)
+ def do_get_n_columns(self):
+ """Internal method."""
+ return self.on_get_n_columns()
+
+ @handle_exception((False, None))
+ def do_get_column_type(self, index):
+ """Internal method."""
+ return self.on_get_column_type(index)
+
+ @handle_exception((False, None))
+ def do_get_iter(self, path):
+ """Internal method."""
+ return self._create_tree_iter(self.on_get_iter(path))
+
+ @handle_exception(False)
+ def do_iter_next(self, iter):
+ """Internal method."""
+ if iter is None:
+ next_data = self.on_iter_next(None)
+ else:
+ next_data = self.on_iter_next(self.get_user_data(iter))
+
+ self.set_user_data(iter, next_data)
+ return next_data is not None
+
+ @handle_exception(None)
+ def do_get_path(self, iter):
+ """Internal method."""
+ path = self.on_get_path(self.get_user_data(iter))
+ if path is None:
+ return None
+ else:
+ return Gtk.TreePath(path)
+
+ @handle_exception(None)
+ def do_get_value(self, iter, column):
+ """Internal method."""
+ return self.on_get_value(self.get_user_data(iter), column)
+
+ @handle_exception((False, None))
+ def do_iter_children(self, parent):
+ """Internal method."""
+ data = self.get_user_data(parent) if parent else None
+ return self._create_tree_iter(self.on_iter_children(data))
+
+ @handle_exception(False)
+ def do_iter_has_child(self, parent):
+ """Internal method."""
+ return self.on_iter_has_child(self.get_user_data(parent))
+
+ @handle_exception(0)
+ def do_iter_n_children(self, iter):
+ """Internal method."""
+ if iter is None:
+ return self.on_iter_n_children(None)
+ return self.on_iter_n_children(self.get_user_data(iter))
+
+ @handle_exception((False, None))
+ def do_iter_nth_child(self, parent, n):
+ """Internal method."""
+ if parent is None:
+ data = self.on_iter_nth_child(None, n)
+ else:
+ data = self.on_iter_nth_child(self.get_user_data(parent), n)
+ return self._create_tree_iter(data)
+
+ @handle_exception((False, None))
+ def do_iter_parent(self, child):
+ """Internal method."""
+ return self._create_tree_iter(self.on_iter_parent(self.get_user_data(child)))
+
+ @handle_exception(None)
+ def do_ref_node(self, iter):
+ self.on_ref_node(self.get_user_data(iter))
+
+ @handle_exception(None)
+ def do_unref_node(self, iter):
+ self.on_unref_node(self.get_user_data(iter))
+
+ #
+ # Python Subclass Overridables
+ #
+ def on_get_flags(self):
+ """Overridable.
+
+ :Returns Gtk.TreeModelFlags:
+ The flags for this model. See: Gtk.TreeModelFlags
+ """
+ raise NotImplementedError
+
+ def on_get_n_columns(self):
+ """Overridable.
+
+ :Returns:
+ The number of columns for this model.
+ """
+ raise NotImplementedError
+
+ def on_get_column_type(self, index):
+ """Overridable.
+
+ :Returns:
+ The column type for the given index.
+ """
+ raise NotImplementedError
+
+ def on_get_iter(self, path):
+ """Overridable.
+
+ :Returns:
+ A python object (node) for the given TreePath.
+ """
+ raise NotImplementedError
+
+ def on_iter_next(self, node):
+ """Overridable.
+
+ :Parameters:
+ node : object
+ Node at current level.
+
+ :Returns:
+ A python object (node) following the given node at the current level.
+ """
+ raise NotImplementedError
+
+ def on_get_path(self, node):
+ """Overridable.
+
+ :Returns:
+ A TreePath for the given node.
+ """
+ raise NotImplementedError
+
+ def on_get_value(self, node, column):
+ """Overridable.
+
+ :Parameters:
+ node : object
+ column : int
+ Column index to get the value from.
+
+ :Returns:
+ The value of the column for the given node."""
+ raise NotImplementedError
+
+ def on_iter_children(self, parent):
+ """Overridable.
+
+ :Returns:
+ The first child of parent or None if parent has no children.
+ If parent is None, return the first node of the model.
+ """
+ raise NotImplementedError
+
+ def on_iter_has_child(self, node):
+ """Overridable.
+
+ :Returns:
+ True if the given node has children.
+ """
+ raise NotImplementedError
+
+ def on_iter_n_children(self, node):
+ """Overridable.
+
+ :Returns:
+ The number of children for the given node. If node is None,
+ return the number of top level nodes.
+ """
+ raise NotImplementedError
+
+ def on_iter_nth_child(self, parent, n):
+ """Overridable.
+
+ :Parameters:
+ parent : object
+ n : int
+ Index of child within parent.
+
+ :Returns:
+ The child for the given parent index starting at 0. If parent None,
+ return the top level node corresponding to "n".
+ If "n" is larger then available nodes, return None.
+ """
+ raise NotImplementedError
+
+ def on_iter_parent(self, child):
+ """Overridable.
+
+ :Returns:
+ The parent node of child or None if child is a top level node."""
+ raise NotImplementedError
+
+ def on_ref_node(self, node):
+ pass
+
+ def on_unref_node(self, node):
+ pass