Merge lp:~elementary-pantheon/granite/source-list into lp:~elementary-pantheon/granite/granite

Proposed by Victor Martinez
Status: Merged
Merged at revision: 437
Proposed branch: lp:~elementary-pantheon/granite/source-list
Merge into: lp:~elementary-pantheon/granite/granite
Diff against target: 2321 lines (+2300/-0)
3 files modified
lib/CMakeLists.txt (+2/-0)
lib/Widgets/CellRendererExpander.vala (+98/-0)
lib/Widgets/SourceList.vala (+2200/-0)
To merge this branch: bzr merge lp:~elementary-pantheon/granite/source-list
Reviewer Review Type Date Requested Status
Tom Beckmann (community) Approve
Victor Martinez (community) Abstain
elementary Pantheon team Pending
Review via email: mp+122931@code.launchpad.net

Description of the change

Implementation of new Sidebar API: http://tiny.cc/s3f6jw

(I don't know what else to mention here. I guess reading the embedded documentation should be enough.)

To post a comment you must log in.
Revision history for this message
Rico Tzschichholz (ricotz) wrote :

quite some merge conflicts here ;)

Revision history for this message
Victor Martinez (victored) wrote :

Merge conflicts fixed.

Revision history for this message
Rico Tzschichholz (ricotz) wrote :

Could you explain the need of renaming it to SourceList?

Revision history for this message
Cassidy James Blaede (cassidyjames) wrote :
Revision history for this message
Rico Tzschichholz (ricotz) wrote :

Ah, I see

Revision history for this message
Rico Tzschichholz (ricotz) wrote :

@victored: I guess it should be "SourcesList" then not "SourceList"?

Revision history for this message
Victor Martinez (victored) wrote :

@Rico: Daniel suggested "SourceList" and I used that, but I'm OK with renaming it again. I agree that it should use the same name used in the HIG.

Revision history for this message
Victor Martinez (victored) wrote :

By the way, if you want to see the widget in action, please give lp:~elementary-pantheon/granite/new-granite-demo a try ;)

Revision history for this message
Victor Martinez (victored) wrote :

Some outstanding issues I've found:

1) Tree.start_editing_item() should check the "editing" property before starting another edit. Currently, if you hit the F2 key twice, the second key release will cancel the editing you started.

2) Comment in lines 1501-1505 is a falacy. Remove it.

3) The documentation of Sidebar.VisibleFunc feels a bit incomplete. It should point out that no item property should be modified from that function, or everything will freeze.

4) In the documentation of Sidebar.set_visible_func, "re-filter" should be "refilter", or valadoc will fail.

review: Needs Fixing
Revision history for this message
Victor Martinez (victored) wrote :

Another outstanding bug: the primary expander is clickable even if the arrow is not visible. This is bad because it will trigger the toggled() signal on an ExpandableItem with no visible children, and will also prevent non-expandable items from being selected if the user clicks over that area.

review: Needs Fixing
Revision history for this message
Victor Martinez (victored) wrote :

For those with better English than mine (~99.8% of people in the world), please take a look to the documentation and fix anything that sucks. I don't care if you rewrite it all as long as it helps to make it better.

Revision history for this message
lacthiea (lacthiea) wrote :

I think prepending an item should be possible.

Revision history for this message
Tom Beckmann (tombeckmann) wrote :

As discussed, a get_next (Item item) would be very helpful.

Revision history for this message
Pim Vullers (pimvullers) wrote :

I just build granite including this patch and tested it with Tom's editor. One thing I noticed is that the triangle behind the main directory (the one in bold) moves slightly to the left when going deeper into the tree underneath it.

Revision history for this message
Pim Vullers (pimvullers) wrote :

Merge into granite trunk (0.1.1) is currently broken.

Revision history for this message
Victor Martinez (victored) wrote :

I've fixed all the bugs mentioned above, including the last two reported by Pim Vullers (thanks Pim!).

Features Pending:

1) Drag and Drop.
2) Improve sorting API: Make it possible to have a sort function per expandable item, mapping their children order to that in the model.
3) Sexy badge renderer.
4) Progress spinner.

I guess the current API is already more than enough for our needs. We need to design (1) and (2) carefully. I still need to talk to a cairo expert (Rico, Tom, anyone?) and convince him to write a badge cell renderer (3) for us, though we may need to see a mockup from Daniel first.

Adding a progress spinner (4) is trivial, but such thing is still not part of the SourceList.Item API.

Thanks for your comments guys!

Revision history for this message
Victor Martinez (victored) :
review: Abstain
439. By Victor Martinez

[API] Add methods that allow navigating the actual branch:
+ SourceList.get_n_visible_children
+ SourceList.get_first_child
+ SourceList.get_last_child
+ SourceList.get_previous_item
+ SourceList.get_next_item

440. By Victor Martinez

Prevent non-selectable items from being edited and update documentation of ExpandableItem

Revision history for this message
Tom Beckmann (tombeckmann) wrote :

It should go into luna's granite as the API will be pretty much the same, even after having added those new features.

review: Approve
441. By Victor Martinez

Coding-style fixes

442. By Victor Martinez

Fix a couple of errors in the documentation. Also suggest using ThinPaned instead of SidebarPaned.

443. By Victor Martinez

Improve code that handles expander clicks.

