[tracker/tracker-needle-improved-tagging: 6/6] tracker-needle: Added tracker-tags-view to edit tags as GtkVBox



commit a6f899c841b4000be50e225cd5ef1f20acf336e9
Author: Martyn Russell <martyn lanedo com>
Date:   Tue Oct 4 18:52:53 2011 +0100

    tracker-needle: Added tracker-tags-view to edit tags as GtkVBox
    
    Made available through right click on hits

 src/tracker-needle/Makefile.am            |   17 +-
 src/tracker-needle/tracker-needle.ui      |  141 ++++++-
 src/tracker-needle/tracker-needle.vala    |    4 +-
 src/tracker-needle/tracker-taglist.vala   |    2 +-
 src/tracker-needle/tracker-tags-view.vala |  734 +++++++++++++++++++++++++++++
 src/tracker-needle/tracker-view.vala      |   52 ++-
 6 files changed, 917 insertions(+), 33 deletions(-)
---
diff --git a/src/tracker-needle/Makefile.am b/src/tracker-needle/Makefile.am
index 5dbc7eb..0b85ee9 100644
--- a/src/tracker-needle/Makefile.am
+++ b/src/tracker-needle/Makefile.am
@@ -24,14 +24,15 @@ LDADD =                                            \
 	$(BUILD_LIBS)                                  \
 	$(TRACKER_NEEDLE_LIBS)
 
-tracker_needle_SOURCES =                           \
-	tracker-history.vala                           \
-	tracker-query.vala                             \
-	tracker-result-store.vala                      \
-	tracker-stats.vala                             \
-	tracker-taglist.vala                           \
-	tracker-utils.vala                             \
-	tracker-needle.vala                            \
+tracker_needle_SOURCES = \
+	tracker-history.vala \
+	tracker-query.vala \
+	tracker-result-store.vala \
+	tracker-stats.vala \
+	tracker-taglist.vala \
+	tracker-tags-view.vala \
+	tracker-utils.vala \
+	tracker-needle.vala \
 	tracker-view.vala
 
 @INTLTOOL_DESKTOP_RULE@
diff --git a/src/tracker-needle/tracker-needle.ui b/src/tracker-needle/tracker-needle.ui
index f0f9142..db73bfe 100644
--- a/src/tracker-needle/tracker-needle.ui
+++ b/src/tracker-needle/tracker-needle.ui
@@ -1,12 +1,136 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 2.12 -->
+  <requires lib="gtk+" version="2.24"/>
+  <!-- interface-naming-policy toplevel-contextual -->
   <object class="GtkListStore" id="liststore_search">
     <columns>
       <!-- column-name text -->
       <column type="gchararray"/>
     </columns>
   </object>
+  <object class="GtkVBox" id="vbox_tags">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="spacing">6</property>
+    <child>
+      <object class="GtkLabel" id="label_tag">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">_Set the tags you want to associate with the %d selected items:</property>
+        <property name="use_underline">True</property>
+        <property name="justify">fill</property>
+        <property name="wrap">True</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkHBox" id="hbox1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">12</property>
+        <child>
+          <object class="GtkEntry" id="entry_tag">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="has_focus">True</property>
+            <property name="invisible_char">â</property>
+            <property name="activates_default">True</property>
+            <property name="primary_icon_activatable">False</property>
+            <property name="secondary_icon_activatable">False</property>
+            <property name="primary_icon_sensitive">True</property>
+            <property name="secondary_icon_sensitive">True</property>
+            <signal name="activate" handler="tracker_tags_view_entry_tag_activate_cb" swapped="no"/>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkHBox" id="hbox2">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkButton" id="button_add">
+                <property name="label">gtk-add</property>
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="can_default">True</property>
+                <property name="has_default">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_action_appearance">False</property>
+                <property name="use_stock">True</property>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="button_remove">
+                <property name="label">gtk-remove</property>
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="use_action_appearance">False</property>
+                <property name="use_stock">True</property>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkScrolledWindow" id="scrolledwindow_tags">
+        <property name="width_request">200</property>
+        <property name="height_request">300</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="hscrollbar_policy">never</property>
+        <property name="vscrollbar_policy">automatic</property>
+        <property name="shadow_type">in</property>
+        <child>
+          <object class="GtkTreeView" id="treeview_tags">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="headers_visible">False</property>
+            <property name="headers_clickable">False</property>
+            <property name="enable_search">False</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+  </object>
   <object class="GtkWindow" id="window_needle">
     <property name="can_focus">False</property>
     <property name="title" translatable="yes">Desktop Search</property>
