2
# Kiwi: a Framework and Enhanced Widgets for Python
4
# Copyright (C) 2001-2007 Async Open Source
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.
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.
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
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>
27
"""High level wrapper for GtkTreeView"""
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
45
_ = lambda m: gettext.dgettext('kiwi', m)
47
log = Logger('objectlist')
49
str2type = converter.str_to_type
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):
57
def str2bool(value, from_string=converter.from_string):
58
"converts a boolean to a enum"
59
return from_string(bool, value)
61
class Column(PropertyObject, gobject.GObject):
63
Specifies a column for an L{ObjectList}, see the ObjectList documentation
68
- B{title}: string I{mandatory}
69
- the title of the column, defaulting to the capitalized form of
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
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
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)
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
169
# This is called after the renderer property is set, to allow
170
# us to set custom rendering properties
173
# This is called when the renderer is created, so we can set/fetch
175
on_attach_renderer = None
177
def __init__(self, attribute='', title=None, data_type=None, **kwargs):
179
Creates a new Column, which describes how a column in a
180
ObjectList should be rendered.
182
@param attribute: a string with the name of the instance attribute the
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
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.
195
# XXX: filter function?
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)
202
self.from_string = None
204
kwargs['attribute'] = attribute
205
kwargs['title'] = title or attribute.capitalize()
208
kwargs['data_type'] = data_type
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:
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
220
format_func = kwargs.get('format_func')
222
if not callable(format_func):
223
raise TypeError("format_func must be callable")
224
if 'format' in kwargs:
226
"format and format_func can not be used at the same time")
228
# editable_attribute always turns on editable
229
if 'editable_attribute' in kwargs:
230
if not kwargs.get('editable', True):
232
"editable cannot be disabled when using editable_attribute")
233
kwargs['editable'] = True
235
PropertyObject.__init__(self, **kwargs)
236
gobject.GObject.__init__(self, attribute=attribute)
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)
242
def prop_set_data_type(self, data):
244
conv = converter.get_converter(data)
245
self.compare = conv.get_compare_function()
246
self.from_string = conv.from_string
250
namespace = self.__dict__.copy()
251
return "<%s: %s>" % (self.__class__.__name__, namespace)
253
def as_string(self, data):
254
data_type = self.data_type
257
elif self.format_func:
258
text = self.format_func(data)
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)
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
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)
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
291
row = model[treeiter]
293
sequence_id = len(model) - row.path[0]
295
sequence_id = row.path[0] + 1
297
row[COL_MODEL]._kiwi_sequence_id = sequence_id
300
renderer.set_property(renderer_prop, sequence_id)
302
raise TypeError("%r does not support parameter %s" %
303
(renderer, renderer_prop))
305
class ColoredColumn(Column):
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
311
Example, to colorize negative values to red:
313
>>> def colorize(value):
316
... ColoredColumn('age', data_type=int, color='red',
317
... data_func=colorize),
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")
327
self._color = gdk.color_parse(color)
328
self._color_normal = None
330
self._data_func = data_func
332
Column.__init__(self, attribute, title, data_type, **kwargs)
334
def on_attach_renderer(self, renderer):
335
renderer.set_property('foreground-set', True)
336
self._color_normal = renderer.get_property('foreground-gdk')
338
def renderer_func(self, renderer, data):
339
if self._data_func(data):
342
color = self._color_normal
344
renderer.set_property('foreground-gdk', color)
346
class _ContextMenu(gtk.Menu):
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.
354
def __init__(self, treeview):
355
gtk.Menu.__init__(self)
358
self._signal_ids = []
359
self._treeview = treeview
360
self._treeview.connect('columns-changed',
361
self._on_treeview__columns_changed)
365
for child in self.get_children():
368
for menuitem, signal_id in self._signal_ids:
369
menuitem.disconnect(signal_id)
370
self._signal_ids = []
372
def popup(self, event):
374
gtk.Menu.popup(self, None, None, None,
375
event.button, event.time)
383
for column in self._treeview.get_columns():
384
header_widget = column.get_widget()
385
if not header_widget:
387
title = header_widget.get_text()
389
menuitem = gtk.CheckMenuItem(title)
390
menuitem.set_active(column.get_visible())
391
signal_id = menuitem.connect("activate",
392
self._on_menuitem__activate,
394
self._signal_ids.append((menuitem, signal_id))
396
self.append(menuitem)
400
def _on_treeview__columns_changed(self, treeview):
403
def _on_menuitem__activate(self, menuitem, column):
404
active = menuitem.get_active()
405
column.set_visible(active)
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
411
model = self._treeview.get_model()
413
model.row_changed(row.path, row.iter)
415
children = self.get_children()
417
# Make sure all items are selectable
418
for child in children:
419
child.set_sensitive(True)
421
# Protect so we can't hide all the menu items
422
# If there's only one menuitem less to select, set
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)
433
class ObjectList(PropertyObject, gtk.ScrolledWindow):
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.
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}.
448
>>> apple.name = 'Apple'
449
>>> apple.description = 'Worm house'
452
>>> banana.name = 'Banana'
453
>>> banana.description = 'Monkey food'
455
>>> fruits = ObjectList([Column('name'),
456
>>> Column('description')])
457
>>> fruits.append(apple)
458
>>> fruits.append(banana)
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
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
482
- B{selection-mode}: gtk.SelectionMode I{gtk.SELECTION_BROWSE}
483
- Represents the selection-mode of a GtkTreeSelection of a GtkTreeView.
486
__gtype_name__ = 'ObjectList'
489
gsignal('row-activated', object)
492
gsignal('selection-changed', object)
495
gsignal('double-click', object)
498
gsignal('right-click', object, gtk.gdk.Event)
501
gsignal('middle-click', object, gtk.gdk.Event)
503
# edited object, attribute name
504
gsignal('cell-edited', object, str)
506
# emitted when empty or non-empty status changes
507
gsignal('has-rows', bool)
509
gproperty('selection-mode', gtk.SelectionMode,
510
default=gtk.SELECTION_BROWSE, nick="SelectionMode")
512
def __init__(self, columns=None,
514
mode=gtk.SELECTION_BROWSE,
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
526
# allow to specify only one column
527
if isinstance(columns, Column):
529
elif not isinstance(columns, list):
530
raise TypeError("columns must be a list or a Column")
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")
539
self._sortable = sortable
542
# Mapping of instance id -> treeiter
544
self._cell_data_caches = {}
545
self._autosize = True
546
self._vscrollbar = None
548
gtk.ScrolledWindow.__init__(self)
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
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())
562
model = gtk.ListStore(object)
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)
575
# these tooltips are used for the columns
576
self._tooltips = gtk.Tooltips()
578
# create a popup menu for showing or hiding columns
579
self._popup = _ContextMenu(self._treeview)
581
# when setting the column definition the columns are created
582
self.set_columns(columns)
585
self.add_list(objects, clear=True)
587
# Set selection mode last to avoid spurious events
588
selection = self._treeview.get_selection()
589
selection.connect("changed", self._on_selection__changed)
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)
595
# Depends on treeview and selection being set up
596
PropertyObject.__init__(self)
598
self.set_selection_mode(mode)
600
# Python list object implementation
601
# These methods makes the kiwi list behave more or less
602
# like a normal python list
606
# __add__, __eq__, __ge__, __gt__, __iadd__,
607
# __imul__, __le__, __lt__, __mul__, __ne__,
611
# __delitem__, __hash__, __reduce__, __reduce_ex__
616
return len(self._model)
618
def __nonzero__(self):
622
def __contains__(self, instance):
624
return bool(self._iters.get(id(instance), False))
635
def next(self, model=self._model):
638
return model[self._index][COL_MODEL]
642
return ModelIterator()
644
def __getitem__(self, arg):
646
if isinstance(arg, (int, gtk.TreeIter, str)):
647
item = self._model[arg][COL_MODEL]
648
elif isinstance(arg, slice):
650
return [model[item][COL_MODEL]
651
for item in slicerange(arg, len(self._model))]
653
raise TypeError("argument arg must be int, gtk.Treeiter or "
654
"slice, not %s" % type(arg))
657
def __setitem__(self, arg, item):
659
if isinstance(arg, (int, gtk.TreeIter, str)):
661
olditem = model[arg][COL_MODEL]
664
# Update iterator cache
666
iters[id(item)] = model[arg].iter
667
del iters[id(olditem)]
669
elif isinstance(arg, slice):
670
raise NotImplementedError("slices for list are not implemented")
672
raise TypeError("argument arg must be int or gtk.Treeiter,"
673
" not %s" % type(arg))
675
# append and remove are below
677
def extend(self, iterable):
679
Extend list by appending elements from the iterable
684
return self.add_list(iterable, clear=False)
686
def index(self, item, start=None, stop=None):
688
Return first index of value
695
if start is not None or stop is not None:
696
raise NotImplementedError("start and stop")
698
treeiter = self._iters.get(id(item), _marker)
699
if treeiter is _marker:
700
raise ValueError("item %r is not in the list" % item)
702
return self._model[treeiter].path[0]
704
def count(self, item):
705
"L.count(item) -> integer -- return number of occurrences of value"
708
for row in self._model:
709
if row[COL_MODEL] == item:
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.
719
self._treeview.freeze_notify()
721
row_iter = self._model.insert(index, (instance,))
722
self._iters[id(instance)] = row_iter
725
self._treeview.columns_autosize()
728
self._select_and_focus_row(row_iter)
729
self._treeview.thaw_notify()
731
def pop(self, index):
733
Remove and return item at index (default last)
736
raise NotImplementedError
738
def reverse(self, pos, item):
739
"L.reverse() -- reverse *IN PLACE*"
740
raise NotImplementedError
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
747
def sort_by_attribute(self, attribute, order=gtk.SORT_ASCENDING):
749
Sort by an attribute in the object model.
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
756
def _sort_func(model, iter1, iter2):
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)
766
def prop_set_selection_mode(self, mode):
767
self.set_selection_mode(mode)
769
def prop_get_selection_mode(self):
770
return self.get_selection_mode()
774
def _load(self, instances, clear):
775
# do nothing if empty list or None provided
786
old_instances = [row[COL_MODEL] for row in model]
789
selected_instances = []
791
selection = self._treeview.get_selection()
792
_, paths = selection.get_selected_rows()
794
selected_instances = [model[path][COL_MODEL]
795
for (path,) in paths]
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
803
for instance in iter(instances):
805
# If the instance is not in the list insert it after
806
# the previous inserted object
807
if not objid in iters:
809
prev = model.append((instance,))
811
prev = model.insert_after(prev, (instance,))
816
# Optimization when we were empty, we wont need to remove anything
817
# nor restore selection
820
objids = [id(instance) for instance in instances]
821
for instance in old_instances:
827
for instance in iter(instances):
828
iters[id(instance)] = model.append((instance,))
831
for instance in selected_instances:
834
selection.select_iter(iters[objid])
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
840
self._treeview.columns_autosize()
841
self._autosize = False
843
def _setup_columns(self, columns):
847
for column in columns:
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
857
self._sortable = self._sortable or sorted
859
for column in columns:
860
self._setup_column(column)
863
column = gtk.TreeViewColumn()
864
self._treeview.append_column(column)
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")
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)
877
self._model.set_sort_func(index,
878
self._model_sort_func,
879
(column, column.attribute))
880
treeview_column.set_sort_column_id(index)
883
self._model.set_sort_column_id(index, column.order)
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:
891
elif justify == gtk.JUSTIFY_CENTER:
893
elif justify in (gtk.JUSTIFY_LEFT,
898
renderer.set_property("xalign", xalign)
899
treeview_column.set_property("alignment", xalign)
902
renderer.set_property('ellipsize', column.ellipsize)
904
renderer.set_property('font-desc',
905
pango.FontDescription(column.font_desc))
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
911
cell_data_func = self._cell_data_text_func
913
if column.cell_data_func:
914
cell_data_func = column.cell_data_func
916
self._cell_data_caches[column.attribute] = {}
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)
924
treeview_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
925
treeview_column.set_fixed_width(column.width)
927
widget = self._get_column_button(treeview_column)
928
if widget is not None:
929
self._tooltips.set_tip(widget, column.tooltip)
933
treeview_column.set_expand(True)
936
treeview_column.set_sort_indicator(True)
939
self._autosize = False
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,
950
if not issubclass(column.data_type, bool):
951
raise TypeError("You can only use radio for boolean columns")
954
self._treeview.set_expander_column(treeview_column)
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)
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
968
label = gtk.Label(column.title)
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)
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
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,
988
# TODO: Move to column
989
data_type = column.data_type
990
if data_type is bool:
991
renderer = gtk.CellRendererToggle()
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.
998
renderer.set_radio(True)
999
cb = self._on_renderer_toggle_radio__toggled
1001
cb = self._on_renderer_toggle_check__toggled
1002
renderer.connect('toggled', cb, self._model, column.attribute)
1004
elif column.use_stock or data_type == gdk.Pixbuf:
1005
renderer = gtk.CellRendererPixbuf()
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")
1013
model = gtk.ListStore(str, object)
1014
items = data_type.names.items()
1016
for key, value in items:
1017
model.append((key.lower().capitalize(), value))
1019
renderer = gtk.CellRendererCombo()
1020
renderer.set_property('model', model)
1021
renderer.set_property('text-column', 0)
1022
renderer.set_property('has-entry', True)
1024
renderer.set_property('editable', True)
1025
renderer.connect('edited', self._on_renderer_combo__edited,
1026
self._model, column.attribute, column)
1028
elif issubclass(data_type, (datetime.date, datetime.time,
1031
renderer = gtk.CellRendererText()
1032
if column.use_markup:
1037
renderer.set_property('editable', True)
1038
renderer.connect('edited', self._on_renderer_text__edited,
1039
self._model, column.attribute, column,
1043
raise ValueError("the type %s is not supported yet" % data_type)
1045
return renderer, prop
1048
def _select_and_focus_row(self, row_iter):
1049
self._treeview.set_cursor(self._model[row_iter].path)
1051
# handlers & callbacks
1054
def _on_model__row_inserted(self, model, path, iter):
1056
self.emit('has-rows', True)
1058
def _on_model__row_deleted(self, model, path):
1060
self.emit('has-rows', False)
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))
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()
1077
raise AssertionError
1078
self.emit('selection-changed', item)
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)
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.
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,
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
1099
winx, winy = gdk_window.get_origin()
1100
self._popup_window.move(winx + old_alloc.x,
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()):
1112
def _on_treeview_header__button_release_event(self, button, event):
1113
if event.button == 3:
1114
self._popup.popup(event)
1118
def _after_treeview__row_activated(self, treeview, path, view_column):
1119
"After activated (double clicked or pressed enter) on a row"
1121
row = self._model[path]
1123
print 'path %s was not found in model: %s' % (
1124
path, map(list, self._model))
1126
item = row[COL_MODEL]
1127
self.emit('row-activated', item)
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:
1137
item = self.get_selected()
1140
def _emit_button_press_signal(self, signal_name, event):
1141
item = self._get_selection_or_selected_rows()
1143
self.emit(signal_name, item, event)
1145
def _on_treeview__button_press_event(self, treeview, event):
1146
"Generic button-press-event handler to be able to catch double clicks"
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'
1156
gobject.idle_add(self._emit_button_press_signal, signal_name,
1159
elif event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1:
1160
item = self._get_selection_or_selected_rows()
1162
self.emit('double-click', item)
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)
1178
raise AssertionError
1181
cache = self._cell_data_caches[column.attribute]
1186
data = column.get_attribute(row[COL_MODEL],
1187
column.attribute, None)
1190
data = column.get_attribute(row[COL_MODEL],
1191
column.attribute, None)
1193
text = column.as_string(data)
1195
renderer.set_property(renderer_prop, text)
1197
if column.renderer_func:
1198
column.renderer_func(renderer, data)
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)
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())
1218
def _on_renderer__toggled(self, renderer, path, column):
1219
setattr(self._model[path][COL_MODEL], column.attribute,
1220
not renderer.get_active())
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)
1228
def _on_renderer_toggle_radio__toggled(self, renderer, path, model, attr):
1230
old = renderer.get_data('kiwilist::radio-active')
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
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)
1245
setattr(old, attr, False)
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)
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)
1261
def _on_renderer_combo__edited(self, renderer, path, text,
1262
model, attr, column):
1263
obj = model[path][COL_MODEL]
1267
value_model = renderer.get_property('model')
1268
for row in value_model:
1273
raise AssertionError
1274
setattr(obj, attr, value)
1275
self.emit('cell-edited', obj, attr)
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)
1282
# XXX convert new_text to the proper data type
1283
setattr(self._model[path][COL_MODEL], column.attribute, value)
1286
def _get_column_button(self, column):
1287
"""Return the button widget of a particular TreeViewColumn.
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, ...
1292
Use this function at your own risk
1295
button = column.get_widget()
1296
assert button is not None, ("You must call column.set_widget() "
1297
"before calling _get_column_button")
1299
while not isinstance(button, gtk.Button):
1300
button = button.get_parent()
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
1308
Internally it uses a POPUP window so you can tell how *Evil* is this.
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()
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)
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
1324
if isinstance(widget, gtk.VScrollbar):
1325
self._vscrollbar = widget
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()
1333
# end of the popup button hack
1338
def get_model(self):
1339
"Return treemodel of the current list"
1342
def get_treeview(self):
1343
"Return treeview of the current list"
1344
return self._treeview
1346
def get_columns(self):
1347
return self._columns
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:
1355
raise LookupError("There is no column called %s" % name)
1357
def get_treeview_column(self, column):
1359
@param column: a @Column
1361
if not isinstance(column, Column):
1364
if not column in self._columns:
1367
index = self._columns.index(column)
1368
tree_columns = self._treeview.get_columns()
1369
return tree_columns[index]
1371
def grab_focus(self):
1373
Grabs the focus of the ObjectList
1375
self._treeview.grab_focus()
1377
def _clear_columns(self):
1378
# Reset the sort function for all model columns
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)
1384
# Remove all columns
1385
treeview = self._treeview
1386
while treeview.get_columns():
1387
treeview.remove_column(treeview.get_column(0))
1392
def set_columns(self, columns):
1394
@param columns: a sequence of L{Column} objects.
1397
if not isinstance(columns, (list, tuple)):
1398
raise ValueError("columns must be a list or a tuple")
1400
self._clear_columns()
1401
self._columns = columns
1402
self._setup_columns(columns)
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.
1410
# Freeze and save original selection mode to avoid blinking
1411
self._treeview.freeze_notify()
1413
row_iter = self._model.append((instance,))
1414
self._iters[id(instance)] = row_iter
1417
self._treeview.columns_autosize()
1420
self._select_and_focus_row(row_iter)
1421
self._treeview.thaw_notify()
1423
def _remove(self, objid):
1424
# linear search for the instance to remove
1425
treeiter = self._iters.pop(objid)
1429
# Remove any references to this path
1430
self._clear_cache_for_iter(treeiter)
1432
# All references to the iter gone, now it can be removed
1433
self._model.remove(treeiter)
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():
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
1450
@param select: if true, the previous item will be selected
1454
objid = id(instance)
1455
if not objid in self._iters:
1456
raise ValueError("instance %r is not in the list" % instance)
1460
prev = self.get_previous(instance)
1461
rv = self._remove(objid)
1462
if prev != instance:
1465
rv = self._remove(objid)
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)
1476
def refresh(self, view_only=False):
1478
Reloads the values from all objects.
1480
@param view_only: if True, only force a refresh of the
1481
visible part of this objectlist's Treeview.
1484
self._treeview.queue_draw()
1486
self._model.foreach(gtk.TreeModel.row_changed)
1488
def set_column_visibility(self, column_index, visibility):
1489
treeview_column = self._treeview.get_column(column_index)
1490
treeview_column.set_visible(visibility)
1492
def get_selection_mode(self):
1493
selection = self._treeview.get_selection()
1495
return selection.get_mode()
1497
def set_selection_mode(self, mode):
1498
selection = self._treeview.get_selection()
1500
self.notify('selection-mode')
1501
return selection.set_mode(mode)
1503
def unselect_all(self):
1504
selection = self._treeview.get_selection()
1506
selection.unselect_all()
1508
def select_paths(self, paths):
1510
Selects a number of rows corresponding to paths
1512
@param paths: rows to be selected
1515
selection = self._treeview.get_selection()
1516
if selection.get_mode() == gtk.SELECTION_NONE:
1517
raise TypeError("Selection not allowed")
1519
selection.unselect_all()
1521
selection.select_path(path)
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")
1528
objid = id(instance)
1529
if not objid in self._iters:
1530
raise ValueError("instance %s is not in the list" % repr(instance))
1532
treeiter = self._iters[objid]
1534
selection.select_iter(treeiter)
1537
self._treeview.scroll_to_cell(self._model[treeiter].path,
1540
def get_selected(self):
1541
"""Returns the currently selected object
1542
If an object is not selected, None is returned
1544
selection = self._treeview.get_selection()
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 '
1556
model, treeiter = selection.get_selected()
1558
return model[treeiter][COL_MODEL]
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
1564
selection = self._treeview.get_selection()
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 '
1576
model, paths = selection.get_selected_rows()
1578
return [model[path][COL_MODEL] for (path,) in paths]
1581
def add_list(self, instances, clear=True):
1583
Allows a list to be loaded, by default clearing it first.
1584
freeze() and thaw() are called internally to avoid flashing.
1586
@param instances: a list to be added
1587
@param clear: a boolean that specifies whether or not to
1591
self._treeview.freeze_notify()
1593
ret = self._load(instances, clear)
1595
self._treeview.thaw_notify()
1600
"""Removes all the instances of the list"""
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] = {}
1609
def get_next(self, instance):
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.
1615
@param instance: the instance
1618
objid = id(instance)
1619
if not objid in self._iters:
1620
raise ValueError("instance %r is not in the list" % instance)
1622
treeiter = self._iters[objid]
1625
pos = model[treeiter].path[0]
1626
if pos >= len(model) - 1:
1630
return model[pos][COL_MODEL]
1632
def get_previous(self, instance):
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.
1638
@param instance: the instance
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]
1647
pos = model[treeiter].path[0]
1649
pos = len(model) - 1
1652
return model[pos][COL_MODEL]
1654
def get_selected_row_number(self):
1656
@returns: the selected row number or None if no rows were selected
1658
selection = self._treeview.get_selection()
1659
if selection.get_mode() == gtk.SELECTION_MULTIPLE:
1660
model, paths = selection.get_selected_rows()
1664
model, iter = selection.get_selected()
1666
return model[iter].path[0]
1668
def double_click(self, rowno):
1670
Same as double clicking on the row rowno
1672
@param rowno: integer
1674
columns = self._treeview.get_columns()
1676
raise AssertionError(
1677
"%s has no columns" % self.get_name())
1679
self._treeview.row_activated(rowno, columns[0])
1681
def set_headers_visible(self, value):
1683
@param value: if true, shows the headers, if false hide then
1685
self._treeview.set_headers_visible(value)
1687
def set_visible_rows(self, rows):
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
1692
@param rows: number of rows to show
1695
treeview = self._treeview
1696
if treeview.get_headers_visible():
1698
header_h = self._get_header_height()
1702
column = treeview.get_columns()[0]
1703
h = column.cell_get_size()[-1]
1705
focus_padding = treeview.style_get_property('focus-line-width') * 2
1706
treeview.set_size_request(-1, header_h + (rows * (h + focus_padding)))
1708
type_register(ObjectList)
1710
class ObjectTree(ObjectList):
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.
1718
__gtype_name__ = 'ObjectTree'
1720
gsignal('row-expanded', object)
1722
def __init__(self, columns=[], objects=None, mode=gtk.SELECTION_BROWSE,
1723
sortable=False, model=None):
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)
1729
def _append_internal(self, parent, instance, select, prepend):
1731
parent_id = id(parent)
1732
if parent_id in iters:
1733
parent_iter = iters[parent_id]
1734
elif parent is None:
1737
raise TypeError("parent must be an Object, ObjectRow or None")
1739
# Freeze and save original selection mode to avoid blinking
1740
self._treeview.freeze_notify()
1743
row_iter = self._model.prepend(parent_iter, (instance,))
1745
row_iter = self._model.append(parent_iter, (instance,))
1747
self._iters[id(instance)] = row_iter
1750
self._treeview.columns_autosize()
1753
self._select_and_focus_row(row_iter)
1754
self._treeview.thaw_notify()
1758
def append(self, parent, instance, select=False):
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
1765
return self._append_internal(parent, instance, select, prepend=False)
1767
def prepend(self, parent, instance, select=False):
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
1774
return self._append_internal(parent, instance, select, prepend=True)
1776
def expand(self, instance, open_all=True):
1778
This method opens the row specified by path so its children
1780
@param instance: an instance to expand at
1781
@param open_all: If True, expand all rows, otherwise just the
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]
1789
self.get_treeview().expand_row(
1790
self._model[treeiter].path, open_all)
1792
def collapse(self, instance):
1794
This method collapses the row specified by path
1795
(hides its child rows, if they exist).
1796
@param instance: an instance to collapse
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]
1803
self.get_treeview().collapse_row(
1804
self._model[treeiter].path)
1806
def _on_treeview__row_expanded(self, treeview, treeiter, treepath):
1807
self.emit('row-expanded', self.get_model()[treeiter][COL_MODEL])
1809
type_register(ObjectTree)
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
1816
def __init__(self, klist, column, label='', value_format='%s',
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
1825
@param value_format: format string used to format value
1826
@type value_format: string
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__)
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
1840
gtk.HBox.__init__(self)
1845
self._value_widget.modify_font(font_desc)
1846
self._label_widget.modify_font(font_desc)
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
1856
self._value_widget.set_markup(self._value_format % value)
1858
def get_value_widget(self):
1859
return self._value_widget
1861
def get_label_widget(self):
1862
return self._label_widget
1866
def _create_ui(self):
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
1873
tree_column = self._klist.get_treeview_column(self._column)
1874
tree_column.connect('notify::width',
1875
self._on_treeview_column__notify_width)
1877
button = self._klist._get_column_button(tree_column)
1878
button.connect('size-allocate',
1879
self._on_treeview_column_button__size_allocate)
1881
self._label_widget = gtk.Label()
1882
self._label_widget.set_markup(self._label)
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()
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()
1896
def _resize(self, position=-1, width=-1):
1899
if self._label_width > position:
1900
self._label_widget.set_text('')
1902
self._label_widget.set_markup(self._label)
1904
# XXX: Replace 12 with a constant
1906
self._label_widget.set_size_request(position - 12, -1)
1909
self._value_widget.set_size_request(width, -1)
1913
def _on_treeview_column_button__size_allocate(self, label, rect):
1914
self._resize(position=rect[0])
1916
def _on_treeview_column__notify_width(self, treeview, pspec):
1917
value = treeview.get_property(pspec.name)
1918
self._resize(width=value)
1920
def _on_list__size_allocate(self, list, rect):
1921
self._resize(position=rect[0], width=rect[2])
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."""
1930
def __init__(self, klist, column, label=_('Total:'), value_format='%s',
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)
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
1947
value = sum([get_attribute(obj, attr, 0) for obj in self._klist],
1948
column.data_type('0'))
1950
self.set_value(column.as_string(value))
1954
def _on_klist__cell_edited(self, klist, object, attribute):