Merge lp:~elementary-pantheon/granite/source-list into lp:~elementary-pantheon/granite/granite
- source-list
- Merge into granite
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 |
Related bugs: | |
Related blueprints: |
Source List Widget (Sidebar)
(Medium)
|
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 |
Commit message
Description of the change
Implementation of new Sidebar API: http://
(I don't know what else to mention here. I guess reading the embedded documentation should be enough.)
Rico Tzschichholz (ricotz) wrote : | # |
Victor Martinez (victored) wrote : | # |
Merge conflicts fixed.
Rico Tzschichholz (ricotz) wrote : | # |
Could you explain the need of renaming it to SourceList?
Cassidy James Blaede (cassidyjames) wrote : | # |
Rico Tzschichholz (ricotz) wrote : | # |
Ah, I see
Rico Tzschichholz (ricotz) wrote : | # |
@victored: I guess it should be "SourcesList" then not "SourceList"?
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.
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 ;)
Victor Martinez (victored) wrote : | # |
Some outstanding issues I've found:
1) Tree.start_
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.
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.
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.
lacthiea (lacthiea) wrote : | # |
I think prepending an item should be possible.
Tom Beckmann (tombeckmann) wrote : | # |
As discussed, a get_next (Item item) would be very helpful.
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.
Pim Vullers (pimvullers) wrote : | # |
Merge into granite trunk (0.1.1) is currently broken.
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!
Victor Martinez (victored) : | # |
- 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
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.
- 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
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 | +} |
quite some merge conflicts here ;)