@@ -113,6 +237,8 @@
             </child>
             <child>
               <object class="GtkRadioToolButton" id="toolbutton_find_in_all">
+                <property name="can_focus">False</property>
+                <property name="use_action_appearance">False</property>
                 <property name="use_underline">True</property>
                 <property name="stock_id">gtk-select-all</property>
                 <property name="active">True</property>
@@ -164,17 +290,16 @@
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
                     <property name="yscale">0</property>
-                    <property name="left_padding">4</property>
-                    <property name="right_padding">4</property>
+                    <property name="left_padding">6</property>
+                    <property name="right_padding">6</property>
                     <child>
-                      <object class="GtkComboBoxText" id="comboboxtext_search">
+                      <object class="GtkComboBox" id="combobox_search">
                         <property name="visible">True</property>
-                        <property name="can_focus">True</property>
+                        <property name="can_focus">False</property>
                         <property name="has_focus">True</property>
-                        <property name="has_entry">True</property>
                         <property name="model">liststore_search</property>
-                        <accelerator key="s" signal="grab-focus" modifiers="GDK_CONTROL_MASK"/>
-                        <accelerator key="f" signal="grab-focus" modifiers="GDK_CONTROL_MASK"/>
+                        <property name="has_entry">True</property>
+                        <property name="entry_text_column">0</property>
                       </object>
                     </child>
                   </object>
