[pygobject] pygtkcompat: Add pygtk compatible GenericTreeModel implementation
- From: Simon Feltman <sfeltman src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pygobject] pygtkcompat: Add pygtk compatible GenericTreeModel implementation
- Date: Mon, 18 Feb 2013 11:03:57 +0000 (UTC)
commit a10fb7216de57046d1ecacb73dd032eaadcbad09
Author: Simon Feltman <s feltman gmail com>
Date: Wed Aug 29 03:46:23 2012 -0700
pygtkcompat: Add pygtk compatible GenericTreeModel implementation
Add Python implementation of the GenericTreeModel that was
available in pygtk. The implementation attempts a better job
than the original at ref counting by guaranteeing no leaks
upon deletion of the model itself. Or by using the extra "node"
argument to the row_deleted signal. The model is available in
the pygtkcompat package directly as
pygtkcompat.generictreemodel.GenericTreeModel or with as
gtk.GenericTreeModel when pygtkcompat.enable_gtk() is set.
Add file list and tree demos making use of GenericTreeModel
to gtk-demo.
Auto-expand gtk-demo app tree to give a better overview of
the demos available.
https://bugzilla.gnome.org/show_bug.cgi?id=682933
.../gtk-demo/demos/Tree View/treemodel_filelist.py | 234 +++++++++++
.../gtk-demo/demos/Tree View/treemodel_filetree.py | 279 +++++++++++++
demos/gtk-demo/gtk-demo.py | 2 +-
gi/pygtkcompat.py | 2 +-
pygtkcompat/Makefile.am | 1 +
pygtkcompat/generictreemodel.py | 420 ++++++++++++++++++++
pygtkcompat/pygtkcompat.py | 3 +
tests/Makefile.am | 1 +
tests/test_generictreemodel.py | 406 +++++++++++++++++++
9 files changed, 1346 insertions(+), 2 deletions(-)
---
diff --git a/demos/gtk-demo/demos/Tree View/treemodel_filelist.py b/demos/gtk-demo/demos/Tree
View/treemodel_filelist.py
new file mode 100644
index 0000000..96c2620
--- /dev/null
+++ b/demos/gtk-demo/demos/Tree View/treemodel_filelist.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python
+
+title = "File List (GenericTreeModel)"
+description = """
+This is a file list demo which makes use of the GenericTreeModel python
+implementation of the Gtk.TreeModel interface. This demo shows what methods
+need to be overridden to provide a valid TreeModel to a TreeView.
+"""
+
+import os
+import stat
+import time
+
+import pygtkcompat
+pygtkcompat.enable()
+pygtkcompat.enable_gtk('3.0')
+
+import gtk
+
+
+folderxpm = [
+ "17 16 7 1",
+ " c #000000",
+ ". c #808000",
+ "X c yellow",
+ "o c #808080",
+ "O c #c0c0c0",
+ "+ c white",
+ "@ c None",
+ "@@@@@@@@@@@@@@@@@",
+ "@@@@@@@@@@@@@@@@@",
+ "@@+XXXX.@@@@@@@@@",
+ "@+OOOOOO.@@@@@@@@",
+ "@+OXOXOXOXOXOXO. ",
+ "@+XOXOXOXOXOXOX. ",
+ "@+OXOXOXOXOXOXO. ",
+ "@+XOXOXOXOXOXOX. ",
+ "@+OXOXOXOXOXOXO. ",
+ "@+XOXOXOXOXOXOX. ",
+ "@+OXOXOXOXOXOXO. ",
+ "@+XOXOXOXOXOXOX. ",
+ "@+OOOOOOOOOOOOO. ",
+ "@ ",
+ "@@@@@@@@@@@@@@@@@",
+ "@@@@@@@@@@@@@@@@@"
+ ]
+folderpb = gtk.gdk.pixbuf_new_from_xpm_data(folderxpm)
+
+filexpm = [
+ "12 12 3 1",
+ " c #000000",
+ ". c #ffff04",
+ "X c #b2c0dc",
+ "X XXX",
+ "X ...... XXX",
+ "X ...... X",
+ "X . ... X",
+ "X ........ X",
+ "X . .... X",
+ "X ........ X",
+ "X . .. X",
+ "X ........ X",
+ "X . .. X",
+ "X ........ X",
+ "X X"
+ ]
+filepb = gtk.gdk.pixbuf_new_from_xpm_data(filexpm)
+
+
+class FileListModel(gtk.GenericTreeModel):
+ __gtype_name__ = 'DemoFileListModel'
+
+ column_types = (gtk.gdk.Pixbuf, str, int, str, str)
+ column_names = ['Name', 'Size', 'Mode', 'Last Changed']
+
+ def __init__(self, dname=None):
+ gtk.GenericTreeModel.__init__(self)
+ self._sort_column_id = 0
+ self._sort_order = gtk.SORT_ASCENDING
+
+ if not dname:
+ self.dirname = os.path.expanduser('~')
+ else:
+ self.dirname = os.path.abspath(dname)
+ self.files = ['..'] + [f for f in os.listdir(self.dirname)]
+ return
+
+ def get_pathname(self, path):
+ filename = self.files[path[0]]
+ return os.path.join(self.dirname, filename)
+
+ def is_folder(self, path):
+ filename = self.files[path[0]]
+ pathname = os.path.join(self.dirname, filename)
+ filestat = os.stat(pathname)
+ if stat.S_ISDIR(filestat.st_mode):
+ return True
+ return False
+
+ def get_column_names(self):
+ return self.column_names[:]
+
+ #
+ # GenericTreeModel Implementation
+ #
+ def on_get_flags(self):
+ return 0 # gtk.TREE_MODEL_ITERS_PERSIST
+
+ def on_get_n_columns(self):
+ return len(self.column_types)
+
+ def on_get_column_type(self, n):
+ return self.column_types[n]
+
+ def on_get_iter(self, path):
+ return self.files[path[0]]
+
+ def on_get_path(self, rowref):
+ return self.files.index(rowref)
+
+ def on_get_value(self, rowref, column):
+ fname = os.path.join(self.dirname, rowref)
+ try:
+ filestat = os.stat(fname)
+ except OSError:
+ return None
+ mode = filestat.st_mode
+ if column is 0:
+ if stat.S_ISDIR(mode):
+ return folderpb
+ else:
+ return filepb
+ elif column is 1:
+ return rowref
+ elif column is 2:
+ return filestat.st_size
+ elif column is 3:
+ return oct(stat.S_IMODE(mode))
+ return time.ctime(filestat.st_mtime)
+
+ def on_iter_next(self, rowref):
+ try:
+ i = self.files.index(rowref) + 1
+ return self.files[i]
+ except IndexError:
+ return None
+
+ def on_iter_children(self, rowref):
+ if rowref:
+ return None
+ return self.files[0]
+
+ def on_iter_has_child(self, rowref):
+ return False
+
+ def on_iter_n_children(self, rowref):
+ if rowref:
+ return 0
+ return len(self.files)
+
+ def on_iter_nth_child(self, rowref, n):
+ if rowref:
+ return None
+ try:
+ return self.files[n]
+ except IndexError:
+ return None
+
+ def on_iter_parent(child):
+ return None
+
+
+class GenericTreeModelExample:
+ def delete_event(self, widget, event, data=None):
+ gtk.main_quit()
+ return False
+
+ def __init__(self):
+ # Create a new window
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+
+ self.window.set_size_request(300, 200)
+
+ self.window.connect("delete_event", self.delete_event)
+
+ self.listmodel = FileListModel()
+
+ # create the TreeView
+ self.treeview = gtk.TreeView()
+
+ self.tvcolumns = []
+
+ # create the TreeViewColumns to display the data
+ for n, name in enumerate(self.listmodel.get_column_names()):
+ if n == 0:
+ cellpb = gtk.CellRendererPixbuf()
+ col = gtk.TreeViewColumn(name, cellpb, pixbuf=0)
+ cell = gtk.CellRendererText()
+ col.pack_start(cell, False)
+ col.add_attribute(cell, 'text', 1)
+ else:
+ cell = gtk.CellRendererText()
+ col = gtk.TreeViewColumn(name, cell, text=n + 1)
+ if n == 1:
+ cell.set_property('xalign', 1.0)
+
+ self.treeview.append_column(col)
+
+ self.treeview.connect('row-activated', self.open_file)
+
+ self.scrolledwindow = gtk.ScrolledWindow()
+ self.scrolledwindow.add(self.treeview)
+ self.window.add(self.scrolledwindow)
+ self.treeview.set_model(self.listmodel)
+ self.window.set_title(self.listmodel.dirname)
+ self.window.show_all()
+
+ def open_file(self, treeview, path, column):
+ model = treeview.get_model()
+ if model.is_folder(path):
+ pathname = model.get_pathname(path)
+ new_model = FileListModel(pathname)
+ self.window.set_title(new_model.dirname)
+ treeview.set_model(new_model)
+ return
+
+
+def main(demoapp=None):
+ demo = GenericTreeModelExample()
+ demo
+ gtk.main()
+
+if __name__ == "__main__":
+ main()
diff --git a/demos/gtk-demo/demos/Tree View/treemodel_filetree.py b/demos/gtk-demo/demos/Tree
View/treemodel_filetree.py
new file mode 100644
index 0000000..3179de2
--- /dev/null
+++ b/demos/gtk-demo/demos/Tree View/treemodel_filetree.py
@@ -0,0 +1,279 @@
+#!/usr/bin/env python
+
+title = "File Tree (GenericTreeModel)"
+description = """
+This is a file list demo which makes use of the GenericTreeModel python
+implementation of the Gtk.TreeModel interface. This demo shows what methods
+need to be overridden to provide a valid TreeModel to a TreeView.
+"""
+
+import os
+import stat
+import time
+from collections import OrderedDict
+
+import pygtkcompat
+pygtkcompat.enable_gtk('3.0')
+
+import gtk
+
+
+folderxpm = [
+ "17 16 7 1",
+ " c #000000",
+ ". c #808000",
+ "X c yellow",
+ "o c #808080",
+ "O c #c0c0c0",
+ "+ c white",
+ "@ c None",
+ "@@@@@@@@@@@@@@@@@",
+ "@@@@@@@@@@@@@@@@@",
+ "@@+XXXX.@@@@@@@@@",
+ "@+OOOOOO.@@@@@@@@",
+ "@+OXOXOXOXOXOXO. ",
+ "@+XOXOXOXOXOXOX. ",
+ "@+OXOXOXOXOXOXO. ",
+ "@+XOXOXOXOXOXOX. ",
+ "@+OXOXOXOXOXOXO. ",
+ "@+XOXOXOXOXOXOX. ",
+ "@+OXOXOXOXOXOXO. ",
+ "@+XOXOXOXOXOXOX. ",
+ "@+OOOOOOOOOOOOO. ",
+ "@ ",
+ "@@@@@@@@@@@@@@@@@",
+ "@@@@@@@@@@@@@@@@@"
+ ]
+folderpb = gtk.gdk.pixbuf_new_from_xpm_data(folderxpm)
+
+filexpm = [
+ "12 12 3 1",
+ " c #000000",
+ ". c #ffff04",
+ "X c #b2c0dc",
+ "X XXX",
+ "X ...... XXX",
+ "X ...... X",
+ "X . ... X",
+ "X ........ X",
+ "X . .... X",
+ "X ........ X",
+ "X . .. X",
+ "X ........ X",
+ "X . .. X",
+ "X ........ X",
+ "X X"
+ ]
+filepb = gtk.gdk.pixbuf_new_from_xpm_data(filexpm)
+
+
+class FileTreeModel(gtk.GenericTreeModel):
+ __gtype_name__ = 'DemoFileTreeModel'
+
+ column_types = (gtk.gdk.Pixbuf, str, int, str, str)
+ column_names = ['Name', 'Size', 'Mode', 'Last Changed']
+
+ def __init__(self, dname=None):
+ gtk.GenericTreeModel.__init__(self)
+ if not dname:
+ self.dirname = os.path.expanduser('~')
+ else:
+ self.dirname = os.path.abspath(dname)
+ self.files = self.build_file_dict(self.dirname)
+ return
+
+ def build_file_dict(self, dirname):
+ """
+ :Returns:
+ A dictionary containing the files in the given dirname keyed by filename.
+ If the child filename is a sub-directory, the dict value is a dict.
+ Otherwise it will be None.
+ """
+ d = OrderedDict()
+ for fname in os.listdir(dirname):
+ try:
+ filestat = os.stat(os.path.join(dirname, fname))
+ except OSError:
+ d[fname] = None
+ else:
+ d[fname] = OrderedDict() if stat.S_ISDIR(filestat.st_mode) else None
+
+ return d
+
+ def get_node_from_treepath(self, path):
+ """
+ :Returns:
+ The node stored at the given tree path in local storage.
+ """
+ # TreePaths are a series of integer indices so just iterate through them
+ # and index values by each integer since we are using an OrderedDict
+ if path is None:
+ path = []
+ node = self.files
+ for index in path:
+ node = list(node.values())[index]
+ return node
+
+ def get_node_from_filepath(self, filepath):
+ """
+ :Returns:
+ The node stored at the given file path in local storage.
+ """
+ if not filepath:
+ return self.files
+ node = self.files
+ for key in filepath.split(os.path.sep):
+ node = node[key]
+ return node
+
+ def get_column_names(self):
+ return self.column_names[:]
+
+ #
+ # GenericTreeModel Implementation
+ #
+
+ def on_get_flags(self):
+ return 0
+
+ def on_get_n_columns(self):
+ return len(self.column_types)
+
+ def on_get_column_type(self, n):
+ return self.column_types[n]
+
+ def on_get_path(self, relpath):
+ path = []
+ node = self.files
+ for key in relpath.split(os.path.sep):
+ path.append(list(node.keys()).index(key))
+ node = node[key]
+ return path
+
+ def on_get_value(self, relpath, column):
+ fname = os.path.join(self.dirname, relpath)
+ try:
+ filestat = os.stat(fname)
+ except OSError:
+ return None
+ mode = filestat.st_mode
+ if column is 0:
+ if stat.S_ISDIR(mode):
+ return folderpb
+ else:
+ return filepb
+ elif column is 1:
+ return os.path.basename(relpath)
+ elif column is 2:
+ return filestat.st_size
+ elif column is 3:
+ return oct(stat.S_IMODE(mode))
+ return time.ctime(filestat.st_mtime)
+
+ def on_get_iter(self, path):
+ filepath = ''
+ value = self.files
+ for index in path:
+ filepath = os.path.join(filepath, list(value.keys())[index])
+ value = list(value.values())[index]
+ return filepath
+
+ def on_iter_next(self, filepath):
+ parent_path, child_path = os.path.split(filepath)
+ parent = self.get_node_from_filepath(parent_path)
+
+ # Index of filepath within its parents child list
+ sibling_names = list(parent.keys())
+ index = sibling_names.index(child_path)
+ try:
+ return os.path.join(parent_path, sibling_names[index + 1])
+ except IndexError:
+ return None
+
+ def on_iter_children(self, filepath):
+ if filepath:
+ children = list(self.get_node_from_filepath(filepath).keys())
+ if children:
+ return os.path.join(filepath, children[0])
+ elif self.files:
+ return list(self.files.keys())[0]
+
+ return None
+
+ def on_iter_has_child(self, filepath):
+ return bool(self.get_node_from_filepath(filepath))
+
+ def on_iter_n_children(self, filepath):
+ return len(self.get_node_from_filepath(filepath))
+
+ def on_iter_nth_child(self, filepath, n):
+ try:
+ child = list(self.get_node_from_filepath(filepath).keys())[n]
+ if filepath:
+ return os.path.join(filepath, child)
+ else:
+ return child
+ except IndexError:
+ return None
+
+ def on_iter_parent(self, filepath):
+ return os.path.dirname(filepath)
+
+ def on_ref_node(self, filepath):
+ value = self.get_node_from_filepath(filepath)
+ if value is not None:
+ value.update(self.build_file_dict(os.path.join(self.dirname, filepath)))
+
+ def on_unref_node(self, filepath):
+ pass
+
+
+class GenericTreeModelExample:
+ def delete_event(self, widget, event, data=None):
+ gtk.main_quit()
+ return False
+
+ def __init__(self):
+ # Create a new window
+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+ self.window.set_size_request(300, 200)
+ self.window.connect("delete_event", self.delete_event)
+
+ self.listmodel = FileTreeModel()
+
+ # create the TreeView
+ self.treeview = gtk.TreeView()
+
+ # create the TreeViewColumns to display the data
+ column_names = self.listmodel.get_column_names()
+ self.tvcolumn = [None] * len(column_names)
+ cellpb = gtk.CellRendererPixbuf()
+ self.tvcolumn[0] = gtk.TreeViewColumn(column_names[0],
+ cellpb, pixbuf=0)
+ cell = gtk.CellRendererText()
+ self.tvcolumn[0].pack_start(cell, False)
+ self.tvcolumn[0].add_attribute(cell, 'text', 1)
+ self.treeview.append_column(self.tvcolumn[0])
+ for n in range(1, len(column_names)):
+ cell = gtk.CellRendererText()
+ if n == 1:
+ cell.set_property('xalign', 1.0)
+ self.tvcolumn[n] = gtk.TreeViewColumn(column_names[n],
+ cell, text=n + 1)
+ self.treeview.append_column(self.tvcolumn[n])
+
+ self.scrolledwindow = gtk.ScrolledWindow()
+ self.scrolledwindow.add(self.treeview)
+ self.window.add(self.scrolledwindow)
+ self.treeview.set_model(self.listmodel)
+ self.window.set_title(self.listmodel.dirname)
+ self.window.show_all()
+
+
+def main(demoapp=None):
+ demo = GenericTreeModelExample()
+ demo
+ gtk.main()
+
+if __name__ == "__main__":
+ main()
diff --git a/demos/gtk-demo/gtk-demo.py b/demos/gtk-demo/gtk-demo.py
index 6de2e30..eeb0553 100755
--- a/demos/gtk-demo/gtk-demo.py
+++ b/demos/gtk-demo/gtk-demo.py
@@ -277,7 +277,7 @@ class GtkDemoWindow(Gtk.Window):
tree_view.append_column(column)
- tree_view.collapse_all()
+ tree_view.expand_all()
tree_view.set_headers_visible(False)
scrolled_window = Gtk.ScrolledWindow(hadjustment=None,
vadjustment=None)
diff --git a/gi/pygtkcompat.py b/gi/pygtkcompat.py
index 91b5cc1..9a80eef 100644
--- a/gi/pygtkcompat.py
+++ b/gi/pygtkcompat.py
@@ -2,7 +2,7 @@ import warnings
from gi import PyGIDeprecationWarning
warnings.warn('gi.pygtkcompat is being deprecated in favor of using "pygtkcompat" directly.',
- PyGIDeprecationWarning, stacklevel=2)
+ PyGIDeprecationWarning)
# pyflakes.ignore
from pygtkcompat import (enable,
diff --git a/pygtkcompat/Makefile.am b/pygtkcompat/Makefile.am
index 6a73cb4..914b3e2 100644
--- a/pygtkcompat/Makefile.am
+++ b/pygtkcompat/Makefile.am
@@ -2,6 +2,7 @@ pygtkcompatdir = $(pyexecdir)/pygtkcompat
pygtkcompat_PYTHON = \
__init__.py \
+ generictreemodel.py \
pygtkcompat.py
# if we build in a separate tree, we need to symlink the *.py files from the
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
diff --git a/pygtkcompat/pygtkcompat.py b/pygtkcompat/pygtkcompat.py
index 18ebd7b..02394f9 100644
--- a/pygtkcompat/pygtkcompat.py
+++ b/pygtkcompat/pygtkcompat.py
@@ -427,6 +427,9 @@ def enable_gtk(version='2.0'):
value = getattr(Gdk, name)
setattr(keysyms, target, value)
+ from . import generictreemodel
+ Gtk.GenericTreeModel = generictreemodel.GenericTreeModel
+
def enable_vte():
gi.require_version('Vte', '0.0')
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 1d40539..7ccde54 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -107,6 +107,7 @@ EXTRA_DIST = \
test_overrides_gdk.py \
test_overrides_gtk.py \
test_atoms.py \
+ test_generictreemodel.py \
compat_test_pygtk.py \
gi/__init__.py \
gi/overrides/__init__.py \
diff --git a/tests/test_generictreemodel.py b/tests/test_generictreemodel.py
new file mode 100644
index 0000000..ff0f523
--- /dev/null
+++ b/tests/test_generictreemodel.py
@@ -0,0 +1,406 @@
+# -*- Mode: Python; py-indent-offset: 4 -*-
+# test_generictreemodel - Tests for GenericTreeModel
+# Copyright (C) 2013 Simon Feltman
+#
+# test_generictreemodel.py: Tests for GenericTreeModel
+#
+# 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 gc
+import sys
+import weakref
+import unittest
+
+# pygobject
+from gi.repository import GObject
+from gi.repository import Gtk
+from pygtkcompat.generictreemodel import GenericTreeModel
+from pygtkcompat.generictreemodel import _get_user_data_as_pyobject
+
+
+class Node(object):
+ """Represents a generic node with name, value, and children."""
+ def __init__(self, name, value, *children):
+ self.name = name
+ self.value = value
+ self.children = list(children)
+ self.parent = None
+ self.next = None
+
+ for i, child in enumerate(children):
+ child.parent = weakref.ref(self)
+ if i < len(children) - 1:
+ child.next = weakref.ref(children[i + 1])
+
+ def __repr__(self):
+ return 'Node("%s", %s)' % (self.name, self.value)
+
+
+class TesterModel(GenericTreeModel):
+ def __init__(self):
+ super(TesterModel, self).__init__()
+ self.root = Node('root', 0,
+ Node('spam', 1,
+ Node('sushi', 2),
+ Node('bread', 3)
+ ),
+ Node('eggs', 4)
+ )
+
+ def on_get_flags(self):
+ return 0
+
+ def on_get_n_columns(self):
+ return 2
+
+ def on_get_column_type(self, n):
+ return (str, int)[n]
+
+ def on_get_iter(self, path):
+ node = self.root
+ path = list(path)
+ idx = path.pop(0)
+ while path:
+ idx = path.pop(0)
+ node = node.children[idx]
+ return node
+
+ def on_get_path(self, node):
+ def rec_get_path(n):
+ for i, child in enumerate(n.children):
+ if child == node:
+ return [i]
+ else:
+ res = rec_get_path(child)
+ if res:
+ res.insert(0, i)
+
+ return rec_get_path(self.root)
+
+ def on_get_value(self, node, column):
+ if column == 0:
+ return node.name
+ elif column == 1:
+ return node.value
+
+ def on_iter_has_child(self, node):
+ return bool(node.children)
+
+ def on_iter_next(self, node):
+ if node.next:
+ return node.next()
+
+ def on_iter_children(self, node):
+ if node:
+ return node.children[0]
+ else:
+ return self.root
+
+ def on_iter_n_children(self, node):
+ if node is None:
+ return 1
+ return len(node.children)
+
+ def on_iter_nth_child(self, node, n):
+ if node is None:
+ assert n == 0
+ return self.root
+ return node.children[n]
+
+ def on_iter_parent(self, child):
+ if child.parent:
+ return child.parent()
+
+
+class TestReferences(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def test_c_tree_iter_user_data_as_pyobject(self):
+ obj = object()
+ obj_id = id(obj)
+ ref_count = sys.getrefcount(obj)
+
+ # This is essentially a stolen ref in the context of _CTreeIter.get_user_data_as_pyobject
+ it = Gtk.TreeIter()
+ it.user_data = obj_id
+
+ obj2 = _get_user_data_as_pyobject(it)
+ self.assertEqual(obj, obj2)
+ self.assertEqual(sys.getrefcount(obj), ref_count + 1)
+
+ def test_leak_references_on(self):
+ model = TesterModel()
+ obj_ref = weakref.ref(model.root)
+ # Initial refcount is 1 for model.root + the temporary
+ self.assertEqual(sys.getrefcount(model.root), 2)
+
+ # Iter increases by 1 do to assignment to iter.user_data
+ res, it = model.do_get_iter([0])
+ self.assertEqual(id(model.root), it.user_data)
+ self.assertEqual(sys.getrefcount(model.root), 3)
+
+ # Verify getting a TreeIter more then once does not further increase
+ # the ref count.
+ res2, it2 = model.do_get_iter([0])
+ self.assertEqual(id(model.root), it2.user_data)
+ self.assertEqual(sys.getrefcount(model.root), 3)
+
+ # Deleting the iter does not decrease refcount because references
+ # leak by default (they are stored in the held_refs pool)
+ del it
+ gc.collect()
+ self.assertEqual(sys.getrefcount(model.root), 3)
+
+ # Deleting a model should free all held references to user data
+ # stored by TreeIters
+ del model
+ gc.collect()
+ self.assertEqual(obj_ref(), None)
+
+ def test_row_deleted_frees_refs(self):
+ model = TesterModel()
+ obj_ref = weakref.ref(model.root)
+ # Initial refcount is 1 for model.root + the temporary
+ self.assertEqual(sys.getrefcount(model.root), 2)
+
+ # Iter increases by 1 do to assignment to iter.user_data
+ res, it = model.do_get_iter([0])
+ self.assertEqual(id(model.root), it.user_data)
+ self.assertEqual(sys.getrefcount(model.root), 3)
+
+ # Notifying the underlying model of a row_deleted should decrease the
+ # ref count.
+ model.row_deleted(Gtk.TreePath('0'), model.root)
+ self.assertEqual(sys.getrefcount(model.root), 2)
+
+ # Finally deleting the actual object should collect it completely
+ del model.root
+ gc.collect()
+ self.assertEqual(obj_ref(), None)
+
+ def test_leak_references_off(self):
+ model = TesterModel()
+ model.leak_references = False
+
+ obj_ref = weakref.ref(model.root)
+ # Initial refcount is 1 for model.root + the temporary
+ self.assertEqual(sys.getrefcount(model.root), 2)
+
+ # Iter does not increas count by 1 when leak_references is false
+ res, it = model.do_get_iter([0])
+ self.assertEqual(id(model.root), it.user_data)
+ self.assertEqual(sys.getrefcount(model.root), 2)
+
+ # Deleting the iter does not decrease refcount because assigning user_data
+ # eats references and does not release them.
+ del it
+ gc.collect()
+ self.assertEqual(sys.getrefcount(model.root), 2)
+
+ # Deleting the model decreases the final ref, and the object is collected
+ del model
+ gc.collect()
+ self.assertEqual(obj_ref(), None)
+
+ def test_iteration_refs(self):
+ # Pull iterators off the model using the wrapped C API which will
+ # then call back into the python overrides.
+ model = TesterModel()
+ nodes = [node for node in model.iter_depth_first()]
+ values = [node.value for node in nodes]
+
+ # Verify depth first ordering
+ self.assertEqual(values, [0, 1, 2, 3, 4])
+
+ # Verify ref counts for each of the nodes.
+ # 5 refs for each node at this point:
+ # 1 - ref held in getrefcount function
+ # 2 - ref held by "node" var during iteration
+ # 3 - ref held by local "nodes" var
+ # 4 - ref held by the root/children graph itself
+ # 5 - ref held by the model "held_refs" instance var
+ for node in nodes:
+ self.assertEqual(sys.getrefcount(node), 5)
+
+ # A second iteration and storage of the nodes in a new list
+ # should only increase refcounts by 1 even though new
+ # iterators are created and assigned.
+ nodes2 = [node for node in model.iter_depth_first()]
+ for node in nodes2:
+ self.assertEqual(sys.getrefcount(node), 6)
+
+ # Hold weak refs and start verifying ref collection.
+ node_refs = [weakref.ref(node) for node in nodes]
+
+ # First round of collection
+ del nodes2
+ gc.collect()
+ for node in nodes:
+ self.assertEqual(sys.getrefcount(node), 5)
+
+ # Second round of collection, no more local lists of nodes.
+ del nodes
+ gc.collect()
+ for ref in node_refs:
+ node = ref()
+ self.assertEqual(sys.getrefcount(node), 4)
+
+ # Using invalidate_iters or row_deleted(path, node) will clear out
+ # the pooled refs held internal to the GenericTreeModel implementation.
+ model.invalidate_iters()
+ self.assertEqual(len(model._held_refs), 0)
+ gc.collect()
+ for ref in node_refs:
+ node = ref()
+ self.assertEqual(sys.getrefcount(node), 3)
+
+ # Deleting the root node at this point should allow all nodes to be collected
+ # as there is no longer a way to reach the children
+ del node # node still in locals() from last iteration
+ del model.root
+ gc.collect()
+ for ref in node_refs:
+ self.assertEqual(ref(), None)
+
+
+class TestIteration(unittest.TestCase):
+ def test_iter_next_root(self):
+ model = TesterModel()
+ it = model.get_iter([0])
+ self.assertEqual(it.user_data, id(model.root))
+ self.assertEqual(model.root.next, None)
+
+ it = model.iter_next(it)
+ self.assertEqual(it, None)
+
+ def test_iter_next_multiple(self):
+ model = TesterModel()
+ it = model.get_iter([0, 0])
+ self.assertEqual(it.user_data, id(model.root.children[0]))
+
+ it = model.iter_next(it)
+ self.assertEqual(it.user_data, id(model.root.children[1]))
+
+ it = model.iter_next(it)
+ self.assertEqual(it, None)
+
+
+class ErrorModel(GenericTreeModel):
+ # All on_* methods will raise a NotImplementedError by default
+ pass
+
+
+class ExceptHook(object):
+ """
+ Temporarily installs an exception hook in a context which
+ expects the given exc_type to be raised. This allows verification
+ of exceptions that occur within python gi callbacks but
+ are never bubbled through from python to C back to python.
+ This works because exception hooks are called in PyErr_Print.
+ """
+ def __init__(self, exc_type):
+ self._exc_type = exc_type
+ self._exceptions = []
+
+ def _excepthook(self, exc_type, value, traceback):
+ self._exceptions.append(exc_type)
+
+ def __enter__(self):
+ self._oldhook = sys.excepthook
+ sys.excepthook = self._excepthook
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ sys.excepthook = self._oldhook
+ assert len(self._exceptions) == 1, 'Expecting exactly one exception of type %s' % self._exc_type
+ assert issubclass(self._exceptions[0], self._exc_type), 'Expecting exactly one exception of type %s'
% self._exc_type
+
+
+class TestReturnsAfterError(unittest.TestCase):
+ def setUp(self):
+ self.model = ErrorModel()
+
+ def test_get_flags(self):
+ with ExceptHook(NotImplementedError):
+ flags = self.model.get_flags()
+ self.assertEqual(flags, 0)
+
+ def test_get_n_columns(self):
+ with ExceptHook(NotImplementedError):
+ count = self.model.get_n_columns()
+ self.assertEqual(count, 0)
+
+ def test_get_column_type(self):
+ with ExceptHook(NotImplementedError):
+ col_type = self.model.get_column_type(0)
+ self.assertEqual(col_type, GObject.TYPE_INVALID)
+
+ def test_get_iter(self):
+ with ExceptHook(NotImplementedError):
+ self.assertRaises(ValueError, self.model.get_iter, Gtk.TreePath(0))
+
+ def test_get_path(self):
+ it = self.model.create_tree_iter('foo')
+ with ExceptHook(NotImplementedError):
+ path = self.model.get_path(it)
+ self.assertEqual(path, None)
+
+ def test_get_value(self):
+ it = self.model.create_tree_iter('foo')
+ with ExceptHook(NotImplementedError):
+ try:
+ self.model.get_value(it, 0)
+ except TypeError:
+ pass # silence TypeError converting None to GValue
+
+ def test_iter_has_child(self):
+ it = self.model.create_tree_iter('foo')
+ with ExceptHook(NotImplementedError):
+ res = self.model.iter_has_child(it)
+ self.assertEqual(res, False)
+
+ def test_iter_next(self):
+ it = self.model.create_tree_iter('foo')
+ with ExceptHook(NotImplementedError):
+ res = self.model.iter_next(it)
+ self.assertEqual(res, None)
+
+ def test_iter_children(self):
+ with ExceptHook(NotImplementedError):
+ res = self.model.iter_children(None)
+ self.assertEqual(res, None)
+
+ def test_iter_n_children(self):
+ with ExceptHook(NotImplementedError):
+ res = self.model.iter_n_children(None)
+ self.assertEqual(res, 0)
+
+ def test_iter_nth_child(self):
+ with ExceptHook(NotImplementedError):
+ res = self.model.iter_nth_child(None, 0)
+ self.assertEqual(res, None)
+
+ def test_iter_parent(self):
+ child = self.model.create_tree_iter('foo')
+ with ExceptHook(NotImplementedError):
+ res = self.model.iter_parent(child)
+ self.assertEqual(res, None)
+
+if __name__ == '__main__':
+ unittest.main()
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]