1# -*- Mode: Python; py-indent-offset: 4 -*-
2# generictreemodel - GenericTreeModel implementation for pygtk compatibility.
3# Copyright (C) 2013 Simon Feltman
4#
5#   generictreemodel.py: GenericTreeModel implementation for pygtk compatibility
6#
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.
11#
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.
16#
17# You should have received a copy of the GNU Lesser General Public
18# License along with this library; if not, see <http://www.gnu.org/licenses/>.
19
20
21# System
22import sys
23import random
24import collections
25import ctypes
26
27# GObject
28from gi.repository import GObject
29from gi.repository import Gtk
30
31
32class _CTreeIter(ctypes.Structure):
33    _fields_ = [('stamp', ctypes.c_int),
34                ('user_data', ctypes.c_void_p),
35                ('user_data2', ctypes.c_void_p),
36                ('user_data3', ctypes.c_void_p)]
37
38    @classmethod
39    def from_iter(cls, iter):
40        offset = sys.getsizeof(object())  # size of PyObject_HEAD
41        return ctypes.POINTER(cls).from_address(id(iter) + offset)
42
43
44def _get_user_data_as_pyobject(iter):
45    citer = _CTreeIter.from_iter(iter)
46    return ctypes.cast(citer.contents.user_data, ctypes.py_object).value
47
48
49def handle_exception(default_return):
50    """Returns a function which can act as a decorator for wrapping exceptions and
51    returning "default_return" upon an exception being thrown.
52
53    This is used to wrap Gtk.TreeModel "do_" method implementations so we can return
54    a proper value from the override upon an exception occurring with client code
55    implemented by the "on_" methods.
56    """
57    def decorator(func):
58        def wrapped_func(*args, **kargs):
59            try:
60                return func(*args, **kargs)
61            except:
62                # Use excepthook directly to avoid any printing to the screen
63                # if someone installed an except hook.
64                sys.excepthook(*sys.exc_info())
65            return default_return
66        return wrapped_func
67    return decorator
68
69
70class GenericTreeModel(GObject.GObject, Gtk.TreeModel):
71    """A base implementation of a Gtk.TreeModel for python.
72
73    The GenericTreeModel eases implementing the Gtk.TreeModel interface in Python.
74    The class can be subclassed to provide a TreeModel implementation which works
75    directly with Python objects instead of iterators.
76
77    All of the on_* methods should be overridden by subclasses to provide the
78    underlying implementation a way to access custom model data. For the purposes of
79    this API, all custom model data supplied or handed back through the overridable
80    API will use the argument names: node, parent, and child in regards to user data
81    python objects.
82
83    The create_tree_iter, set_user_data, invalidate_iters, iter_is_valid methods are
84    available to help manage Gtk.TreeIter objects and their Python object references.
85
86    GenericTreeModel manages a pool of user data nodes that have been used with iters.
87    This pool stores a references to user data nodes as a dictionary value with the
88    key being the integer id of the data. This id is what the Gtk.TreeIter objects
89    use to reference data in the pool.
90    References will be removed from the pool when the model is deleted or explicitly
91    by using the optional "node" argument to the "row_deleted" method when notifying
92    the model of row deletion.
93    """
94
95    leak_references = GObject.Property(default=True, type=bool,
96                                       blurb="If True, strong references to user data attached to iters are "
97                                       "stored in a dictionary pool (default). Otherwise the user data is "
98                                       "stored as a raw pointer to a python object without a reference.")
99
100    #
101    # Methods
102    #
103    def __init__(self):
104        """Initialize. Make sure to call this from derived classes if overridden."""
105        super(GenericTreeModel, self).__init__()
106        self.stamp = 0
107
108        #: Dictionary of (id(user_data): user_data), used when leak-refernces=False
109        self._held_refs = dict()
110
111        # Set initial stamp
112        self.invalidate_iters()
113
114    def iter_depth_first(self):
115        """Depth-first iteration of the entire TreeModel yielding the python nodes."""
116        stack = collections.deque([None])
117        while stack:
118            it = stack.popleft()
119            if it is not None:
120                yield self.get_user_data(it)
121            children = [self.iter_nth_child(it, i) for i in range(self.iter_n_children(it))]
122            stack.extendleft(reversed(children))
123
124    def invalidate_iter(self, iter):
125        """Clear user data and its reference from the iter and this model."""
126        iter.stamp = 0
127        if iter.user_data:
128            if iter.user_data in self._held_refs:
129                del self._held_refs[iter.user_data]
130            iter.user_data = None
131
132    def invalidate_iters(self):
133        """
134        This method invalidates all TreeIter objects associated with this custom tree model
135        and frees their locally pooled references.
136        """
137        self.stamp = random.randint(-2147483648, 2147483647)
138        self._held_refs.clear()
139
140    def iter_is_valid(self, iter):
141        """
142        :Returns:
143            True if the gtk.TreeIter specified by iter is valid for the custom tree model.
144        """
145        return iter.stamp == self.stamp
146
147    def get_user_data(self, iter):
148        """Get the user_data associated with the given TreeIter.
149
150        GenericTreeModel stores arbitrary Python objects mapped to instances of Gtk.TreeIter.
151        This method allows to retrieve the Python object held by the given iterator.
152        """
153        if self.leak_references:
154            return self._held_refs[iter.user_data]
155        else:
156            return _get_user_data_as_pyobject(iter)
157
158    def set_user_data(self, iter, user_data):
159        """Applies user_data and stamp to the given iter.
160
161        If the models "leak_references" property is set, a reference to the
162        user_data is stored with the model to ensure we don't run into bad
163        memory problems with the TreeIter.
164        """
165        iter.user_data = id(user_data)
166
167        if user_data is None:
168            self.invalidate_iter(iter)
169        else:
170            iter.stamp = self.stamp
171            if self.leak_references:
172                self._held_refs[iter.user_data] = user_data
173
174    def create_tree_iter(self, user_data):
175        """Create a Gtk.TreeIter instance with the given user_data specific for this model.
176
177        Use this method to create Gtk.TreeIter instance instead of directly calling
178        Gtk.Treeiter(), this will ensure proper reference managment of wrapped used_data.
179        """
180        iter = Gtk.TreeIter()
181        self.set_user_data(iter, user_data)
182        return iter
183
184    def _create_tree_iter(self, data):
185        """Internal creation of a (bool, TreeIter) pair for returning directly
186        back to the view interfacing with this model."""
187        if data is None:
188            return (False, None)
189        else:
190            it = self.create_tree_iter(data)
191            return (True, it)
192
193    def row_deleted(self, path, node=None):
194        """Notify the model a row has been deleted.
195
196        Use the node parameter to ensure the user_data reference associated
197        with the path is properly freed by this model.
198
199        :Parameters:
200            path : Gtk.TreePath
201                Path to the row that has been deleted.
202            node : object
203                Python object used as the node returned from "on_get_iter". This is
204                optional but ensures the model will not leak references to this object.
205        """
206        super(GenericTreeModel, self).row_deleted(path)
207        node_id = id(node)
208        if node_id in self._held_refs:
209            del self._held_refs[node_id]
210
211    #
212    # GtkTreeModel Interface Implementation
213    #
214    @handle_exception(0)
215    def do_get_flags(self):
216        """Internal method."""
217        return self.on_get_flags()
218
219    @handle_exception(0)
220    def do_get_n_columns(self):
221        """Internal method."""
222        return self.on_get_n_columns()
223
224    @handle_exception(GObject.TYPE_INVALID)
225    def do_get_column_type(self, index):
226        """Internal method."""
227        return self.on_get_column_type(index)
228
229    @handle_exception((False, None))
230    def do_get_iter(self, path):
231        """Internal method."""
232        return self._create_tree_iter(self.on_get_iter(path))
233
234    @handle_exception(False)
235    def do_iter_next(self, iter):
236        """Internal method."""
237        if iter is None:
238            next_data = self.on_iter_next(None)
239        else:
240            next_data = self.on_iter_next(self.get_user_data(iter))
241
242        self.set_user_data(iter, next_data)
243        return next_data is not None
244
245    @handle_exception(None)
246    def do_get_path(self, iter):
247        """Internal method."""
248        path = self.on_get_path(self.get_user_data(iter))
249        if path is None:
250            return None
251        else:
252            return Gtk.TreePath(path)
253
254    @handle_exception(None)
255    def do_get_value(self, iter, column):
256        """Internal method."""
257        return self.on_get_value(self.get_user_data(iter), column)
258
259    @handle_exception((False, None))
260    def do_iter_children(self, parent):
261        """Internal method."""
262        data = self.get_user_data(parent) if parent else None
263        return self._create_tree_iter(self.on_iter_children(data))
264
265    @handle_exception(False)
266    def do_iter_has_child(self, parent):
267        """Internal method."""
268        return self.on_iter_has_child(self.get_user_data(parent))
269
270    @handle_exception(0)
271    def do_iter_n_children(self, iter):
272        """Internal method."""
273        if iter is None:
274            return self.on_iter_n_children(None)
275        return self.on_iter_n_children(self.get_user_data(iter))
276
277    @handle_exception((False, None))
278    def do_iter_nth_child(self, parent, n):
279        """Internal method."""
280        if parent is None:
281            data = self.on_iter_nth_child(None, n)
282        else:
283            data = self.on_iter_nth_child(self.get_user_data(parent), n)
284        return self._create_tree_iter(data)
285
286    @handle_exception((False, None))
287    def do_iter_parent(self, child):
288        """Internal method."""
289        return self._create_tree_iter(self.on_iter_parent(self.get_user_data(child)))
290
291    @handle_exception(None)
292    def do_ref_node(self, iter):
293        self.on_ref_node(self.get_user_data(iter))
294
295    @handle_exception(None)
296    def do_unref_node(self, iter):
297        self.on_unref_node(self.get_user_data(iter))
298
299    #
300    # Python Subclass Overridables
301    #
302    def on_get_flags(self):
303        """Overridable.
304
305        :Returns Gtk.TreeModelFlags:
306            The flags for this model. See: Gtk.TreeModelFlags
307        """
308        raise NotImplementedError
309
310    def on_get_n_columns(self):
311        """Overridable.
312
313        :Returns:
314            The number of columns for this model.
315        """
316        raise NotImplementedError
317
318    def on_get_column_type(self, index):
319        """Overridable.
320
321        :Returns:
322            The column type for the given index.
323        """
324        raise NotImplementedError
325
326    def on_get_iter(self, path):
327        """Overridable.
328
329        :Returns:
330            A python object (node) for the given TreePath.
331        """
332        raise NotImplementedError
333
334    def on_iter_next(self, node):
335        """Overridable.
336
337        :Parameters:
338            node : object
339                Node at current level.
340
341        :Returns:
342            A python object (node) following the given node at the current level.
343        """
344        raise NotImplementedError
345
346    def on_get_path(self, node):
347        """Overridable.
348
349        :Returns:
350            A TreePath for the given node.
351        """
352        raise NotImplementedError
353
354    def on_get_value(self, node, column):
355        """Overridable.
356
357        :Parameters:
358            node : object
359            column : int
360                Column index to get the value from.
361
362        :Returns:
363            The value of the column for the given node."""
364        raise NotImplementedError
365
366    def on_iter_children(self, parent):
367        """Overridable.
368
369        :Returns:
370            The first child of parent or None if parent has no children.
371            If parent is None, return the first node of the model.
372        """
373        raise NotImplementedError
374
375    def on_iter_has_child(self, node):
376        """Overridable.
377
378        :Returns:
379            True if the given node has children.
380        """
381        raise NotImplementedError
382
383    def on_iter_n_children(self, node):
384        """Overridable.
385
386        :Returns:
387            The number of children for the given node. If node is None,
388            return the number of top level nodes.
389        """
390        raise NotImplementedError
391
392    def on_iter_nth_child(self, parent, n):
393        """Overridable.
394
395        :Parameters:
396            parent : object
397            n : int
398                Index of child within parent.
399
400        :Returns:
401            The child for the given parent index starting at 0. If parent None,
402            return the top level node corresponding to "n".
403            If "n" is larger then available nodes, return None.
404        """
405        raise NotImplementedError
406
407    def on_iter_parent(self, child):
408        """Overridable.
409
410        :Returns:
411            The parent node of child or None if child is a top level node."""
412        raise NotImplementedError
413
414    def on_ref_node(self, node):
415        pass
416
417    def on_unref_node(self, node):
418        pass
419