Merge lp:~sgringwe/granite/fastview into lp:~elementary-pantheon/granite/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
Reviewer Review Type Date Requested Status
elementary Pantheon team Pending
Review via email: mp+94295@code.launchpad.net

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 :

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.

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://code.launchpad.net/~sgringwe/granite/fastview/+merge/94295
> You are the owner of lp:~sgringwe/granite/fastview.
>

Unmerged revisions

185. By Scott <email address hidden>

users may now quickly add and remove objects from a fastview

184. By Scott <email address hidden>

more work on fastview/fastmodel

183. By Scott <email address hidden>

fastview stuff

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+}

Subscribers

People subscribed via source and target branches