diff --git a/src/tracker-needle/tracker-needle.vala b/src/tracker-needle/tracker-needle.vala
index 24a25e8..2655bf1 100644
--- a/src/tracker-needle/tracker-needle.vala
+++ b/src/tracker-needle/tracker-needle.vala
@@ -37,7 +37,7 @@ public class Tracker.Needle {
 	private ToggleToolButton find_in_titles;
 	private ToggleToolButton find_in_all;
 	private ToolItem search_entry;
-	private ComboBoxText search_list;
+	private ComboBox search_list;
 	private Entry search;
 	private Spinner spinner;
 	private ToolItem spinner_shell;
@@ -270,7 +270,7 @@ public class Tracker.Needle {
 		find_in_all.toggled.connect (find_in_toggled);
 
 		search_entry = builder.get_object ("toolitem_search_entry") as ToolItem;
-		search_list = builder.get_object ("comboboxtext_search") as ComboBoxText;
+		search_list = builder.get_object ("combobox_search") as ComboBox;
 		search = search_list.get_child () as Entry;
 		search.changed.connect (search_changed);
 		search.activate.connect (search_activated);
diff --git a/src/tracker-needle/tracker-taglist.vala b/src/tracker-needle/tracker-taglist.vala
index c5c9796..6f8c328 100644
--- a/src/tracker-needle/tracker-taglist.vala
+++ b/src/tracker-needle/tracker-taglist.vala
@@ -20,7 +20,7 @@
 using Gtk;
 
 public class Tracker.TagList : ScrolledWindow {
-	static Sparql.Connection connection;
+	private static Sparql.Connection connection;
 	private TreeView treeview;
 	private ListStore store;
 	private int offset;
diff --git a/src/tracker-needle/tracker-tags-view.vala b/src/tracker-needle/tracker-tags-view.vala
new file mode 100644
index 0000000..ad73c9f
--- /dev/null
+++ b/src/tracker-needle/tracker-tags-view.vala
@@ -0,0 +1,734 @@
+/*
+ * Copyright (C) 2011, Martyn Russell <martyn lanedo com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+using Gtk;
+using Tracker;
+
+private class TagData {
+	public TrackerTagsView tv;
+	public Cancellable cancellable;
+	public string tag_id;
+	public TreeIter iter;
+	public int items;
+	public bool update;
+	public bool selected;
+
+	public TagData (string? _tag_id, TreeIter? _iter, bool _update, bool _selected, int _items, TrackerTagsView _tv) {
+		debug ("Creating tag data");
+
+		tv = _tv;
+		cancellable = new Cancellable ();
+		tag_id = _tag_id;
+
+		if (_iter != null) {
+			iter = _iter;
+		}
+
+		items = _items;
+		update = _update;
+		selected = _selected;
+	}
+
+	~TagData () {
+		if (cancellable != null) {
+			cancellable.cancel ();
+			cancellable = null;
+		}
+	}
+}
+
+public class TrackerTagsView : VBox {
+	private Sparql.Connection connection;
+	private Cancellable cancellable;
+
+	private List<TagData> tag_data_requests;
+	private List<string> files;
+
+	private ListStore store;
+
+	private const string UI_FILE = "tracker-needle.ui";
+
+	private VBox vbox;
+	private Label label;
+	private Entry entry;
+	private Button button_add;
+	private Button button_remove;
+	private ScrolledWindow scrolled_window;
+	private TreeView view;
+
+	enum Col {
+		SELECTION,
+		TAG_ID,
+		TAG_NAME,
+		TAG_COUNT,
+		TAG_COUNT_VALUE,
+		N_COLUMNS
+	}
+
+	enum Selection {
+		INCONSISTENT = -1,
+		FALSE = 0,
+		TRUE = 1
+	}
+
+	public TrackerTagsView (List<string> _files) requires (_files != null) {
+		try {
+			connection = Sparql.Connection.get ();
+		} catch (GLib.Error e) {
+			warning ("Could not get Sparql connection: %s", e.message);
+		}
+
+		files = _files.copy ();
+
+		cancellable = new Cancellable ();
+
+		store = new ListStore (Col.N_COLUMNS,
+		                       typeof (int),      /* Selection type */
+		                       typeof (string),   /* Tag ID */
+		                       typeof (string),   /* Tag Name */
+		                       typeof (string),   /* Tag Count String */
+		                       typeof (int));     /* Tag Count */
+
+		create_ui ();
+	}
+
+	~TrackerTagsView () {
+		if (cancellable != null) {
+			cancellable.cancel ();
+			cancellable = null;
+		}
+
+		if (files != null) {
+			foreach (string url in files) {
+				url = null;
+			}
+
+			files = null;
+		}
+
+		if (tag_data_requests != null) {
+			foreach (TagData td in tag_data_requests) {
+				td = null;
+			};
+
+			tag_data_requests = null;
+		}
+	}
+
+	private void show_error_dialog (Error e) {
+		string str = e.message != null ? e.message : _("No error was given");
+
+		var msg = new MessageDialog (null,
+		                             DialogFlags.MODAL,
+		                             MessageType.ERROR,
+		                             ButtonsType.CLOSE,
+		                             "%s",
+		                             str);
+		msg.run ();
+	}
+
+	[CCode (instance_pos = -1)]
+	public void button_remove_clicked_cb (Button source) {
+		debug ("Remove clicked");
+
+		TreeIter iter;
+		TreeModel model;
+
+		TreeSelection selection = view.get_selection ();
+
+		if (selection.get_selected (out model, out iter)) {
+			string id;
+
+			model.get (iter, Col.TAG_ID, out id, -1);
+
+			TagData td = new TagData (id, iter, false, true, 1, this);
+			tag_data_requests.prepend (td);
+
+			remove_tag (td);
+		}
+	}
+
+	[CCode (instance_pos = -1)]
+	public void button_add_clicked_cb (Button source) {
+		debug ("Add clicked");
+		unowned string tag = entry.get_text ();
+		add_tag (tag);
+	}
+
+	[CCode (instance_pos = -1)]
+	public void entry_tag_activated_cb (Entry source) {
+		debug ("Entry activated");
+		((Widget) button_add).activate ();
+	}
+
+	[CCode (instance_pos = -1)]
+	public void entry_tag_changed_cb (Editable source) {
+		debug ("Entry changed");
+
+		unowned string tag = entry.get_text ();
+		TreeIter iter;
+
+		if (find_tag (tag, out iter)) {
+			((Widget) button_add).set_sensitive (false);
+		} else {
+			((Widget) button_add).set_sensitive ((tag != null && tag != ""));
+		}
+	}
+
+	[CCode (instance_pos = -1)]
+	public void treeview_tags_cell_toggled_cb (CellRendererToggle source, string path_string) {
+		debug ("Treeview row cell toggled");
+		TreePath path = new TreePath.from_string (path_string);
+		model_toggle_row (path);
+	}
+
+	[CCode (instance_pos = -1)]
+	public void treeview_tags_row_selected_cb (TreeSelection selection) {
+		debug ("Treeview row selected");
+
+		TreeIter iter;
+		TreeModel model;
+
+		if (selection.get_selected (out model, out iter)) {
+			button_remove.set_sensitive (true);
+		} else {
+			button_remove.set_sensitive (false);
+		}
+	}
+
+	[CCode (instance_pos = -1)]
+	public void treeview_tags_row_activated_cb (TreeView source, TreePath path, TreeViewColumn column) {
+		debug ("Treeview row activated");
+		model_toggle_row (path);
+	}
+
+	[CCode (instance_pos = -1)]
+	private void treeview_tags_toggle_cell_data_func (Gtk.CellLayout layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter) {
+		int selection;
+
+		model.get (iter, Col.SELECTION, out selection, -1);
+		((Gtk.CellRendererToggle) cell).set_active (selection == Selection.TRUE);
+		((Gtk.CellRendererToggle) cell).inconsistent = (selection == Selection.INCONSISTENT);
+	}
+
+	private void create_ui () {
+		var builder = new Gtk.Builder ();
+
+		try {
+			debug ("Trying to use UI file:'%s'", SRCDIR + UI_FILE);
+			builder.add_from_file (SRCDIR + UI_FILE);
+		} catch (GLib.Error e) {
+			//now the install location
+			try {
+				debug ("Trying to use UI file:'%s'", UIDIR + UI_FILE);
+				builder.add_from_file (UIDIR + UI_FILE);
+			} catch (GLib.Error e) {
+				var msg = new MessageDialog (null,
+				                             DialogFlags.MODAL,
+				                             MessageType.ERROR,
+				                             ButtonsType.CANCEL,
+				                             "Failed to load UI file, %s\n",
+				                             e.message);
+				msg.run ();
+				Gtk.main_quit();
+			}
+		}
+
+		// Get widgets from .ui file
+		vbox = builder.get_object ("vbox_tags") as VBox;
+		label = builder.get_object ("label_tag") as Label;
+		entry = builder.get_object ("entry_tag") as Entry;
+		button_add = builder.get_object ("button_add") as Button;
+		button_remove = builder.get_object ("button_remove") as Button;
+		scrolled_window = builder.get_object ("scrolled_window_tags") as ScrolledWindow;
+		view = builder.get_object ("treeview_tags") as TreeView;
+
+		// Set label based on files selected
+		string str = dngettext (null,
+		                        "_Set the tags you want to associate with the %d selected item:",
+		                        "_Set the tags you want to associate with the %d selected items:",
+		                        files.length ()).printf (files.length ());
+		label.set_text_with_mnemonic (str);
+
+		// Set up signal handlers (didn't work from glade)
+		((Editable) entry).changed.connect (entry_tag_changed_cb);
+		button_add.clicked.connect (button_add_clicked_cb);
+		button_remove.clicked.connect (button_remove_clicked_cb);
+
+		// Set up treeview
+		Gtk.TreeViewColumn col;
+		Gtk.CellRenderer renderer;
+
+		// List column: Tag
+		renderer = new CellRendererToggle ();
+		renderer.xpad = 5;
+		renderer.ypad = 5;
+		((CellRendererToggle) renderer).toggled.connect (treeview_tags_cell_toggled_cb);
+		((CellRendererToggle) renderer).set_radio (false);
+
+		col = new Gtk.TreeViewColumn ();
+		col.set_title ("-");
+		col.set_resizable (false);
+		col.set_sizing (Gtk.TreeViewColumnSizing.FIXED);
+		col.set_fixed_width (50);
+		col.pack_start (renderer, false);
+		col.set_cell_data_func (renderer, treeview_tags_toggle_cell_data_func);
+		view.append_column (col);
+
+		// List column: Name
+		renderer = new CellRendererText ();
+		renderer.xpad = 5;
+		renderer.ypad = 5;
+		((CellRendererText) renderer).ellipsize = Pango.EllipsizeMode.END;
+		((CellRendererText) renderer).ellipsize_set = true;
+
+		col = new Gtk.TreeViewColumn ();
+		col.set_title (_("Name"));
+		col.set_resizable (true);
+		col.set_sizing (Gtk.TreeViewColumnSizing.AUTOSIZE);
+		col.set_expand (true);
+		col.pack_start (renderer, true);
+		col.add_attribute (renderer, "text", Col.TAG_NAME);
+
+		view.append_column (col);
+
+		// List coumnn: Count
+		renderer = new CellRendererText ();
+		renderer.xpad = 5;
+		renderer.ypad = 5;
+
+		col = new Gtk.TreeViewColumn ();
+		col.set_title ("-");
+		col.set_resizable (false);
+		col.set_sizing (Gtk.TreeViewColumnSizing.FIXED);
+		col.set_fixed_width (50);
+		col.pack_end (renderer, false);
+		col.add_attribute (renderer, "text", Col.TAG_COUNT);
+
+		view.append_column (col);
+
+		// Selection signals
+		var selection = view.get_selection ();
+		selection.changed.connect (treeview_tags_row_selected_cb);
+
+		// Model details
+		TreeModel model = store as TreeModel;
+		TreeSortable sortable = model as TreeSortable;
+
+		view.set_model (model);
+		view.row_activated.connect (treeview_tags_row_activated_cb);
+		sortable.set_sort_column_id (2, SortType.ASCENDING);
+
+		// Add vbox to this widget's vbox
+		base.pack_start (vbox, true, true, 0);
+
+		query_tags ();
+	}
+
+	private async void model_toggle_row (TreePath path) {
+		TreeModel model;
+		TreeIter iter;
+		string id, tag;
+		int selection;
+
+		model = view.get_model ();
+
+		if (model.get_iter (out iter, path) == false) {
+			return;
+		}
+
+		model.get (iter,
+		           Col.SELECTION, out selection,
+		           Col.TAG_ID, out id,
+		           Col.TAG_NAME, out tag,
+		           -1);
+
+		int new_value = selection == Selection.FALSE ? Selection.TRUE : Selection.FALSE;
+
+		string tag_escaped = sparql_get_escaped_string (tag);
+		string filter = sparql_get_filter_string (null);
+		string query = null;
+
+		TagData td;
+
+		// NOTE: Was if (selection) ...
+		if (new_value != Selection.FALSE) {
+			// NB: ?f is used in filter.
+			query = "INSERT {
+			           ?urn nao:hasTag ?label
+			         } WHERE {
+			           ?urn nie:url ?f .
+			           ?label nao:prefLabel %s .
+			           %s
+			         }".printf (tag_escaped, filter);
+		} else {
+			// NB: ?f is used in filter.
+			query = "DELETE {
+			           ?urn nao:hasTag ?label
+			         } WHERE {
+			           ?urn nie:url ?f .
+			           ?label nao:prefLabel %s .
+			           %s
+			         }".printf (tag_escaped, filter);
+
+			/* Check if there are any files left with this tag and
+			 * remove tag if not.
+			 */
+			td = new TagData (id, iter, false, true, 1, this);
+			tag_data_requests.prepend (td);
+
+			query_files_for_tag_id (td);
+		}
+
+		filter = null;
+		tag_escaped = null;
+
+		if (connection == null) {
+			warning ("Can't update tags, no SPARQL connection available");
+			return;
+		}
+
+		debug ("Updating tags for uris");
+
+		entry.set_sensitive (false);
+
+		td = new TagData (id, iter, true, (new_value != Selection.FALSE), 1, this);
+		tag_data_requests.prepend (td);
+
+		try {
+			yield connection.update_async (query, Priority.DEFAULT, td.cancellable);
+
+			debug ("Updated tags");
+			update_tag_data (td);
+
+			entry.set_text ("");
+		} catch (GLib.Error e) {
+			warning ("Could not run Sparql update query: %s", e.message);
+			show_error_dialog (e);
+		}
+
+		tag_data_requests.remove (td);
+		td = null;
+
+		entry.set_sensitive (true);
+	}
+
+	private bool find_tag (string tag, out TreeIter iter) {
+		if (tag == null || tag == "") {
+			return false;
+		}
+
+		TreeModel model = view.get_model ();
+		TreeIter found_iter = { 0 };
+		bool found = false;
+
+		model.foreach ((model, path, foreach_iter) => {
+			string foreach_tag;
+
+			model.get (foreach_iter, Col.TAG_NAME, out foreach_tag, -1);
+
+			if (foreach_tag != null && foreach_tag == tag) {
+				found = true;
+				found_iter = foreach_iter;
+				return true;
+			}
+
+			return false;
+		});
+
+		if (found == true) {
+			iter = found_iter;
+			return true;
+		}
+
+		return false;
+	}
+
+	private async void remove_tag (TagData td) {
+		if (connection == null) {
+			warning ("Can't remove tag '%s', no SPARQL connection available", td.tag_id);
+			tag_data_requests.remove (td);
+			td = null;
+			return;
+		}
+
+		string query = "DELETE { <%s> a rdfs:Resource }".printf (td.tag_id);
+
+		try {
+			yield connection.update_async (query, Priority.DEFAULT, td.cancellable);
+
+			debug ("Tag removed");
+			store.remove (td.iter);
+		} catch (GLib.Error e) {
+			warning ("Could not run Sparql update query: %s", e.message);
+			show_error_dialog (e);
+		}
+
+		tag_data_requests.remove (td);
+		td = null;
+	}
+
+	private async void add_tag (string tag) {
+		string query = null;
+
+		if (connection == null) {
+			warning ("Can't add tag '%s', no SPARQL connection available", tag);
+			return;
+		}
+
+		entry.set_sensitive (false);
+
+		if (files.length () > 0) {
+			query = "";
+
+			string filter = sparql_get_filter_string (null);
+			string tag_escaped = sparql_get_escaped_string (tag);
+
+			foreach (string url in files) {
+				query += "INSERT {
+				            _:file a nie:DataObject ;
+				             nie:url '%s'
+				          } WHERE {
+				            OPTIONAL {
+				               ?file a nie:DataObject ;
+				               nie:url '%s'
+				            } .
+				            FILTER (!bound(?file))
+				          }".printf (url, url);
+			}
+
+			query += "INSERT {
+			            _:tag a nao:Tag;
+			            nao:prefLabel %s .
+			          } WHERE {
+			            OPTIONAL {
+			              ?tag a nao:Tag ;
+			              nao:prefLabel %s
+			            } .
+			            FILTER (!bound(?tag))
+			          }
+			          INSERT {
+			            ?urn nao:hasTag ?label
+			          } WHERE {
+			            ?urn nie:url ?f .
+			            ?label nao:prefLabel %s
+			            %s
+			          }".printf (tag_escaped, tag_escaped, tag_escaped, filter);
+		} else {
+			string tag_label_escaped = sparql_get_escaped_string (tag);
+
+			query = "INSERT {
+			           _:tag a nao:Tag ;
+			           nao:prefLabel %s .
+			         } WHERE {
+			           OPTIONAL {
+			             ?tag a nao:Tag ;
+			             nao:prefLabel %s
+			           } .
+			           FILTER (!bound(?tag))
+			         }".printf (tag_label_escaped, tag_label_escaped);
+		}
+
+		TagData td = new TagData (null, null, false, true, (int) files.length (), this);
+		tag_data_requests.prepend (td);
+
+		try {
+			yield connection.update_async (query, Priority.DEFAULT, td.cancellable);
+
+			debug ("Updated tags");
+			update_tag_data (td);
+
+			// Only do this on success
+			entry.set_text ("");
+		} catch (GLib.Error e) {
+			warning ("Could not run Sparql update query: %s", e.message);
+			show_error_dialog (e);
+		}
+
+		tag_data_requests.remove (td);
+		td = null;
+
+		entry.set_sensitive (true);
+	}
+
+	private void update_tag_data (TagData td) {
+		unowned string tag = entry.get_text ();
+
+		if (td.update == false) {
+			TreeIter iter;
+
+			debug ("Setting tag selection state to ON (new)");
+
+			store.append (out iter);
+			store.set (iter,
+			           Col.TAG_ID, td.tag_id,
+			           Col.TAG_NAME, tag,
+			           Col.TAG_COUNT, "%d".printf (td.items),
+			           Col.TAG_COUNT_VALUE, td.items,
+			           Col.SELECTION, Selection.TRUE,
+			           -1);
+		} else if (td.selected == true) {
+			debug ("Setting tag selection state to ON");
+
+			store.set (td.iter, Col.SELECTION, Selection.TRUE, -1);
+
+			tag_data_requests.prepend (td);
+			query_files_for_tag_id (td);
+		} else {
+			debug ("Setting tag selection state to FALSE");
+
+			store.set (td.iter, Col.SELECTION, Selection.FALSE, -1);
+
+			tag_data_requests.prepend (td);
+			query_files_for_tag_id (td);
+		}
+	}
+
+	private async void query_tags () {
+		// Get all tags
+		string query = "SELECT ?urn ?label WHERE { ?urn a nao:Tag ; nao:prefLabel ?label . } ORDER BY ?label";
+
+		debug ("Clearing tags in store");
+		store.clear ();
+
+		try {
+			Sparql.Cursor cursor = yield connection.query_async (query, null);
+
+			while (yield cursor.next_async ()) {
+
+				debug ("Adding all tags...");
+
+				unowned string id = cursor.get_string (0);
+				unowned string label = cursor.get_string (1);
+
+				debug ("  Adding tag id:'%s' with label:'%s' to store", id, label);
+
+				TreeIter iter;
+				store.append (out iter);
+
+				store.set (iter,
+				           Col.TAG_ID, id,
+				           Col.TAG_NAME, label,
+				           Col.SELECTION, Selection.FALSE,
+				           -1);
+
+				TagData td = new TagData (id, iter, false, true, 1, this);
+				tag_data_requests.prepend (td);
+
+				query_files_for_tag_id (td);
+			}
+		} catch (GLib.Error e) {
+			warning ("Could not run Sparql query: %s", e.message);
+			show_error_dialog (e);
+		}
+	}
+
+	private async void query_files_for_tag_id (TagData td) {
+		if (connection == null) {
+			warning ("Can't query files for tag id '%s', no SPARQL connection available", td.tag_id);
+			tag_data_requests.remove (td);
+			td = null;
+			return;
+		}
+
+		string query = "SELECT ?url WHERE { ?urn a rdfs:Resource ; nie:url ?url ; nao:hasTag <%s> . }".printf (td.tag_id);
+
+		try {
+			Sparql.Cursor cursor = yield connection.query_async (query, td.cancellable);
+
+			uint has_tag_in_selection = 0;
+			uint files_with_tag = 0;
+			uint files_selected = files.length ();
+
+			while (yield cursor.next_async ()) {
+				files_with_tag++;
+
+				foreach (string url in files) {
+					unowned string url_returned = cursor.get_string (0);
+
+					debug ("--> '%s' vs '%s'", url, url_returned);
+
+					if (url_returned == null) {
+						continue;
+					}
+
+					if (url_returned == url) {
+						has_tag_in_selection++;
+						break;
+					}
+				}
+			}
+
+			debug ("Querying files with tag, in selection:%ld, in total:%ld, selected:%ld",
+			       has_tag_in_selection, files_with_tag, files_selected);
+
+			if (has_tag_in_selection == 0) {
+				store.set (td.iter, Col.SELECTION, Selection.FALSE, -1);
+			} else if (files_selected != has_tag_in_selection) {
+				store.set (td.iter, Col.SELECTION, Selection.INCONSISTENT, -1);
+			} else {
+				store.set (td.iter, Col.SELECTION, Selection.TRUE, -1);
+			}
+
+			string str = "%ld".printf (files_with_tag);
+			store.set (td.iter, Col.TAG_COUNT, str, Col.TAG_COUNT_VALUE, files_with_tag, -1);
+
+			debug ("Tags for file updated");
+		} catch (GLib.Error e) {
+			warning ("Could not run Sparql query: %s", e.message);
+			show_error_dialog (e);
+		}
+
+		tag_data_requests.remove (td);
+		td = null;
+	}
+
+	private string sparql_get_filter_string (string? tag) requires (files != null) {
+		string filter = "FILTER (";
+
+		if (tag != null && tag != "") {
+			filter += "(";
+		}
+
+		bool first = true;
+
+		foreach (string url in files) {
+			if (!first) {
+				filter += " || ";
+			}
+
+			filter += "?f = \"%s\"".printf (url);
+			first = false;
+		}
+
+		if (tag != null && tag != "") {
+			filter += ") && ?t = <%s>".printf (tag);
+		}
+
+		filter += ")";
+
+		return filter;
+	}
+
+	private string sparql_get_escaped_string (string str) requires (str != null) {
+		string escaped = Sparql.escape_string (str);
+		return "\"%s\"".printf (escaped);
+	}
+}
+
diff --git a/src/tracker-needle/tracker-view.vala b/src/tracker-needle/tracker-view.vala
index 0c51af3..f9ff95d 100644
--- a/src/tracker-needle/tracker-view.vala
+++ b/src/tracker-needle/tracker-view.vala
@@ -452,14 +452,8 @@ public class Tracker.View : ScrolledWindow {
 		var separator = new SeparatorMenuItem ();
 		context_menu.append (separator);
 
-		item = new MenuItem.with_mnemonic (_("_Add Tag..."));
-		item.activate.connect (context_menu_tag_add_clicked);
-		item.sensitive = false;
-		context_menu.append (item);
-
-		item = new MenuItem.with_mnemonic (_("_Remove Tag..."));
-		item.activate.connect (context_menu_tag_remove_clicked);
-		item.sensitive = false;
+		item = new MenuItem.with_mnemonic (_("_Tags..."));
+		item.activate.connect (context_menu_tags_clicked);
 		context_menu.append (item);
 
 		context_menu.show_all ();
@@ -522,12 +516,42 @@ public class Tracker.View : ScrolledWindow {
 		tracker_model_launch_selected_parent_dir (model, path, 1);
 	}
 
-	private void context_menu_tag_add_clicked () {
-		warning ("Not yet implemented");
-	}
+	private void context_menu_tags_clicked () {
+		TreeModel model = get_model ();
+		TreePath path = get_selected_path ();
+		TreeIter iter;
+		model.get_iter (out iter, path);
 
-	private void context_menu_tag_remove_clicked () {
-		warning ("Not yet implemented");
-	}
+		weak string uri;
+		model.get (iter, 1, out uri);
 
+		if (uri == null) {
+			return;
+		}
+
+		debug ("Showing tags dialog for uri:'%s'", uri);
+
+		// Create dialog and embed vbox.
+		Dialog dialog = new Dialog.with_buttons (_("Tags"),
+		                                         (Window) this.get_toplevel (),
+		                                         DialogFlags.MODAL | DialogFlags.DESTROY_WITH_PARENT,
+		                                         Stock.CLOSE, ResponseType.CLOSE,
+		                                         null);
+		dialog.set_default_size (400, 300);
+		dialog.border_width = 12;
+		dialog.response.connect (() => {
+			dialog.destroy ();
+		});
+
+		List<string> files = null;
+		files.prepend (uri);
+		VBox vbox = new TrackerTagsView (files);
+
+		var content = dialog.get_content_area () as Box;
+		content.pack_start (vbox, true, true, 6);
+		content.spacing = 10;
+
+		((Widget) dialog).show_all ();
+		dialog.run ();
+	}
 }



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]