← Back to team overview

elementaryart team mailing list archive

[Merge] lp:~sgringwe/granite/fastview into lp:granite

 

Scott Ringwelski has proposed merging lp:~sgringwe/granite/fastview into lp:granite.

Requested reviews:
  elementary Pantheon team (elementary-pantheon)

For more details, see:
https://code.launchpad.net/~sgringwe/granite/fastview/+merge/94295

Added a FastView and FastModel class to allow users to quickly search, populate, and sort a TreeView using HashTables.
-- 
https://code.launchpad.net/~sgringwe/granite/fastview/+merge/94295
Your team elementaryart (old) is subscribed to branch lp:granite.
=== modified file 'demo/main.vala'
--- demo/main.vala	2012-02-19 20:14:25 +0000
+++ demo/main.vala	2012-02-22 23:24:19 +0000
@@ -21,6 +21,17 @@
 using Granite.Widgets;
 using Granite.Services;
 
+public class TestObject : GLib.Object {
+	public int id { get; set; }
+	public string name { get; set; }
+	public string address { get; set; }
+	public string phone { get; set; }
+	
+	public TestObject(int id) {
+		this.id = id;
+	}
+}
+
 public class Granite.Demo : Granite.Application
 {
     construct
@@ -204,11 +215,140 @@
         }
         contractor_tab.add(text_view);
         contractor_tab.add(new ContractorView("file:///home/user/file.txt", "text/plain"));
-
+		
+		/* FastModel */
+		var model_tab = new Gtk.VBox (false, 0);
+        notebook.append_page (model_tab, new Gtk.Label ("FastModel"));
+        
+        var types = new List<Type>();
+        types.append(typeof(int));
+        types.append(typeof(string));
+        types.append(typeof(string));
+        types.append(typeof(string));
+        var view = new Granite.Widgets.FastView(types);
+        
+        // add columns
+        view.insert_column_with_attributes(-1, "id", new Gtk.CellRendererText(), "text", 0, null);
+        view.insert_column_with_attributes(-1, "name", new Gtk.CellRendererText(), "text", 1, null);
+        view.insert_column_with_attributes(-1, "address", new Gtk.CellRendererText(), "text", 2, null);
+        view.insert_column_with_attributes(-1, "phone", new Gtk.CellRendererText(), "text", 3, null);
+        
+        for(int i = 0; i < 4; ++i) {
+			view.get_column(i).sizing = Gtk.TreeViewColumnSizing.FIXED;
+			view.get_column(i).fixed_width = 70;
+			view.get_column(i).clickable = true;
+			view.get_column(i).sort_column_id = i;
+		}
+        
+        // set funcs specific to TestObject type
+        view.set_compare_func(view_compare_func);
+        view.set_search_func(view_search_func);
+        view.set_value_func(view_value_func);
+		
+		var scroll = new Gtk.ScrolledWindow(null, null);
+		scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+		scroll.add(view);
+		model_tab.pack_start(scroll, true, true, 0);
+		
+		//var set_model_list = new Gtk.Button.with_label("Populate 20,000 items");
+		//model_tab.pack_end(set_model_list, false, true, 0);
+		//set_model_list.clicked.connect( ()=> {
+			var to_add = new HashTable<int, GLib.Object>(null, null);
+			var to_remove = new HashTable<int, GLib.Object>(null, null);
+			for(int i = 0; i < 20000; ++i) {
+				TestObject to = new TestObject(i);
+				
+				var val = (i + 20000).to_string();
+				to.name = val;
+				to.address = val;
+				to.phone = val;
+				to_add.set(i, to);
+				
+				if(i > 10000)
+					to_remove.set(i, to);
+			}
+			
+			view.set_table(to_add);
+		//});
+		var remove_all = new Gtk.Button.with_label("Remove last 10,000");
+		model_tab.pack_end(remove_all, false, true, 0);
+		remove_all.clicked.connect( ()=> {
+			view.remove_objects(to_remove.get_values());
+		});
+		var add_more = new Gtk.Button.with_label("Add 20,000 to start");
+		model_tab.pack_end(add_more, false, true, 0);
+		add_more.clicked.connect( ()=> {
+			var odds = new List<GLib.Object>();
+			for(int i = 0; i < 20000; ++i) {
+				TestObject to = new TestObject(i);
+				
+				var val = (i).to_string();
+				to.name = val;
+				to.address = val;
+				to.phone = val;
+				odds.append(to);
+			}
+			
+			view.add_objects(odds);
+		});
+			
+		var search_list = new Gtk.Entry();
+		model_tab.pack_start(search_list, false, true, 0);
+		search_list.changed.connect( () => {
+			view.do_search(search_list.get_text());
+		});
+		
         /* window properties */
         win.show_all();
         win.resize(800, 600);
     }
