~ubuntu-branches/ubuntu/hardy/pida/hardy

« back to all changes in this revision

Viewing changes to contrib/kiwi/kiwi/ui/objectlist.py

  • Committer: Bazaar Package Importer
  • Author(s): Jan Luebbe
  • Date: 2007-09-05 17:54:09 UTC
  • mfrom: (1.1.3 upstream)
  • Revision ID: james.westby@ubuntu.com-20070905175409-ty9f6qpuctyjv1sd
Tags: 0.5.1-2
* Depend on librsvg2-common, which is not pulled in by the other depends
  (closes: #394860)
* gvim is no alternative for python-gnome2 and python-gnome2-extras
  (closes: #436431)
* Pida now uses ~/.pida2, so it can no longer be confused by old
  configurations (closes: #421378)
* Culebra is no longer supported by upstream (closes: #349009)
* Update manpage (closes: #440375)
* Update watchfile (pida is now called PIDA)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#
 
2
# Kiwi: a Framework and Enhanced Widgets for Python
 
3
#
 
4
# Copyright (C) 2001-2007 Async Open Source
 
5
#
 
6
# This library is free software; you can redistribute it and/or
 
7
# modify it under the terms of the GNU Lesser General Public
 
8
# License as published by the Free Software Foundation; either
 
9
# version 2.1 of the License, or (at your option) any later version.
 
10
#
 
11
# This library is distributed in the hope that it will be useful,
 
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
14
# Lesser General Public License for more details.
 
15
#
 
16
# You should have received a copy of the GNU Lesser General Public
 
17
# License along with this library; if not, write to the Free Software
 
18
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 
19
# USA
 
20
#
 
21
# Author(s): Christian Reis <kiko@async.com.br>
 
22
#            Lorenzo Gil Sanchez <lgs@sicem.biz>
 
23
#            Gustavo Rahal <gustavo@async.com.br>
 
24
#            Johan Dahlin <jdahlin@async.com.br>
 
25
#
 
26
 
 
27
"""High level wrapper for GtkTreeView"""
 
28
 
 
29
import datetime
 
30
import gettext
 
31
 
 
32
import gobject
 
33
import pango
 
34
import gtk
 
35
from gtk import gdk
 
36
 
 
37
from kiwi.accessor import kgetattr
 
38
from kiwi.datatypes import converter, number, Decimal
 
39
from kiwi.currency import currency # after datatypes
 
40
from kiwi.enums import Alignment
 
41
from kiwi.log import Logger
 
42
from kiwi.python import enum, slicerange
 
43
from kiwi.utils import PropertyObject, gsignal, gproperty, type_register
 
44
 
 
45
_ = lambda m: gettext.dgettext('kiwi', m)
 
46
 
 
47
log = Logger('objectlist')
 
48
 
 
49
str2type = converter.str_to_type
 
50
 
 
51
def str2enum(value_name, enum_class):
 
52
    "converts a string to a enum"
 
53
    for _, enum in enum_class.__enum_values__.items():
 
54
        if value_name in (enum.value_name, enum.value_nick):
 
55
            return enum
 
56
 
 
57
def str2bool(value, from_string=converter.from_string):
 
58
    "converts a boolean to a enum"
 
59
    return from_string(bool, value)
 
60
 
 
61
class Column(PropertyObject, gobject.GObject):
 
62
    """
 
63
    Specifies a column for an L{ObjectList}, see the ObjectList documentation
 
64
    for a simple example.
 
65
 
 
66
    Properties
 
67
    ==========
 
68
      - B{title}: string I{mandatory}
 
69
        - the title of the column, defaulting to the capitalized form of
 
70
          the attribute
 
71
      - B{data-type}: object I{str}
 
72
        - the type of the attribute that will be inserted into the column.
 
73
          Supported data types: bool, int, float, str, unicode,
 
74
          decimal.Decimal, datetime.date, datetime.time, datetime.datetime,
 
75
          gtk.gdk.Pixbuf, L{kiwi.currency.currency}, L{kiwi.python.enum}.
 
76
      - B{visible}: bool I{True}
 
77
        - specifying if it is initially hidden or shown.
 
78
      - B{justify}: gtk.Justification I{None}
 
79
        - one of gtk.JUSTIFY_LEFT, gtk.JUSTIFY_RIGHT or gtk.JUSTIFY_CENTER
 
80
          or None. If None, the justification will be determined by the type
 
81
          of the attribute value of the first instance to be inserted in the
 
82
          ObjectList (for instance numbers will be right-aligned).
 
83
      - B{format}: string I{""}
 
84
        - a format string to be applied to the attribute value upon insertion
 
85
          in the list.
 
86
      - B{width}: integer I{65535}
 
87
        - the width in pixels of the column, if not set, uses the default to
 
88
          ObjectList. If no Column specifies a width, columns_autosize() will
 
89
          be called on the ObjectList upon append() or the first add_list().
 
90
      - B{sorted}: bool I{False}
 
91
        - whether or not the ObjectList is to be sorted by this column.
 
92
          If no Columns are sorted, the ObjectList will be created unsorted.
 
93
      - B{order}: GtkSortType I{-1}
 
94
        - one of gtk.SORT_ASCENDING, gtk.SORT_DESCENDING or -1
 
95
          The value -1 is mean that the column is not sorted.
 
96
      - B{expand}: bool I{False}
 
97
        - if set column will expand. Note: this space is shared equally amongst
 
98
          all columns that have the expand set to True.
 
99
      - B{tooltip}: string I{""}
 
100
        - a string which will be used as a tooltip for the column header
 
101
      - B{format_func}: object I{None}
 
102
        -  a callable which will be used to format the output of a column.
 
103
           The function will take one argument which is the value to convert
 
104
           and is expected to return a string.
 
105
           I{Note}: that you cannot use format and format_func at the same time,
 
106
           if you provide a format function you'll be responsible for
 
107
           converting the value to a string.
 
108
      - B{editable}: bool I{False}
 
109
        - if true the field is editable and when you modify the contents of
 
110
          the cell the model will be updated.
 
111
      - B{searchable}: bool I{False}
 
112
        - if true the attribute values of the column can be searched using
 
113
          type ahead search. Only string attributes are currently supported.
 
114
      - B{radio}: bool I{False}
 
115
        -  If true render the column as a radio instead of toggle.
 
116
           Only applicable for columns with boolean data types.
 
117
      - B{cache}: bool I{False}
 
118
        -  If true, the value will only be fetched once, and the same value
 
119
           will be reused for futher access.
 
120
      - B{use_stock}: bool I{False}
 
121
        - If true, this will be rendered as pixbuf from the value which
 
122
          should be a stock id.
 
123
      - B{icon_size}: gtk.IconSize I{gtk.ICON_SIZE_MENU}
 
124
      - B{editable_attribute}: string I{""}
 
125
        - a string which is the attribute which should decide if the
 
126
          cell is editable or not.
 
127
      - B{use_markup}: bool I{False}
 
128
        - If true, the text will be rendered with markup
 
129
      - B{expander}: bool I{False}
 
130
        - If True, this column will be used as the tree expander column
 
131
      - B{ellipsize}: pango.EllipsizeMode I{pango.ELLIPSIZE_NONE}
 
132
        - One of pango.ELLIPSIZE_{NONE, START, MIDDLE_END}, it describes
 
133
          where characters should be removed in case ellipsization
 
134
          (where to put the ...) is needed.
 
135
      - B{font-desc}: str I{""}
 
136
        - A string passed to pango.FontDescription, for instance "Sans" or
 
137
          "Monospace 28".
 
138
    """
 
139
    __gtype_name__ = 'Column'
 
140
    gproperty('attribute', str, flags=(gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT_ONLY))
 
141
    gproperty('title', str)
 
142
    gproperty('data-type', object)
 
143
    gproperty('visible', bool, default=True)
 
144
    gproperty('justify', gtk.Justification, default=gtk.JUSTIFY_LEFT)
 
145
    gproperty('format', str)
 
146
    gproperty('width', int, maximum=2**16)
 
147
    gproperty('sorted', bool, default=False)
 
148
    gproperty('order', gtk.SortType, default=gtk.SORT_ASCENDING)
 
149
    gproperty('expand', bool, default=False)
 
150
    gproperty('tooltip', str)
 
151
    gproperty('format_func', object)
 
152
    gproperty('editable', bool, default=False)
 
153
    gproperty('searchable', bool, default=False)
 
154
    gproperty('radio', bool, default=False)
 
155
    gproperty('cache', bool, default=False)
 
156
    gproperty('use-stock', bool, default=False)
 
157
    gproperty('use-markup', bool, default=False)
 
158
    gproperty('icon-size', gtk.IconSize, default=gtk.ICON_SIZE_MENU)
 
159
    gproperty('editable-attribute', str)
 
160
    gproperty('expander', bool, False)
 
161
    gproperty('ellipsize', pango.EllipsizeMode, default=pango.ELLIPSIZE_NONE)
 
162
    gproperty('font-desc', str)
 
163
    #gproperty('title_pixmap', str)
 
164
 
 
165
    # This can be set in subclasses, to be able to allow custom
 
166
    # cell_data_functions, used by SequentialColumn
 
167
    cell_data_func = None
 
168
 
 
169
    # This is called after the renderer property is set, to allow
 
170
    # us to set custom rendering properties
 
171
    renderer_func = None
 
172
 
 
173
    # This is called when the renderer is created, so we can set/fetch
 
174
    # initial properties
 
175
    on_attach_renderer = None
 
176
 
 
177
    def __init__(self, attribute='', title=None, data_type=None, **kwargs):
 
178
        """
 
179
        Creates a new Column, which describes how a column in a
 
180
        ObjectList should be rendered.
 
181
 
 
182
        @param attribute: a string with the name of the instance attribute the
 
183
            column represents.
 
184
        @param title: the title of the column, defaulting to the capitalized
 
185
            form of the attribute.
 
186
        @param data_type: the type of the attribute that will be inserted
 
187
            into the column.
 
188
 
 
189
        @keyword title_pixmap: (TODO) if set to a filename a pixmap will be
 
190
            used *instead* of the title set. The title string will still be
 
191
            used to identify the column in the column selection and in a
 
192
            tooltip, if a tooltip is not set.
 
193
        """
 
194
 
 
195
        # XXX: filter function?
 
196
        if ' ' in attribute:
 
197
            msg = ("The attribute can not contain spaces, otherwise I can"
 
198
                   " not find the value in the instances: %s" % attribute)
 
199
            raise AttributeError(msg)
 
200
 
 
201
        self.compare = None
 
202
        self.from_string = None
 
203
 
 
204
        kwargs['attribute'] = attribute
 
205
        kwargs['title'] = title or attribute.capitalize()
 
206
        if not data_type:
 
207
            data_type = str
 
208
        kwargs['data_type'] = data_type
 
209
 
 
210
        # If we don't specify a justification, right align it for int/float
 
211
        # center for bools and left align it for everything else.
 
212
        if "justify" not in kwargs:
 
213
            if data_type:
 
214
                conv = converter.get_converter(data_type)
 
215
                if issubclass(data_type, bool):
 
216
                    kwargs['justify'] = gtk.JUSTIFY_CENTER
 
217
                elif conv.align == Alignment.RIGHT:
 
218
                    kwargs['justify'] = gtk.JUSTIFY_RIGHT
 
219
 
 
220
        format_func = kwargs.get('format_func')
 
221
        if format_func:
 
222
            if not callable(format_func):
 
223
                raise TypeError("format_func must be callable")
 
224
            if 'format' in kwargs:
 
225
                raise TypeError(
 
226
                    "format and format_func can not be used at the same time")
 
227
 
 
228
        # editable_attribute always turns on editable
 
229
        if 'editable_attribute' in kwargs:
 
230
            if not kwargs.get('editable', True):
 
231
                raise TypeError(
 
232
                    "editable cannot be disabled when using editable_attribute")
 
233
            kwargs['editable'] = True
 
234
 
 
235
        PropertyObject.__init__(self, **kwargs)
 
236
        gobject.GObject.__init__(self, attribute=attribute)
 
237
 
 
238
    # This is meant to be subclassable, we're using kgetattr, as
 
239
    # a staticmethod as an optimization, so we can avoid a function call.
 
240
    get_attribute = staticmethod(kgetattr)
 
241
 
 
242
    def prop_set_data_type(self, data):
 
243
        if data is not None:
 
244
            conv = converter.get_converter(data)
 
245
            self.compare = conv.get_compare_function()
 
246
            self.from_string = conv.from_string
 
247
        return data
 
248
 
 
249
    def __repr__(self):
 
250
        namespace = self.__dict__.copy()
 
251
        return "<%s: %s>" % (self.__class__.__name__, namespace)
 
252
 
 
253
    def as_string(self, data):
 
254
        data_type = self.data_type
 
255
        if data is None:
 
256
            text = ''
 
257
        elif self.format_func:
 
258
            text = self.format_func(data)
 
259
        elif (self.format or
 
260
            data_type == float or
 
261
            data_type == Decimal or
 
262
            data_type == currency or
 
263
            data_type == datetime.date or
 
264
            data_type == datetime.datetime or
 
265
            data_type == datetime.time or
 
266
            issubclass(data_type, enum)):
 
267
            conv = converter.get_converter(data_type)
 
268
            text = conv.as_string(data, format=self.format or None)
 
269
        else:
 
270
            text = data
 
271
 
 
272
        return text
 
273
 
 
274
class SequentialColumn(Column):
 
275
    """I am a column which will display a sequence of numbers, which
 
276
    represent the row number. The value is independent of the data in
 
277
    the other columns, so no matter what I will always display 1 in
 
278
    the first column, unless you reverse it by clicking on the column
 
279
    header.
 
280
 
 
281
    If you don't give me any argument I'll have the title of a hash (#) and
 
282
    right justify the sequences."""
 
283
    def __init__(self, title='#', justify=gtk.JUSTIFY_RIGHT, **kwargs):
 
284
        Column.__init__(self, '_kiwi_sequence_id',
 
285
                        title=title, justify=justify, data_type=int, **kwargs)
 
286
 
 
287
    def cell_data_func(self, tree_column, renderer, model, treeiter,
 
288
                       (column, renderer_prop)):
 
289
        reversed = tree_column.get_sort_order() == gtk.SORT_DESCENDING
 
290
 
 
291
        row = model[treeiter]
 
292
        if reversed:
 
293
            sequence_id = len(model) - row.path[0]
 
294
        else:
 
295
            sequence_id = row.path[0] + 1
 
296
 
 
297
        row[COL_MODEL]._kiwi_sequence_id = sequence_id
 
298
 
 
299
        try:
 
300
            renderer.set_property(renderer_prop, sequence_id)
 
301
        except TypeError:
 
302
            raise TypeError("%r does not support parameter %s" %
 
303
                            (renderer, renderer_prop))
 
304
 
 
305
class ColoredColumn(Column):
 
306
    """
 
307
    I am a column which can colorize the text of columns under
 
308
    certain circumstances. I take a color and an extra function
 
309
    which will be called for each row
 
310
 
 
311
    Example, to colorize negative values to red:
 
312
 
 
313
        >>> def colorize(value):
 
314
        ...   return value < 0
 
315
        ...
 
316
        ... ColoredColumn('age', data_type=int, color='red',
 
317
        ...               data_func=colorize),
 
318
    """
 
319
 
 
320
    def __init__(self, attribute, title=None, data_type=None,
 
321
                 color=None, data_func=None, **kwargs):
 
322
        if not issubclass(data_type, number):
 
323
            raise TypeError("data type must be a number")
 
324
        if not callable(data_func):
 
325
            raise TypeError("data func must be callable")
 
326
 
 
327
        self._color = gdk.color_parse(color)
 
328
        self._color_normal = None
 
329
 
 
330
        self._data_func = data_func
 
331
 
 
332
        Column.__init__(self, attribute, title, data_type, **kwargs)
 
333
 
 
334
    def on_attach_renderer(self, renderer):
 
335
        renderer.set_property('foreground-set', True)
 
336
        self._color_normal = renderer.get_property('foreground-gdk')
 
337
 
 
338
    def renderer_func(self, renderer, data):
 
339
        if self._data_func(data):
 
340
            color = self._color
 
341
        else:
 
342
            color = self._color_normal
 
343
 
 
344
        renderer.set_property('foreground-gdk', color)
 
345
 
 
346
class _ContextMenu(gtk.Menu):
 
347
 
 
348
    """
 
349
    ContextMenu is a wrapper for the menu that's displayed when right
 
350
    clicking on a column header. It monitors the treeview and rebuilds
 
351
    when columns are added, removed or moved.
 
352
    """
 
353
 
 
354
    def __init__(self, treeview):
 
355
        gtk.Menu.__init__(self)
 
356
 
 
357
        self._dirty = True
 
358
        self._signal_ids = []
 
359
        self._treeview = treeview
 
360
        self._treeview.connect('columns-changed',
 
361
                              self._on_treeview__columns_changed)
 
362
        self._create()
 
363
 
 
364
    def clean(self):
 
365
        for child in self.get_children():
 
366
            self.remove(child)
 
367
 
 
368
        for menuitem, signal_id in self._signal_ids:
 
369
            menuitem.disconnect(signal_id)
 
370
        self._signal_ids = []
 
371
 
 
372
    def popup(self, event):
 
373
        self._create()
 
374
        gtk.Menu.popup(self, None, None, None,
 
375
                       event.button, event.time)
 
376
 
 
377
    def _create(self):
 
378
        if not self._dirty:
 
379
            return
 
380
 
 
381
        self.clean()
 
382
 
 
383
        for column in self._treeview.get_columns():
 
384
            header_widget = column.get_widget()
 
385
            if not header_widget:
 
386
                continue
 
387
            title = header_widget.get_text()
 
388
 
 
389
            menuitem = gtk.CheckMenuItem(title)
 
390
            menuitem.set_active(column.get_visible())
 
391
            signal_id = menuitem.connect("activate",
 
392
                                         self._on_menuitem__activate,
 
393
                                         column)
 
394
            self._signal_ids.append((menuitem, signal_id))
 
395
            menuitem.show()
 
396
            self.append(menuitem)
 
397
 
 
398
        self._dirty = False
 
399
 
 
400
    def _on_treeview__columns_changed(self, treeview):
 
401
        self._dirty = True
 
402
 
 
403
    def _on_menuitem__activate(self, menuitem, column):
 
404
        active = menuitem.get_active()
 
405
        column.set_visible(active)
 
406
 
 
407
        # The width or height of some of the rows might have
 
408
        # changed after changing the visibility of the column,
 
409
        # so we have to re-measure all the rows, this can be done
 
410
        # using row_changed.
 
411
        model = self._treeview.get_model()
 
412
        for row in model:
 
413
            model.row_changed(row.path, row.iter)
 
414
 
 
415
        children = self.get_children()
 
416
        if active:
 
417
            # Make sure all items are selectable
 
418
            for child in children:
 
419
                child.set_sensitive(True)
 
420
        else:
 
421
            # Protect so we can't hide all the menu items
 
422
            # If there's only one menuitem less to select, set
 
423
            # it to insensitive
 
424
            active_children = [child for child in children
 
425
                                         if child.get_active()]
 
426
            if len(active_children) == 1:
 
427
                active_children[0].set_sensitive(False)
 
428
 
 
429
COL_MODEL = 0
 
430
 
 
431
_marker = object()
 
432
 
 
433
class ObjectList(PropertyObject, gtk.ScrolledWindow):
 
434
    """
 
435
    An enhanced version of GtkTreeView, which provides pythonic wrappers
 
436
    for accessing rows, and optional facilities for column sorting (with
 
437
    types) and column selection.
 
438
 
 
439
    Items in an ObjectList is stored in objects. Each row represents an object
 
440
    and each column represents an attribute in the object.
 
441
    The column description object must be a subclass of L{Column}.
 
442
    Simple example
 
443
 
 
444
    >>> class Fruit:
 
445
    >>>    pass
 
446
 
 
447
    >>> apple = Fruit()
 
448
    >>> apple.name = 'Apple'
 
449
    >>> apple.description = 'Worm house'
 
450
 
 
451
    >>> banana = Fruit()
 
452
    >>> banana.name = 'Banana'
 
453
    >>> banana.description = 'Monkey food'
 
454
 
 
455
    >>> fruits = ObjectList([Column('name'),
 
456
    >>>                      Column('description')])
 
457
    >>> fruits.append(apple)
 
458
    >>> fruits.append(banana)
 
459
 
 
460
    Signals
 
461
    =======
 
462
      - B{row-activated} (list, object):
 
463
        - Emitted when a row is "activated", eg double clicked or pressing
 
464
          enter. See the GtkTreeView documentation for more information
 
465
      - B{selection-changed} (list, object):
 
466
        - Emitted when the selection changes for the ObjectList
 
467
          enter. See the documentation on GtkTreeSelection::changed
 
468
          for more information
 
469
      - B{double-click} (list, object):
 
470
        - Emitted when a row is double-clicked, mostly you want to use
 
471
          the row-activated signal instead to be able catch keyboard events.
 
472
      - B{right-click} (list, object):
 
473
        - Emitted when a row is clicked with the right mouse button.
 
474
      - B{cell-edited} (list, object, attribute):
 
475
        - Emitted when a cell is edited.
 
476
      - B{has-rows} (list, bool):
 
477
        - Emitted when the objectlist goes from an empty to a non-empty
 
478
          state or vice verse.
 
479
 
 
480
    Properties
 
481
    ==========
 
482
      - B{selection-mode}: gtk.SelectionMode I{gtk.SELECTION_BROWSE}
 
483
        - Represents the selection-mode of a GtkTreeSelection of a GtkTreeView.
 
484
    """
 
485
 
 
486
    __gtype_name__ = 'ObjectList'
 
487
 
 
488
    # row activated
 
489
    gsignal('row-activated', object)
 
490
 
 
491
    # selected row(s)
 
492
    gsignal('selection-changed', object)
 
493
 
 
494
    # row double-clicked
 
495
    gsignal('double-click', object)
 
496
 
 
497
    # row right-clicked
 
498
    gsignal('right-click', object, gtk.gdk.Event)
 
499
 
 
500
    # row middle-clicked
 
501
    gsignal('middle-click', object, gtk.gdk.Event)
 
502
 
 
503
    # edited object, attribute name
 
504
    gsignal('cell-edited', object, str)
 
505
 
 
506
    # emitted when empty or non-empty status changes
 
507
    gsignal('has-rows', bool)
 
508
 
 
509
    gproperty('selection-mode', gtk.SelectionMode,
 
510
              default=gtk.SELECTION_BROWSE, nick="SelectionMode")
 
511
 
 
512
    def __init__(self, columns=None,
 
513
                 objects=None,
 
514
                 mode=gtk.SELECTION_BROWSE,
 
515
                 sortable=False,
 
516
                 model=None):
 
517
        """
 
518
        @param columns:       a list of L{Column}s
 
519
        @param objects:       a list of objects to be inserted or None
 
520
        @param mode:          selection mode
 
521
        @param sortable:      whether the user can sort the list
 
522
        @param model:         gtk.TreeModel to use or None to create one
 
523
        """
 
524
        if columns is None:
 
525
            columns = []
 
526
        # allow to specify only one column
 
527
        if isinstance(columns, Column):
 
528
            columns = [columns]
 
529
        elif not isinstance(columns, list):
 
530
            raise TypeError("columns must be a list or a Column")
 
531
 
 
532
        if not isinstance(mode, gtk.SelectionMode):
 
533
            raise TypeError("mode must be an gtk.SelectionMode enum")
 
534
        # gtk.SELECTION_EXTENDED & gtk.SELECTION_MULTIPLE are both 3.
 
535
        # so we can't do this check.
 
536
        #elif mode == gtk.SELECTION_EXTENDED:
 
537
        #    raise TypeError("gtk.SELECTION_EXTENDED is deprecated")
 
538
 
 
539
        self._sortable = sortable
 
540
 
 
541
        self._columns = []
 
542
        # Mapping of instance id -> treeiter
 
543
        self._iters = {}
 
544
        self._cell_data_caches = {}
 
545
        self._autosize = True
 
546
        self._vscrollbar = None
 
547
 
 
548
        gtk.ScrolledWindow.__init__(self)
 
549
 
 
550
        # we always want a vertical scrollbar. Otherwise the button on top
 
551
        # of it doesn't make sense. This button is used to display the popup
 
552
        # menu
 
553
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
 
554
        self.set_shadow_type(gtk.SHADOW_ETCHED_IN)
 
555
        # This is required for gobject.new to work, since scrolledwindow.add
 
556
        # requires valid adjustments and they are for some reason not
 
557
        # properly set when using gobject.new.
 
558
        self.set_hadjustment(gtk.Adjustment())
 
559
        self.set_vadjustment(gtk.Adjustment())
 
560
 
 
561
        if not model:
 
562
            model = gtk.ListStore(object)
 
563
        self._model = model
 
564
        self._model.connect('row-inserted', self._on_model__row_inserted)
 
565
        self._model.connect('row-deleted', self._on_model__row_deleted)
 
566
        self._treeview = gtk.TreeView(self._model)
 
567
        self._treeview.connect('button-press-event',
 
568
                               self._on_treeview__button_press_event)
 
569
        self._treeview.connect_after('row-activated',
 
570
                                     self._after_treeview__row_activated)
 
571
        self._treeview.set_rules_hint(True)
 
572
        self._treeview.show()
 
573
        self.add(self._treeview)
 
574
 
 
575
        # these tooltips are used for the columns
 
576
        self._tooltips = gtk.Tooltips()
 
577
 
 
578
        # create a popup menu for showing or hiding columns
 
579
        self._popup = _ContextMenu(self._treeview)
 
580
 
 
581
        # when setting the column definition the columns are created
 
582
        self.set_columns(columns)
 
583
 
 
584
        if objects:
 
585
            self.add_list(objects, clear=True)
 
586
 
 
587
        # Set selection mode last to avoid spurious events
 
588
        selection = self._treeview.get_selection()
 
589
        selection.connect("changed", self._on_selection__changed)
 
590
 
 
591
        # Select the first item if no items are selected
 
592
        if mode != gtk.SELECTION_NONE and objects:
 
593
            selection.select_iter(self._model[COL_MODEL].iter)
 
594
 
 
595
        # Depends on treeview and selection being set up
 
596
        PropertyObject.__init__(self)
 
597
 
 
598
        self.set_selection_mode(mode)
 
599
 
 
600
    # Python list object implementation
 
601
    # These methods makes the kiwi list behave more or less
 
602
    # like a normal python list
 
603
    #
 
604
    # TODO:
 
605
    #   operators
 
606
    #      __add__, __eq__, __ge__, __gt__, __iadd__,
 
607
    #      __imul__,  __le__, __lt__, __mul__, __ne__,
 
608
    #      __rmul__
 
609
    #
 
610
    #   misc
 
611
    #     __delitem__, __hash__, __reduce__, __reduce_ex__
 
612
    #     __reversed__
 
613
 
 
614
    def __len__(self):
 
615
        "len(list)"
 
616
        return len(self._model)
 
617
 
 
618
    def __nonzero__(self):
 
619
        "if list"
 
620
        return True
 
621
 
 
622
    def __contains__(self, instance):
 
623
        "item in list"
 
624
        return bool(self._iters.get(id(instance), False))
 
625
 
 
626
    def __iter__(self):
 
627
        "for item in list"
 
628
        class ModelIterator:
 
629
            def __init__(self):
 
630
                self._index = -1
 
631
 
 
632
            def __iter__(self):
 
633
                return self
 
634
 
 
635
            def next(self, model=self._model):
 
636
                try:
 
637
                    self._index += 1
 
638
                    return model[self._index][COL_MODEL]
 
639
                except IndexError:
 
640
                    raise StopIteration
 
641
 
 
642
        return ModelIterator()
 
643
 
 
644
    def __getitem__(self, arg):
 
645
        "list[n]"
 
646
        if isinstance(arg, (int, gtk.TreeIter, str)):
 
647
            item = self._model[arg][COL_MODEL]
 
648
        elif isinstance(arg, slice):
 
649
            model = self._model
 
650
            return [model[item][COL_MODEL]
 
651
                        for item in slicerange(arg, len(self._model))]
 
652
        else:
 
653
            raise TypeError("argument arg must be int, gtk.Treeiter or "
 
654
                            "slice, not %s" % type(arg))
 
655
        return item
 
656
 
 
657
    def __setitem__(self, arg, item):
 
658
        "list[n] = m"
 
659
        if isinstance(arg, (int, gtk.TreeIter, str)):
 
660
            model = self._model
 
661
            olditem = model[arg][COL_MODEL]
 
662
            model[arg] = (item,)
 
663
 
 
664
            # Update iterator cache
 
665
            iters = self._iters
 
666
            iters[id(item)] = model[arg].iter
 
667
            del iters[id(olditem)]
 
668
 
 
669
        elif isinstance(arg, slice):
 
670
            raise NotImplementedError("slices for list are not implemented")
 
671
        else:
 
672
            raise TypeError("argument arg must be int or gtk.Treeiter,"
 
673
                            " not %s" % type(arg))
 
674
 
 
675
    # append and remove are below
 
676
 
 
677
    def extend(self, iterable):
 
678
        """
 
679
        Extend list by appending elements from the iterable
 
680
 
 
681
        @param iterable:
 
682
        """
 
683
 
 
684
        return self.add_list(iterable, clear=False)
 
685
 
 
686
    def index(self, item, start=None, stop=None):
 
687
        """
 
688
        Return first index of value
 
689
 
 
690
        @param item:
 
691
        @param start:
 
692
        @param stop
 
693
        """
 
694
 
 
695
        if start is not None or stop is not None:
 
696
            raise NotImplementedError("start and stop")
 
697
 
 
698
        treeiter = self._iters.get(id(item), _marker)
 
699
        if treeiter is _marker:
 
700
            raise ValueError("item %r is not in the list" % item)
 
701
 
 
702
        return self._model[treeiter].path[0]
 
703
 
 
704
    def count(self, item):
 
705
        "L.count(item) -> integer -- return number of occurrences of value"
 
706
 
 
707
        count = 0
 
708
        for row in self._model:
 
709
            if row[COL_MODEL] == item:
 
710
                count += 1
 
711
        return count
 
712
 
 
713
    def insert(self, index, instance, select=False):
 
714
        """Inserts an instance to the list
 
715
        @param index: position to insert the instance at
 
716
        @param instance: the instance to be added (according to the columns spec)
 
717
        @param select: whether or not the new item should appear selected.
 
718
        """
 
719
        self._treeview.freeze_notify()
 
720
 
 
721
        row_iter = self._model.insert(index, (instance,))
 
722
        self._iters[id(instance)] = row_iter
 
723
 
 
724
        if self._autosize:
 
725
            self._treeview.columns_autosize()
 
726
 
 
727
        if select:
 
728
            self._select_and_focus_row(row_iter)
 
729
        self._treeview.thaw_notify()
 
730
 
 
731
    def pop(self, index):
 
732
        """
 
733
        Remove and return item at index (default last)
 
734
        @param index:
 
735
        """
 
736
        raise NotImplementedError
 
737
 
 
738
    def reverse(self, pos, item):
 
739
        "L.reverse() -- reverse *IN PLACE*"
 
740
        raise NotImplementedError
 
741
 
 
742
    def sort(self, pos, item):
 
743
        """L.sort(cmp=None, key=None, reverse=False) -- stable sort *IN PLACE*;
 
744
        cmp(x, y) -> -1, 0, 1"""
 
745
        raise NotImplementedError
 
746
 
 
747
    def sort_by_attribute(self, attribute, order=gtk.SORT_ASCENDING):
 
748
        """
 
749
        Sort by an attribute in the object model.
 
750
 
 
751
        @param attribute: attribute to sort on
 
752
        @type attribute: string
 
753
        @param order: one of gtk.SORT_ASCENDING, gtk.SORT_DESCENDING
 
754
        @type order: gtk.SortType
 
755
        """
 
756
        def _sort_func(model, iter1, iter2):
 
757
            return cmp(
 
758
                getattr(model[iter1][0], attribute, None),
 
759
                getattr(model[iter2][0], attribute, None))
 
760
        unused_sort_col_id = len(self._columns)
 
761
        self._model.set_sort_func(unused_sort_col_id, _sort_func)
 
762
        self._model.set_sort_column_id(unused_sort_col_id, order)
 
763
 
 
764
    # Properties
 
765
 
 
766
    def prop_set_selection_mode(self, mode):
 
767
        self.set_selection_mode(mode)
 
768
 
 
769
    def prop_get_selection_mode(self):
 
770
        return self.get_selection_mode()
 
771
 
 
772
    # Columns handling
 
773
 
 
774
    def _load(self, instances, clear):
 
775
        # do nothing if empty list or None provided
 
776
        model = self._model
 
777
        if clear:
 
778
            if not instances:
 
779
                self.unselect_all()
 
780
                self.clear()
 
781
                return
 
782
 
 
783
        model = self._model
 
784
        iters = self._iters
 
785
 
 
786
        old_instances = [row[COL_MODEL] for row in model]
 
787
 
 
788
        # Save selection
 
789
        selected_instances = []
 
790
        if old_instances:
 
791
            selection = self._treeview.get_selection()
 
792
            _, paths = selection.get_selected_rows()
 
793
            if paths:
 
794
                selected_instances = [model[path][COL_MODEL]
 
795
                                          for (path,) in paths]
 
796
 
 
797
        iters = self._iters
 
798
        prev = None
 
799
        # Do not always just clear the list, check if we have the same
 
800
        # instances in the list we want to insert and merge in the new
 
801
        # items
 
802
        if clear:
 
803
            for instance in iter(instances):
 
804
                objid = id(instance)
 
805
                # If the instance is not in the list insert it after
 
806
                # the previous inserted object
 
807
                if not objid in iters:
 
808
                    if prev is None:
 
809
                        prev = model.append((instance,))
 
810
                    else:
 
811
                        prev = model.insert_after(prev, (instance,))
 
812
                    iters[objid] = prev
 
813
                else:
 
814
                    prev = iters[objid]
 
815
 
 
816
            # Optimization when we were empty, we wont need to remove anything
 
817
            # nor restore selection
 
818
            if old_instances:
 
819
                # Remove
 
820
                objids = [id(instance) for instance in instances]
 
821
                for instance in old_instances:
 
822
                    objid = id(instance)
 
823
                    if objid in objids:
 
824
                        continue
 
825
                    self._remove(objid)
 
826
        else:
 
827
            for instance in iter(instances):
 
828
                iters[id(instance)] = model.append((instance,))
 
829
 
 
830
        # Restore selection
 
831
        for instance in selected_instances:
 
832
            objid = id(instance)
 
833
            if objid in iters:
 
834
                selection.select_iter(iters[objid])
 
835
 
 
836
        # As soon as we have data for that list, we can autosize it, and
 
837
        # we don't want to autosize again, or we may cancel user
 
838
        # modifications.
 
839
        if self._autosize:
 
840
            self._treeview.columns_autosize()
 
841
            self._autosize = False
 
842
 
 
843
    def _setup_columns(self, columns):
 
844
        searchable = None
 
845
        sorted = None
 
846
        expand = False
 
847
        for column in columns:
 
848
            if column.sorted:
 
849
                if sorted:
 
850
                    raise ValueError("Can't make column %s sorted, column"
 
851
                                     " %s is already set as sortable" % (
 
852
                        column.attribute, sorted.attribute))
 
853
                sorted = column.sorted
 
854
            if column.expand:
 
855
                expand = True
 
856
 
 
857
        self._sortable = self._sortable or sorted
 
858
 
 
859
        for column in columns:
 
860
            self._setup_column(column)
 
861
 
 
862
        if not expand:
 
863
            column = gtk.TreeViewColumn()
 
864
            self._treeview.append_column(column)
 
865
 
 
866
    def _setup_column(self, column):
 
867
        # You can't subclass bool, so this is okay
 
868
        if (column.data_type is bool and column.format):
 
869
            raise TypeError("format is not supported for boolean columns")
 
870
 
 
871
        index = self._columns.index(column)
 
872
        treeview_column = self._treeview.get_column(index)
 
873
        if treeview_column is None:
 
874
            treeview_column = self._create_column(column)
 
875
 
 
876
        if self._sortable:
 
877
            self._model.set_sort_func(index,
 
878
                                      self._model_sort_func,
 
879
                                      (column, column.attribute))
 
880
            treeview_column.set_sort_column_id(index)
 
881
 
 
882
        if column.sorted:
 
883
            self._model.set_sort_column_id(index, column.order)
 
884
 
 
885
        renderer, renderer_prop = self._guess_renderer_for_type(column)
 
886
        if column.on_attach_renderer:
 
887
            column.on_attach_renderer(renderer)
 
888
        justify = column.justify
 
889
        if justify == gtk.JUSTIFY_RIGHT:
 
890
            xalign = 1.0
 
891
        elif justify == gtk.JUSTIFY_CENTER:
 
892
            xalign = 0.5
 
893
        elif justify in (gtk.JUSTIFY_LEFT,
 
894
                         gtk.JUSTIFY_FILL):
 
895
            xalign = 0.0
 
896
        else:
 
897
            raise AssertionError
 
898
        renderer.set_property("xalign", xalign)
 
899
        treeview_column.set_property("alignment", xalign)
 
900
 
 
901
        if column.ellipsize:
 
902
            renderer.set_property('ellipsize', column.ellipsize)
 
903
        if column.font_desc:
 
904
            renderer.set_property('font-desc',
 
905
                                  pango.FontDescription(column.font_desc))
 
906
        if column.use_stock:
 
907
            cell_data_func = self._cell_data_pixbuf_func
 
908
        elif issubclass(column.data_type, enum):
 
909
            cell_data_func = self._cell_data_combo_func
 
910
        else:
 
911
            cell_data_func = self._cell_data_text_func
 
912
 
 
913
        if column.cell_data_func:
 
914
            cell_data_func = column.cell_data_func
 
915
        elif column.cache:
 
916
            self._cell_data_caches[column.attribute] = {}
 
917
 
 
918
        treeview_column.pack_start(renderer)
 
919
        treeview_column.set_cell_data_func(renderer, cell_data_func,
 
920
                                           (column, renderer_prop))
 
921
        treeview_column.set_visible(column.visible)
 
922
 
 
923
        if column.width:
 
924
            treeview_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
 
925
            treeview_column.set_fixed_width(column.width)
 
926
        if column.tooltip:
 
927
            widget = self._get_column_button(treeview_column)
 
928
            if widget is not None:
 
929
                self._tooltips.set_tip(widget, column.tooltip)
 
930
 
 
931
        if column.expand:
 
932
            # Default is False
 
933
            treeview_column.set_expand(True)
 
934
 
 
935
        if column.sorted:
 
936
            treeview_column.set_sort_indicator(True)
 
937
 
 
938
        if column.width:
 
939
            self._autosize = False
 
940
 
 
941
        if column.searchable:
 
942
            if not issubclass(column.data_type, basestring):
 
943
                raise TypeError("Unsupported data type for "
 
944
                                "searchable column: %s" % column.data_type)
 
945
            self._treeview.set_search_column(index)
 
946
            self._treeview.set_search_equal_func(self._treeview_search_equal_func,
 
947
                                                 column)
 
948
 
 
949
        if column.radio:
 
950
            if not issubclass(column.data_type, bool):
 
951
                raise TypeError("You can only use radio for boolean columns")
 
952
 
 
953
        if column.expander:
 
954
            self._treeview.set_expander_column(treeview_column)
 
955
 
 
956
        # typelist here may be none. It's okay; justify_columns will try
 
957
        # and use the specified justifications and if not present will
 
958
        # not touch the column. When typelist is not set,
 
959
        # append/add_list have a chance to fix up the remaining
 
960
        # justification by looking at the first instance's data.
 
961
#        self._justify_columns(columns, typelist)
 
962
 
 
963
    def _create_column(self, column):
 
964
        treeview_column = gtk.TreeViewColumn()
 
965
        # we need to set our own widget because otherwise
 
966
        # __get_column_button won't work
 
967
 
 
968
        label = gtk.Label(column.title)
 
969
        label.show()
 
970
        treeview_column.set_widget(label)
 
971
        treeview_column.set_resizable(True)
 
972
        treeview_column.set_clickable(True)
 
973
        treeview_column.set_reorderable(True)
 
974
        self._treeview.append_column(treeview_column)
 
975
 
 
976
        # setup the button to show the popup menu
 
977
        button = self._get_column_button(treeview_column)
 
978
        button.connect('button-release-event',
 
979
                       self._on_treeview_header__button_release_event)
 
980
        return treeview_column
 
981
 
 
982
    def _guess_renderer_for_type(self, column):
 
983
        """Gusses which CellRenderer we should use for a given type.
 
984
        It also set the property of the renderer that depends on the model,
 
985
        in the renderer.
 
986
        """
 
987
 
 
988
        # TODO: Move to column
 
989
        data_type = column.data_type
 
990
        if data_type is bool:
 
991
            renderer = gtk.CellRendererToggle()
 
992
            if column.editable:
 
993
                renderer.set_property('activatable', True)
 
994
                # Boolean can be either a radio or a checkbox.
 
995
                # Changes are handled by the toggled callback, which
 
996
                # should only be connected if the column is editable.
 
997
                if column.radio:
 
998
                    renderer.set_radio(True)
 
999
                    cb = self._on_renderer_toggle_radio__toggled
 
1000
                else:
 
1001
                    cb = self._on_renderer_toggle_check__toggled
 
1002
                renderer.connect('toggled', cb, self._model, column.attribute)
 
1003
            prop = 'active'
 
1004
        elif column.use_stock or data_type == gdk.Pixbuf:
 
1005
            renderer = gtk.CellRendererPixbuf()
 
1006
            prop = 'pixbuf'
 
1007
            if column.editable:
 
1008
                raise TypeError("use-stock columns cannot be editable")
 
1009
        elif issubclass(data_type, enum):
 
1010
            if data_type is enum:
 
1011
                raise TypeError("data_type must be a subclass of enum")
 
1012
 
 
1013
            model = gtk.ListStore(str, object)
 
1014
            items = data_type.names.items()
 
1015
            items.sort()
 
1016
            for key, value in items:
 
1017
                model.append((key.lower().capitalize(), value))
 
1018
 
 
1019
            renderer =  gtk.CellRendererCombo()
 
1020
            renderer.set_property('model', model)
 
1021
            renderer.set_property('text-column', 0)
 
1022
            renderer.set_property('has-entry', True)
 
1023
            if column.editable:
 
1024
                renderer.set_property('editable', True)
 
1025
                renderer.connect('edited', self._on_renderer_combo__edited,
 
1026
                                 self._model, column.attribute, column)
 
1027
            prop = 'model'
 
1028
        elif issubclass(data_type, (datetime.date, datetime.time,
 
1029
                                    basestring, number,
 
1030
                                    currency)):
 
1031
            renderer = gtk.CellRendererText()
 
1032
            if column.use_markup:
 
1033
                prop = 'markup'
 
1034
            else:
 
1035
                prop = 'text'
 
1036
            if column.editable:
 
1037
                renderer.set_property('editable', True)
 
1038
                renderer.connect('edited', self._on_renderer_text__edited,
 
1039
                                 self._model, column.attribute, column,
 
1040
                                 column.from_string)
 
1041
 
 
1042
        else:
 
1043
            raise ValueError("the type %s is not supported yet" % data_type)
 
1044
 
 
1045
        return renderer, prop
 
1046
 
 
1047
    # selection methods
 
1048
    def _select_and_focus_row(self, row_iter):
 
1049
        self._treeview.set_cursor(self._model[row_iter].path)
 
1050
 
 
1051
    # handlers & callbacks
 
1052
 
 
1053
    # Model
 
1054
    def _on_model__row_inserted(self, model, path, iter):
 
1055
        if len(model) == 1:
 
1056
            self.emit('has-rows', True)
 
1057
 
 
1058
    def _on_model__row_deleted(self, model, path):
 
1059
        if not len(model):
 
1060
            self.emit('has-rows', False)
 
1061
 
 
1062
    def _model_sort_func(self, model, iter1, iter2, (column, attr)):
 
1063
        "This method is used to sort the GtkTreeModel"
 
1064
        return column.compare(
 
1065
            column.get_attribute(model[iter1][COL_MODEL], attr),
 
1066
            column.get_attribute(model[iter2][COL_MODEL], attr))
 
1067
 
 
1068
    # Selection
 
1069
    def _on_selection__changed(self, selection):
 
1070
        "This method is used to proxy selection::changed to selection-changed"
 
1071
        mode = selection.get_mode()
 
1072
        if mode == gtk.SELECTION_MULTIPLE:
 
1073
            item = self.get_selected_rows()
 
1074
        elif mode in (gtk.SELECTION_SINGLE, gtk.SELECTION_BROWSE):
 
1075
            item = self.get_selected()
 
1076
        else:
 
1077
            raise AssertionError
 
1078
        self.emit('selection-changed', item)
 
1079
 
 
1080
    # ScrolledWindow
 
1081
    def _on_scrolled_window__realize(self, widget):
 
1082
        toplevel = widget.get_toplevel()
 
1083
        self._popup_window.set_transient_for(toplevel)
 
1084
        self._popup_window.set_destroy_with_parent(True)
 
1085
 
 
1086
    def _on_scrolled_window__size_allocate(self, widget, allocation):
 
1087
        """Resize the Vertical Scrollbar to make it smaller and let space
 
1088
        for the popup button. Also put that button there.
 
1089
        """
 
1090
        old_alloc = self._vscrollbar.get_allocation()
 
1091
        height = self._get_header_height()
 
1092
        new_alloc = gtk.gdk.Rectangle(old_alloc.x, old_alloc.y + height,
 
1093
                                      old_alloc.width,
 
1094
                                      old_alloc.height - height)
 
1095
        self._vscrollbar.size_allocate(new_alloc)
 
1096
        # put the popup_window in its position
 
1097
        gdk_window = self.window
 
1098
        if gdk_window:
 
1099
            winx, winy = gdk_window.get_origin()
 
1100
            self._popup_window.move(winx + old_alloc.x,
 
1101
                                    winy + old_alloc.y)
 
1102
 
 
1103
    # TreeView
 
1104
    def _treeview_search_equal_func(self, model, tree_column, key, treeiter, column):
 
1105
        "for searching inside the treeview, case-insensitive by default"
 
1106
        data = column.get_attribute(model[treeiter][COL_MODEL],
 
1107
                                    column.attribute, None)
 
1108
        if data.lower().startswith(key.lower()):
 
1109
            return False
 
1110
        return True
 
1111
 
 
1112
    def _on_treeview_header__button_release_event(self, button, event):
 
1113
        if event.button == 3:
 
1114
            self._popup.popup(event)
 
1115
 
 
1116
        return False
 
1117
 
 
1118
    def _after_treeview__row_activated(self, treeview, path, view_column):
 
1119
        "After activated (double clicked or pressed enter) on a row"
 
1120
        try:
 
1121
            row = self._model[path]
 
1122
        except IndexError:
 
1123
            print 'path %s was not found in model: %s' % (
 
1124
                path, map(list, self._model))
 
1125
            return
 
1126
        item = row[COL_MODEL]
 
1127
        self.emit('row-activated', item)
 
1128
 
 
1129
    def _get_selection_or_selected_rows(self):
 
1130
        selection = self._treeview.get_selection()
 
1131
        mode = selection.get_mode()
 
1132
        if mode == gtk.SELECTION_MULTIPLE:
 
1133
            item = self.get_selected_rows()
 
1134
        elif mode == gtk.SELECTION_NONE:
 
1135
            return
 
1136
        else:
 
1137
            item = self.get_selected()
 
1138
        return item
 
1139
 
 
1140
    def _emit_button_press_signal(self, signal_name, event):
 
1141
        item = self._get_selection_or_selected_rows()
 
1142
        if item:
 
1143
            self.emit(signal_name, item, event)
 
1144
 
 
1145
    def _on_treeview__button_press_event(self, treeview, event):
 
1146
        "Generic button-press-event handler to be able to catch double clicks"
 
1147
 
 
1148
        # Right and Middle click
 
1149
        if event.type == gtk.gdk.BUTTON_PRESS:
 
1150
            if event.button == 3:
 
1151
                signal_name = 'right-click'
 
1152
            elif event.button == 2:
 
1153
                signal_name = 'middle-click'
 
1154
            else:
 
1155
                return
 
1156
            gobject.idle_add(self._emit_button_press_signal, signal_name,
 
1157
                             event.copy())
 
1158
        # Double left click
 
1159
        elif event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1:
 
1160
            item = self._get_selection_or_selected_rows()
 
1161
            if item:
 
1162
                self.emit('double-click', item)
 
1163
 
 
1164
    # CellRenderers
 
1165
    def _cell_data_text_func(self, tree_column, renderer, model, treeiter,
 
1166
                             (column, renderer_prop)):
 
1167
        "To render the data of a cell renderer text"
 
1168
        row = model[treeiter]
 
1169
        if column.editable_attribute:
 
1170
            data = column.get_attribute(row[COL_MODEL],
 
1171
                                        column.editable_attribute, None)
 
1172
            data_type = column.data_type
 
1173
            if isinstance(renderer, gtk.CellRendererToggle):
 
1174
                renderer.set_property('activatable', data)
 
1175
            elif isinstance(renderer, gtk.CellRendererText):
 
1176
                renderer.set_property('editable', data)
 
1177
            else:
 
1178
                raise AssertionError
 
1179
 
 
1180
        if column.cache:
 
1181
            cache = self._cell_data_caches[column.attribute]
 
1182
            path = row.path[0]
 
1183
            if path in cache:
 
1184
                data = cache[path]
 
1185
            else:
 
1186
                data = column.get_attribute(row[COL_MODEL],
 
1187
                                            column.attribute, None)
 
1188
                cache[path] = data
 
1189
        else:
 
1190
            data = column.get_attribute(row[COL_MODEL],
 
1191
                                        column.attribute, None)
 
1192
 
 
1193
        text = column.as_string(data)
 
1194
 
 
1195
        renderer.set_property(renderer_prop, text)
 
1196
 
 
1197
        if column.renderer_func:
 
1198
            column.renderer_func(renderer, data)
 
1199
 
 
1200
    def _cell_data_pixbuf_func(self, tree_column, renderer, model, treeiter,
 
1201
                               (column, renderer_prop)):
 
1202
        "To render the data of a cell renderer pixbuf"
 
1203
        row = model[treeiter]
 
1204
        data = column.get_attribute(row[COL_MODEL],
 
1205
                                    column.attribute, None)
 
1206
        if data is not None:
 
1207
            pixbuf = self.render_icon(data, column.icon_size)
 
1208
            renderer.set_property(renderer_prop, pixbuf)
 
1209
 
 
1210
    def _cell_data_combo_func(self, tree_column, renderer, model, treeiter,
 
1211
                              (column, renderer_prop)):
 
1212
        row = model[treeiter]
 
1213
        data = column.get_attribute(row[COL_MODEL],
 
1214
                                    column.attribute, None)
 
1215
        text = column.as_string(data)
 
1216
        renderer.set_property('text', text.lower().capitalize())
 
1217
 
 
1218
    def _on_renderer__toggled(self, renderer, path, column):
 
1219
        setattr(self._model[path][COL_MODEL], column.attribute,
 
1220
                not renderer.get_active())
 
1221
 
 
1222
    def _on_renderer_toggle_check__toggled(self, renderer, path, model, attr):
 
1223
        obj = model[path][COL_MODEL]
 
1224
        value = not getattr(obj, attr, None)
 
1225
        setattr(obj, attr, value)
 
1226
        self.emit('cell-edited', obj, attr)
 
1227
 
 
1228
    def _on_renderer_toggle_radio__toggled(self, renderer, path, model, attr):
 
1229
        # Deactive old one
 
1230
        old = renderer.get_data('kiwilist::radio-active')
 
1231
 
 
1232
        # If we don't have the radio-active set it means we're doing
 
1233
        # This for the first time, so scan and see which one is currently
 
1234
        # active, so we can deselect it
 
1235
        if not old:
 
1236
            # XXX: Handle multiple values set to True, this
 
1237
            #      algorithm just takes the first one it finds
 
1238
            for row in self._model:
 
1239
                obj = row[COL_MODEL]
 
1240
                value = getattr(obj, attr)
 
1241
                if value == True:
 
1242
                    old = obj
 
1243
                    break
 
1244
        if old:
 
1245
            setattr(old, attr, False)
 
1246
 
 
1247
        # Active new and save a reference to the object of the
 
1248
        # previously selected row
 
1249
        new = model[path][COL_MODEL]
 
1250
        setattr(new, attr, True)
 
1251
        renderer.set_data('kiwilist::radio-active', new)
 
1252
        self.emit('cell-edited', new, attr)
 
1253
 
 
1254
    def _on_renderer_text__edited(self, renderer, path, text,
 
1255
                                  model, attr, column, from_string):
 
1256
        obj = model[path][COL_MODEL]
 
1257
        value = from_string(text)
 
1258
        setattr(obj, attr, value)
 
1259
        self.emit('cell-edited', obj, attr)
 
1260
 
 
1261
    def _on_renderer_combo__edited(self, renderer, path, text,
 
1262
                                  model, attr, column):
 
1263
        obj = model[path][COL_MODEL]
 
1264
        if not text:
 
1265
            return
 
1266
 
 
1267
        value_model = renderer.get_property('model')
 
1268
        for row in value_model:
 
1269
            if row[0] == text:
 
1270
                value = row[1]
 
1271
                break
 
1272
        else:
 
1273
            raise AssertionError
 
1274
        setattr(obj, attr, value)
 
1275
        self.emit('cell-edited', obj, attr)
 
1276
 
 
1277
    def _on_renderer__edited(self, renderer, path, value, column):
 
1278
        data_type = column.data_type
 
1279
        if data_type in number:
 
1280
            value = data_type(value)
 
1281
 
 
1282
        # XXX convert new_text to the proper data type
 
1283
        setattr(self._model[path][COL_MODEL], column.attribute, value)
 
1284
 
 
1285
    # hacks
 
1286
    def _get_column_button(self, column):
 
1287
        """Return the button widget of a particular TreeViewColumn.
 
1288
 
 
1289
        This hack is needed since that widget is private of the TreeView but
 
1290
        we need access to them for Tooltips, right click menus, ...
 
1291
 
 
1292
        Use this function at your own risk
 
1293
        """
 
1294
 
 
1295
        button = column.get_widget()
 
1296
        assert button is not None, ("You must call column.set_widget() "
 
1297
                                    "before calling _get_column_button")
 
1298
 
 
1299
        while not isinstance(button, gtk.Button):
 
1300
            button = button.get_parent()
 
1301
 
 
1302
        return button
 
1303
 
 
1304
    # start of the hack to put a button on top of the vertical scrollbar
 
1305
    def _setup_popup_button(self):
 
1306
        """Put a button on top of the vertical scrollbar to show the popup
 
1307
        menu.
 
1308
        Internally it uses a POPUP window so you can tell how *Evil* is this.
 
1309
        """
 
1310
        self._popup_window = gtk.Window(gtk.WINDOW_POPUP)
 
1311
        self._popup_button = gtk.Button('*')
 
1312
        self._popup_window.add(self._popup_button)
 
1313
        self._popup_window.show_all()
 
1314
 
 
1315
        self.forall(self._find_vertical_scrollbar)
 
1316
        self.connect('size-allocate', self._on_scrolled_window__size_allocate)
 
1317
        self.connect('realize', self._on_scrolled_window__realize)
 
1318
 
 
1319
    def _find_vertical_scrollbar(self, widget):
 
1320
        """This method is called from a .forall() method in the ScrolledWindow.
 
1321
        It just save a reference to the vertical scrollbar for doing evil
 
1322
        things later.
 
1323
        """
 
1324
        if isinstance(widget, gtk.VScrollbar):
 
1325
            self._vscrollbar = widget
 
1326
 
 
1327
    def _get_header_height(self):
 
1328
        treeview_column = self._treeview.get_column(0)
 
1329
        button = self._get_column_button(treeview_column)
 
1330
        alloc = button.get_allocation()
 
1331
        return alloc.height
 
1332
 
 
1333
    # end of the popup button hack
 
1334
 
 
1335
    #
 
1336
    # Public API
 
1337
    #
 
1338
    def get_model(self):
 
1339
        "Return treemodel of the current list"
 
1340
        return self._model
 
1341
 
 
1342
    def get_treeview(self):
 
1343
        "Return treeview of the current list"
 
1344
        return self._treeview
 
1345
 
 
1346
    def get_columns(self):
 
1347
        return self._columns
 
1348
 
 
1349
    def get_column_by_name(self, name):
 
1350
        """Returns the name of a column"""
 
1351
        for column in self._columns:
 
1352
            if column.attribute == name:
 
1353
                return column
 
1354
 
 
1355
        raise LookupError("There is no column called %s" % name)
 
1356
 
 
1357
    def get_treeview_column(self, column):
 
1358
        """
 
1359
        @param column: a @Column
 
1360
        """
 
1361
        if not isinstance(column, Column):
 
1362
            raise TypeError
 
1363
 
 
1364
        if not column in self._columns:
 
1365
            raise ValueError
 
1366
 
 
1367
        index = self._columns.index(column)
 
1368
        tree_columns = self._treeview.get_columns()
 
1369
        return tree_columns[index]
 
1370
 
 
1371
    def grab_focus(self):
 
1372
        """
 
1373
        Grabs the focus of the ObjectList
 
1374
        """
 
1375
        self._treeview.grab_focus()
 
1376
 
 
1377
    def _clear_columns(self):
 
1378
        # Reset the sort function for all model columns
 
1379
        model = self._model
 
1380
        for i, column in enumerate(self._columns):
 
1381
            # Bug in PyGTK, it should be possible to remove a sort func.
 
1382
            model.set_sort_func(i, lambda m, i1, i2: -1)
 
1383
 
 
1384
        # Remove all columns
 
1385
        treeview = self._treeview
 
1386
        while treeview.get_columns():
 
1387
            treeview.remove_column(treeview.get_column(0))
 
1388
 
 
1389
        self._popup.clean()
 
1390
        self._columns = []
 
1391
 
 
1392
    def set_columns(self, columns):
 
1393
        """
 
1394
        @param columns: a sequence of L{Column} objects.
 
1395
        """
 
1396
 
 
1397
        if not isinstance(columns, (list, tuple)):
 
1398
            raise ValueError("columns must be a list or a tuple")
 
1399
 
 
1400
        self._clear_columns()
 
1401
        self._columns = columns
 
1402
        self._setup_columns(columns)
 
1403
 
 
1404
    def append(self, instance, select=False):
 
1405
        """Adds an instance to the list.
 
1406
        @param instance: the instance to be added (according to the columns spec)
 
1407
        @param select: whether or not the new item should appear selected.
 
1408
        """
 
1409
 
 
1410
        # Freeze and save original selection mode to avoid blinking
 
1411
        self._treeview.freeze_notify()
 
1412
 
 
1413
        row_iter = self._model.append((instance,))
 
1414
        self._iters[id(instance)] = row_iter
 
1415
 
 
1416
        if self._autosize:
 
1417
            self._treeview.columns_autosize()
 
1418
 
 
1419
        if select:
 
1420
            self._select_and_focus_row(row_iter)
 
1421
        self._treeview.thaw_notify()
 
1422
 
 
1423
    def _remove(self, objid):
 
1424
        # linear search for the instance to remove
 
1425
        treeiter = self._iters.pop(objid)
 
1426
        if not treeiter:
 
1427
            return False
 
1428
 
 
1429
        # Remove any references to this path
 
1430
        self._clear_cache_for_iter(treeiter)
 
1431
 
 
1432
        # All references to the iter gone, now it can be removed
 
1433
        self._model.remove(treeiter)
 
1434
 
 
1435
        return True
 
1436
 
 
1437
    def _clear_cache_for_iter(self, treeiter):
 
1438
        # Not as inefficent as it looks
 
1439
        path = self._model[treeiter].path[0]
 
1440
        for cache in self._cell_data_caches.values():
 
1441
            if path in cache:
 
1442
                del cache[path]
 
1443
 
 
1444
    def remove(self, instance, select=False):
 
1445
        """Remove an instance from the list.
 
1446
        If the instance is not in the list it returns False. Otherwise it
 
1447
        returns True.
 
1448
 
 
1449
        @param instance:
 
1450
        @param select: if true, the previous item will be selected
 
1451
          if there is one.
 
1452
        """
 
1453
 
 
1454
        objid = id(instance)
 
1455
        if not objid in self._iters:
 
1456
            raise ValueError("instance %r is not in the list" % instance)
 
1457
 
 
1458
 
 
1459
        if select:
 
1460
            prev = self.get_previous(instance)
 
1461
            rv = self._remove(objid)
 
1462
            if prev != instance:
 
1463
                self.select(prev)
 
1464
        else:
 
1465
            rv = self._remove(objid)
 
1466
        return rv
 
1467
 
 
1468
    def update(self, instance):
 
1469
        objid = id(instance)
 
1470
        if not objid in self._iters:
 
1471
            raise ValueError("instance %r is not in the list" % instance)
 
1472
        treeiter = self._iters[objid]
 
1473
        self._clear_cache_for_iter(treeiter)
 
1474
        self._model.row_changed(self._model[treeiter].path, treeiter)
 
1475
 
 
1476
    def refresh(self, view_only=False):
 
1477
        """
 
1478
        Reloads the values from all objects.
 
1479
 
 
1480
        @param view_only: if True, only force a refresh of the
 
1481
            visible part of this objectlist's Treeview.
 
1482
        """
 
1483
        if view_only:
 
1484
            self._treeview.queue_draw()
 
1485
        else:
 
1486
            self._model.foreach(gtk.TreeModel.row_changed)
 
1487
 
 
1488
    def set_column_visibility(self, column_index, visibility):
 
1489
        treeview_column = self._treeview.get_column(column_index)
 
1490
        treeview_column.set_visible(visibility)
 
1491
 
 
1492
    def get_selection_mode(self):
 
1493
        selection = self._treeview.get_selection()
 
1494
        if selection:
 
1495
            return selection.get_mode()
 
1496
 
 
1497
    def set_selection_mode(self, mode):
 
1498
        selection = self._treeview.get_selection()
 
1499
        if selection:
 
1500
            self.notify('selection-mode')
 
1501
            return selection.set_mode(mode)
 
1502
 
 
1503
    def unselect_all(self):
 
1504
        selection = self._treeview.get_selection()
 
1505
        if selection:
 
1506
            selection.unselect_all()
 
1507
 
 
1508
    def select_paths(self, paths):
 
1509
        """
 
1510
        Selects a number of rows corresponding to paths
 
1511
 
 
1512
        @param paths: rows to be selected
 
1513
        """
 
1514
 
 
1515
        selection = self._treeview.get_selection()
 
1516
        if selection.get_mode() == gtk.SELECTION_NONE:
 
1517
            raise TypeError("Selection not allowed")
 
1518
 
 
1519
        selection.unselect_all()
 
1520
        for path in paths:
 
1521
            selection.select_path(path)
 
1522
 
 
1523
    def select(self, instance, scroll=True):
 
1524
        selection = self._treeview.get_selection()
 
1525
        if selection.get_mode() == gtk.SELECTION_NONE:
 
1526
            raise TypeError("Selection not allowed")
 
1527
 
 
1528
        objid = id(instance)
 
1529
        if not objid in self._iters:
 
1530
            raise ValueError("instance %s is not in the list" % repr(instance))
 
1531
 
 
1532
        treeiter = self._iters[objid]
 
1533
 
 
1534
        selection.select_iter(treeiter)
 
1535
 
 
1536
        if scroll:
 
1537
            self._treeview.scroll_to_cell(self._model[treeiter].path,
 
1538
                                          None, True, 0.5, 0)
 
1539
 
 
1540
    def get_selected(self):
 
1541
        """Returns the currently selected object
 
1542
        If an object is not selected, None is returned
 
1543
        """
 
1544
        selection = self._treeview.get_selection()
 
1545
        if not selection:
 
1546
            # AssertionError ?
 
1547
            return
 
1548
 
 
1549
        mode = selection.get_mode()
 
1550
        if mode == gtk.SELECTION_NONE:
 
1551
            raise TypeError("Selection not allowed in %r mode" % mode)
 
1552
        elif mode not in (gtk.SELECTION_SINGLE, gtk.SELECTION_BROWSE):
 
1553
            log.warn('get_selected() called when multiple rows '
 
1554
                     'can be selected')
 
1555
 
 
1556
        model, treeiter = selection.get_selected()
 
1557
        if treeiter:
 
1558
            return model[treeiter][COL_MODEL]
 
1559
 
 
1560
    def get_selected_rows(self):
 
1561
        """Returns a list of currently selected objects
 
1562
        If no objects are selected an empty list is returned
 
1563
        """
 
1564
        selection = self._treeview.get_selection()
 
1565
        if not selection:
 
1566
            # AssertionError ?
 
1567
            return
 
1568
 
 
1569
        mode = selection.get_mode()
 
1570
        if mode == gtk.SELECTION_NONE:
 
1571
            raise TypeError("Selection not allowed in %r mode" % mode)
 
1572
        elif mode in (gtk.SELECTION_SINGLE, gtk.SELECTION_BROWSE):
 
1573
            log.warn('get_selected_rows() called when only a single row '
 
1574
                     'can be selected')
 
1575
 
 
1576
        model, paths = selection.get_selected_rows()
 
1577
        if paths:
 
1578
            return [model[path][COL_MODEL] for (path,) in paths]
 
1579
        return []
 
1580
 
 
1581
    def add_list(self, instances, clear=True):
 
1582
        """
 
1583
        Allows a list to be loaded, by default clearing it first.
 
1584
        freeze() and thaw() are called internally to avoid flashing.
 
1585
 
 
1586
        @param instances: a list to be added
 
1587
        @param clear: a boolean that specifies whether or not to
 
1588
          clear the list
 
1589
        """
 
1590
 
 
1591
        self._treeview.freeze_notify()
 
1592
 
 
1593
        ret = self._load(instances, clear)
 
1594
 
 
1595
        self._treeview.thaw_notify()
 
1596
 
 
1597
        return ret
 
1598
 
 
1599
    def clear(self):
 
1600
        """Removes all the instances of the list"""
 
1601
        self._model.clear()
 
1602
        self._iters = {}
 
1603
 
 
1604
        # Don't clear the whole cache, just the
 
1605
        # individual column caches
 
1606
        for key in self._cell_data_caches:
 
1607
            self._cell_data_caches[key] = {}
 
1608
 
 
1609
    def get_next(self, instance):
 
1610
        """
 
1611
        Returns the item after instance in the list.
 
1612
        Note that the instance must be inserted before this can be called
 
1613
        If there are no instances after,  the first item will be returned.
 
1614
 
 
1615
        @param instance: the instance
 
1616
        """
 
1617
 
 
1618
        objid = id(instance)
 
1619
        if not objid in self._iters:
 
1620
            raise ValueError("instance %r is not in the list" % instance)
 
1621
 
 
1622
        treeiter = self._iters[objid]
 
1623
 
 
1624
        model = self._model
 
1625
        pos = model[treeiter].path[0]
 
1626
        if pos >= len(model) - 1:
 
1627
            pos = 0
 
1628
        else:
 
1629
            pos += 1
 
1630
        return model[pos][COL_MODEL]
 
1631
 
 
1632
    def get_previous(self, instance):
 
1633
        """
 
1634
        Returns the item before instance in the list.
 
1635
        Note that the instance must be inserted before this can be called
 
1636
        If there are no instances before,  the last item will be returned.
 
1637
 
 
1638
        @param instance: the instance
 
1639
        """
 
1640
 
 
1641
        objid = id(instance)
 
1642
        if not objid in self._iters:
 
1643
            raise ValueError("instance %r is not in the list" % instance)
 
1644
        treeiter = self._iters[objid]
 
1645
 
 
1646
        model = self._model
 
1647
        pos = model[treeiter].path[0]
 
1648
        if pos == 0:
 
1649
            pos = len(model) - 1
 
1650
        else:
 
1651
            pos -= 1
 
1652
        return model[pos][COL_MODEL]
 
1653
 
 
1654
    def get_selected_row_number(self):
 
1655
        """
 
1656
        @returns: the selected row number or None if no rows were selected
 
1657
        """
 
1658
        selection = self._treeview.get_selection()
 
1659
        if selection.get_mode() == gtk.SELECTION_MULTIPLE:
 
1660
            model, paths = selection.get_selected_rows()
 
1661
            if paths:
 
1662
                return paths[0][0]
 
1663
        else:
 
1664
            model, iter = selection.get_selected()
 
1665
            if iter:
 
1666
                return model[iter].path[0]
 
1667
 
 
1668
    def double_click(self, rowno):
 
1669
        """
 
1670
        Same as double clicking on the row rowno
 
1671
 
 
1672
        @param rowno: integer
 
1673
        """
 
1674
        columns = self._treeview.get_columns()
 
1675
        if not columns:
 
1676
            raise AssertionError(
 
1677
                "%s has no columns" % self.get_name())
 
1678
 
 
1679
        self._treeview.row_activated(rowno, columns[0])
 
1680
 
 
1681
    def set_headers_visible(self, value):
 
1682
        """
 
1683
        @param value: if true, shows the headers, if false hide then
 
1684
        """
 
1685
        self._treeview.set_headers_visible(value)
 
1686
 
 
1687
    def set_visible_rows(self, rows):
 
1688
        """
 
1689
        Sets the number of visible rows of the treeview. This is useful to use
 
1690
        instead of set_size_request() directly, since you can avoid using raw
 
1691
        pixel sizes.
 
1692
        @param rows: number of rows to show
 
1693
        """
 
1694
 
 
1695
        treeview = self._treeview
 
1696
        if treeview.get_headers_visible():
 
1697
            treeview.realize()
 
1698
            header_h = self._get_header_height()
 
1699
        else:
 
1700
            header_h = 0
 
1701
 
 
1702
        column = treeview.get_columns()[0]
 
1703
        h = column.cell_get_size()[-1]
 
1704
 
 
1705
        focus_padding = treeview.style_get_property('focus-line-width') * 2
 
1706
        treeview.set_size_request(-1, header_h + (rows * (h + focus_padding)))
 
1707
 
 
1708
type_register(ObjectList)
 
1709
 
 
1710
class ObjectTree(ObjectList):
 
1711
    """
 
1712
    Signals
 
1713
    =======
 
1714
      - B{row-expanded} (list, object):
 
1715
        - Emitted when a row is "expanded", eg the littler arrow to the left
 
1716
          is opened. See the GtkTreeView documentation for more information.
 
1717
    """
 
1718
    __gtype_name__ = 'ObjectTree'
 
1719
 
 
1720
    gsignal('row-expanded', object)
 
1721
 
 
1722
    def __init__(self, columns=[], objects=None, mode=gtk.SELECTION_BROWSE,
 
1723
                 sortable=False, model=None):
 
1724
        if not model:
 
1725
            model = gtk.TreeStore(object)
 
1726
        ObjectList.__init__(self, columns, objects, mode, sortable, model)
 
1727
        self.get_treeview().connect('row-expanded', self._on_treeview__row_expanded)
 
1728
 
 
1729
    def _append_internal(self, parent, instance, select, prepend):
 
1730
        iters = self._iters
 
1731
        parent_id = id(parent)
 
1732
        if parent_id in iters:
 
1733
            parent_iter = iters[parent_id]
 
1734
        elif parent is None:
 
1735
            parent_iter = None
 
1736
        else:
 
1737
            raise TypeError("parent must be an Object, ObjectRow or None")
 
1738
 
 
1739
        # Freeze and save original selection mode to avoid blinking
 
1740
        self._treeview.freeze_notify()
 
1741
 
 
1742
        if prepend:
 
1743
            row_iter = self._model.prepend(parent_iter, (instance,))
 
1744
        else:
 
1745
            row_iter = self._model.append(parent_iter, (instance,))
 
1746
 
 
1747
        self._iters[id(instance)] = row_iter
 
1748
 
 
1749
        if self._autosize:
 
1750
            self._treeview.columns_autosize()
 
1751
 
 
1752
        if select:
 
1753
            self._select_and_focus_row(row_iter)
 
1754
        self._treeview.thaw_notify()
 
1755
 
 
1756
        return instance
 
1757
 
 
1758
    def append(self, parent, instance, select=False):
 
1759
        """
 
1760
        @param parent: Object or None, representing the parent
 
1761
        @param instance: the instance to be added
 
1762
        @param select: select the row
 
1763
        @returns: the appended object
 
1764
        """
 
1765
        return self._append_internal(parent, instance, select, prepend=False)
 
1766
 
 
1767
    def prepend(self, parent, instance, select=False):
 
1768
        """
 
1769
        @param parent: Object or None, representing the parent
 
1770
        @param instance: the instance to be added
 
1771
        @param select: select the row
 
1772
        @returns: the prepended object
 
1773
        """
 
1774
        return self._append_internal(parent, instance, select, prepend=True)
 
1775
 
 
1776
    def expand(self, instance, open_all=True):
 
1777
        """
 
1778
        This method opens the row specified by path so its children
 
1779
        are visible.
 
1780
        @param instance: an instance to expand at
 
1781
        @param open_all: If True, expand all rows, otherwise just the
 
1782
        immediate children
 
1783
        """
 
1784
        objid = id(instance)
 
1785
        if not objid in self._iters:
 
1786
            raise ValueError("instance %r is not in the list" % instance)
 
1787
        treeiter = self._iters[objid]
 
1788
 
 
1789
        self.get_treeview().expand_row(
 
1790
            self._model[treeiter].path, open_all)
 
1791
 
 
1792
    def collapse(self, instance):
 
1793
        """
 
1794
        This method collapses the row specified by path
 
1795
        (hides its child rows, if they exist).
 
1796
        @param instance: an instance to collapse
 
1797
        """
 
1798
        objid = id(instance)
 
1799
        if not objid in self._iters:
 
1800
            raise ValueError("instance %r is not in the list" % instance)
 
1801
        treeiter = self._iters[objid]
 
1802
 
 
1803
        self.get_treeview().collapse_row(
 
1804
            self._model[treeiter].path)
 
1805
 
 
1806
    def _on_treeview__row_expanded(self, treeview, treeiter, treepath):
 
1807
        self.emit('row-expanded', self.get_model()[treeiter][COL_MODEL])
 
1808
 
 
1809
type_register(ObjectTree)
 
1810
 
 
1811
class ListLabel(gtk.HBox):
 
1812
    """I am a subclass of a GtkHBox which you can use if you want
 
1813
    to vertically align a label with a column
 
1814
    """
 
1815
 
 
1816
    def __init__(self, klist, column, label='', value_format='%s',
 
1817
                 font_desc=None):
 
1818
        """
 
1819
        @param klist:        list to follow
 
1820
        @type klist:         kiwi.ui.objectlist.ObjectList
 
1821
        @param column:       name of a column in a klist
 
1822
        @type column:        string
 
1823
        @param label:        label
 
1824
        @type label:         string
 
1825
        @param value_format: format string used to format value
 
1826
        @type value_format:  string
 
1827
        """
 
1828
        self._label = label
 
1829
        self._label_width = -1
 
1830
        if not isinstance(klist, ObjectList):
 
1831
            raise TypeError("list must be a kiwi list and not %r" %
 
1832
                            type(klist).__name__)
 
1833
        self._klist = klist
 
1834
        if not isinstance(column, str):
 
1835
            raise TypeError("column must be a string and not %r" %
 
1836
                            type(column).__name__)
 
1837
        self._column = klist.get_column_by_name(column)
 
1838
        self._value_format = value_format
 
1839
 
 
1840
        gtk.HBox.__init__(self)
 
1841
 
 
1842
        self._create_ui()
 
1843
 
 
1844
        if font_desc:
 
1845
            self._value_widget.modify_font(font_desc)
 
1846
            self._label_widget.modify_font(font_desc)
 
1847
 
 
1848
    # Public API
 
1849
 
 
1850
    def set_value(self, value):
 
1851
        """Sets the value of the label.
 
1852
        Note that it needs to be of the same type as you specified in
 
1853
        value_format in the constructor.
 
1854
        I also support the GMarkup syntax, so you can use "<b>%d</b>" if
 
1855
        you want."""
 
1856
        self._value_widget.set_markup(self._value_format % value)
 
1857
 
 
1858
    def get_value_widget(self):
 
1859
        return self._value_widget
 
1860
 
 
1861
    def get_label_widget(self):
 
1862
        return self._label_widget
 
1863
 
 
1864
    # Private
 
1865
 
 
1866
    def _create_ui(self):
 
1867
 
 
1868
        # When tracking the position/size of a column, we need to pay
 
1869
        # attention to the following two things:
 
1870
        # * treeview_column::width
 
1871
        # * size-allocate of treeview_columns header widget
 
1872
        #
 
1873
        tree_column = self._klist.get_treeview_column(self._column)
 
1874
        tree_column.connect('notify::width',
 
1875
                            self._on_treeview_column__notify_width)
 
1876
 
 
1877
        button = self._klist._get_column_button(tree_column)
 
1878
        button.connect('size-allocate',
 
1879
                       self._on_treeview_column_button__size_allocate)
 
1880
 
 
1881
        self._label_widget = gtk.Label()
 
1882
        self._label_widget.set_markup(self._label)
 
1883
 
 
1884
        layout = self._label_widget.get_layout()
 
1885
        self._label_width = layout.get_pixel_size()[0]
 
1886
        self._label_widget.set_alignment(1.0, 0.5)
 
1887
        self.pack_start(self._label_widget, False, False, padding=6)
 
1888
        self._label_widget.show()
 
1889
 
 
1890
        self._value_widget = gtk.Label()
 
1891
        xalign = tree_column.get_property('alignment')
 
1892
        self._value_widget.set_alignment(xalign, 0.5)
 
1893
        self.pack_start(self._value_widget, False, False)
 
1894
        self._value_widget.show()
 
1895
 
 
1896
    def _resize(self, position=-1, width=-1):
 
1897
        if position != -1:
 
1898
            if position != 0:
 
1899
                if self._label_width > position:
 
1900
                    self._label_widget.set_text('')
 
1901
                else:
 
1902
                    self._label_widget.set_markup(self._label)
 
1903
 
 
1904
            # XXX: Replace 12 with a constant
 
1905
            if position >= 12:
 
1906
                self._label_widget.set_size_request(position - 12, -1)
 
1907
 
 
1908
        if width != -1:
 
1909
            self._value_widget.set_size_request(width, -1)
 
1910
 
 
1911
    # Callbacks
 
1912
 
 
1913
    def _on_treeview_column_button__size_allocate(self, label, rect):
 
1914
        self._resize(position=rect[0])
 
1915
 
 
1916
    def _on_treeview_column__notify_width(self, treeview, pspec):
 
1917
        value = treeview.get_property(pspec.name)
 
1918
        self._resize(width=value)
 
1919
 
 
1920
    def _on_list__size_allocate(self, list, rect):
 
1921
        self._resize(position=rect[0], width=rect[2])
 
1922
 
 
1923
 
 
1924
class SummaryLabel(ListLabel):
 
1925
    """I am a subclass of ListLabel which you can use if you want
 
1926
    to summarize all the values of a specific column.
 
1927
    Please note that I only know how to handle number column
 
1928
    data types and I will complain if you give me something else."""
 
1929
 
 
1930
    def __init__(self, klist, column, label=_('Total:'), value_format='%s',
 
1931
                 font_desc=None):
 
1932
        ListLabel.__init__(self, klist, column, label, value_format, font_desc)
 
1933
        if not issubclass(self._column.data_type, number):
 
1934
            raise TypeError("data_type of column must be a number, not %r",
 
1935
                            self._column.data_type)
 
1936
        klist.connect('cell-edited', self._on_klist__cell_edited)
 
1937
        self.update_total()
 
1938
 
 
1939
    # Public API
 
1940
 
 
1941
    def update_total(self):
 
1942
        """Recalculate the total value of all columns"""
 
1943
        column = self._column
 
1944
        attr = column.attribute
 
1945
        get_attribute = column.get_attribute
 
1946
 
 
1947
        value = sum([get_attribute(obj, attr, 0) for obj in self._klist],
 
1948
                    column.data_type('0'))
 
1949
 
 
1950
        self.set_value(column.as_string(value))
 
1951
 
 
1952
    # Callbacks
 
1953
 
 
1954
    def _on_klist__cell_edited(self, klist, object, attribute):
 
1955
        self.update_total()