1 # -*- Mode: Python; py-indent-offset: 4 -*-
2 # generictreemodel - GenericTreeModel implementation for pygtk compatibility.
3 # Copyright (C) 2013 Simon Feltman
5 # generictreemodel.py: GenericTreeModel implementation for pygtk compatibility
7 # This library is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation; either
10 # version 2.1 of the License, or (at your option) any later version.
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # Lesser General Public License for more details.
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with this library; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
30 from gi.repository import GObject
31 from gi.repository import Gtk
34 class _CTreeIter(ctypes.Structure):
35 _fields_ = [('stamp', ctypes.c_int),
36 ('user_data', ctypes.c_void_p),
37 ('user_data2', ctypes.c_void_p),
38 ('user_data3', ctypes.c_void_p)]
41 def from_iter(cls, iter):
42 offset = sys.getsizeof(object()) # size of PyObject_HEAD
43 return ctypes.POINTER(cls).from_address(id(iter) + offset)
46 def _get_user_data_as_pyobject(iter):
47 citer = _CTreeIter.from_iter(iter)
48 return ctypes.cast(citer.contents.user_data, ctypes.py_object).value
51 def handle_exception(default_return):
52 """Returns a function which can act as a decorator for wrapping exceptions and
53 returning "default_return" upon an exception being thrown.
55 This is used to wrap Gtk.TreeModel "do_" method implementations so we can return
56 a proper value from the override upon an exception occurring with client code
57 implemented by the "on_" methods.
60 def wrapped_func(*args, **kargs):
62 return func(*args, **kargs)
64 # Use excepthook directly to avoid any printing to the screen
65 # if someone installed an except hook.
66 sys.excepthook(*sys.exc_info())
72 class GenericTreeModel(GObject.GObject, Gtk.TreeModel):
73 """A base implementation of a Gtk.TreeModel for python.
75 The GenericTreeModel eases implementing the Gtk.TreeModel interface in Python.
76 The class can be subclassed to provide a TreeModel implementation which works
77 directly with Python objects instead of iterators.
79 All of the on_* methods should be overridden by subclasses to provide the
80 underlying implementation a way to access custom model data. For the purposes of
81 this API, all custom model data supplied or handed back through the overridable
82 API will use the argument names: node, parent, and child in regards to user data
85 The create_tree_iter, set_user_data, invalidate_iters, iter_is_valid methods are
86 available to help manage Gtk.TreeIter objects and their Python object references.
88 GenericTreeModel manages a pool of user data nodes that have been used with iters.
89 This pool stores a references to user data nodes as a dictionary value with the
90 key being the integer id of the data. This id is what the Gtk.TreeIter objects
91 use to reference data in the pool.
92 References will be removed from the pool when the model is deleted or explicitly
93 by using the optional "node" argument to the "row_deleted" method when notifying
94 the model of row deletion.
97 leak_references = GObject.Property(default=True, type=bool,
98 blurb="If True, strong references to user data attached to iters are "
99 "stored in a dictionary pool (default). Otherwise the user data is "
100 "stored as a raw pointer to a python object without a reference.")
106 """Initialize. Make sure to call this from derived classes if overridden."""
107 super(GenericTreeModel, self).__init__()
110 #: Dictionary of (id(user_data): user_data), used when leak-refernces=False
111 self._held_refs = dict()
114 self.invalidate_iters()
116 def iter_depth_first(self):
117 """Depth-first iteration of the entire TreeModel yielding the python nodes."""
118 stack = collections.deque([None])
122 yield self.get_user_data(it)
123 children = [self.iter_nth_child(it, i) for i in range(self.iter_n_children(it))]
124 stack.extendleft(reversed(children))
126 def invalidate_iter(self, iter):
127 """Clear user data and its reference from the iter and this model."""
130 if iter.user_data in self._held_refs:
131 del self._held_refs[iter.user_data]
132 iter.user_data = None
134 def invalidate_iters(self):
136 This method invalidates all TreeIter objects associated with this custom tree model
137 and frees their locally pooled references.
139 self.stamp = random.randint(-2147483648, 2147483647)
140 self._held_refs.clear()
142 def iter_is_valid(self, iter):
145 True if the gtk.TreeIter specified by iter is valid for the custom tree model.
147 return iter.stamp == self.stamp
149 def get_user_data(self, iter):
150 """Get the user_data associated with the given TreeIter.
152 GenericTreeModel stores arbitrary Python objects mapped to instances of Gtk.TreeIter.
153 This method allows to retrieve the Python object held by the given iterator.
155 if self.leak_references:
156 return self._held_refs[iter.user_data]
158 return _get_user_data_as_pyobject(iter)
160 def set_user_data(self, iter, user_data):
161 """Applies user_data and stamp to the given iter.
163 If the models "leak_references" property is set, a reference to the
164 user_data is stored with the model to ensure we don't run into bad
165 memory problems with the TreeIter.
167 iter.user_data = id(user_data)
169 if user_data is None:
170 self.invalidate_iter(iter)
172 iter.stamp = self.stamp
173 if self.leak_references:
174 self._held_refs[iter.user_data] = user_data
176 def create_tree_iter(self, user_data):
177 """Create a Gtk.TreeIter instance with the given user_data specific for this model.
179 Use this method to create Gtk.TreeIter instance instead of directly calling
180 Gtk.Treeiter(), this will ensure proper reference managment of wrapped used_data.
182 iter = Gtk.TreeIter()
183 self.set_user_data(iter, user_data)
186 def _create_tree_iter(self, data):
187 """Internal creation of a (bool, TreeIter) pair for returning directly
188 back to the view interfacing with this model."""
192 it = self.create_tree_iter(data)
195 def row_deleted(self, path, node=None):
196 """Notify the model a row has been deleted.
198 Use the node parameter to ensure the user_data reference associated
199 with the path is properly freed by this model.
203 Path to the row that has been deleted.
205 Python object used as the node returned from "on_get_iter". This is
206 optional but ensures the model will not leak references to this object.
208 super(GenericTreeModel, self).row_deleted(path)
210 if node_id in self._held_refs:
211 del self._held_refs[node_id]
214 # GtkTreeModel Interface Implementation
217 def do_get_flags(self):
218 """Internal method."""
219 return self.on_get_flags()
222 def do_get_n_columns(self):
223 """Internal method."""
224 return self.on_get_n_columns()
226 @handle_exception(GObject.TYPE_INVALID)
227 def do_get_column_type(self, index):
228 """Internal method."""
229 return self.on_get_column_type(index)
231 @handle_exception((False, None))
232 def do_get_iter(self, path):
233 """Internal method."""
234 return self._create_tree_iter(self.on_get_iter(path))
236 @handle_exception(False)
237 def do_iter_next(self, iter):
238 """Internal method."""
240 next_data = self.on_iter_next(None)
242 next_data = self.on_iter_next(self.get_user_data(iter))
244 self.set_user_data(iter, next_data)
245 return next_data is not None
247 @handle_exception(None)
248 def do_get_path(self, iter):
249 """Internal method."""
250 path = self.on_get_path(self.get_user_data(iter))
254 return Gtk.TreePath(path)
256 @handle_exception(None)
257 def do_get_value(self, iter, column):
258 """Internal method."""
259 return self.on_get_value(self.get_user_data(iter), column)
261 @handle_exception((False, None))
262 def do_iter_children(self, parent):
263 """Internal method."""
264 data = self.get_user_data(parent) if parent else None
265 return self._create_tree_iter(self.on_iter_children(data))
267 @handle_exception(False)
268 def do_iter_has_child(self, parent):
269 """Internal method."""
270 return self.on_iter_has_child(self.get_user_data(parent))
273 def do_iter_n_children(self, iter):
274 """Internal method."""
276 return self.on_iter_n_children(None)
277 return self.on_iter_n_children(self.get_user_data(iter))
279 @handle_exception((False, None))
280 def do_iter_nth_child(self, parent, n):
281 """Internal method."""
283 data = self.on_iter_nth_child(None, n)
285 data = self.on_iter_nth_child(self.get_user_data(parent), n)
286 return self._create_tree_iter(data)
288 @handle_exception((False, None))
289 def do_iter_parent(self, child):
290 """Internal method."""
291 return self._create_tree_iter(self.on_iter_parent(self.get_user_data(child)))
293 @handle_exception(None)
294 def do_ref_node(self, iter):
295 self.on_ref_node(self.get_user_data(iter))
297 @handle_exception(None)
298 def do_unref_node(self, iter):
299 self.on_unref_node(self.get_user_data(iter))
302 # Python Subclass Overridables
304 def on_get_flags(self):
307 :Returns Gtk.TreeModelFlags:
308 The flags for this model. See: Gtk.TreeModelFlags
310 raise NotImplementedError
312 def on_get_n_columns(self):
316 The number of columns for this model.
318 raise NotImplementedError
320 def on_get_column_type(self, index):
324 The column type for the given index.
326 raise NotImplementedError
328 def on_get_iter(self, path):
332 A python object (node) for the given TreePath.
334 raise NotImplementedError
336 def on_iter_next(self, node):
341 Node at current level.
344 A python object (node) following the given node at the current level.
346 raise NotImplementedError
348 def on_get_path(self, node):
352 A TreePath for the given node.
354 raise NotImplementedError
356 def on_get_value(self, node, column):
362 Column index to get the value from.
365 The value of the column for the given node."""
366 raise NotImplementedError
368 def on_iter_children(self, parent):
372 The first child of parent or None if parent has no children.
373 If parent is None, return the first node of the model.
375 raise NotImplementedError
377 def on_iter_has_child(self, node):
381 True if the given node has children.
383 raise NotImplementedError
385 def on_iter_n_children(self, node):
389 The number of children for the given node. If node is None,
390 return the number of top level nodes.
392 raise NotImplementedError
394 def on_iter_nth_child(self, parent, n):
400 Index of child within parent.
403 The child for the given parent index starting at 0. If parent None,
404 return the top level node corresponding to "n".
405 If "n" is larger then available nodes, return None.
407 raise NotImplementedError
409 def on_iter_parent(self, child):
413 The parent node of child or None if child is a top level node."""
414 raise NotImplementedError
416 def on_ref_node(self, node):
419 def on_unref_node(self, node):