+    
+    int view_compare_func (int col, Gtk.SortType dir, GLib.Object a_o, GLib.Object b_o) {
+		int rv = 0;
+		
+		TestObject a = (TestObject)a_o;
+		TestObject b = (TestObject)b_o;
+		
+		if(col == 0)
+			rv = a.id - b.id;
+		else if(col == 1)
+			rv = (a.name > b.name) ? 1 : -1;
+		else if(col == 2)
+			rv = (a.address > b.address) ? 1 : -1;
+		else
+			rv = (a.phone > b.phone) ? 1 : -1;
+			
+		if(dir == Gtk.SortType.DESCENDING)
+			rv = (rv > 0) ? -1 : 1;
+			
+		return rv;
+	}
+	
+	void view_search_func (string search, HashTable<int, Object> table, ref HashTable<int, Object> show) {
+		int show_index = 0;
+		
+		for(int i = 0; i < table.size(); ++i) {
+			if(((TestObject)table.get(i)).name.contains(search))
+				show.set(show_index++, table.get(i));
+		}
+	}
+	
+	Value view_value_func (int column, GLib.Object o) {
+		Value val;
+		
+		TestObject to = (TestObject)o;
+		
+		if(column == 0)
+			val = to.id;
+		else if(column == 1)
+			val = to.name;
+		else if(column == 2)
+			val = to.address;
+		else
+			val = to.phone;
+		
+		return val;
+	}
 
     public static int main(string[] args)
     {

=== modified file 'lib/CMakeLists.txt'
--- lib/CMakeLists.txt	2012-02-19 13:58:41 +0000
+++ lib/CMakeLists.txt	2012-02-22 23:24:19 +0000
@@ -68,7 +68,12 @@
     Widgets/ToolButtonWithMenu.vala
     Widgets/PopOver.vala
     Widgets/ContractorView.vala
+<<<<<<< TREE
     Widgets/ContractorMenu.vala
+=======
+    Widgets/FastView.vala
+    Widgets/FastModel.vala
+>>>>>>> MERGE-SOURCE
     Main.vala
     config.vapi
 CUSTOM_VAPIS

=== added file 'lib/Widgets/FastModel.vala'
--- lib/Widgets/FastModel.vala	1970-01-01 00:00:00 +0000
+++ lib/Widgets/FastModel.vala	2012-02-22 23:24:19 +0000
@@ -0,0 +1,200 @@
+using Gtk;
+
+/** Since this class is not publicly facing (the FastView is public part),
+ * this model is low level and optimized. We are not worried about stupid
+ * users here.
+**/
+public class Granite.Widgets.FastModel : GLib.Object, TreeModel, TreeSortable {
+	int stamp; // all iters must match this
+	
+	/* data storage variables */
+	HashTable<int, GLib.Object> rows; // internal id -> user specified object
+	List<Type> columns;
+	
+	private int sort_column_id;
+	private SortType sort_direction;
+	
+	/* user specific function for get_value() */
+	public delegate Value ValueReturnFunc (int column, GLib.Object o);
+	private ValueReturnFunc value_func;
+	
+	public signal void reorder_requested (int column, Gtk.SortType direction);
+
+	/** Initialize data storage, columns, etc. **/
+	public FastModel (List<Type> column_types) {
+		columns = column_types.copy();
+		rows = new HashTable<int, GLib.Object>(null, null);
+		
+		sort_column_id = -2;
+		sort_direction = SortType.ASCENDING;
+		
+		stamp = (int)GLib.Random.next_int();
+	}
+
+	public Type get_column_type (int col) {
+		return columns.nth_data(col);
+	}
+
+	public TreeModelFlags get_flags () {
+		return TreeModelFlags.LIST_ONLY;
+	}
+
+	public bool get_iter (out TreeIter iter, TreePath path) {
+		iter = TreeIter();
+		int path_index = path.get_indices()[0];
+		if(rows.size() == 0 || path_index < 0 || path_index >= rows.size() || rows.get(path_index) == null)
+			return false;
+
+		iter.stamp = this.stamp;
+		iter.user_data = (void*)path_index;
+
+		return true;
+	}
+
+	public int get_n_columns () {
+		return (int)columns.length();
+	}
+
+	public TreePath? get_path (TreeIter iter) {
+		return new TreePath.from_string (((int)iter.user_data).to_string());
+	}
+	
+	public void get_value (TreeIter iter, int column, out Value val) {
+		if(iter.stamp != this.stamp || column < 0 || column >= get_n_columns()) {
+			val = Value(get_column_type(column));
+			return;
+		}
+		
+		if(!((int)iter.user_data >= rows.size())) {
+			var object = rows.get(((int)iter.user_data));
+			val = value_func(column, object);
+		}
+	}
+
+	public bool iter_children (out TreeIter iter, TreeIter? parent) {
+		iter = TreeIter();
+		return false;
+	}
+
+	public bool iter_has_child (TreeIter iter) {
+
+		return false;
+	}
+
+	public int iter_n_children (TreeIter? iter) {
+		if(iter == null)
+			return (int)rows.size();
+
+		return 0;
+	}
+
+	public bool iter_next (ref TreeIter iter) {
+		if(iter.stamp != this.stamp)
+			return false;
+
+		iter.user_data = (void*)(((int)iter.user_data) + 1);
+
+		if(((int)iter.user_data) >= rows.size())
+			return false;
+
+		return true;
+	}
+
+	public bool iter_nth_child (out TreeIter iter, TreeIter? parent, int n) {
+		iter = TreeIter();
+
+		if(n < 0 || n >= rows.size() || parent != null)
+			return false;
+
+		iter.stamp = this.stamp;
+		iter.user_data = (void*)n;
+
+		return true;
+	}
+
+	public bool iter_parent (out TreeIter iter, TreeIter child) {
+		iter = TreeIter();
+
+		return false;
+	}
+	
+	public void append (out TreeIter iter) {
+		iter = TreeIter();
+
+		rows.set((int)rows.size(), new GLib.Object());
+		iter.stamp = this.stamp;
+		iter.user_data = (void*)rows.size;
+	}
+	
+	public void remove (TreeIter iter) {
+		if(iter.stamp != this.stamp)
+			return;
+
+		var path = new TreePath.from_string(((int)iter.user_data).to_string());
+		rows.remove((int)iter.user_data);
+		row_deleted(path);
+		
+		// TODO: swap all indices > this iter's index down to maintain that
+		// the table has row ids 0..n where n is rows.size (consecutive ids)
+	}
+	
+	// Not applicable to this custom treemodel
+	public new void set (TreeIter iter, ...) {
+		return;
+	}
+	
+	public void ref_node (TreeIter iter) {}
+	public void unref_node (TreeIter iter) {}
+
+	/** The beauty of this custom model. This tree model is simply a visual
+	 * representation of a HashTable of objects. Before calling this
+	 * method, the user should set tree_view.set_model(null). After
+	 * calling this, set the tree_view.set_model(fast_model). By doing this
+	 * the treeview will not listen for append events and will recalculate
+	 * and draw when the model is re-added.
+	 * 
+	 * @objects Must be a consecutive ordered hash table with indexes 
+	 * 0-n where n is size of the hashtable (no gaps).
+	**/
+	public void set_table (HashTable<int, GLib.Object> table) {
+		rows = table;
+	}
+	
+	/** Crucial. Must be set by user. Allows for this model to be abstract
+	 * by allowing the user to specify the function that returns values
+	 * based on the object (row) and column. **/
+	public void set_value_func (ValueReturnFunc func) {
+		value_func = func;
+	}
+	
+	/** The following functions are for implementing TreeSortable. We pass
+	 * off responsibility to sort things to the view.
+	**/
+	public bool get_sort_column_id (out int sort_column_id, out SortType order) {
+		sort_column_id = this.sort_column_id;
+		order = this.sort_direction;
+		
+		return true;
+	}
+	
+	public void set_sort_column_id (int column, SortType order) {
+		sort_column_id = column;
+		sort_direction = order;
+		
+		reorder_requested(column, order);
+		sort_column_changed();
+	}
+	
+	/** The following functions are only here to implement TreeSortable **/
+	public bool has_default_sort_func () {
+		return true; // place holder. not used.
+	}
+	
+	public void set_sort_func (int sort_column_id, owned TreeIterCompareFunc sort_func) {
+		// place holder. not used.
+	}
+
+	public void set_default_sort_func (owned TreeIterCompareFunc sort_func) {
+		// place holder. not used.
+	}
+}

=== added file 'lib/Widgets/FastView.vala'
--- lib/Widgets/FastView.vala	1970-01-01 00:00:00 +0000
+++ lib/Widgets/FastView.vala	2012-02-22 23:24:19 +0000
@@ -0,0 +1,151 @@
+using Gtk;
+
+public class Granite.Widgets.FastView : TreeView {
+	FastModel fm;
+	List<Type> columns;
+	HashTable<int, GLib.Object> table; // is not the same object as showing.
+	HashTable<int, GLib.Object> showing; // should never point to table.
+	
+	/* sortable stuff */
+	public delegate int SortCompareFunc (int sort_column_id, Gtk.SortType sort_direction, GLib.Object a, GLib.Object b);
+	private int sort_column_id;
+	private SortType sort_direction;
+	private SortCompareFunc compare_func;
+	
+	// search stuff
+	string last_search;
+	public delegate void ViewSearchFunc (string search, HashTable<int, GLib.Object> table, ref HashTable<int, GLib.Object> showing);
+	private ViewSearchFunc search_func;
+	
+	public FastView (List<Type> types) {
+		columns = types.copy();
+		table = new HashTable<int, GLib.Object>(null, null);
+		showing = new HashTable<int, GLib.Object>(null, null);
+		fm = new FastModel(types);
+		
+		sort_column_id = -2;
+		sort_direction = SortType.ASCENDING;
+		last_search = "";
+		
+		fm.reorder_requested.connect(reorder_requested);
+		
+		set_table(table);
+	}
+	
+	public void set_value_func(FastModel.ValueReturnFunc func) {
+		fm.set_value_func(func);
+	}
+	
+	public void set_table (HashTable<int, GLib.Object> table) {
+		this.table = table;
+		do_search(null);
+	}
+	
+	public void remove_objects (List<GLib.Object> objects) {
+		int index = 0;
+		var new_table = new HashTable<int, Object>(null, null);
+		for(int i = 0; i < table.size(); ++i) {
+			Object o;
+			if((o = table.get(i)) != null && objects.find(o) == null) {
+				new_table.set(index++, o);
+			}
+		}
+		
+		set_table(new_table);
+	}
+	
+	public void add_objects (List<GLib.Object> objects) {
+		foreach(var o in objects) {
+			table.set((int)table.size(), o);
+		}
+		
+		resort();
+	}
+	
+	public void set_search_func (ViewSearchFunc func) {
+		search_func = func;
+	}
+	
+	public void do_search (string? search) {
+		TreePath showing_path;
+		TreeViewColumn showing_col;
+		int cell_x, cell_y;
+		get_path_at_pos(1, 1, out showing_path, out showing_col, out cell_x, out cell_y);
+		
+		set_model(null);
+		showing.remove_all();
+		
+		if(search != null)
+			last_search = search;
+		
+		if(last_search == "") {
+			for(int i = 0; i < table.size(); ++i) {
+				showing.set(i, table.get(i));
+			}
+		}
+		else {
+			search_func(last_search, table, ref showing);
+		}
+		
+		fm.set_table(showing);
+		set_model(fm);
+		
+		if(showing_path != null && showing_col != null)
+			scroll_to_cell(showing_path, showing_col, false, 0f, 0f);
+		else
+			warning("Could not scroll to previous position.\n");
+	}
+	
+	/** Sorting is done in the treeview, not the model. That way the whole
+	 * table is sorted and ready to go and we do not need to resort every
+	 * time we repopulate/search the model
+	**/
+	public void set_sort_column_id (int sort_column_id, SortType order) {
+		reorder_requested(sort_column_id, order);
+	}
+	
+	void reorder_requested (int column, Gtk.SortType direction) {
+		if(column == sort_column_id && direction == sort_direction)
+			return;
+		
+		sort_column_id = column;
+		sort_direction = direction;
+		
+		quicksort(0, (int)(table.size() - 1));
+		do_search(null);
+	}
+	
+	void resort () {
+		quicksort(0, (int)(table.size() - 1));
+		do_search(null);
+	}
+	
+	public void set_compare_func (SortCompareFunc func) {
+		compare_func = func;
+	}
+	
+	// TODO: Is slow.
+	void swap (int a, int b) {
+		GLib.Object temp = table.get(a);
+		table.set(a, table.get(b));
+		table.set(b, temp);
+	}
+	
+	public void quicksort (int start, int end) {
+		GLib.Object pivot = table.get((start+end)/2);
+		int i = start;
+		int j = end;
+		
+		do {
+			while(i < end && compare_func (sort_column_id, sort_direction, table.get(i), pivot) < 0) ++i;
+			while(j > start && compare_func (sort_column_id, sort_direction, table.get(j), pivot) > 0) --j;
+			if(i <= j) {
+				swap(i, j);
+				++i; --j;
+			}
+		} while(i <= j);
+		
+		if(start < j)	quicksort (start, j);
+		if(i < end)		quicksort (i, end);
+	}
+}


Follow ups