Merge lp:~sgringwe/granite/fastview into lp:~elementary-pantheon/granite/granite
- fastview
- Merge into granite
Proposed by
Scott Ringwelski
Status: | Rejected |
---|---|
Rejected by: | kay van der Zander |
Proposed branch: | lp:~sgringwe/granite/fastview |
Merge into: | lp:~elementary-pantheon/granite/granite |
Diff against target: |
541 lines (+497/-1) (has conflicts) 4 files modified
demo/main.vala (+141/-1) lib/CMakeLists.txt (+5/-0) lib/Widgets/FastModel.vala (+200/-0) lib/Widgets/FastView.vala (+151/-0) Text conflict in lib/CMakeLists.txt |
To merge this branch: | bzr merge lp:~sgringwe/granite/fastview |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
elementary Pantheon team | Pending | ||
Review via email: mp+94295@code.launchpad.net |
Commit message
Description of the change
Added a FastView and FastModel class to allow users to quickly search, populate, and sort a TreeView using HashTables.
To post a comment you must log in.
Revision history for this message
Scott Ringwelski (sgringwe) wrote : | # |
Revision history for this message
Victor Martinez (victored) wrote : | # |
This widget is really fast. Judging by the comment in the source code it seems to me that FastModel should be internal. Have you added any improvement since you proposed the merge?
Revision history for this message
Scott Ringwelski (sgringwe) wrote : | # |
Hmmm ya, I have.
On Sun, May 13, 2012 at 5:33 PM, Victor Eduardo <email address hidden>wrote:
> This widget is really fast. Judging by the comment in the source code it
> seems to me that FastModel should be internal. Have you added any
> improvement since you proposed the merge?
> --
> https:/
> You are the owner of lp:~sgringwe/granite/fastview.
>
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'demo/main.vala' |
2 | --- demo/main.vala 2012-02-19 20:14:25 +0000 |
3 | +++ demo/main.vala 2012-02-22 23:24:19 +0000 |
4 | @@ -21,6 +21,17 @@ |
5 | using Granite.Widgets; |
6 | using Granite.Services; |
7 | |
8 | +public class TestObject : GLib.Object { |
9 | + public int id { get; set; } |
10 | + public string name { get; set; } |
11 | + public string address { get; set; } |
12 | + public string phone { get; set; } |
13 | + |
14 | + public TestObject(int id) { |
15 | + this.id = id; |
16 | + } |
17 | +} |
18 | + |
19 | public class Granite.Demo : Granite.Application |
20 | { |
21 | construct |
22 | @@ -204,11 +215,140 @@ |
23 | } |
24 | contractor_tab.add(text_view); |
25 | contractor_tab.add(new ContractorView("file:///home/user/file.txt", "text/plain")); |
26 | - |
27 | + |
28 | + /* FastModel */ |
29 | + var model_tab = new Gtk.VBox (false, 0); |
30 | + notebook.append_page (model_tab, new Gtk.Label ("FastModel")); |
31 | + |
32 | + var types = new List<Type>(); |
33 | + types.append(typeof(int)); |
34 | + types.append(typeof(string)); |
35 | + types.append(typeof(string)); |
36 | + types.append(typeof(string)); |
37 | + var view = new Granite.Widgets.FastView(types); |
38 | + |
39 | + // add columns |
40 | + view.insert_column_with_attributes(-1, "id", new Gtk.CellRendererText(), "text", 0, null); |
41 | + view.insert_column_with_attributes(-1, "name", new Gtk.CellRendererText(), "text", 1, null); |
42 | + view.insert_column_with_attributes(-1, "address", new Gtk.CellRendererText(), "text", 2, null); |
43 | + view.insert_column_with_attributes(-1, "phone", new Gtk.CellRendererText(), "text", 3, null); |
44 | + |
45 | + for(int i = 0; i < 4; ++i) { |
46 | + view.get_column(i).sizing = Gtk.TreeViewColumnSizing.FIXED; |
47 | + view.get_column(i).fixed_width = 70; |
48 | + view.get_column(i).clickable = true; |
49 | + view.get_column(i).sort_column_id = i; |
50 | + } |
51 | + |
52 | + // set funcs specific to TestObject type |
53 | + view.set_compare_func(view_compare_func); |
54 | + view.set_search_func(view_search_func); |
55 | + view.set_value_func(view_value_func); |
56 | + |
57 | + var scroll = new Gtk.ScrolledWindow(null, null); |
58 | + scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); |
59 | + scroll.add(view); |
60 | + model_tab.pack_start(scroll, true, true, 0); |
61 | + |
62 | + //var set_model_list = new Gtk.Button.with_label("Populate 20,000 items"); |
63 | + //model_tab.pack_end(set_model_list, false, true, 0); |
64 | + //set_model_list.clicked.connect( ()=> { |
65 | + var to_add = new HashTable<int, GLib.Object>(null, null); |
66 | + var to_remove = new HashTable<int, GLib.Object>(null, null); |
67 | + for(int i = 0; i < 20000; ++i) { |
68 | + TestObject to = new TestObject(i); |
69 | + |
70 | + var val = (i + 20000).to_string(); |
71 | + to.name = val; |
72 | + to.address = val; |
73 | + to.phone = val; |
74 | + to_add.set(i, to); |
75 | + |
76 | + if(i > 10000) |
77 | + to_remove.set(i, to); |
78 | + } |
79 | + |
80 | + view.set_table(to_add); |
81 | + //}); |
82 | + var remove_all = new Gtk.Button.with_label("Remove last 10,000"); |
83 | + model_tab.pack_end(remove_all, false, true, 0); |
84 | + remove_all.clicked.connect( ()=> { |
85 | + view.remove_objects(to_remove.get_values()); |
86 | + }); |
87 | + var add_more = new Gtk.Button.with_label("Add 20,000 to start"); |
88 | + model_tab.pack_end(add_more, false, true, 0); |
89 | + add_more.clicked.connect( ()=> { |
90 | + var odds = new List<GLib.Object>(); |
91 | + for(int i = 0; i < 20000; ++i) { |
92 | + TestObject to = new TestObject(i); |
93 | + |
94 | + var val = (i).to_string(); |
95 | + to.name = val; |
96 | + to.address = val; |
97 | + to.phone = val; |
98 | + odds.append(to); |
99 | + } |
100 | + |
101 | + view.add_objects(odds); |
102 | + }); |
103 | + |
104 | + var search_list = new Gtk.Entry(); |
105 | + model_tab.pack_start(search_list, false, true, 0); |
106 | + search_list.changed.connect( () => { |
107 | + view.do_search(search_list.get_text()); |
108 | + }); |
109 | + |
110 | /* window properties */ |
111 | win.show_all(); |
112 | win.resize(800, 600); |
113 | } |
114 | + |
115 | + int view_compare_func (int col, Gtk.SortType dir, GLib.Object a_o, GLib.Object b_o) { |
116 | + int rv = 0; |
117 | + |
118 | + TestObject a = (TestObject)a_o; |
119 | + TestObject b = (TestObject)b_o; |
120 | + |
121 | + if(col == 0) |
122 | + rv = a.id - b.id; |
123 | + else if(col == 1) |
124 | + rv = (a.name > b.name) ? 1 : -1; |
125 | + else if(col == 2) |
126 | + rv = (a.address > b.address) ? 1 : -1; |
127 | + else |
128 | + rv = (a.phone > b.phone) ? 1 : -1; |
129 | + |
130 | + if(dir == Gtk.SortType.DESCENDING) |
131 | + rv = (rv > 0) ? -1 : 1; |
132 | + |
133 | + return rv; |
134 | + } |
135 | + |
136 | + void view_search_func (string search, HashTable<int, Object> table, ref HashTable<int, Object> show) { |
137 | + int show_index = 0; |
138 | + |
139 | + for(int i = 0; i < table.size(); ++i) { |
140 | + if(((TestObject)table.get(i)).name.contains(search)) |
141 | + show.set(show_index++, table.get(i)); |
142 | + } |
143 | + } |
144 | + |
145 | + Value view_value_func (int column, GLib.Object o) { |
146 | + Value val; |
147 | + |
148 | + TestObject to = (TestObject)o; |
149 | + |
150 | + if(column == 0) |
151 | + val = to.id; |
152 | + else if(column == 1) |
153 | + val = to.name; |
154 | + else if(column == 2) |
155 | + val = to.address; |
156 | + else |
157 | + val = to.phone; |
158 | + |
159 | + return val; |
160 | + } |
161 | |
162 | public static int main(string[] args) |
163 | { |
164 | |
165 | === modified file 'lib/CMakeLists.txt' |
166 | --- lib/CMakeLists.txt 2012-02-19 13:58:41 +0000 |
167 | +++ lib/CMakeLists.txt 2012-02-22 23:24:19 +0000 |
168 | @@ -68,7 +68,12 @@ |
169 | Widgets/ToolButtonWithMenu.vala |
170 | Widgets/PopOver.vala |
171 | Widgets/ContractorView.vala |
172 | +<<<<<<< TREE |
173 | Widgets/ContractorMenu.vala |
174 | +======= |
175 | + Widgets/FastView.vala |
176 | + Widgets/FastModel.vala |
177 | +>>>>>>> MERGE-SOURCE |
178 | Main.vala |
179 | config.vapi |
180 | CUSTOM_VAPIS |
181 | |
182 | === added file 'lib/Widgets/FastModel.vala' |
183 | --- lib/Widgets/FastModel.vala 1970-01-01 00:00:00 +0000 |
184 | +++ lib/Widgets/FastModel.vala 2012-02-22 23:24:19 +0000 |
185 | @@ -0,0 +1,200 @@ |
186 | +using Gtk; |
187 | + |
188 | +/** Since this class is not publicly facing (the FastView is public part), |
189 | + * this model is low level and optimized. We are not worried about stupid |
190 | + * users here. |
191 | +**/ |
192 | +public class Granite.Widgets.FastModel : GLib.Object, TreeModel, TreeSortable { |
193 | + int stamp; // all iters must match this |
194 | + |
195 | + /* data storage variables */ |
196 | + HashTable<int, GLib.Object> rows; // internal id -> user specified object |
197 | + List<Type> columns; |
198 | + |
199 | + private int sort_column_id; |
200 | + private SortType sort_direction; |
201 | + |
202 | + /* user specific function for get_value() */ |
203 | + public delegate Value ValueReturnFunc (int column, GLib.Object o); |
204 | + private ValueReturnFunc value_func; |
205 | + |
206 | + public signal void reorder_requested (int column, Gtk.SortType direction); |
207 | + |
208 | + /** Initialize data storage, columns, etc. **/ |
209 | + public FastModel (List<Type> column_types) { |
210 | + columns = column_types.copy(); |
211 | + rows = new HashTable<int, GLib.Object>(null, null); |
212 | + |
213 | + sort_column_id = -2; |
214 | + sort_direction = SortType.ASCENDING; |
215 | + |
216 | + stamp = (int)GLib.Random.next_int(); |
217 | + } |
218 | + |
219 | + public Type get_column_type (int col) { |
220 | + return columns.nth_data(col); |
221 | + } |
222 | + |
223 | + public TreeModelFlags get_flags () { |
224 | + return TreeModelFlags.LIST_ONLY; |
225 | + } |
226 | + |
227 | + public bool get_iter (out TreeIter iter, TreePath path) { |
228 | + iter = TreeIter(); |
229 | + int path_index = path.get_indices()[0]; |
230 | + if(rows.size() == 0 || path_index < 0 || path_index >= rows.size() || rows.get(path_index) == null) |
231 | + return false; |
232 | + |
233 | + iter.stamp = this.stamp; |
234 | + iter.user_data = (void*)path_index; |
235 | + |
236 | + return true; |
237 | + } |
238 | + |
239 | + public int get_n_columns () { |
240 | + return (int)columns.length(); |
241 | + } |
242 | + |
243 | + public TreePath? get_path (TreeIter iter) { |
244 | + return new TreePath.from_string (((int)iter.user_data).to_string()); |
245 | + } |
246 | + |
247 | + public void get_value (TreeIter iter, int column, out Value val) { |
248 | + if(iter.stamp != this.stamp || column < 0 || column >= get_n_columns()) { |
249 | + val = Value(get_column_type(column)); |
250 | + return; |
251 | + } |
252 | + |
253 | + if(!((int)iter.user_data >= rows.size())) { |
254 | + var object = rows.get(((int)iter.user_data)); |
255 | + val = value_func(column, object); |
256 | + } |
257 | + } |
258 | + |
259 | + public bool iter_children (out TreeIter iter, TreeIter? parent) { |
260 | + iter = TreeIter(); |
261 | + return false; |
262 | + } |
263 | + |
264 | + public bool iter_has_child (TreeIter iter) { |
265 | + |
266 | + return false; |
267 | + } |
268 | + |
269 | + public int iter_n_children (TreeIter? iter) { |
270 | + if(iter == null) |
271 | + return (int)rows.size(); |
272 | + |
273 | + return 0; |
274 | + } |
275 | + |
276 | + public bool iter_next (ref TreeIter iter) { |
277 | + if(iter.stamp != this.stamp) |
278 | + return false; |
279 | + |
280 | + iter.user_data = (void*)(((int)iter.user_data) + 1); |
281 | + |
282 | + if(((int)iter.user_data) >= rows.size()) |
283 | + return false; |
284 | + |
285 | + return true; |
286 | + } |
287 | + |
288 | + public bool iter_nth_child (out TreeIter iter, TreeIter? parent, int n) { |
289 | + iter = TreeIter(); |
290 | + |
291 | + if(n < 0 || n >= rows.size() || parent != null) |
292 | + return false; |
293 | + |
294 | + iter.stamp = this.stamp; |
295 | + iter.user_data = (void*)n; |
296 | + |
297 | + return true; |
298 | + } |
299 | + |
300 | + public bool iter_parent (out TreeIter iter, TreeIter child) { |
301 | + iter = TreeIter(); |
302 | + |
303 | + return false; |
304 | + } |
305 | + |
306 | + public void append (out TreeIter iter) { |
307 | + iter = TreeIter(); |
308 | + |
309 | + rows.set((int)rows.size(), new GLib.Object()); |
310 | + iter.stamp = this.stamp; |
311 | + iter.user_data = (void*)rows.size; |
312 | + } |
313 | + |
314 | + public void remove (TreeIter iter) { |
315 | + if(iter.stamp != this.stamp) |
316 | + return; |
317 | + |
318 | + var path = new TreePath.from_string(((int)iter.user_data).to_string()); |
319 | + rows.remove((int)iter.user_data); |
320 | + row_deleted(path); |
321 | + |
322 | + // TODO: swap all indices > this iter's index down to maintain that |
323 | + // the table has row ids 0..n where n is rows.size (consecutive ids) |
324 | + } |
325 | + |
326 | + // Not applicable to this custom treemodel |
327 | + public new void set (TreeIter iter, ...) { |
328 | + return; |
329 | + } |
330 | + |
331 | + public void ref_node (TreeIter iter) {} |
332 | + public void unref_node (TreeIter iter) {} |
333 | + |
334 | + /** The beauty of this custom model. This tree model is simply a visual |
335 | + * representation of a HashTable of objects. Before calling this |
336 | + * method, the user should set tree_view.set_model(null). After |
337 | + * calling this, set the tree_view.set_model(fast_model). By doing this |
338 | + * the treeview will not listen for append events and will recalculate |
339 | + * and draw when the model is re-added. |
340 | + * |
341 | + * @objects Must be a consecutive ordered hash table with indexes |
342 | + * 0-n where n is size of the hashtable (no gaps). |
343 | + **/ |
344 | + public void set_table (HashTable<int, GLib.Object> table) { |
345 | + rows = table; |
346 | + } |
347 | + |
348 | + /** Crucial. Must be set by user. Allows for this model to be abstract |
349 | + * by allowing the user to specify the function that returns values |
350 | + * based on the object (row) and column. **/ |
351 | + public void set_value_func (ValueReturnFunc func) { |
352 | + value_func = func; |
353 | + } |
354 | + |
355 | + /** The following functions are for implementing TreeSortable. We pass |
356 | + * off responsibility to sort things to the view. |
357 | + **/ |
358 | + public bool get_sort_column_id (out int sort_column_id, out SortType order) { |
359 | + sort_column_id = this.sort_column_id; |
360 | + order = this.sort_direction; |
361 | + |
362 | + return true; |
363 | + } |
364 | + |
365 | + public void set_sort_column_id (int column, SortType order) { |
366 | + sort_column_id = column; |
367 | + sort_direction = order; |
368 | + |
369 | + reorder_requested(column, order); |
370 | + sort_column_changed(); |
371 | + } |
372 | + |
373 | + /** The following functions are only here to implement TreeSortable **/ |
374 | + public bool has_default_sort_func () { |
375 | + return true; // place holder. not used. |
376 | + } |
377 | + |
378 | + public void set_sort_func (int sort_column_id, owned TreeIterCompareFunc sort_func) { |
379 | + // place holder. not used. |
380 | + } |
381 | + |
382 | + public void set_default_sort_func (owned TreeIterCompareFunc sort_func) { |
383 | + // place holder. not used. |
384 | + } |
385 | +} |
386 | |
387 | === added file 'lib/Widgets/FastView.vala' |
388 | --- lib/Widgets/FastView.vala 1970-01-01 00:00:00 +0000 |
389 | +++ lib/Widgets/FastView.vala 2012-02-22 23:24:19 +0000 |
390 | @@ -0,0 +1,151 @@ |
391 | +using Gtk; |
392 | + |
393 | +public class Granite.Widgets.FastView : TreeView { |
394 | + FastModel fm; |
395 | + List<Type> columns; |
396 | + HashTable<int, GLib.Object> table; // is not the same object as showing. |
397 | + HashTable<int, GLib.Object> showing; // should never point to table. |
398 | + |
399 | + /* sortable stuff */ |
400 | + public delegate int SortCompareFunc (int sort_column_id, Gtk.SortType sort_direction, GLib.Object a, GLib.Object b); |
401 | + private int sort_column_id; |
402 | + private SortType sort_direction; |
403 | + private SortCompareFunc compare_func; |
404 | + |
405 | + // search stuff |
406 | + string last_search; |
407 | + public delegate void ViewSearchFunc (string search, HashTable<int, GLib.Object> table, ref HashTable<int, GLib.Object> showing); |
408 | + private ViewSearchFunc search_func; |
409 | + |
410 | + public FastView (List<Type> types) { |
411 | + columns = types.copy(); |
412 | + table = new HashTable<int, GLib.Object>(null, null); |
413 | + showing = new HashTable<int, GLib.Object>(null, null); |
414 | + fm = new FastModel(types); |
415 | + |
416 | + sort_column_id = -2; |
417 | + sort_direction = SortType.ASCENDING; |
418 | + last_search = ""; |
419 | + |
420 | + fm.reorder_requested.connect(reorder_requested); |
421 | + |
422 | + set_table(table); |
423 | + } |
424 | + |
425 | + public void set_value_func(FastModel.ValueReturnFunc func) { |
426 | + fm.set_value_func(func); |
427 | + } |
428 | + |
429 | + public void set_table (HashTable<int, GLib.Object> table) { |
430 | + this.table = table; |
431 | + do_search(null); |
432 | + } |
433 | + |
434 | + public void remove_objects (List<GLib.Object> objects) { |
435 | + int index = 0; |
436 | + var new_table = new HashTable<int, Object>(null, null); |
437 | + for(int i = 0; i < table.size(); ++i) { |
438 | + Object o; |
439 | + if((o = table.get(i)) != null && objects.find(o) == null) { |
440 | + new_table.set(index++, o); |
441 | + } |
442 | + } |
443 | + |
444 | + set_table(new_table); |
445 | + } |
446 | + |
447 | + public void add_objects (List<GLib.Object> objects) { |
448 | + foreach(var o in objects) { |
449 | + table.set((int)table.size(), o); |
450 | + } |
451 | + |
452 | + resort(); |
453 | + } |
454 | + |
455 | + public void set_search_func (ViewSearchFunc func) { |
456 | + search_func = func; |
457 | + } |
458 | + |
459 | + public void do_search (string? search) { |
460 | + TreePath showing_path; |
461 | + TreeViewColumn showing_col; |
462 | + int cell_x, cell_y; |
463 | + get_path_at_pos(1, 1, out showing_path, out showing_col, out cell_x, out cell_y); |
464 | + |
465 | + set_model(null); |
466 | + showing.remove_all(); |
467 | + |
468 | + if(search != null) |
469 | + last_search = search; |
470 | + |
471 | + if(last_search == "") { |
472 | + for(int i = 0; i < table.size(); ++i) { |
473 | + showing.set(i, table.get(i)); |
474 | + } |
475 | + } |
476 | + else { |
477 | + search_func(last_search, table, ref showing); |
478 | + } |
479 | + |
480 | + fm.set_table(showing); |
481 | + set_model(fm); |
482 | + |
483 | + if(showing_path != null && showing_col != null) |
484 | + scroll_to_cell(showing_path, showing_col, false, 0f, 0f); |
485 | + else |
486 | + warning("Could not scroll to previous position.\n"); |
487 | + } |
488 | + |
489 | + /** Sorting is done in the treeview, not the model. That way the whole |
490 | + * table is sorted and ready to go and we do not need to resort every |
491 | + * time we repopulate/search the model |
492 | + **/ |
493 | + public void set_sort_column_id (int sort_column_id, SortType order) { |
494 | + reorder_requested(sort_column_id, order); |
495 | + } |
496 | + |
497 | + void reorder_requested (int column, Gtk.SortType direction) { |
498 | + if(column == sort_column_id && direction == sort_direction) |
499 | + return; |
500 | + |
501 | + sort_column_id = column; |
502 | + sort_direction = direction; |
503 | + |
504 | + quicksort(0, (int)(table.size() - 1)); |
505 | + do_search(null); |
506 | + } |
507 | + |
508 | + void resort () { |
509 | + quicksort(0, (int)(table.size() - 1)); |
510 | + do_search(null); |
511 | + } |
512 | + |
513 | + public void set_compare_func (SortCompareFunc func) { |
514 | + compare_func = func; |
515 | + } |
516 | + |
517 | + // TODO: Is slow. |
518 | + void swap (int a, int b) { |
519 | + GLib.Object temp = table.get(a); |
520 | + table.set(a, table.get(b)); |
521 | + table.set(b, temp); |
522 | + } |
523 | + |
524 | + public void quicksort (int start, int end) { |
525 | + GLib.Object pivot = table.get((start+end)/2); |
526 | + int i = start; |
527 | + int j = end; |
528 | + |
529 | + do { |
530 | + while(i < end && compare_func (sort_column_id, sort_direction, table.get(i), pivot) < 0) ++i; |
531 | + while(j > start && compare_func (sort_column_id, sort_direction, table.get(j), pivot) > 0) --j; |
532 | + if(i <= j) { |
533 | + swap(i, j); |
534 | + ++i; --j; |
535 | + } |
536 | + } while(i <= j); |
537 | + |
538 | + if(start < j) quicksort (start, j); |
539 | + if(i < end) quicksort (i, end); |
540 | + } |
541 | +} |
Testing with 20,000 items allowed for quick sorting, searching and populating (<1 second). Test with 1,000,000 items took some time to initially populate (6 seconds or so). After it was populated, search was almost instant (1 second to undo search back to ""). Sorting was not attempted, but would likely be very slow due to deep recursive calls on quicksort.