Revision 436 broke the previous method and it needed a slight rewrite. The new implementation is faster and feels more responsive.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/CMakeLists.txt'
2--- lib/CMakeLists.txt 2012-10-27 00:15:37 +0000
3+++ lib/CMakeLists.txt 2012-11-07 17:19:23 +0000
4@@ -83,6 +83,8 @@
5 Widgets/LightWindow.vala
6 Widgets/StatusBar.vala
7 Widgets/SidebarPaned.vala
8+ Widgets/SourceList.vala
9+ Widgets/CellRendererExpander.vala
10 Widgets/ThinPaned.vala
11 Main.vala
12 config.vapi
13
14=== added file 'lib/Widgets/CellRendererExpander.vala'
15--- lib/Widgets/CellRendererExpander.vala 1970-01-01 00:00:00 +0000
16+++ lib/Widgets/CellRendererExpander.vala 2012-11-07 17:19:23 +0000
17@@ -0,0 +1,98 @@
18+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
19+/*
20+ * Copyright (c) 2012 Granite Developers
21+ *
22+ * This library is free software; you can redistribute it and/or
23+ * modify it under the terms of the GNU Lesser General Public License as
24+ * published by the Free Software Foundation; either version 2 of the
25+ * License, or (at your option) any later version.
26+ *
27+ * This is distributed in the hope that it will be useful,
28+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
29+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
30+ * Lesser General Public License for more details.
31+ *
32+ * You should have received a copy of the GNU Lesser General Public
33+ * License along with this program; see the file COPYING. If not,
34+ * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
35+ * Boston, MA 02111-1307, USA.
36+ */
37+
38+/**
39+ * An expander renderer.
40+ *
41+ * For it to draw an expander, the the {@link Gtk.CellRenderer.is_expander} property must
42+ * be set to true; otherwise nothing is drawn. The state of the expander (i.e. expanded or
43+ * collapsed) is controlled by the {@link Gtk.CellRenderer.is_expanded} property.
44+ *
45+ * @since 0.2
46+ */
47+public class Granite.Widgets.CellRendererExpander : Gtk.CellRenderer {
48+
49+ public CellRendererExpander () {
50+ }
51+
52+ public override Gtk.SizeRequestMode get_request_mode () {
53+ return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
54+ }
55+
56+ public override void get_preferred_width (Gtk.Widget widget,
57+ out int minimum_size,
58+ out int natural_size)
59+ {
60+
61+ minimum_size = natural_size = get_arrow_size (widget) + 2 * (int) xpad;
62+ }
63+
64+ public override void get_preferred_height_for_width (Gtk.Widget widget, int width,
65+ out int minimum_height,
66+ out int natural_height)
67+ {
68+ minimum_height = natural_height = get_arrow_size (widget) + 2 * (int) ypad;
69+ }
70+
71+ /**
72+ * Gets the size of the expander arrow.
73+ *
74+ * The default implementation tries to retrieve the "expander-size" style property from
75+ * //widget//, as it is primarily meant to be used along with a {@link Gtk.TreeView}.
76+ * For those with special needs, it is recommended to override this method.
77+ *
78+ * @param widget Widget used to query the "expander-size" style property (should be a Gtk.TreeView.)
79+ * @return Size of the expander arrow.
80+ * @since 0.2
81+ */
82+ public virtual int get_arrow_size (Gtk.Widget widget) {
83+ int arrow_size;
84+ widget.style_get ("expander-size", out arrow_size);
85+ return arrow_size;
86+ }
87+
88+ public override void render (Cairo.Context context, Gtk.Widget widget, Gdk.Rectangle bg_area,
89+ Gdk.Rectangle cell_area, Gtk.CellRendererState flags)
90+ {
91+ if (!is_expander)
92+ return;
93+
94+ Gdk.Rectangle aligned_area = get_aligned_area (widget, flags, cell_area);
95+
96+ int arrow_size = int.min (get_arrow_size (widget), aligned_area.width);
97+ int offset = arrow_size / 2;
98+ int x = aligned_area.x + aligned_area.width / 2 - offset;
99+ int y = aligned_area.y + aligned_area.height / 2 - offset;
100+
101+ var ctx = widget.get_style_context ();
102+ var state = ctx.get_state ();
103+ const Gtk.StateFlags EXPANDED_FLAG = Gtk.StateFlags.ACTIVE;
104+ ctx.set_state (is_expanded ? state | EXPANDED_FLAG : state & ~EXPANDED_FLAG);
105+ ctx.render_expander (context, x, y, arrow_size, arrow_size);
106+ }
107+
108+ [Deprecated (replacement = "Gtk.CellRenderer.get_preferred_size", since = "")]
109+ public override void get_size (Gtk.Widget widget, Gdk.Rectangle? cell_area,
110+ out int x_offset, out int y_offset,
111+ out int width, out int height)
112+ {
113+ assert_not_reached ();
114+ }
115+}
116\ No newline at end of file
117
118=== added file 'lib/Widgets/SourceList.vala'
119--- lib/Widgets/SourceList.vala 1970-01-01 00:00:00 +0000
120+++ lib/Widgets/SourceList.vala 2012-11-07 17:19:23 +0000
121@@ -0,0 +1,2200 @@
122+// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
123+/*
124+ * Copyright (c) 2012 Victor Eduardo <victoreduardm@gmail.com>
125+ *
126+ * This library is free software; you can redistribute it and/or
127+ * modify it under the terms of the GNU Lesser General Public License as
128+ * published by the Free Software Foundation; either version 2 of the
129+ * License, or (at your option) any later version.
130+ *
131+ * This is distributed in the hope that it will be useful,
132+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
133+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
134+ * Lesser General Public License for more details.
135+ *
136+ * You should have received a copy of the GNU Lesser General Public
137+ * License along with this program; see the file COPYING. If not,
138+ * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
139+ * Boston, MA 02111-1307, USA.
140+ */
141+
142+
143+/**
144+ * A widget that can display a list of items organized in categories.
145+ *
146+ * The source list widget consists of a collection of items, some of which are also expandable (and
147+ * thus can contain more items). All the items displayed in the source list are children of the widget's
148+ * root item. The API is meant to be used as follows:
149+ *
150+ * 1. Create the items you want to display in the source list, setting the appropriate values for their
151+ * properties. The desired hierarchy is achieved by creating expandable items and adding items to them.
152+ * These will be displayed as descendants in the widget's tree structure. The expandable items that are
153+ * not nested inside any other item are considered to be at root level, and should be added to
154+ * the widget's root item.<<BR>>
155+ *
156+ * Expandable items located at the root level are treated as categories, and only support text.
157+ *
158+ * ''Example''<<BR>>
159+ * The final tree will have the following structure:
160+ * {{{
161+ * Libraries
162+ * Music
163+ * Stores
164+ * My Store
165+ * Music
166+ * Podcasts
167+ * Devices
168+ * Player 1
169+ * Player 2
170+ * }}}
171+ *
172+ * {{{
173+ * var library_category = new Granite.Widgets.SourceList.ExpandableItem ("Libraries");
174+ * var store_category = new Granite.Widgets.SourceList.ExpandableItem ("Stores");
175+ * var device_category = new Granite.Widgets.SourceList.ExpandableItem ("Devices");
176+ *
177+ * var music_item = new Granite.Widgets.SourceList.Item ("Music");
178+ *
179+ * // "Libraries" will be the parent category of "Music"
180+ * library_category.add (music_item);
181+ *
182+ * // We plan to add sub-items to the store, so let's use an expandable item
183+ * var my_store_item = new Granite.Widgets.SourceList.ExpandableItem ("My Store");
184+ * store_category.add (my_store_item);
185+ *
186+ * var my_store_podcast_item = new Granite.Widgets.SourceList.Item ("Podcasts");
187+ * var my_store_music_item = new Granite.Widgets.SourceList.Item ("Music");
188+ *
189+ * my_store_item.add (my_store_music_item);
190+ * my_store_item.add (my_store_podcast_item);
191+ *
192+ * var player1_item = new Granite.Widgets.SourceList.Item ("Player 1");
193+ * var player2_item = new Granite.Widgets.SourceList.Item ("Player 2");
194+ *
195+ * device_category.add (player1_item);
196+ * device_category.add (player2_item);
197+ * }}}
198+ *
199+ * 2. Create a source list widget.<<BR>>
200+ * {{{
201+ * var source_list = new Granite.Widgets.SourceList ();
202+ * }}}
203+ *
204+ * 3. Add root-level items to the {@link Granite.Widgets.SourceList.root} item.
205+ * This item only serves as a container, and all its properties are ignored by the widget.
206+ *
207+ * {{{
208+ * // This will add the main categories (including their children) to the source list. After
209+ * // having being added to be widget, any other item added to any of these items
210+ * // (or any other child item in a deeper level) will be automatically added too.
211+ * // There's no need to deal with the source list widget directly.
212+ *
213+ * var root = source_list.root;
214+ *
215+ * root.add (library_category);
216+ * root.add (store_category);
217+ * root.add (device_category);
218+ * }}}
219+ *
220+ * The steps mentioned above are enough for initializing the source list. Future changes to the items'
221+ * properties are ''automatically'' reflected by the widget.
222+ *
223+ * Final steps would involve connecting handlers to the source list events, being
224+ * {@link Granite.Widgets.SourceList.item_selected} the most important, as it indicates that
225+ * the selection was modified.
226+ *
227+ * It is strongly recommended to pack the source list into the GUI using the
228+ * {@link Granite.Widgets.ThinPaned} widget. It has aesthetic advantages and offers a wider
229+ * re-size handle than {@link Gtk.Paned}. This is usually done as follows:
230+ * {{{
231+ * var pane = new Granite.Widgets.ThinPaned ();
232+ * pane.pack1 (source_list, true, false);
233+ * pane.pack2 (content_area, true, false);
234+ * }}}
235+ *
236+ * @since 0.2
237+ * @see Granite.Widgets.ThinPaned
238+ */
239+public class Granite.Widgets.SourceList : Gtk.ScrolledWindow {
240+
241+ /**
242+ * = WORKING INTERNALS =
243+ *
244+ * In order to offer a transparent Item-based API, and avoid the need of providing methods
245+ * to deal with items directly on the SourceList widget, it was decided to follow a monitor-like
246+ * implementation, where the source list permanently monitors its root item and any other
247+ * child item added to it. The task of monitoring the properties of the items has been
248+ * divided between different objects, as shown below:
249+ *
250+ * Monitored by: Object::method that receives the signals indicating the property change.
251+ * Applied by: Object::method that actually updates the tree to reflect the property changes
252+ * (directly or indirectly, as in the case of the tree data model).
253+ *
254+ * ---------------------------------------------------------------------------------------------
255+ * PROPERTY | MONITORED BY | APPLIED BY
256+ * ---------------------------------------------------------------------------------------------
257+ * + Item | |
258+ * - parent | Not monitored | N/A
259+ * - name | DataModel::on_item_prop_changed | Tree::name_cell_data_func
260+ * - editable | DataModel::on_item_prop_changed | Queried when needed (See Tree::start_editing_item)
261+ * - visible | DataModel::on_item_prop_changed | DataModel::filter_visible_func
262+ * - icon | DataModel::on_item_prop_changed | Tree::icon_cell_data_func
263+ * - activatable | Same as @icon | Same as @icon
264+ * + ExpandableItem | |
265+ * - no_caption | DataModel::on_item_prop_changed | Tree::name_cell_data_func
266+ * - collapsible | DataModel::on_item_prop_changed | Tree::update_expansion
267+ * | | Tree::expander_cell_data_func
268+ * - expanded | Same as @collapsible | Same as @collapsible
269+ * ---------------------------------------------------------------------------------------------
270+ * * Only automatic properties are monitored. ExpandableItem's additions/removals are handled by
271+ * DataModel::add_item() and DataModel::remove_item()
272+ *
273+ * Other features:
274+ * - Sorting: this happens on the tree-model level (DataModel). Also see SourceList::SortFunc.
275+ */
276+
277+
278+
279+ /**
280+ * A source list entry.
281+ *
282+ * Any change made to any of its properties will be ''automatically'' reflected
283+ * by the {@link Granite.Widgets.SourceList} widget.
284+ *
285+ * @since 0.2
286+ */
287+ public class Item : Object {
288+
289+ /**
290+ * Emitted when the user has finished editing the item's name.
291+ *
292+ * By default, if the name doesn't consist of white space, it is automatically assigned
293+ * to the {@link Granite.Widgets.SourceList.Item.name} property. The default behavior can
294+ * be changed by overriding this signal.
295+ * @param new_name The item's new name (result of editing.)
296+ * @since 0.2
297+ */
298+ public virtual signal void edited (string new_name) {
299+ if (editable && new_name.strip () != "")
300+ this.name = new_name;
301+ }
302+
303+ /**
304+ * The {@link Granite.Widgets.SourceList.Item.activatable} icon was activated.
305+ *
306+ * @see Granite.Widgets.SourceList.Item.activatable
307+ * @since 0.2
308+ */
309+ public virtual signal void action_activated () { }
310+
311+ /**
312+ * Emitted when the item is double-clicked or when it is selected and one of the keys:
313+ * Space, Shift+Space, Return or Enter is pressed. This signal is //also// for
314+ * editable items.
315+ *
316+ * @since 0.2
317+ */
318+ public virtual signal void activated () { }
319+
320+ /**
321+ * Parent {@link Granite.Widgets.SourceList.ExpandableItem} of the item.
322+ * ''Must not'' be modified.
323+ *
324+ * @since 0.2
325+ */
326+ public ExpandableItem parent { get; internal set; }
327+
328+ /**
329+ * The item's name. Primary and most important information.
330+ *
331+ * @since 0.2
332+ */
333+ public string name { get; set; default = ""; }
334+
335+ /**
336+ * A counter shown next to the item's name.
337+ *
338+ * It can be used for displaying the number of unread messages in the "Inbox" item,
339+ * for instance.
340+ *
341+ * @since 0.2
342+ */
343+ public uint count { get; set; default = 0; }
344+
345+ /**
346+ * Whether the item's name can be edited from within the source list.
347+ *
348+ * When this property is set to //true//, users can edit the item by pressing
349+ * the F2 key, or by double-clicking over an item.
350+ *
351+ * ''This property only works for selectable items''. If that property is set to
352+ * //false//, the item won't be editable even if this property is set to //true//.
353+ *
354+ * @see Granite.Widgets.SourceList.Item.selectable
355+ * @see Granite.Widgets.SourceList.start_editing_item
356+ * @since 0.2
357+ */
358+ public bool editable { get; set; default = false; }
359+
360+ /**
361+ * Whether the item should appear in the source list's tree or not.
362+ *
363+ * @since 0.2
364+ */
365+ public bool visible { get; set; default = true; }
366+
367+ /**
368+ * Whether the item can be selected or not.
369+ *
370+ * Setting this property to true doesn't guarantee that the item will actually be
371+ * selectable, since there are other external factors to take into account, like the
372+ * item's {@link Granite.Widgets.SourceList.Item.visible} property; whether the item is
373+ * a category; the parent item is collapsed, etc.
374+ *
375+ * @see Granite.Widgets.SourceList.Item.visible
376+ * @since 0.2
377+ */
378+ public bool selectable { get; set; default = true; }
379+
380+ /**
381+ * Primary icon.
382+ *
383+ * This property should be used to give the user an idea of what the item represents
384+ * (i.e. content type.)
385+ *
386+ * @since 0.2
387+ */
388+ public Icon icon { get; set; }
389+
390+ /**
391+ * An activatable icon that works like a button.
392+ *
393+ * It can be used for e.g. showing an //"eject"// icon on a device's item.
394+ *
395+ * @see Granite.Widgets.SourceList.Item.action_activated
396+ * @since 0.2
397+ */
398+ public Icon activatable { get; set; }
399+
400+ /**
401+ * Creates a new {@link Granite.Widgets.SourceList.Item}.
402+ *
403+ * @param name Name of the item.
404+ * @return (transfer full) A new {@link Granite.Widgets.SourceList.Item}.
405+ * @since 0.2
406+ */
407+ public Item (string name = "") {
408+ this.name = name;
409+ }
410+
411+ /**
412+ * Invoked when the item is secondary-clicked or when the usual menu keys are pressed.
413+ *
414+ * @return A {@link Gtk.Menu} or //null// if nothing should be displayed.
415+ * @since 0.2
416+ */
417+ public virtual Gtk.Menu? get_context_menu () {
418+ return null;
419+ }
420+ }
421+
422+
423+
424+ /**
425+ * An item that can contain more items.
426+ *
427+ * It supports all the properties inherited from {@link Granite.Widgets.SourceList.Item},
428+ * and behaves like a normal item, except when it is located at the root level; in that case,
429+ * the following properties are ignored by the widget:
430+ *
431+ * * {@link Granite.Widgets.SourceList.Item.selectable}
432+ * * {@link Granite.Widgets.SourceList.Item.editable}
433+ * * {@link Granite.Widgets.SourceList.Item.icon}
434+ * * {@link Granite.Widgets.SourceList.Item.activatable}
435+ * * {@link Granite.Widgets.SourceList.Item.count}
436+ *
437+ * Root-level expandable items (i.e. Main Categories) are ''not'' displayed when they contain
438+ * zero visible children.
439+ *
440+ * @since 0.2
441+ */
442+ public class ExpandableItem : Item {
443+
444+ /**
445+ * Emitted when an item is added.
446+ *
447+ * @param item Item added.
448+ * @see Granite.Widgets.SourceList.ExpandableItem.add
449+ * @since 0.2
450+ */
451+ public signal void child_added (Item item);
452+
453+ /**
454+ * Emitted when an item is removed.
455+ *
456+ * @param item Item removed.
457+ * @see Granite.Widgets.SourceList.ExpandableItem.remove
458+ * @since 0.2
459+ */
460+ public signal void child_removed (Item item);
461+
462+ /**
463+ * Emitted when the item is expanded or collapsed.
464+ *
465+ * @since 0.2
466+ */
467+ public virtual signal void toggled () { }
468+
469+ /**
470+ * Whether the item is collapsible or not.
471+ *
472+ * When set to //false//, the item is //always// expanded and the expander is
473+ * not shown. Please note that this will also affect the value returned by the
474+ * {@link Granite.Widgets.SourceList.ExpandableItem.expanded} property.
475+ *
476+ * @see Granite.Widgets.SourceList.ExpandableItem.expanded
477+ * @since 0.2
478+ */
479+ public bool collapsible { get; set; default = true; }
480+
481+ /**
482+ * Whether the item is expanded or not.
483+ *
484+ * The source list widget will obey the value of this property when possible.
485+ *
486+ * This property has no effect when {@link Granite.Widgets.SourceList.ExpandableItem.collapsible}
487+ * is set to //false//. Also keep in mind that, __when set to //true//__, this property
488+ * doesn't always represent the actual expansion state of an item. For example, it might
489+ * be the case that an expandable item is collapsed because it has zero visible children,
490+ * but its //expanded// property value is still //true//; in such case, once one of the
491+ * item's children becomes visible, the item will be expanded again. Same applies to items
492+ * hidden behind a collapsed parent item.
493+ *
494+ * If obtaining the ''actual'' expansion state of an item is important to your needs,
495+ * use {@link Granite.Widgets.SourceList.is_item_expanded} instead.
496+ *
497+ * @see Granite.Widgets.SourceList.ExpandableItem.collapsible
498+ * @see Granite.Widgets.SourceList.is_item_expanded
499+ * @since 0.2
500+ */
501+ private bool _expanded = false;
502+ public bool expanded {
503+ get { return _expanded || !collapsible; } // if not collapsible, always return true
504+ set {
505+ if (value != _expanded) {
506+ _expanded = value;
507+ toggled ();
508+ }
509+ }
510+ }
511+
512+ /**
513+ * Number of children contained by the item.
514+ *
515+ * @since 0.2
516+ */
517+ public uint n_children {
518+ get { return children_set.size; }
519+ }
520+
521+ /**
522+ * The item's children.
523+ *
524+ * @since 0.2
525+ */
526+ public Gee.Collection<Item> children {
527+ owned get {
528+ // Create a copy of the children so that it's safe to iterate it
529+ // (e.g. by using foreach) while removing items. See clear() for
530+ // an example of such case.
531+ var copy = new Gee.LinkedList<Item> ();
532+ foreach (var item in children_set)
533+ copy.add (item);
534+ return copy;
535+ }
536+ }
537+
538+ private Gee.Set<Item> children_set = new Gee.HashSet<Item> ();
539+
540+ /**
541+ * Creates a new {@link Granite.Widgets.SourceList.ExpandableItem}
542+ *
543+ * @param name Title of the item.
544+ * @return (transfer full) A new {@link Granite.Widgets.SourceList.ExpandableItem}.
545+ * @since 0.2
546+ */
547+ public ExpandableItem (string name = "") {
548+ base (name);
549+ editable = false;
550+ }
551+
552+ /**
553+ * Checks whether the item contains the specified child.
554+ *
555+ * This method only considers the item's immediate children.
556+ *
557+ * @param item Item to search.
558+ * @return Whether the item was found or not.
559+ * @since 0.2
560+ */
561+ public bool contains (Item item) {
562+ return item in children_set;
563+ }
564+
565+ /**
566+ * Adds an item.
567+ *
568+ * {@link Granite.Widgets.SourceList.ExpandableItem.child_added} is fired after the item is added.
569+ *
570+ * While adding a child item, //the item it's being added to will set itself as the parent//.
571+ * Please note that items are required to have their //parent// property set to //null// before
572+ * being added, so make sure you remove the item from its previous parent before attempting
573+ * to add it to another item. For instance:
574+ * {{{
575+ * if (item.parent != null)
576+ * item.parent.remove (item); // this will set item's parent to null
577+ * new_parent.add (item);
578+ * }}}
579+ *
580+ * @param item The item to add. Its parent __must__ be //null//.
581+ * @see Granite.Widgets.SourceList.ExpandableItem.child_added
582+ * @see Granite.Widgets.SourceList.ExpandableItem.remove
583+ * @since 0.2
584+ */
585+ public void add (Item item) requires (item.parent == null) requires (!contains (item)) {
586+ item.parent = this;
587+ children_set.add (item);
588+ child_added (item);
589+ }
590+
591+ /**
592+ * Removes an item.
593+ *
594+ * The {@link Granite.Widgets.SourceList.ExpandableItem.child_removed} signal is fired
595+ * //after removing the item//. Finally (i.e. after all the handlers have been invoked),
596+ * the item's {@link Granite.Widgets.SourceList.Item.parent} property is set to //null//.
597+ * This has the advantage of letting signal handlers know the parent from which //item//
598+ * is being removed.
599+ *
600+ * @param item The item to remove. This will fail if item has a different parent.
601+ * @see Granite.Widgets.SourceList.ExpandableItem.child_removed
602+ * @see Granite.Widgets.SourceList.ExpandableItem.clear
603+ * @since 0.2
604+ */
605+ public void remove (Item item) requires (item.parent == this) requires (contains (item)) {
606+ children_set.remove (item);
607+ child_removed (item);
608+ item.parent = null;
609+ }
610+
611+ /**
612+ * Removes all the items contained by the item. It works similarly to
613+ * {@link Granite.Widgets.SourceList.ExpandableItem.remove}.
614+ *
615+ * @see Granite.Widgets.SourceList.ExpandableItem.remove
616+ * @see Granite.Widgets.SourceList.ExpandableItem.child_removed
617+ * @since 0.2
618+ */
619+ public void clear () {
620+ foreach (var item in children)
621+ remove (item);
622+ }
623+
624+ /**
625+ * Expands the item and/or its children.
626+ *
627+ * @param inclusive Whether to also expand this item (true), or only its children (false).
628+ * @param recursive Whether to recursively expand all the children (true), or only
629+ * immediate children (false).
630+ * @see Granite.Widgets.SourceList.ExpandableItem.expanded
631+ * @since 0.2
632+ */
633+ public void expand_all (bool inclusive = true, bool recursive = true) {
634+ set_expansion (this, inclusive, recursive, true);
635+ }
636+
637+ /**
638+ * Collapses the item and/or its children.
639+ *
640+ * @param inclusive Whether to also collapse this item (true), or only its children (false).
641+ * @param recursive Whether to recursively collapse all the children (true), or only
642+ * immediate children (false). The latter case might appear contradictory, given that collapsing
643+ * immediate children will also //visually// collapse non-immediate children, but it makes total
644+ * sense once you've understood what the {@link Granite.Widgets.SourceList.ExpandableItem.expanded}
645+ * property actually means. If you set //recursive// to //true,// the non-immediate children's
646+ * //expanded// property will be set to //false//, and therefore they will __stay collapsed__
647+ * the next time their parents are expanded; otherwise (i.e. if //recursive// is //false//),
648+ * __their previous expansion state will be restored__ once their parents are expanded again.
649+ * @see Granite.Widgets.SourceList.ExpandableItem.expanded
650+ * @since 0.2
651+ */
652+ public void collapse_all (bool inclusive = true, bool recursive = true) {
653+ set_expansion (this, inclusive, recursive, false);
654+ }
655+
656+ private static void set_expansion (ExpandableItem item, bool inclusive, bool recursive, bool expanded) {
657+ if (inclusive)
658+ item.expanded = expanded;
659+
660+ foreach (var child_item in item.children) {
661+ var child_expandable_item = child_item as ExpandableItem;
662+ if (child_expandable_item != null) {
663+ if (recursive)
664+ set_expansion (child_expandable_item, true, true, expanded);
665+ else
666+ child_expandable_item.expanded = expanded;
667+ }
668+ }
669+ }
670+
671+ /**
672+ * Recursively expands the item along with its parent(s).
673+ *
674+ * @see Granite.Widgets.SourceList.ExpandableItem.expanded
675+ * @since 0.2
676+ */
677+ public void expand_with_parents () {
678+ // Update parent items first due to GtkTreeView's working internals:
679+ // Expanding children before their parents would not always work, because
680+ // they could be obscured behind a collapsed row by the time the treeview
681+ // tries to expand them, obviously failing.
682+ if (parent != null)
683+ parent.expand_with_parents ();
684+ expanded = true;
685+ }
686+
687+ /**
688+ * Recursively collapses the item along with its parent(s).
689+ *
690+ * @see Granite.Widgets.SourceList.ExpandableItem.expanded
691+ * @since 0.2
692+ */
693+ public void collapse_with_parents () {
694+ if (parent != null)
695+ parent.collapse_with_parents ();
696+ expanded = false;
697+ }
698+ }
699+
700+
701+
702+ /**
703+ * The model backing the SourceList tree.
704+ *
705+ * It monitors item property changes, and handles children additions and removals. It also controls
706+ * the visibility of the items based on their "visible" property, and on their number of children,
707+ * if they happen to be categories. Its main purpose is to provide an easy and practical interface
708+ * for sorting, adding, removing and updating items, eliminating the need of repeatedly dealing with
709+ * the Gtk.TreeModel API directly.
710+ */
711+ private class DataModel : Gtk.TreeModelFilter {
712+
713+ /**
714+ * An object that references a particular row in a model. This class is a wrapper built around
715+ * Gtk.TreeRowReference, and exists with the purpose of ensuring we never use invalid tree paths
716+ * or iters in the model, since most of these errors provoke failures due to GTK+ assertions
717+ * or, even worse, unexpected behavior.
718+ */
719+ private class NodeWrapper {
720+
721+ /**
722+ * The actual reference to the node. If is is null, it is treated as invalid.
723+ */
724+ private Gtk.TreeRowReference? row_reference;
725+
726+ /**
727+ * A newly-created Gtk.TreeIter pointing to the node if it exists; null otherwise.
728+ */
729+ public Gtk.TreeIter? iter {
730+ owned get {
731+ Gtk.TreeIter? rv = null;
732+
733+ if (valid) {
734+ var _path = this.path;
735+ if (_path != null) {
736+ Gtk.TreeIter _iter;
737+ if (row_reference.get_model ().get_iter (out _iter, _path))
738+ rv = _iter;
739+ }
740+ }
741+
742+ return rv;
743+ }
744+ }
745+
746+ /**
747+ * A newly-created Gtk.TreePath pointing to the node if it exists; null otherwise.
748+ */
749+ public Gtk.TreePath? path {
750+ owned get { return valid ? row_reference.get_path () : null; }
751+ }
752+
753+ /**
754+ * Whether the node is valid or not. When it is not valid, no valid references are
755+ * returned by the object to avoid errors (null is returned instead).
756+ */
757+ public bool valid {
758+ get { return row_reference != null && row_reference.valid (); }
759+ }
760+
761+ public NodeWrapper (Gtk.TreeModel model, Gtk.TreeIter iter) {
762+ row_reference = new Gtk.TreeRowReference (model, model.get_path (iter));
763+ }
764+ }
765+
766+ /**
767+ * Helper object used to monitor item property changes.
768+ */
769+ private class ItemMonitor {
770+ public signal void changed (Item self, string prop_name);
771+ private Item item;
772+
773+ public ItemMonitor (Item item) {
774+ this.item = item;
775+ item.notify.connect_after (on_notify);
776+ }
777+
778+ ~ItemMonitor () {
779+ item.notify.disconnect (on_notify);
780+ }
781+
782+ private void on_notify (ParamSpec prop) {
783+ changed (item, prop.name);
784+ }
785+ }
786+
787+ private enum Column {
788+ ITEM,
789+ N_COLUMNS;
790+
791+ public Type type () {
792+ switch (this) {
793+ case ITEM:
794+ return typeof (Item);
795+
796+ default:
797+ assert_not_reached (); // a Type must be returned for every valid column
798+ }
799+ }
800+ }
801+
802+ private enum SortColumn {
803+ UNSORTED = Gtk.SortColumn.UNSORTED,
804+ SORTED = Gtk.SortColumn.DEFAULT + 1
805+ }
806+
807+ public signal void item_updated (Item item);
808+
809+ /**
810+ * Used by push_parent_update() as key to associate the respective data to the objects.
811+ */
812+ private const string ITEM_PARENT_NEEDS_UPDATE = "item-parent-needs-update";
813+
814+ private Gtk.SortType sort_dir = Gtk.SortType.ASCENDING;
815+ public Gtk.SortType sort_direction {
816+ get { return sort_dir; }
817+ set {
818+ sort_dir = value;
819+ child_tree.set_sort_column_id (this.sort_column, sort_dir);
820+ }
821+ }
822+
823+ private ExpandableItem _root;
824+
825+ /**
826+ * Root item.
827+ *
828+ * This item is not actually part of the model. It's only used as a proxy
829+ * for adding and removing items.
830+ */
831+ public ExpandableItem root {
832+ get { return _root; }
833+ set {
834+ if (_root != null) {
835+ remove_children_monitor (_root);
836+ foreach (var item in _root.children)
837+ remove_item (item);
838+ }
839+
840+ _root = value;
841+
842+ add_children_monitor (_root);
843+ foreach (var item in _root.children)
844+ add_item (item);
845+ }
846+ }
847+
848+ /**
849+ * This hash map stores items and their respective child node references. For that reason, the
850+ * references it contains should only be used on the child_tree model, or converted to filter
851+ * iters/paths using convert_child_*_to_*() before using them with the filter (i.e. this) model.
852+ */
853+ private Gee.HashMap<Item, NodeWrapper> items = new Gee.HashMap<Item, NodeWrapper> ();
854+
855+ private Gee.HashMap<Item, ItemMonitor> monitors = new Gee.HashMap<Item, ItemMonitor> ();
856+
857+ private Gtk.TreeStore child_tree;
858+ private SourceList.SortFunc? sort_func;
859+ private unowned SourceList.VisibleFunc? filter_func;
860+ private SortColumn sort_column = SortColumn.UNSORTED;
861+
862+ public DataModel () {
863+ var child_tree = new Gtk.TreeStore (Column.N_COLUMNS, Column.ITEM.type ());
864+ Object (child_model: child_tree, virtual_root: null);
865+ this.child_tree = child_tree;
866+ set_visible_func (filter_visible_func);
867+ }
868+
869+ public bool has_item (Item item) {
870+ return items.has_key (item);
871+ }
872+
873+ public void update_item (Item item) requires (has_item (item)) {
874+ assert (root != null);
875+
876+ // Emitting row_changed() for this item's row in the child model causes the filter
877+ // (i.e. this model) to re-evaluate whether a row is visible or not, calling
878+ // filter_visible_func() for that row again, and that's exactly what we want.
879+ var node_reference = items.get (item);
880+ if (node_reference != null) {
881+ var path = node_reference.path;
882+ var iter = node_reference.iter;
883+ if (path != null && iter != null) {
884+ child_tree.row_changed (path, iter);
885+ item_updated (item);
886+ }
887+ }
888+ }
889+
890+ private void add_item (Item item) requires (!has_item (item)) {
891+ assert (root != null);
892+
893+ // Find the parent iter
894+ Gtk.TreeIter? parent_child_iter = null, child_iter;
895+ var parent = item.parent;
896+
897+ if (parent != null && parent != root) {
898+ // Add parent if it hasn't been added yet
899+ if (!has_item (parent))
900+ add_item (parent);
901+
902+ // Try to find the parent's iter
903+ parent_child_iter = get_item_child_iter (parent);
904+
905+ // Parent must have been added prior to adding this item
906+ assert (parent_child_iter != null);
907+ }
908+
909+ child_tree.append (out child_iter, parent_child_iter);
910+ child_tree.set (child_iter, Column.ITEM, item, -1);
911+
912+ items.set (item, new NodeWrapper (child_tree, child_iter));
913+
914+ // This is equivalent to a property change. The tree still needs to update
915+ // some of the new item's properties through this signal's handler.
916+ item_updated (item);
917+
918+ add_property_monitor (item);
919+
920+ push_parent_update (parent);
921+
922+ // If the item is expandable, also add children
923+ var expandable = item as ExpandableItem;
924+ if (expandable != null) {
925+ foreach (var child_item in expandable.children)
926+ add_item (child_item);
927+
928+ // Monitor future additions/removals through signal handlers
929+ add_children_monitor (expandable);
930+ }
931+ }
932+
933+ private void remove_item (Item item) requires (has_item (item)) {
934+ assert (root != null);
935+
936+ remove_property_monitor (item);
937+
938+ // get_item_child_iter() depends on items.get(item) for retrieving the right reference,
939+ // so don't unset the item from @items yet! We first get the child iter and then
940+ // unset the value.
941+ var child_iter = get_item_child_iter (item);
942+
943+ // Now we remove the item from the table, because that way get_item_child_iter() and
944+ // all the methods that depend on it won't return invalid iters or items when
945+ // called. This is important because child_tree.remove() will emit row_deleted(),
946+ // and its handlers could potentially depend on one of the methods mentioned above.
947+ items.unset (item);
948+
949+ if (child_iter != null) {
950+#if VALA_0_18
951+ // Workaround for a bug in valac 0.18 that tries to pass an invalid pointer type
952+ // (GtkTreeIter** instead of GtkTreeIter*) to gtk_tree_store_remove() in the
953+ // generated C code. https://bugzilla.gnome.org/show_bug.cgi?id=685177
954+ Gtk.TreeIter iter = child_iter;
955+
956+ child_tree.remove (ref iter);
957+#else
958+ child_tree.remove (child_iter);
959+#endif
960+ }
961+
962+ push_parent_update (item.parent);
963+
964+ // If the item is expandable, also remove children
965+ var expandable = item as ExpandableItem;
966+ if (expandable != null) {
967+ // No longer monitor future additions or removals
968+ remove_children_monitor (expandable);
969+
970+ foreach (var child_item in expandable.children)
971+ remove_item (child_item);
972+ }
973+ }
974+
975+ private void add_property_monitor (Item item) {
976+ var wrapper = new ItemMonitor (item);
977+ monitors[item] = wrapper;
978+ wrapper.changed.connect (on_item_prop_changed);
979+ }
980+
981+ private void remove_property_monitor (Item item) {
982+ var wrapper = monitors[item];
983+ if (wrapper != null)
984+ wrapper.changed.disconnect (on_item_prop_changed);
985+ monitors.unset (item);
986+ }
987+
988+ private void add_children_monitor (ExpandableItem item) {
989+ item.child_added.connect_after (on_item_child_added);
990+ item.child_removed.connect_after (on_item_child_removed);
991+ }
992+
993+ private void remove_children_monitor (ExpandableItem item) {
994+ item.child_added.disconnect (on_item_child_added);
995+ item.child_removed.disconnect (on_item_child_removed);
996+ }
997+
998+ private void on_item_child_added (Item item) {
999+ add_item (item);
1000+ }
1001+
1002+ private void on_item_child_removed (Item item) {
1003+ remove_item (item);
1004+ }
1005+
1006+ private void on_item_prop_changed (Item item, string prop_name) {
1007+ if (prop_name != "parent")
1008+ update_item (item);
1009+ }
1010+
1011+ /**
1012+ * Pushes a call to update_item() if //parent// is not //null//.
1013+ *
1014+ * This is needed because the visibility of categories depends on their n_children property,
1015+ * and also because item expansion should be updated after adding or removing items.
1016+ * If many updates are pushed, and the item has still not been updated, only one is processed.
1017+ * This guarantees efficiency as updating a category item could trigger expensive actions.
1018+ */
1019+ private void push_parent_update (ExpandableItem? parent) {
1020+ if (parent == null)
1021+ return;
1022+
1023+ bool needs_update = parent.get_data<bool> (ITEM_PARENT_NEEDS_UPDATE);
1024+
1025+ // If an update is already waiting to be processed, just return, as we
1026+ // don't need to queue another one for the same item.
1027+ if (needs_update)
1028+ return;
1029+
1030+ var path = get_item_path (parent);
1031+
1032+ if (path != null) {
1033+ // Let's mark this item for update
1034+ parent.set_data<bool> (ITEM_PARENT_NEEDS_UPDATE, true);
1035+
1036+ Idle.add (() => {
1037+ if (parent != null) {
1038+ update_item (parent);
1039+
1040+ // Already updated. No longer needs an update.
1041+ parent.set_data<bool> (ITEM_PARENT_NEEDS_UPDATE, false);
1042+ }
1043+
1044+ return false;
1045+ });
1046+ }
1047+ }
1048+
1049+ /**
1050+ * Returns the Item pointed by iter, or null if the iter doesn't refer to a valid item.
1051+ */
1052+ public Item? get_item (Gtk.TreeIter iter) {
1053+ Item? item;
1054+ get (iter, Column.ITEM, out item, -1);
1055+ return item;
1056+ }
1057+
1058+ /**
1059+ * Returns the Item pointed by path, or null if the path doesn't refer to a valid item.
1060+ */
1061+ public Item? get_item_from_path (Gtk.TreePath path) {
1062+ Gtk.TreeIter iter;
1063+ if (get_iter (out iter, path))
1064+ return get_item (iter);
1065+
1066+ return null;
1067+ }
1068+
1069+ /**
1070+ * Returns a newly-created path pointing to the item, or null in case a valid path
1071+ * is not found.
1072+ */
1073+ public Gtk.TreePath? get_item_path (Item item) {
1074+ Gtk.TreePath? path = null, child_path = get_item_child_path (item);
1075+
1076+ // We want a filter path, not a child_model path
1077+ if (child_path != null)
1078+ path = convert_child_path_to_path (child_path);
1079+
1080+ return path;
1081+ }
1082+
1083+ /**
1084+ * Returns a newly-created iterator pointing to the item, or null in case a valid iter
1085+ * was not found.
1086+ */
1087+ public Gtk.TreeIter? get_item_iter (Item item) {
1088+ var child_iter = get_item_child_iter (item);
1089+
1090+ if (child_iter != null) {
1091+ Gtk.TreeIter iter;
1092+ if (convert_child_iter_to_iter (out iter, child_iter))
1093+ return iter;
1094+ }
1095+
1096+ return null;
1097+ }
1098+
1099+ /**
1100+ * Sets the sort function, or "unsets" it if null is passed. Please note though
1101+ * that unsetting the sort function doesn't bring the items back to their initial
1102+ * order.
1103+ */
1104+ public void set_sort_func (owned SourceList.SortFunc? sort_func) {
1105+ this.sort_func = (owned) sort_func;
1106+ this.sort_column = this.sort_func != null ? SortColumn.SORTED : SortColumn.UNSORTED;
1107+
1108+ child_tree.set_sort_func (SortColumn.SORTED, child_model_sort_func);
1109+ sort_direction = sort_dir;
1110+ }
1111+
1112+ /**
1113+ * External "extra" filter method.
1114+ */
1115+ public void set_filter_func (SourceList.VisibleFunc? visible_func) {
1116+ this.filter_func = visible_func;
1117+ }
1118+
1119+ /**
1120+ * Checks whether an item is a category (i.e. a root-level expandable item).
1121+ * The caller must pass an iter or path pointing to the item, but not both
1122+ * (one of them must be null.)
1123+ *
1124+ * TODO: instead of checking the position of the iter or path, we should simply
1125+ * check whether the item's parent is the root item and whether the item is
1126+ * expandable. We don't do so right now because vala still allows client code
1127+ * to access the Item.parent property, even though its setter is defined as internal.
1128+ */
1129+ public bool is_category (Item item, Gtk.TreeIter? iter, Gtk.TreePath? path = null) {
1130+ bool is_category = false;
1131+ // either iter or path has to be null
1132+ if (item is ExpandableItem) {
1133+ if (iter != null) {
1134+ assert (path == null);
1135+ is_category = is_iter_at_root_level (iter);
1136+ } else {
1137+ assert (iter == null);
1138+ is_category = is_path_at_root_level (path);
1139+ }
1140+ }
1141+ return is_category;
1142+ }
1143+
1144+ public bool is_iter_at_root_level (Gtk.TreeIter iter) {
1145+ return is_path_at_root_level (get_path (iter));
1146+ }
1147+
1148+ public bool is_path_at_root_level (Gtk.TreePath path) {
1149+ return path.get_depth () == 1;
1150+ }
1151+
1152+ /**
1153+ * Actual sort function. It simply returns zero if sort_func is null.
1154+ */
1155+ private int child_model_sort_func (Gtk.TreeModel model, Gtk.TreeIter a, Gtk.TreeIter b) {
1156+ // Return zero by default, since a different value would not be reflexive nor symmetric when
1157+ // sort_func is null.
1158+ int sort = 0;
1159+
1160+ Item? item_a, item_b;
1161+ child_tree.get (a, Column.ITEM, out item_a, -1);
1162+ child_tree.get (b, Column.ITEM, out item_b, -1);
1163+
1164+ if (sort_func != null && item_a != null && item_b != null)
1165+ sort = sort_func (item_a, item_b);
1166+
1167+ return sort;
1168+ }
1169+
1170+ private Gtk.TreeIter? get_item_child_iter (Item item) {
1171+ Gtk.TreeIter? child_iter = null;
1172+
1173+ var child_node_wrapper = items.get (item);
1174+ if (child_node_wrapper != null)
1175+ child_iter = child_node_wrapper.iter;
1176+
1177+ return child_iter;
1178+ }
1179+
1180+ private Gtk.TreePath? get_item_child_path (Item item) {
1181+ Gtk.TreePath? child_path = null;
1182+
1183+ var child_node_wrapper = items.get (item);
1184+ if (child_node_wrapper != null)
1185+ child_path = child_node_wrapper.path;
1186+
1187+ return child_path;
1188+ }
1189+
1190+ /**
1191+ * Filters the child-tree items based on their "visible" property.
1192+ */
1193+ private bool filter_visible_func (Gtk.TreeModel child_model, Gtk.TreeIter iter) {
1194+ bool item_visible = false;
1195+
1196+ Item? item;
1197+ child_tree.get (iter, Column.ITEM, out item, -1);
1198+
1199+ if (item != null) {
1200+ item_visible = item.visible;
1201+
1202+ // If the item is a category, also query the number of visible child items
1203+ // because empty categories should not be displayed.
1204+ var expandable = item as ExpandableItem;
1205+ if (expandable != null && child_tree.iter_depth (iter) == 0) {
1206+ uint n_visible_children = 0;
1207+ foreach (var child_item in expandable.children) {
1208+ if (child_item.visible)
1209+ n_visible_children++;
1210+ }
1211+ item_visible = item_visible && n_visible_children > 0;
1212+ }
1213+ }
1214+
1215+ if (filter_func != null)
1216+ item_visible = item_visible && filter_func (item);
1217+
1218+ return item_visible;
1219+ }
1220+ }
1221+
1222+
1223+
1224+ /**
1225+ * Class responsible for rendering Item.icon and Item.activatable. It also
1226+ * notifies about clicks through the activated() signal.
1227+ */
1228+ private class CellRendererIcon : Gtk.CellRendererPixbuf {
1229+ public signal void activated (string path);
1230+
1231+ private const Gtk.IconSize ICON_SIZE = Gtk.IconSize.MENU;
1232+
1233+ public CellRendererIcon () {
1234+ mode = Gtk.CellRendererMode.ACTIVATABLE;
1235+ stock_size = ICON_SIZE;
1236+ follow_state = true;
1237+ }
1238+
1239+ public override bool activate (Gdk.Event event, Gtk.Widget widget, string path,
1240+ Gdk.Rectangle background_area, Gdk.Rectangle cell_area,
1241+ Gtk.CellRendererState flags)
1242+ {
1243+ activated (path);
1244+ return true;
1245+ }
1246+ }
1247+
1248+
1249+
1250+ /**
1251+ * A cell renderer that only adds space.
1252+ */
1253+ private class CellRendererSpacer : Gtk.CellRenderer {
1254+ /**
1255+ * Indentation level represented by this cell renderer
1256+ */
1257+ public int level { get; set; default = -1; }
1258+
1259+ public override Gtk.SizeRequestMode get_request_mode () {
1260+ return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
1261+ }
1262+
1263+ public override void get_preferred_width (Gtk.Widget widget, out int min_size, out int natural_size) {
1264+ min_size = natural_size = 2 * (int) xpad;
1265+ }
1266+
1267+ public override void get_preferred_height_for_width (Gtk.Widget widget, int width,
1268+ out int min_height, out int natural_height)
1269+ {
1270+ min_height = natural_height = 2 * (int) ypad;
1271+ }
1272+
1273+ public override void render (Cairo.Context context, Gtk.Widget widget, Gdk.Rectangle bg_area,
1274+ Gdk.Rectangle cell_area, Gtk.CellRendererState flags)
1275+ {
1276+ // Nothing to do. This renderer only adds space.
1277+ }
1278+
1279+ [Deprecated (replacement = "Gtk.CellRenderer.get_preferred_size", since = "")]
1280+ public override void get_size (Gtk.Widget widget, Gdk.Rectangle? cell_area,
1281+ out int x_offset, out int y_offset,
1282+ out int width, out int height)
1283+ {
1284+ assert_not_reached ();
1285+ }
1286+ }
1287+
1288+
1289+
1290+ /**
1291+ * The tree that actually displays the items.
1292+ *
1293+ * All the user interaction happens here.
1294+ */
1295+ private class Tree : Gtk.TreeView {
1296+
1297+ public DataModel data_model { get; set; }
1298+
1299+ public signal void item_selected (Item? item);
1300+
1301+ public Item? selected_item {
1302+ get { return selected; }
1303+ set { set_selected (value, true); }
1304+ }
1305+
1306+ public bool editing {
1307+ get { return text_cell.editing; }
1308+ }
1309+
1310+ public Pango.EllipsizeMode ellipsize_mode {
1311+ get { return text_cell.ellipsize; }
1312+ set { text_cell.ellipsize = value; }
1313+ }
1314+
1315+ private enum Column {
1316+ ITEM,
1317+ N_COLS
1318+ }
1319+
1320+ // Padding added at the beginning of every new level. Must be an even number.
1321+ private const uint LEVEL_INDENTATION = 10;
1322+
1323+ // Padding added on the left side of the sidebar. Must be an even number.
1324+ private const uint LEFT_PADDING = 4;
1325+
1326+ private Item? selected;
1327+ private unowned Item? edited;
1328+
1329+ private Gtk.Entry? editable_entry;
1330+ private Gtk.CellRendererText text_cell;
1331+ private CellRendererIcon icon_cell;
1332+ private CellRendererIcon activatable_cell;
1333+ private Gtk.CellRenderer primary_expander_cell;
1334+ private Gtk.CellRenderer secondary_expander_cell;
1335+ private Gee.HashMap<int, CellRendererSpacer> spacer_cells; // cells used for left spacing
1336+
1337+ public Tree (DataModel data_model) {
1338+ get_style_context ().add_class (Gtk.STYLE_CLASS_SIDEBAR);
1339+
1340+ this.data_model = data_model;
1341+ set_model (data_model);
1342+
1343+ halign = valign = Gtk.Align.FILL;
1344+ expand = true;
1345+
1346+ enable_search = false;
1347+ headers_visible = false;
1348+ enable_grid_lines = Gtk.TreeViewGridLines.NONE;
1349+
1350+ // Deactivate GtkTreeView's built-in expander functionality
1351+ expander_column = null;
1352+ show_expanders = false;
1353+
1354+ var item_column = new Gtk.TreeViewColumn ();
1355+ item_column.expand = true;
1356+
1357+ insert_column (item_column, Column.ITEM);
1358+
1359+ // Now pack the cell renderers. We insert them in reverse order (using pack_end)
1360+ // because we want to use TreeViewColumn.pack_start exclusively for inserting
1361+ // spacer cell renderers for level-indentation purposes.
1362+ // See add_spacer_cell_for_level() for more details.
1363+
1364+ // Second expander. Used for main categories
1365+ secondary_expander_cell = new CellRendererExpander ();
1366+ secondary_expander_cell.xpad = 10;
1367+ item_column.pack_end (secondary_expander_cell, false);
1368+ item_column.set_cell_data_func (secondary_expander_cell, expander_cell_data_func);
1369+
1370+ activatable_cell = new CellRendererIcon ();
1371+ activatable_cell.xpad = 6;
1372+ activatable_cell.activated.connect (on_activatable_activated);
1373+ item_column.pack_end (activatable_cell, false);
1374+ item_column.set_cell_data_func (activatable_cell, icon_cell_data_func);
1375+
1376+ text_cell = new Gtk.CellRendererText ();
1377+ text_cell.editable_set = true;
1378+ text_cell.editable = false;
1379+ text_cell.editing_started.connect (on_editing_started);
1380+ text_cell.editing_canceled.connect (on_editing_canceled);
1381+ text_cell.ellipsize = Pango.EllipsizeMode.END;
1382+ text_cell.xalign = 0;
1383+ item_column.pack_end (text_cell, true);
1384+ item_column.set_cell_data_func (text_cell, name_cell_data_func);
1385+
1386+ icon_cell = new CellRendererIcon ();
1387+ icon_cell.xpad = 2;
1388+ item_column.pack_end (icon_cell, false);
1389+ item_column.set_cell_data_func (icon_cell, icon_cell_data_func);
1390+
1391+ // First expander. Used for normal expandable items
1392+ primary_expander_cell = new CellRendererExpander ();
1393+ primary_expander_cell.xpad = 3;
1394+ item_column.pack_end (primary_expander_cell, false);
1395+ item_column.set_cell_data_func (primary_expander_cell, expander_cell_data_func);
1396+
1397+ // Selection
1398+ var selection = get_selection ();
1399+ selection.mode = Gtk.SelectionMode.BROWSE;
1400+ selection.set_select_function (select_func);
1401+
1402+ // Monitor item changes
1403+ data_model.item_updated.connect_after (on_model_item_updated);
1404+
1405+ // Add root-level indentation. New levels will be added by update_item_expansion()
1406+ add_spacer_cell_for_level (1);
1407+ }
1408+
1409+ ~Tree () {
1410+ text_cell.editing_started.disconnect (on_editing_started);
1411+ text_cell.editing_canceled.disconnect (on_editing_canceled);
1412+ data_model.item_updated.disconnect (on_model_item_updated);
1413+ }
1414+
1415+ private void on_model_item_updated (Item item) {
1416+ // Currently, all the other properties are updated automatically by the
1417+ // cell-data functions after a change in the model.
1418+ var expandable_item = item as ExpandableItem;
1419+ if (expandable_item != null)
1420+ update_expansion (expandable_item);
1421+ }
1422+
1423+ private void add_spacer_cell_for_level (int level, bool check_previous = true)
1424+ requires (level > 0)
1425+ {
1426+ if (spacer_cells == null)
1427+ spacer_cells = new Gee.HashMap<int, CellRendererSpacer> ();
1428+
1429+ if (spacer_cells[level] == null) {
1430+ var spacer_cell = new CellRendererSpacer ();
1431+ spacer_cell.level = level;
1432+ spacer_cells[level] = spacer_cell;
1433+
1434+ uint cell_xpadding;
1435+
1436+ // The primary expander is not visible for root-level (i.e. first level)
1437+ // items, so for the second level of indentation we use a low padding
1438+ // because the primary expander will add enough space. For the root level,
1439+ // we use LEFT_PADDING, and LEVEL_INDENTATION for the remaining levels.
1440+ // The value of cell_xpadding will be allocated *twice* by the cell renderer,
1441+ // so we set the value to a half of actual (desired) value.
1442+ switch (level) {
1443+ case 1: // root
1444+ cell_xpadding = LEFT_PADDING / 2;
1445+ break;
1446+
1447+ case 2: // second level
1448+ cell_xpadding = 0;
1449+ break;
1450+
1451+ default: // remaining levels
1452+ cell_xpadding = LEVEL_INDENTATION / 2;
1453+ break;
1454+ }
1455+
1456+ spacer_cell.xpad = cell_xpadding;
1457+
1458+ var item_column = get_column (Column.ITEM);
1459+ item_column.pack_start (spacer_cell, false);
1460+ item_column.set_cell_data_func (spacer_cell, spacer_cell_data_func);
1461+
1462+ // Make sure that the previous indentation levels also exist
1463+ if (check_previous) {
1464+ for (int i = level - 1; i > 0; i--)
1465+ add_spacer_cell_for_level (i, false);
1466+ }
1467+ }
1468+ }
1469+
1470+ /**
1471+ * Evaluates whether the item at the specified path can be selected or not.
1472+ */
1473+ private bool select_func (Gtk.TreeSelection selection, Gtk.TreeModel model,
1474+ Gtk.TreePath path, bool path_currently_selected)
1475+ {
1476+ bool selectable = false;
1477+ var item = data_model.get_item_from_path (path);
1478+
1479+ if (item != null) {
1480+ // Main categories ARE NOT selectable, so check for that
1481+ if (!data_model.is_category (item, null, path))
1482+ selectable = item.selectable;
1483+ }
1484+
1485+ return selectable;
1486+ }
1487+
1488+ private Gtk.TreePath? get_selected_path () {
1489+ Gtk.TreePath? selected_path = null;
1490+ Gtk.TreeSelection? selection = get_selection ();
1491+
1492+ if (selection != null) {
1493+ Gtk.TreeModel? model;
1494+ var selected_rows = selection.get_selected_rows (out model);
1495+ if (selected_rows.length () == 1)
1496+ selected_path = selected_rows.nth_data (0);
1497+ }
1498+
1499+ return selected_path;
1500+ }
1501+
1502+ private void set_selected (Item? item, bool scroll_to_item) {
1503+ if (item == null) {
1504+ Gtk.TreeSelection? selection = get_selection ();
1505+ if (selection != null)
1506+ selection.unselect_all ();
1507+
1508+ // As explained in cursor_changed(), we cannot emit signals for this special
1509+ // case from there because that wouldn't allow us to implement the behavior
1510+ // we want (i.e. restoring the old selection after expanding a previously
1511+ // collapsed category) without emitting the undesired item_selected() signal
1512+ // along the way. This special case is handled manually, because it *should*
1513+ // only happen in response to client code requests and never in response to
1514+ // user interaction. We do that here because there's no way to determine
1515+ // whether the cursor change came from code (i.e. this method) or user
1516+ // interaction from cursor_changed().
1517+ this.selected = null;
1518+ item_selected (null);
1519+ } else if (item.selectable) {
1520+ if (scroll_to_item)
1521+ this.scroll_to_item (item);
1522+
1523+ var to_select = data_model.get_item_path (item);
1524+ if (to_select != null)
1525+ set_cursor_on_cell (to_select, get_column (Column.ITEM), text_cell, false);
1526+ }
1527+ }
1528+
1529+ public override void cursor_changed () {
1530+ var path = get_selected_path ();
1531+ Item? new_item = path != null ? data_model.get_item_from_path (path) : null;
1532+
1533+ // Don't do anything if @new_item is null.
1534+ //
1535+ // The only way 'this.selected' can be null is by setting it explicitly to
1536+ // that value from client code, and thus we handle that case in set_selected().
1537+ // THIS CANNOT HAPPEN IN RESPONSE TO USER INTERACTION. For example, if an
1538+ // item is un-selected because its parent category has been collapsed, then it will
1539+ // remain as the current selected item (not in reality, just as the value of
1540+ // this.selected) and will be re-selected after the parent is expanded again.
1541+ // THIS ALL HAPPENS SILENTLY BEHIND THE SCENES, so client code will never know
1542+ // it ever happened; the value of selected_item remains unchanged and item_selected()
1543+ // is not emitted.
1544+ if (new_item != null && new_item != this.selected) {
1545+ this.selected = new_item;
1546+ item_selected (new_item);
1547+ }
1548+ }
1549+
1550+ public bool scroll_to_item (Item item, bool use_align = false, float row_align = 0) {
1551+ bool scrolled = false;
1552+
1553+ var path = data_model.get_item_path (item);
1554+ if (path != null) {
1555+ scroll_to_cell (path, null, use_align, row_align, 0);
1556+ scrolled = true;
1557+ }
1558+
1559+ return scrolled;
1560+ }
1561+
1562+ public bool start_editing_item (Item item) requires (item.editable) requires (item.selectable) {
1563+ if (editing && item == edited) // If same item again, simply return.
1564+ return false;
1565+
1566+ var path = data_model.get_item_path (item);
1567+ if (path != null) {
1568+ edited = item;
1569+ text_cell.editable = true;
1570+ set_cursor_on_cell (path, get_column (Column.ITEM), text_cell, true);
1571+ } else {
1572+ warning ("Could not edit \"%s\": path not found", item.name);
1573+ }
1574+
1575+ return editing;
1576+ }
1577+
1578+ public void stop_editing () {
1579+ if (editing && edited != null) {
1580+ var path = data_model.get_item_path (edited);
1581+
1582+ // Setting the cursor on the same cell without starting an edit cancels any
1583+ // editing operation going on.
1584+ if (path != null)
1585+ set_cursor_on_cell (path, get_column (Column.ITEM), text_cell, false);
1586+ }
1587+ }
1588+
1589+ private void on_editing_started (Gtk.CellEditable editable, string path) {
1590+ editable_entry = editable as Gtk.Entry;
1591+ if (editable_entry != null) {
1592+ editable_entry.editing_done.connect (on_editing_done);
1593+ editable_entry.editable = true;
1594+ }
1595+ }
1596+
1597+ private void on_editing_canceled () {
1598+ if (editable_entry != null) {
1599+ editable_entry.editable = false;
1600+ editable_entry.editing_done.disconnect (on_editing_done);
1601+ }
1602+
1603+ text_cell.editable = false;
1604+ edited = null;
1605+ }
1606+
1607+ private void on_editing_done () {
1608+ if (edited != null && edited.editable && editable_entry != null)
1609+ edited.edited (editable_entry.get_text ());
1610+
1611+ // Same actions as when canceling editing
1612+ on_editing_canceled ();
1613+ }
1614+
1615+ private void on_activatable_activated (string item_path_str) {
1616+ var item = get_item_from_path_string (item_path_str);
1617+ if (item != null)
1618+ item.action_activated ();
1619+ }
1620+
1621+ private Item? get_item_from_path_string (string item_path_str) {
1622+ var item_path = new Gtk.TreePath.from_string (item_path_str);
1623+ return data_model.get_item_from_path (item_path);
1624+ }
1625+
1626+ private bool toggle_expansion (ExpandableItem item) {
1627+ if (item.collapsible) {
1628+ item.expanded = !item.expanded;
1629+ return true;
1630+ }
1631+ return false;
1632+ }
1633+
1634+ /**
1635+ * Updates the tree to reflect the ''expanded'' property of expandable_item.
1636+ */
1637+ public void update_expansion (ExpandableItem expandable_item) {
1638+ var path = data_model.get_item_path (expandable_item);
1639+
1640+ if (path != null) {
1641+ // Make sure that the indentation cell for the item's level exists.
1642+ // We use +1 because the method will make sure that the previous
1643+ // indentation levels exist too.
1644+ add_spacer_cell_for_level (path.get_depth () + 1);
1645+
1646+ if (expandable_item.expanded) {
1647+ expand_row (path, false);
1648+
1649+ // Since collapsing an item un-selects any child item previously selected,
1650+ // we need to restore the selection. This will be done silently because
1651+ // set_selected checks for equality between the previously "selected"
1652+ // item and the newly selected, and only emits the item_selected() signal
1653+ // if they are different. See cursor_changed() for a better explanation
1654+ // of this behavior.
1655+ if (selected != null && selected.parent == expandable_item)
1656+ set_selected (selected, true);
1657+
1658+ // Collapsing expandable_item's row also collapsed all its children,
1659+ // and thus we need to update the "expanded" property of each of them
1660+ // to reflect their previous state.
1661+ foreach (var child_item in expandable_item.children) {
1662+ var child_expandable_item = child_item as ExpandableItem;
1663+ if (child_expandable_item != null)
1664+ update_expansion (child_expandable_item);
1665+ }
1666+ } else {
1667+ collapse_row (path);
1668+ }
1669+ }
1670+ }
1671+
1672+ public override void row_activated (Gtk.TreePath path, Gtk.TreeViewColumn column) {
1673+ if (column == get_column (Column.ITEM)) {
1674+ var item = data_model.get_item_from_path (path);
1675+ if (item != null)
1676+ item.activated ();
1677+ }
1678+ }
1679+
1680+ public override bool key_release_event (Gdk.EventKey event) {
1681+ if (selected_item != null) {
1682+ switch (event.keyval) {
1683+ case Gdk.Key.F2:
1684+ var modifiers = Gtk.accelerator_get_default_mod_mask ();
1685+ // try to start editing selected item
1686+ if ((event.state & modifiers) == 0 && selected_item.editable)
1687+ start_editing_item (selected_item);
1688+ break;
1689+ }
1690+ }
1691+
1692+ return base.key_release_event (event);
1693+ }
1694+
1695+ public override bool button_press_event (Gdk.EventButton event) {
1696+ if (event.window != get_bin_window ())
1697+ return base.button_press_event (event);
1698+
1699+ Gtk.TreePath path;
1700+ Gtk.TreeViewColumn column;
1701+ int x = (int) event.x, y = (int) event.y, cell_x, cell_y;
1702+
1703+ if (get_path_at_pos (x, y, out path, out column, out cell_x, out cell_y)) {
1704+ var item = data_model.get_item_from_path (path);
1705+
1706+ // This is needed because the treeview adds an offset at the beginning of every level
1707+ Gdk.Rectangle start_cell_area;
1708+ get_cell_area (path, get_column (0), out start_cell_area);
1709+ cell_x -= start_cell_area.x;
1710+
1711+ if (item != null && column == get_column (Column.ITEM)) {
1712+ // Cancel any editing operation going on
1713+ stop_editing ();
1714+
1715+ if (((Gdk.Event*) (&event))->triggers_context_menu ()) {
1716+ popup_context_menu (item, event);
1717+ } else if (event.button == Gdk.BUTTON_PRIMARY) {
1718+ // Check whether an expander (or an equivalent area) was clicked.
1719+ bool is_expandable = item is ExpandableItem;
1720+ bool is_category = is_expandable && data_model.is_category (item, null, path);
1721+
1722+ if (event.type == Gdk.EventType.BUTTON_PRESS) {
1723+ if (is_expandable) {
1724+ // Checking for secondary_expander_cell is not necessary because the entire row
1725+ // serves for this purpose when the item is a category. It is only a visual indicator.
1726+ bool expander_clicked = is_category || over_primary_expander (column, path, cell_x);
1727+ if (expander_clicked && toggle_expansion (item as ExpandableItem))
1728+ return true;
1729+ }
1730+ } else if (event.type == Gdk.EventType.2BUTTON_PRESS
1731+ && !is_category // Main categories are *not* editable
1732+ && item.editable
1733+ && item.selectable
1734+ && over_cell (column, path, text_cell, cell_x)
1735+ && start_editing_item (item))
1736+ {
1737+ // The user double-clicked over the text cell, and editing started successfully.
1738+ return true;
1739+ }
1740+ }
1741+ }
1742+ }
1743+
1744+ return base.button_press_event (event);
1745+ }
1746+
1747+ private bool over_primary_expander (Gtk.TreeViewColumn col, Gtk.TreePath path, int x) {
1748+ Gtk.TreeIter iter;
1749+ if (!model.get_iter (out iter, path))
1750+ return false;
1751+
1752+ // Call the cell-data function and make it assign the proper visibility state to the cell
1753+ expander_cell_data_func (col, primary_expander_cell, model, iter);
1754+
1755+ if (!primary_expander_cell.visible)
1756+ return false;
1757+
1758+ // We want to return false if the cell is not expandable (i.e. the arrow is hidden)
1759+ if (model.iter_n_children (iter) < 1)
1760+ return false;
1761+
1762+ // Now that we're sure that the item is expandable, let's see if the user clicked
1763+ // over the expander area. We don't do so directly by querying the primary expander
1764+ // position because it's not fixed, yielding incorrect coordinates depending on whether
1765+ // a different area was re-drawn before this method was called. We know that the last
1766+ // spacer cell precedes (in a LTR fashion) the expander cell. Because the position
1767+ // of the spacer cell is fixed, we can safely query it.
1768+ int indentation_level = path.get_depth ();
1769+ var last_spacer_cell = spacer_cells[indentation_level];
1770+
1771+ if (last_spacer_cell != null) {
1772+ int cell_x, cell_width;
1773+
1774+ if (col.cell_get_position (last_spacer_cell, out cell_x, out cell_width)) {
1775+ // Add a pixel so that the expander area is a bit wider
1776+ int expander_width = get_cell_width (primary_expander_cell) + 1;
1777+
1778+ if (get_direction () == Gtk.TextDirection.LTR) {
1779+ int indentation_offset = cell_x + cell_width;
1780+ return x >= indentation_offset && x <= indentation_offset + expander_width;
1781+ }
1782+
1783+ return x <= cell_x && x >= cell_x - expander_width;
1784+ }
1785+ }
1786+
1787+ return false;
1788+ }
1789+
1790+ private bool over_cell (Gtk.TreeViewColumn col, Gtk.TreePath path, Gtk.CellRenderer cell, int x) {
1791+ int cell_x, cell_width;
1792+ bool found = col.cell_get_position (cell, out cell_x, out cell_width);
1793+ return found && x > cell_x && x < cell_x + cell_width;
1794+ }
1795+
1796+ private int get_cell_width (Gtk.CellRenderer cell_renderer) {
1797+ Gtk.Requisition min_req;
1798+ cell_renderer.get_preferred_size (this, out min_req, null);
1799+ return min_req.width;
1800+ }
1801+
1802+ public override bool popup_menu () {
1803+ return popup_context_menu (null, null);
1804+ }
1805+
1806+ private bool popup_context_menu (Item? item, Gdk.EventButton? event) {
1807+ if (item == null)
1808+ item = selected_item;
1809+
1810+ if (item != null) {
1811+ var menu = item.get_context_menu ();
1812+ if (menu != null) {
1813+ var time = (event != null) ? event.time : Gtk.get_current_event_time ();
1814+ var button = (event != null) ? event.button : 0;
1815+
1816+ menu.attach_to_widget (this, null);
1817+
1818+ if (event != null) {
1819+ menu.popup (null, null, null, button, time);
1820+ } else {
1821+ menu.popup (null, null, menu_position_func, button, time);
1822+ menu.select_first (false);
1823+ }
1824+
1825+ return true;
1826+ }
1827+ }
1828+
1829+ return false;
1830+ }
1831+
1832+ /**
1833+ * Positions a menu based on an item's coordinates.
1834+ *
1835+ * This function is only used for menu pop-ups triggered by events other than button
1836+ * presses (e.g. key-press events). Since such events provide no coordinates, it is
1837+ * assumed that the item in question is the one currently selected.
1838+ */
1839+ private void menu_position_func (Gtk.Menu menu, out int x, out int y, out bool push_in) {
1840+ push_in = true;
1841+ x = y = 0;
1842+
1843+ if (selected_item == null || !get_realized ())
1844+ return;
1845+
1846+ var path = data_model.get_item_path (selected_item);
1847+ if (path == null)
1848+ return;
1849+
1850+ // Try to find the position of the item
1851+ Gdk.Rectangle item_bin_coords;
1852+ get_cell_area (path, get_column (Column.ITEM), out item_bin_coords);
1853+
1854+ int item_y = item_bin_coords.y + item_bin_coords.height / 2;
1855+ int item_x = item_bin_coords.x;
1856+
1857+ bool is_rtl = get_direction () == Gtk.TextDirection.RTL;
1858+
1859+ if (!is_rtl)
1860+ item_x += item_bin_coords.width - 6;
1861+
1862+ int widget_x, widget_y;
1863+ convert_bin_window_to_widget_coords (item_x, item_y, out widget_x, out widget_y);
1864+
1865+ get_window ().get_origin (out x, out y);
1866+ x += widget_x.clamp (0, get_allocated_width ());
1867+ y += widget_y.clamp (0, get_allocated_height ());
1868+
1869+ if (is_rtl) {
1870+ Gtk.Requisition menu_req;
1871+ menu.get_preferred_size (out menu_req, null);
1872+ y -= menu_req.width;
1873+ }
1874+ }
1875+
1876+ private static Item? get_item_from_model (Gtk.TreeModel model, Gtk.TreeIter iter) {
1877+ var data_model = model as DataModel;
1878+ assert (data_model != null);
1879+ return data_model.get_item (iter);
1880+ }
1881+
1882+ private static void spacer_cell_data_func (Gtk.CellLayout layout, Gtk.CellRenderer renderer,
1883+ Gtk.TreeModel model, Gtk.TreeIter iter)
1884+ {
1885+ var spacer = renderer as CellRendererSpacer;
1886+ assert (spacer != null);
1887+ assert (spacer.level > 0);
1888+
1889+ var path = model.get_path (iter);
1890+
1891+ int level = -1;
1892+ if (path != null)
1893+ level = path.get_depth ();
1894+
1895+ renderer.visible = spacer.level <= level;
1896+ }
1897+
1898+ private void name_cell_data_func (Gtk.CellLayout layout, Gtk.CellRenderer renderer,
1899+ Gtk.TreeModel model, Gtk.TreeIter iter)
1900+ {
1901+ var text_renderer = renderer as Gtk.CellRendererText;
1902+ assert (text_renderer != null);
1903+
1904+ var text = new StringBuilder ();
1905+ var weight = Pango.Weight.NORMAL;
1906+
1907+ var item = get_item_from_model (model, iter);
1908+ if (item != null) {
1909+ text.append (item.name);
1910+
1911+ if (data_model.is_category (item, iter))
1912+ weight = Pango.Weight.BOLD;
1913+
1914+ if (item.count > 0) {
1915+ text.append (" (");
1916+ text.append (item.count.to_string ());
1917+ text.append_unichar (')');
1918+ }
1919+ }
1920+
1921+ text_renderer.weight = weight;
1922+ text_renderer.text = text.str;
1923+ }
1924+
1925+ private void icon_cell_data_func (Gtk.CellLayout layout, Gtk.CellRenderer renderer,
1926+ Gtk.TreeModel model, Gtk.TreeIter iter)
1927+ {
1928+ var icon_renderer = renderer as CellRendererIcon;
1929+ assert (icon_renderer != null);
1930+
1931+ bool visible = false;
1932+ Icon? icon = null;
1933+
1934+ var item = get_item_from_model (model, iter);
1935+ if (item != null) {
1936+ // Icons are not displayed for main categories
1937+ visible = !data_model.is_category (item, iter);
1938+
1939+ if (visible) {
1940+ if (icon_renderer == icon_cell)
1941+ icon = item.icon;
1942+ else if (icon_renderer == activatable_cell)
1943+ icon = item.activatable;
1944+ else
1945+ assert_not_reached ();
1946+ }
1947+ }
1948+
1949+ visible = visible && icon != null;
1950+
1951+ icon_renderer.visible = visible;
1952+ icon_renderer.gicon = visible ? icon : null;
1953+ }
1954+
1955+ /**
1956+ * Controls expander visibility.
1957+ */
1958+ private void expander_cell_data_func (Gtk.CellLayout layout, Gtk.CellRenderer renderer,
1959+ Gtk.TreeModel model, Gtk.TreeIter iter)
1960+ {
1961+ var item = get_item_from_model (model, iter);
1962+ if (item != null) {
1963+ // Gtk.CellRenderer.is_expander takes into account whether the item has children or not.
1964+ // The tree-view checks for that and sets this property for us. It also sets
1965+ // Gtk.CellRenderer.is_expanded, and thus we don't need to check for that either.
1966+ var expandable_item = item as ExpandableItem;
1967+ if (expandable_item != null)
1968+ renderer.is_expander = renderer.is_expander && expandable_item.collapsible;
1969+ }
1970+
1971+ if (renderer == primary_expander_cell)
1972+ renderer.visible = !data_model.is_iter_at_root_level (iter);
1973+ else if (renderer == secondary_expander_cell)
1974+ renderer.visible = data_model.is_category (item, iter);
1975+ else
1976+ assert_not_reached ();
1977+ }
1978+ }
1979+
1980+
1981+
1982+ /**
1983+ * Emitted when the source list selection changes.
1984+ *
1985+ * @param item Selected item; //null// if nothing is selected.
1986+ * @since 0.2
1987+ */
1988+ public virtual signal void item_selected (Item? item) { }
1989+
1990+ /**
1991+ * A {@link Granite.Widgets.SourceList.SortFunc} should return a negative integer, zero, or a
1992+ * positive integer if ''a'' sorts //before// ''b'', ''a'' sorts //with// ''b'', or ''a'' sorts
1993+ * //after// ''b'' respectively. If two items compare as equal, their order in the sorted
1994+ * source list is undefined.
1995+ *
1996+ * In order to ensure that the source list behaves as expected, the {@link Granite.Widgets.SourceList.SortFunc}
1997+ * must define a partial order on the source list tree; i.e. it must be reflexive, antisymmetric and
1998+ * transitive.
1999+ *
2000+ * (Same description as {@link Gtk.TreeIterCompareFunc}.)
2001+ *
2002+ * @param a First item.
2003+ * @param b Second item.
2004+ * @return A //negative// integer if //a// sorts after //b//, //zero// if //a// equals //b//,
2005+ * or a //positive// integer if //a// sorts before //b//.
2006+ * @since 0.2
2007+ */
2008+ public delegate int SortFunc (Item a, Item b);
2009+
2010+ /**
2011+ * A {@link Granite.Widgets.SourceList.VisibleFunc} should return true if the item should be
2012+ * visible; false otherwise. If //item//'s {@link Granite.Widgets.SourceList.Item.visible}
2013+ * property is set to //false//, then it won't be displayed even if this method returns true.
2014+ *
2015+ * It is important to note that the method ''must not modify any property of //item//'',
2016+ * because doing so would cause re-entrancy, because the widget's internal data model invokes the
2017+ * method to filter an item again after every property change, resulting in an infinite chain
2018+ * of recursive calls.
2019+ *
2020+ * Usually, modifying the {@link Granite.Widgets.SourceList.Item.visible} property is enough.
2021+ * The advantage of using this method is that its nature is non-destructive, and the
2022+ * changes it makes can be easily reverted (see {@link Granite.Widgets.SourceList.refilter}).
2023+ *
2024+ * @param item Item to be checked.
2025+ * @return Whether //item// should be visible or not.
2026+ * @since 0.2
2027+ */
2028+ public delegate bool VisibleFunc (Item item);
2029+
2030+ /**
2031+ * Root-level expandable item.
2032+ *
2033+ * This item contains the first-level source list items. It //only serves as an item container//.
2034+ * It is used to add and remove items to/from the widget.
2035+ *
2036+ * Internally, it allows the source list to connect to its {@link Granite.Widgets.SourceList.ExpandableItem.child_added}
2037+ * and {@link Granite.Widgets.SourceList.ExpandableItem.child_removed} signals in order to monitor
2038+ * new children additions/removals.
2039+ *
2040+ * @since 0.2
2041+ */
2042+ public ExpandableItem root { get; private set; default = new ExpandableItem (); }
2043+
2044+ /**
2045+ * The current selected item.
2046+ *
2047+ * Setting it to //null// un-selects the previously selected item, if there was any.
2048+ * {@link Granite.Widgets.SourceList.ExpandableItem.expand_with_parents} is called on the
2049+ * item's parent to make sure it's possible to select it.
2050+ *
2051+ * @since 0.2
2052+ */
2053+ public Item? selected {
2054+ get { return tree.selected_item; }
2055+ set {
2056+ if (value != null && value.parent != null)
2057+ value.parent.expand_with_parents ();
2058+ tree.selected_item = value;
2059+ }
2060+ }
2061+
2062+ /**
2063+ * Text ellipsize mode.
2064+ *
2065+ * @since 0.2
2066+ */
2067+ public Pango.EllipsizeMode ellipsize_mode {
2068+ get { return tree.ellipsize_mode; }
2069+ set { tree.ellipsize_mode = value; }
2070+ }
2071+
2072+ /**
2073+ * Whether an item is being edited.
2074+ *
2075+ * @see Granite.Widgets.SourceList.start_editing_item
2076+ * @since 0.2
2077+ */
2078+ public bool editing {
2079+ get { return tree.editing; }
2080+ }
2081+
2082+ /**
2083+ * Sort direction to use along with the sort function.
2084+ *
2085+ * @see Granite.Widgets.SourceList.set_sort_func
2086+ * @since 0.2
2087+ */
2088+ public Gtk.SortType sort_direction {
2089+ get { return data_model.sort_direction; }
2090+ set { data_model.sort_direction = value; }
2091+ }
2092+
2093+ private Tree tree;
2094+ private DataModel data_model { get { return tree.data_model; } }
2095+
2096+ /**
2097+ * Creates a new {@link Granite.Widgets.SourceList}.
2098+ *
2099+ * @return (transfer full) a new {@link Granite.Widgets.SourceList}.
2100+ * @since 0.2
2101+ */
2102+ public SourceList () {
2103+ var model = new DataModel ();
2104+ model.root = root;
2105+
2106+ push_composite_child ();
2107+ tree = new Tree (model);
2108+ tree.set_composite_name ("treeview");
2109+ pop_composite_child ();
2110+
2111+ set_policy (Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
2112+ add (tree);
2113+ show_all ();
2114+
2115+ tree.item_selected.connect ((item) => item_selected (item));
2116+ }
2117+
2118+ /**
2119+ * Checks whether //item// is part of the source list.
2120+ *
2121+ * @param item The item to query.
2122+ * @return //true// if the item belongs to the source list; //false// otherwise.
2123+ * @since 0.2
2124+ */
2125+ public bool has_item (Item item) {
2126+ return data_model.has_item (item);
2127+ }
2128+
2129+ /**
2130+ * Sets the method used for sorting items.
2131+ *
2132+ * @param sort_func The method to use for sorting items.
2133+ * @see Granite.Widgets.SourceList.SortFunc
2134+ * @since 0.2
2135+ */
2136+ public void set_sort_func (owned SortFunc? sort_func) {
2137+ data_model.set_sort_func ((owned) sort_func);
2138+ }
2139+
2140+ /**
2141+ * Sets the method used for filtering out items.
2142+ *
2143+ * @param visible_func The method to use for filtering items.
2144+ * @param refilter Whether to call {@link Granite.Widgets.SourceList.refilter} using the new function.
2145+ * @see Granite.Widgets.SourceList.VisibleFunc
2146+ * @see Granite.Widgets.SourceList.refilter
2147+ * @since 0.2
2148+ */
2149+ public void set_filter_func (VisibleFunc? visible_func, bool refilter) {
2150+ data_model.set_filter_func (visible_func);
2151+ if (refilter)
2152+ this.refilter ();
2153+ }
2154+
2155+ /**
2156+ * Applies the filter method set by {@link Granite.Widgets.SourceList.set_filter_func}
2157+ * to all the items that are part of the current tree.
2158+ *
2159+ * @see Granite.Widgets.SourceList.VisibleFunc
2160+ * @see Granite.Widgets.SourceList.set_filter_func
2161+ * @since 0.2
2162+ */
2163+ public void refilter () {
2164+ data_model.refilter ();
2165+ }
2166+
2167+ /**
2168+ * Queries the actual expansion state of //item//.
2169+ *
2170+ * @see Granite.Widgets.SourceList.ExpandableItem.expanded
2171+ * @return Whether //item// is expanded or not.
2172+ * @since 0.2
2173+ */
2174+ public bool is_item_expanded (Item item) requires (has_item (item)) {
2175+ var path = data_model.get_item_path (item);
2176+ return path != null && tree.is_row_expanded (path);
2177+ }
2178+
2179+ /**
2180+ * If //item// is editable, this activates the editor; otherwise, it does nothing.
2181+ * If an item was already being edited, this will fail.
2182+ *
2183+ * @param item Item to edit.
2184+ * @see Granite.Widgets.SourceList.Item.editable
2185+ * @see Granite.Widgets.SourceList.editing
2186+ * @see Granite.Widgets.SourceList.stop_editing
2187+ * @return true if the editing started successfully; false otherwise.
2188+ * @since 0.2
2189+ */
2190+ public bool start_editing_item (Item item) requires (has_item (item))
2191+ {
2192+ return tree.start_editing_item (item);
2193+ }
2194+
2195+ /**
2196+ * Cancels any editing operation going on.
2197+ *
2198+ * @see Granite.Widgets.SourceList.editing
2199+ * @see Granite.Widgets.SourceList.start_editing_item
2200+ * @since 0.2
2201+ */
2202+ public void stop_editing () {
2203+ if (editing)
2204+ tree.stop_editing ();
2205+ }
2206+
2207+ /**
2208+ * Scrolls the source list tree to make //item// visible.
2209+ *
2210+ * {@link Granite.Widgets.SourceList.ExpandableItem.expand_with_parents} is called
2211+ * for the item's parent if //expand_parents// is //true//, to make sure it's not
2212+ * hidden behind a collapsed row.
2213+ *
2214+ * If use_align is //false//, then the row_align argument is ignored, and the tree
2215+ * does the minimum amount of work to scroll the item onto the screen. This means that
2216+ * the item will be scrolled to the edge closest to its current position. If the item
2217+ * is currently visible on the screen, nothing is done.
2218+ *
2219+ * @param item Item to scroll to.
2220+ * @param expand_parents Whether to recursively expand item's parent in case they are collapsed.
2221+ * @param use_align Whether to use the //row_align// argument.
2222+ * @param row_align The vertical alignment of //item//. 0.0 means top, 0.5 center, and 1.0 bottom.
2223+ * @return //true// if successful; //false// otherwise.
2224+ * @since 0.2
2225+ */
2226+ public bool scroll_to_item (Item item, bool expand_parents = true, bool use_align = false, float row_align = 0)
2227+ requires (has_item (item))
2228+ {
2229+ if (expand_parents && item.parent != null)
2230+ item.parent.expand_with_parents ();
2231+
2232+ return tree.scroll_to_item (item, use_align, row_align);
2233+ }
2234+
2235+ /**
2236+ * Gets the previous item with respect to //reference//.
2237+ *
2238+ * @param reference Item to use as reference.
2239+ * @return The item that appears before //reference//, or //null// if there's none.
2240+ * @since 0.2
2241+ */
2242+ public Item? get_previous_item (Item reference) requires (has_item (reference)) {
2243+ // this will return null for root, so iter_n_children() will always work fine
2244+ var iter = data_model.get_item_iter (reference);
2245+ if (iter != null) {
2246+ Gtk.TreeIter new_iter = iter; // workaround for valac 0.18
2247+ if (data_model.iter_previous (ref new_iter))
2248+ return data_model.get_item (new_iter);
2249+ }
2250+
2251+ return null;
2252+ }
2253+
2254+ /**
2255+ * Gets the next item with respect to //reference//.
2256+ *
2257+ * @param reference Item to use as reference.
2258+ * @return The item that appears after //reference//, or //null// if there's none.
2259+ * @since 0.2
2260+ */
2261+ public Item? get_next_item (Item reference) requires (has_item (reference)) {
2262+ // this will return null for root, so iter_n_children() will always work fine
2263+ var iter = data_model.get_item_iter (reference);
2264+ if (iter != null) {
2265+ Gtk.TreeIter new_iter = iter; // workaround for valac 0.18
2266+ if (data_model.iter_next (ref new_iter))
2267+ return data_model.get_item (new_iter);
2268+ }
2269+
2270+ return null;
2271+ }
2272+
2273+ /**
2274+ * Gets the first visible child of an expandable item.
2275+ *
2276+ * @param parent Parent of the child to look up.
2277+ * @return The first visible child of //parent//, or null if it was not found.
2278+ * @since 0.2
2279+ */
2280+ public Item? get_first_child (ExpandableItem parent) {
2281+ return get_nth_child (parent, 0);
2282+ }
2283+
2284+ /**
2285+ * Gets the last visible child of an expandable item.
2286+ *
2287+ * @param parent Parent of the child to look up.
2288+ * @return The last visible child of //parent//, or null if it was not found.
2289+ * @since 0.2
2290+ */
2291+ public Item? get_last_child (ExpandableItem parent) {
2292+ return get_nth_child (parent, (int) get_n_visible_children (parent) - 1);
2293+ }
2294+
2295+ /**
2296+ * Gets the number of visible children of an expandable item.
2297+ *
2298+ * @param parent Item to query.
2299+ * @return Number of visible children of //parent//.
2300+ * @since 0.2
2301+ */
2302+ public uint get_n_visible_children (ExpandableItem parent) {
2303+ // this will return null for root, so iter_n_children() will always work properly.
2304+ var parent_iter = data_model.get_item_iter (parent);
2305+ return data_model.iter_n_children (parent_iter);
2306+ }
2307+
2308+ private Item? get_nth_child (ExpandableItem parent, int index) {
2309+ if (index < 0)
2310+ return null;
2311+
2312+ // this will return null for root, so iter_nth_child() will always work properly.
2313+ var parent_iter = data_model.get_item_iter (parent);
2314+
2315+ Gtk.TreeIter child_iter;
2316+ if (data_model.iter_nth_child (out child_iter, parent_iter, index))
2317+ return data_model.get_item (child_iter);
2318+
2319+ return null;
2320+ }
2321+}

Subscribers

People subscribed via source and target branches