Tag Query Patch



With all the talk and excitement over Nat's tagging patch, I wanted to
bring my query patch back up to get feedback, patches, etc.

The patch allows for arbitrary queries with AND, OR, and NOT operators,
doesn't use a dialog box (it looks very much like the "Find" bar that
pops up when you do a simple query now) and supports modifying the query
via the context menu, program menu, and drag and drop.

It also supports shift-F10 context menus and the patch includes a class
that makes supporting that much simpler - so hopefully we can fix the
other context menus in F-Spot soon.

Issues it has include
- if you query based on a category, it should count any child of that
category as a match as well.
- discoverability; I've thought about putting buttons at the top of the
tag list that make querying more discoverable
- tags in the query without photos are invisible
- triggers a few too many reloads

You can see a screenshot that gives you some idea of it's functionality
here: http://bugzilla.gnome.org/attachment.cgi?id=54566&action=view

In addition to applying the patch with patch -p0 < query.patch from the
src directory, you'll need to copy the attached f-spot-not.png image
into the icons directory.

Peace,

Gabriel Burt

diff -rup ../../f-spot-HEAD/src/DirectoryAdaptor.cs DirectoryAdaptor.cs
--- ../../f-spot-HEAD/src/DirectoryAdaptor.cs	2005-11-10 00:10:28.000000000 -0600
+++ DirectoryAdaptor.cs	2005-11-10 01:59:05.000000000 -0600
@@ -81,7 +81,7 @@ namespace FSpot {
 		public override void Reload () 
 		{
 			System.Collections.Hashtable ht = new System.Collections.Hashtable ();
-			Photo [] photos = query.Store.Query (null, null);
+			Photo [] photos = query.Store.Query (null, null, null);
 			
 			foreach (Photo p in photos) {
 				if (ht.Contains (p.DirectoryPath)) {
diff -rup ../../f-spot-HEAD/src/f-spot.glade f-spot.glade
--- ../../f-spot-HEAD/src/f-spot.glade	2005-11-10 00:10:29.000000000 -0600
+++ f-spot.glade	2005-11-10 02:34:13.000000000 -0600
@@ -7169,9 +7169,17 @@ Photo Details</property>
 		  </child>
 
 		  <child>
-		    <widget class="GtkMenuItem" id="find_tag">
+		    <widget class="GtkMenuItem" id="include_tag">
 		      <property name="visible">True</property>
-		      <property name="label" translatable="yes">Find by _Tag</property>
+		      <property name="label" translatable="yes">_Include Tag</property>
+		      <property name="use_underline">True</property>
+		    </widget>
+		  </child>
+		  
+		  <child>
+		    <widget class="GtkMenuItem" id="require_tag">
+		      <property name="visible">True</property>
+		      <property name="label" translatable="yes">_Require Tag</property>
 		      <property name="use_underline">True</property>
 		    </widget>
 		  </child>
diff -rup ../../f-spot-HEAD/src/MainWindow.cs MainWindow.cs
--- ../../f-spot-HEAD/src/MainWindow.cs	2005-11-10 00:10:28.000000000 -0600
+++ MainWindow.cs	2005-11-10 02:02:43.000000000 -0600
@@ -74,7 +74,8 @@ public class MainWindow {
 
 	[Glade.Widget] MenuItem attach_tag;
 	[Glade.Widget] MenuItem remove_tag;
-	[Glade.Widget] MenuItem find_tag;
+	[Glade.Widget] MenuItem require_tag;
+	[Glade.Widget] MenuItem include_tag;
 	
 	[Glade.Widget] Scale zoom_scale;
 
@@ -82,6 +83,11 @@ public class MainWindow {
 
 	[Glade.Widget] Gtk.Image near_image;
 	[Glade.Widget] Gtk.Image far_image;
+	
+	// Not used in this version, only for adding buttons at the top
+	// of the tag list for including or requiring a tag for discoverability sake
+	[Glade.Widget] Gtk.ToggleButton tag_query_include;
+	[Glade.Widget] Gtk.ToggleButton tag_query_require;
 
 	Gtk.Toolbar toolbar;
 
@@ -97,6 +103,7 @@ public class MainWindow {
 	FSpot.FullScreenView fsview;
 	FSpot.PhotoQuery query;
 	FSpot.GroupSelector group_selector;
+	FSpot.QueryWidget query_widget;
 	
 	FSpot.Delay slide_delay;
 	
@@ -105,9 +112,10 @@ public class MainWindow {
 	bool write_metadata = false;
 
 	// Drag and Drop
-	enum TargetType {
+	public enum TargetType {
 		UriList,
 		TagList,
+		TagQueryItem,
 		PhotoList,
 		RootWindow
 	};
@@ -129,6 +137,7 @@ public class MainWindow {
 
 	private static TargetEntry [] tag_dest_target_table = new TargetEntry [] {
 		new TargetEntry ("application/x-fspot-photos", 0, (uint) TargetType.PhotoList),
+		new TargetEntry ("application/x-fspot-tag-query-item", 0, (uint) TargetType.TagQueryItem),
 		new TargetEntry ("text/uri-list", 0, (uint) TargetType.UriList),
 		new TargetEntry ("application/x-fspot-tags", 0, (uint) TargetType.TagList),
 	};
@@ -182,6 +191,7 @@ public class MainWindow {
 				  DragAction.Copy | DragAction.Move ); 
 
 		tag_selection_widget.ButtonPressEvent += HandleTagSelectionButtonPressEvent;
+		tag_selection_widget.RowActivated += HandleTagSelectionRowActivated;
 
 		info_box = new InfoBox ();
 		info_box.VersionIdChanged += HandleInfoBoxVersionIdChange;
@@ -205,9 +215,9 @@ public class MainWindow {
 		view_vbox.PackStart (group_selector, false, false, 0);
 		view_vbox.ReorderChild (group_selector, 0);
 
-		FSpot.QueryDisplay query_display = new FSpot.QueryDisplay (query, tag_selection_widget);
-		view_vbox.PackStart (query_display, false, false, 0);
-		view_vbox.ReorderChild (query_display, 1);
+		query_widget = new FSpot.QueryWidget (query, db, tag_selection_widget);
+		view_vbox.PackStart (query_widget, false, false, 0);
+		view_vbox.ReorderChild (query_widget, 1);
 
 		icon_view = new QueryView (query);
 		LoadPreference (Preferences.THUMBNAIL_WIDTH);
@@ -237,8 +247,11 @@ public class MainWindow {
 		near_image.SetFromStock ("f-spot-stock_near", IconSize.SmallToolbar);
 		far_image.SetFromStock ("f-spot-stock_far", IconSize.SmallToolbar);
 
-		menu = new TagMenu (find_tag, db.Tags);
-		menu.TagSelected += HandleFindTagMenuSelected;
+		menu = new TagMenu (include_tag, db.Tags);
+		menu.TagSelected += HandleFindTagIncluded;
+		
+		menu = new TagMenu (require_tag, db.Tags);
+		menu.TagSelected += HandleFindTagRequired;
 
 		PhotoTagMenu pmenu = new PhotoTagMenu ();
 		pmenu.TagSelected += HandleRemoveTagMenuSelected;
@@ -281,7 +294,8 @@ public class MainWindow {
 
 		main_window.DeleteEvent += HandleDeleteEvent;
 
-		query_display.HandleChanged (query);
+		query_widget.HandleChanged (query);
+		query_widget.Hide ();
 
 		if (Toplevel == null)
 			Toplevel = this;
@@ -380,6 +394,16 @@ public class MainWindow {
 		}
 	}
 
+	public bool TagIncluded (Tag tag)
+	{
+		return query_widget.TagIncluded (tag);
+	}
+	
+	public bool TagRequired (Tag tag)
+	{
+		return query_widget.TagRequired (tag);
+	}
+	
 	void HandleViewNotebookSwitchPage (object sender, SwitchPageArgs args)
 	{
 		switch (view_notebook.CurrentPage) {
@@ -515,6 +539,11 @@ public class MainWindow {
 			args.RetVal = true;
 		}
 	}
+	
+	void HandleTagSelectionRowActivated (object sender, RowActivatedArgs args)
+	{
+		query_widget.Include (new Tag [] {tag_selection_widget.TagByPath (args.Path)});
+	}
 
 	void HandleTagSelectionDragBegin (object sender, DragBeginArgs args)
 	{
@@ -558,7 +587,7 @@ public class MainWindow {
 		
 			args.SelectionData.Set (targets[0], 8, data, data.Length);
 			break;
-		} 
+		}
 	}
 
 	void HandleTagSelectionDragDrop (object sender, DragDropArgs args)
@@ -591,6 +620,8 @@ public class MainWindow {
 			foreach (int num in SelectedIds ()) {
 				AddTagExtended (num, tags);
 			}
+
+			query_widget.PhotoTagsChanged (tags);
 			break;
 		case (uint)TargetType.UriList:
 			UriList list = new UriList (args.SelectionData);
@@ -960,19 +991,64 @@ public class MainWindow {
 		foreach (int num in SelectedIds ()) {
 			AddTagExtended (num, new Tag [] {t});
 		}
+
+		query_widget.PhotoTagsChanged (new Tag[] {t});
+	}
+	
+	void HandleFindTagIncluded (Tag t)
+	{
+		query_widget.Include (new Tag [] {t});
+	}
+	
+	void HandleFindTagRequired (Tag t)
+	{
+		query_widget.Require (new Tag [] {t});
 	}
 	
-	void HandleFindTagMenuSelected (Tag t)
+	public void HandleTagIncludeToggled (object sender, EventArgs args)
 	{
-		tag_selection_widget.TagSelection = new Tag [] {t};
+		if (tag_query_include.Active)
+			HandleIncludeTag (null, null);
+		else
+			HandleUnIncludeTag (null, null);
+	}
+	
+	public void HandleTagRequireToggled (object sender, EventArgs args)
+	{
+		if (tag_query_require.Active)
+			HandleRequireTag (null, null);
+		else
+			HandleUnRequireTag (null, null);
+	}
+	
+	public void HandleIncludeTag (object sender, EventArgs args)
+	{
+		query_widget.Include (tag_selection_widget.TagHighlight ());
+	}
+	
+	public void HandleUnIncludeTag (object sender, EventArgs args)
+	{
+		query_widget.UnInclude (tag_selection_widget.TagHighlight ());
+	}
+	
+	public void HandleRequireTag (object sender, EventArgs args)
+	{
+		query_widget.Require (tag_selection_widget.TagHighlight ());
 	}
 
+	public void HandleUnRequireTag (object sender, EventArgs args)
+	{
+		query_widget.UnRequire (tag_selection_widget.TagHighlight ());
+	}
+	
 	public void HandleRemoveTagMenuSelected (Tag t)
 	{
 		foreach (int num in SelectedIds ()) {
 			query.Photos [num].RemoveTag (t);
 			query.Commit (num);
 		}
+			
+		query_widget.PhotoTagsChanged (new Tag [] {t});
 	}
 
 	//
@@ -1328,6 +1404,8 @@ public class MainWindow {
 		foreach (int num in ids) {
 			AddTagExtended (num, tags);
 		}
+		
+		query_widget.PhotoTagsChanged (tags);
 	}
 
 	public void HandleRemoveTagCommand (object obj, EventArgs args)
@@ -1338,6 +1416,8 @@ public class MainWindow {
 			query.Photos [num].RemoveTag (tags);
 			query.Commit (num);
 		}
+
+		query_widget.PhotoTagsChanged (tags);
 	}
 
 	public void HandleEditSelectedTag (object obj, EventArgs args)
@@ -1463,7 +1543,7 @@ public class MainWindow {
 		else
 			group_selector.Show ();
 	}
-
+	
 	void HandleDisplayInfoSidebar (object sender, EventArgs args)
 	{
 		if (info_vpaned.Visible)
@@ -1966,6 +2046,9 @@ public class MainWindow {
 
 		attach_tag.Sensitive = active_selection;
 		remove_tag.Sensitive = active_selection;
+		
+		//tag_query_include.Sensitive = tag_sensitive;
+		//tag_query_require.Sensitive = tag_sensitive;
 
 		rotate_left.Sensitive = active_selection;
 		rotate_right.Sensitive = active_selection;
@@ -1988,5 +2071,13 @@ public class MainWindow {
 		remove_tag_from_selection.Sensitive = tag_sensitive && active_selection;
 	}
 
+	public void GetWidgetPosition(Widget widget, out int x, out int y)
+	{
+		main_window.GdkWindow.GetOrigin(out x, out y);
+		
+		x += widget.Allocation.X;
+		y += widget.Allocation.Y;
+	}
+
 }
 
diff -rup ../../f-spot-HEAD/src/Makefile.am Makefile.am
--- ../../f-spot-HEAD/src/Makefile.am	2005-11-10 00:10:28.000000000 -0600
+++ Makefile.am	2005-11-10 02:20:06.000000000 -0600
@@ -65,6 +65,7 @@ F_SPOT_CSDISTFILES =				\
 	$(srcdir)/PixbufUtils.cs		\
 	$(srcdir)/PixbufCache.cs		\
 	$(srcdir)/PixelBuffer.cs		\
+	$(srcdir)/PopupManager.cs		\
 	$(srcdir)/Preferences.cs 		\
 	$(srcdir)/PreviewPopup.cs 		\
 	$(srcdir)/PrintDialog.cs		\
@@ -80,6 +81,7 @@ F_SPOT_CSDISTFILES =				\
 	$(srcdir)/TagCommands.cs		\
 	$(srcdir)/TagMenu.cs			\
 	$(srcdir)/TagPopup.cs			\
+	$(srcdir)/TagQueryWidget.cs		\
 	$(srcdir)/TagSelectionWidget.cs		\
 	$(srcdir)/TagStore.cs			\
 	$(srcdir)/TagView.cs			\
@@ -94,6 +96,7 @@ F_SPOT_CSDISTFILES =				\
 	$(srcdir)/ThumbnailCommand.cs		\
 	$(srcdir)/QueryDisplay.cs		\
 	$(srcdir)/QueryView.cs			\
+	$(srcdir)/QueryWidget.cs		\
 	$(srcdir)/ZoomUtils.cs			\
 	$(srcdir)/GPhotoCamera.cs		\
 	$(srcdir)/CameraSelectionDialog.cs	\
@@ -126,6 +129,7 @@ RESOURCES =										\
 	-resource:$(top_srcdir)/icons/f-spot-hidden.png,f-spot-hidden.png 		\
 	-resource:$(top_srcdir)/icons/f-spot-loading.png,f-spot-loading.png		\
 	-resource:$(top_srcdir)/icons/f-spot-logo.png,f-spot-logo.png			\
+	-resource:$(top_srcdir)/icons/f-spot-not.png,f-spot-not.png			\
 	-resource:$(top_srcdir)/icons/f-spot-other.png,f-spot-other.png			\
 	-resource:$(top_srcdir)/icons/f-spot-people.png,f-spot-people.png 		\
 	-resource:$(top_srcdir)/icons/f-spot-places.png,f-spot-places.png 		\
Only in : Makefile.in
diff -rup ../../f-spot-HEAD/src/PhotoQuery.cs PhotoQuery.cs
--- ../../f-spot-HEAD/src/PhotoQuery.cs	2005-11-10 00:10:28.000000000 -0600
+++ PhotoQuery.cs	2005-11-10 01:57:25.000000000 -0600
@@ -7,13 +7,14 @@ namespace FSpot {
 		private Photo [] photos;
 		private PhotoStore store;
 		private Tag [] tags;
+		private string extra_condition;
 		private PhotoStore.DateRange range = null;
 		
 		// Constructor
 		public PhotoQuery (PhotoStore store)
 		{
 			this.store = store;
-			photos = store.Query (null, range);
+			photos = store.Query (null, null, range);
 		}
 
 		public int Count {
@@ -61,7 +62,19 @@ namespace FSpot {
 			
 			set {
 				tags = value;
-				photos = store.Query (tags, range);
+				photos = store.Query (tags, extra_condition, range);
+				RequestReload ();
+			}
+		}
+		
+		public string ExtraCondition {
+			get {
+				return extra_condition;
+			}
+			
+			set {
+				extra_condition = value;
+				photos = store.Query (tags, extra_condition, range);
 				RequestReload ();
 			}
 		}
@@ -72,7 +85,7 @@ namespace FSpot {
 			}
 			set {
 				range = value;
-				photos = store.Query (tags, range);
+				photos = store.Query (tags, extra_condition, range);
 				RequestReload ();
 			}
 		}
diff -rup ../../f-spot-HEAD/src/PhotoStore.cs PhotoStore.cs
--- ../../f-spot-HEAD/src/PhotoStore.cs	2005-11-10 00:10:28.000000000 -0600
+++ PhotoStore.cs	2005-11-10 01:58:44.000000000 -0600
@@ -1033,15 +1033,16 @@ public class PhotoStore : DbStore {
 	// Queries.
 	public Photo [] Query (Tag [] tags, DateTime start, DateTime end)
 	{
-		return Query (tags, new DateRange (start, end));
+		return Query (tags, null, new DateRange (start, end));
 	}
 	
 	public Photo [] Query (Tag [] tags) {
-		return Query (tags, null);
+		return Query (tags, null, null);
 	}
 
 	public Photo [] Query (string query)
 	{
+		//Console.WriteLine("Query: {0}", query);
 		//Console.WriteLine ("Query Start {0}", System.DateTime.Now.ToLongTimeString ());
 
 		SqliteCommand command = new SqliteCommand ();
@@ -1111,7 +1112,7 @@ public class PhotoStore : DbStore {
 		return Query (query_string);
 	}	   
 
-	public Photo [] Query (Tag [] tags, DateRange range)
+	public Photo [] Query (Tag [] tags, string extra_condition, DateRange range)
 	{
 		string query;
 
@@ -1134,9 +1135,10 @@ public class PhotoStore : DbStore {
 		//        photos.default_version_id
 		//                  FROM photos, photo_tags
 		//                  WHERE photos.id = photo_tags.photo_id
-		// 		          AND (photo_tags.tag_id = tag1
-		//			       OR photo_tags.tag_id = tag2
-		//                             OR photo_tags.tag_id = tag3 ...)
+		// 		          AND (photo_tags.tag_id = cat1tag1
+		//			       OR photo_tags.tag_id = cat1tag2 ) 
+		// 		          AND (photo_tags.tag_id = cat2tag1
+		//			       OR photo_tags.tag_id = cat2tag2 )
 		//                  GROUP BY photos.id
 		
 		StringBuilder query_builder = new StringBuilder ();
@@ -1169,8 +1171,7 @@ public class PhotoStore : DbStore {
 					continue;
 				
 				if (first) {
-					query_builder.Append (String.Format ("{0} photos.id IN (SELECT photo_id FROM photo_tags WHERE tag_id IN (",
-									     hide || range != null ? " AND " : " WHERE "));
+					query_builder.Append (String.Format ("{0} photos.id IN (SELECT photo_id FROM photo_tags WHERE tag_id IN (", hide || range != null ? " AND " : " WHERE "));
 				}
 				
 				query_builder.Append (String.Format ("{0}{1} ", first ? "" : ", ", t.Id));
@@ -1181,6 +1182,16 @@ public class PhotoStore : DbStore {
 			if (!first)
 				query_builder.Append (")) ");
 		}
+			
+		if (extra_condition != null) {
+			query_builder.Append (
+				String.Format (
+					"{0} {1} ",
+					(hide || range != null || (tags != null && tags.Length > 0)) ? " AND " : " WHERE ",
+					extra_condition
+				)
+			);
+		}
 		
 		query_builder.Append ("ORDER BY photos.time");
 		query = query_builder.ToString ();
diff -rup ../../f-spot-HEAD/src/TagSelectionWidget.cs TagSelectionWidget.cs
--- ../../f-spot-HEAD/src/TagSelectionWidget.cs	2005-11-10 00:10:28.000000000 -0600
+++ TagSelectionWidget.cs	2005-11-10 01:34:43.000000000 -0600
@@ -48,24 +48,34 @@ public class TagSelectionWidget : TreeVi
 	public Tag TagAtPosition (int x, int y) 
 	{
 		TreePath path;
-		TreeIter iter;
 
 		// Work out which tag we're dropping onto
 		if (!this.GetPathAtPos (x, y, out path))
 			return null;
 
+		return TagByPath (path);
+	}
+
+	public Tag TagByPath (TreePath path) 
+	{
+		TreeIter iter;
+
 		if (!Model.GetIter (out iter, path))
 			return null;
+		
+		return TagByIter (iter);
+	}
+	
+	public Tag TagByIter (TreeIter iter)
+	{
+		GLib.Value val = new GLib.Value ();
 
-		GLib.Value value = new GLib.Value ();
-
-		Model.GetValue (iter, 0, ref value);
-		uint tag_id = (uint) value;
-		Tag tag = tag_store.Get (tag_id) as Tag;
+		Model.GetValue (iter, 0, ref val);
+		uint tag_id = (uint) val;
 
-		return tag;
+		return tag_store.Get (tag_id) as Tag;
 	}
-
+	
 	public void Select (Tag tag)
 	{
 		if (! selection.Contains (tag.Id))
@@ -438,8 +448,8 @@ public class TagSelectionWidget : TreeVi
 		toggle_renderer.Toggled += new ToggledHandler (OnCellToggled);
 
 		TreeViewColumn column;
-		column = AppendColumn ("check", toggle_renderer, new TreeCellDataFunc (CheckBoxDataFunc));
-		column.SortColumnId = 0;
+		//column = AppendColumn ("check", toggle_renderer, new TreeCellDataFunc (CheckBoxDataFunc));
+		//column.SortColumnId = 0;
 
 		AppendColumn ("icon", new CellRendererPixbuf (), new TreeCellDataFunc (IconDataFunc));
 
diff -rup ../../f-spot-HEAD/src/TimeAdaptor.cs TimeAdaptor.cs
--- ../../f-spot-HEAD/src/TimeAdaptor.cs	2005-11-10 00:10:29.000000000 -0600
+++ TimeAdaptor.cs	2005-11-10 01:59:43.000000000 -0600
@@ -117,7 +117,7 @@ namespace FSpot {
 		{
 			years.Clear ();
 
-			Photo [] photos = query.Store.Query (null, null);
+			Photo [] photos = query.Store.Query (null, null, null);
 			Array.Sort (query.Photos);
 			Array.Reverse (query.Photos);
 			Array.Reverse (photos);
--- /dev/null	2005-11-08 00:39:39.504664232 -0600
+++ TagQueryWidget.cs	2005-11-10 02:24:19.000000000 -0600
@@ -0,0 +1,1177 @@
+using System;
+using System.Collections;
+using System.Text;
+using Mono.Posix;
+using Gtk;
+using Gdk;
+
+namespace FSpot.Tags {
+	public class LiteralPopup : AbstractPopup {
+		private Literal literal;
+
+		public LiteralPopup (Gtk.Widget es, Literal literal) : base(es)
+		{
+			this.literal = literal;
+		}
+
+		protected override object GetObject ()
+		{
+			return EventSource;
+		}
+
+		public override void Populate(Gtk.Menu popup_menu)
+		{
+			GtkUtil.MakeMenuItem (popup_menu,
+					Catalog.GetString ((literal.IsNegated ? "Include" : "Filter")),
+					new EventHandler (literal.HandleToggleNegatedCommand), true);
+			
+			int required_by, grouped_with;
+			bool required = LogicWidget.Root.TagRequired (literal.Tag, out required_by, out grouped_with);
+			if (required && (required_by > 1 || grouped_with > 1))
+				GtkUtil.MakeMenuItem (popup_menu, Mono.Posix.Catalog.GetString ("Do not require"),
+						      new EventHandler (literal.HandleUnRequireTag), true);
+			else if (!required)
+				GtkUtil.MakeMenuItem (popup_menu, Mono.Posix.Catalog.GetString ("Require"),
+					      new EventHandler (literal.HandleRequireTag), true);
+			
+			GtkUtil.MakeMenuItem (popup_menu, Catalog.GetString ("Remove"),
+					      new EventHandler (literal.HandleRemoveCommand), true);
+		
+			GtkUtil.MakeMenuSeparator (popup_menu);
+		
+			MenuItem attach_item = new MenuItem (Catalog.GetString ("With"));
+			TagMenu attach_menu = new TagMenu (attach_item, MainWindow.Toplevel.Database.Tags);
+			attach_menu.TagSelected += literal.HandleAttachTagCommand;
+			attach_item.ShowAll ();
+			popup_menu.Append (attach_item);
+		}
+	}
+
+	public abstract class LogicTerm {
+		public LogicTerm (LogicTerm parent, Literal after)
+		{
+			this.parent = parent;
+							
+			if (parent != null) {
+				if (after == null)
+					parent.Add (this);
+				else
+					parent.SubTerms.Insert (parent.SubTerms.IndexOf (after) + 1, this);
+			}
+		}
+
+		/** Properties **/
+
+		public bool HasMultiple {
+			get {
+				return (SubTerms.Count > 1);
+			}
+		}
+		
+		public LogicTerm Last {
+			get {
+				// Return the last Literal in this term
+				if (SubTerms.Count > 0)
+					return SubTerms[SubTerms.Count - 1] as LogicTerm;
+				else
+					return null;
+			}
+		}
+		
+		public int Count {
+			get {
+				return SubTerms.Count;
+			}
+		}
+
+		public LogicTerm Parent {
+			get {
+				return parent;
+			}
+		}
+
+		/** Methods **/
+		
+		public void Add (LogicTerm term)
+		{
+			SubTerms.Add (term);
+		}
+		
+		public void Remove (LogicTerm term)
+		{
+			SubTerms.Remove (term);
+
+			// Remove ourselves if we're now empty
+			if (SubTerms.Count == 0)
+				if (Parent != null)
+					Parent.Remove (this);
+		}
+
+		public ArrayList FindByTag (Tag t)
+		{
+			return FindByTag (t, true);
+		}
+
+		public ArrayList FindByTag (Tag t, bool recursive)
+		{
+			ArrayList results = new ArrayList ();
+
+			if (tag != null && tag == t)
+				results.Add (this);
+
+			if (recursive)
+				foreach (LogicTerm term in SubTerms)
+					results.AddRange (term.FindByTag (t, true));
+			else
+				foreach (LogicTerm term in SubTerms) {
+					foreach (LogicTerm literal in SubTerms) {
+						if (literal.tag != null && literal.tag == t) {
+							results.Add (literal);
+						}
+					}
+						
+					if (term.tag != null && term.tag == t) {
+						results.Add (term);
+					}
+				}
+
+			return results;
+		}
+		
+		public ArrayList LiteralParents ()
+		{
+			ArrayList results = new ArrayList ();
+
+			bool meme = false;
+			foreach (LogicTerm term in SubTerms) {
+				if (term is Literal)
+					meme = true;
+				
+				results.AddRange (term.LiteralParents ());
+			}
+
+			if (meme)
+				results.Add (this);
+
+			return results;
+		}
+
+		public bool TagIncluded(Tag t)
+		{
+			ArrayList parents = LiteralParents ();
+
+			if (parents.Count == 0)
+				return false;
+
+			foreach (LogicTerm term in parents) {
+				bool termHasTag = false;
+				bool onlyTerm = true;
+				foreach (LogicTerm literal in term.SubTerms) {
+					if (literal.tag != null) {
+						if (literal.tag == t) {
+							termHasTag = true;
+						} else {
+							onlyTerm = false;
+						}
+					}
+				}
+
+				if (termHasTag && onlyTerm)
+					return true;
+			}
+
+			return false;
+		}
+		
+		public bool TagRequired(Tag t)
+		{
+			int count, grouped_with;
+			return TagRequired(t, out count, out grouped_with);
+		}
+
+		public bool TagRequired(Tag t, out int num_terms, out int grouped_with)
+		{
+			ArrayList parents = LiteralParents ();
+
+			num_terms = 0;
+			grouped_with = 100;
+			int min_grouped_with = 100;
+
+			if (parents.Count == 0)
+				return false;
+
+			foreach (LogicTerm term in parents) {
+				bool termHasTag = false;
+
+				// Don't count it as required if it's the only subterm..though it is..
+				// it is more clearly identified as Included at that point.
+				//if (term.Count > 1) {
+					foreach (LogicTerm literal in term.SubTerms) {
+						if (literal.tag != null) {
+							if (literal.tag == t) {
+								num_terms++;
+								termHasTag = true;
+								grouped_with = term.SubTerms.Count;
+								break;
+							}
+						}
+					}
+				//}
+
+				if (grouped_with < min_grouped_with)
+					min_grouped_with = grouped_with;
+
+				if (!termHasTag)
+					return false;
+			}
+
+			grouped_with = min_grouped_with;
+
+			return true;
+		}
+		
+		// Recursively generate the SQL condition clause that this
+		// term represents.
+		public virtual string ConditionString ()
+		{
+			
+			StringBuilder condition = new StringBuilder ("(");
+
+			for (int i = 0; i < SubTerms.Count; i++) {
+				LogicTerm term = SubTerms[i] as LogicTerm;
+				condition.Append (term.ConditionString ());
+
+				if (i != SubTerms.Count - 1)
+					condition.Append (SQLOperator ());
+			}
+
+			condition.Append(")");
+
+			return condition.ToString ();
+		}
+
+		public virtual Gtk.Widget SeparatorWidget ()
+		{
+			return null;
+		}
+		
+		public virtual string SQLOperator ()
+		{
+			return "";
+		}
+		
+		private ArrayList SubTerms = new ArrayList ();
+		private LogicTerm parent = null;
+		private string separator;
+
+		protected Tag tag = null;
+	}
+
+	public class AndTerm : LogicTerm {
+		public AndTerm (LogicTerm parent, Literal after) : base (parent, after) {}
+		
+		public override Widget SeparatorWidget ()
+		{
+			Widget sep = new Label ("");
+			sep.SetSizeRequest (3, 1);
+			sep.Show ();
+			return sep;
+			//return null;
+		}
+		
+		public override string SQLOperator ()
+		{
+			return " AND ";
+		}
+	}
+	
+	public class OrTerm : LogicTerm {
+		public OrTerm (LogicTerm parent, Literal after) : base (parent, after) {}
+			
+		private static string OR = " " + Catalog.GetString ("or") + " ";
+			
+		public override Gtk.Widget SeparatorWidget ()
+		{
+			Widget label = new Label (OR);
+			label.Show ();
+			return label;
+		}
+		
+		public override string SQLOperator ()
+		{
+			return " OR ";
+		}
+	}
+	
+	public class Literal : LogicTerm {
+		public Literal (LogicTerm parent, Tag tag, Literal after) : base (parent, after) {
+			this.tag = tag;
+		}
+
+		/** Properties **/
+
+		public static ArrayList FocusedLiterals
+		{
+			get {
+				return focusedLiterals;
+			}
+			set {
+				focusedLiterals = value;
+			}
+		}
+
+		public static Tooltips Tips {
+			set {
+				tips = value;
+			}
+		}
+
+		public Tag Tag {
+			get {
+				return tag;
+			}
+		}
+
+		public bool IsNegated {
+			get {
+				return isNegated;
+			}
+
+			set {
+				isNegated = value;
+
+				UpdateImage ();
+
+				if (NegatedToggled != null)
+					NegatedToggled (this);
+			}
+		}
+		
+		private Pixbuf NegatedIcon
+		{
+			get {
+				if (negated_icon != null)
+					return negated_icon;
+
+
+				negated_icon = normal_icon.Copy ();
+
+				int offset = thumbnail_size - overlay_size;
+				(NegatedOverlay ()).Composite (negated_icon, offset, 0, overlay_size, overlay_size, offset, 0, 1.0, 1.0, InterpType.Bilinear, 200);
+
+				return negated_icon;
+			}
+
+			set {
+				negated_icon = null;
+			}
+		}
+		
+		public Widget Widget {
+			get {
+				if (widget != null)
+					return widget;
+
+
+				EventBox container = new EventBox ();
+				image = new Gtk.Image (NormalIcon);
+				container.Add (image);
+				//image.Ypad = 2;
+				
+				/*Button button = new Button (image);
+				container.Add (button);
+				button.Show ();
+
+				button.BorderWidth = 0;
+				button.Style.XThickness = 0;
+				button.Style.YThickness = 0;
+				button.Relief = ReliefStyle.None;*/
+
+				container.CanFocus = true;
+				//container.Add (image);
+
+				container.KeyPressEvent		+= KeyHandler;
+				container.ButtonPressEvent	+= ClickHandler;
+				container.FocusInEvent		+= HandleFocusIn;
+				
+				//image.KeyPressEvent		+= KeyHandler;
+				//image.ButtonPressEvent		+= ClickHandler;
+				//image.FocusInEvent		+= HandleFocusIn;
+			
+				PopupManager pm = new PopupManager (new LiteralPopup (container, this));
+
+				// Setup this widget as a drag source (so tags can be moved after being placed)
+				container.DragDataGet	+= HandleDragDataGet;
+				container.DragBegin	+= HandleDragBegin;
+				container.DragEnd	+= HandleDragEnd;
+
+				Gtk.Drag.SourceSet (container, Gdk.ModifierType.Button1Mask | Gdk.ModifierType.Button3Mask,
+					tag_target_table, DragAction.Copy | DragAction.Move);
+				
+				// Setup this widget as a drag destination (so tags can be added to our parent's LogicTerm)
+				container.DragDataReceived	+= HandleDragDataReceived;
+
+				Gtk.Drag.DestSet (container, DestDefaults.All, tag_dest_target_table, 
+						  DragAction.Copy | DragAction.Move ); 
+
+				tips.SetTip (container, tag.Name, null);
+
+				image.Show ();
+				container.Show ();
+
+				widget = container;
+
+				return widget;
+			}
+		}
+
+		private Pixbuf NormalIcon
+		{
+			get {
+				if (normal_icon != null)
+					return normal_icon;
+
+				//Pixbuf normal_icon = new Pixbuf (Gdk.Colorspace.Rgb, true, 8, 32, 32);
+				//normal_icon.Fill (0x00000000);
+				
+				Pixbuf scaled = null;
+				scaled = tag.Icon;
+
+				for (Category category = tag.Category; category != null && scaled == null; category = category.Category)
+					scaled = category.Icon;
+				
+				// FIXME need to have a default icon here for tags w/o icons
+				if (scaled == null) {
+					Console.WriteLine ("FIXME: Tag icon is null, so using all black icon!");
+					normal_icon = new Pixbuf (Gdk.Colorspace.Rgb, true, 8, 30, 30);
+					normal_icon.Fill (0x00000000);
+					return normal_icon;
+				}
+				
+				if (scaled.Width != thumbnail_size) {
+					scaled = scaled.ScaleSimple (thumbnail_size, thumbnail_size, InterpType.Bilinear);
+				}
+					
+				normal_icon = scaled;
+
+				return normal_icon;
+			}
+
+			set {
+				normal_icon = null;
+			}
+		}
+
+		/** Methods **/
+		public void Update ()
+		{
+			// Clear out the old icons
+			NormalIcon = null;
+			NegatedIcon = null;
+
+			UpdateImage ();
+		}
+		
+		public void RemoveSelf ()
+		{
+			if (Removing != null)
+				Removing (this);
+
+			if (Parent != null)
+				Parent.Remove (this);
+			
+			if (Removed != null)
+				Removed (this);
+		}
+		
+		public override string ConditionString ()
+		{
+			return String.Format (
+					"id {0}IN (SELECT photo_id FROM photo_tags WHERE tag_id={1})",
+					(IsNegated ? "NOT " : ""), tag.Id);
+		}
+
+		public override Gtk.Widget SeparatorWidget ()
+		{
+			return new Label ("ERR");
+		}
+		
+		private void UpdateImage ()
+		{
+			if (IsNegated) {
+				tips.SetTip (widget, String.Format (Catalog.GetString ("Not {0}"), tag.Name), null);
+				image.Pixbuf = NegatedIcon;
+			} else {
+				tips.SetTip (widget, tag.Name, null);
+				image.Pixbuf = NormalIcon;
+			}
+		}
+	
+		private static Pixbuf NegatedOverlay ()
+		{
+			// Probably should have a listener for thumbnail_size that will
+			// regenerate all the icons and negated icons
+			if (negated_overlay == null) {
+				System.Reflection.Assembly assembly = System.Reflection.Assembly.GetCallingAssembly ();
+				negated_overlay = new Pixbuf (assembly.GetManifestResourceStream ("f-spot-not.png"));
+				negated_overlay = negated_overlay.ScaleSimple (overlay_size, overlay_size, InterpType.Bilinear);
+			}
+
+			return negated_overlay;
+		}
+
+		public static void RemoveFocusedLiterals ()
+		{
+			if (focusedLiterals != null)
+				foreach (Literal literal in focusedLiterals)
+					literal.RemoveSelf ();
+		}
+
+		/** Handlers **/
+
+		private void KeyHandler (object o, KeyPressEventArgs args)
+		{
+			args.RetVal = false;
+
+			switch (args.Event.Key) {
+			case Gdk.Key.Delete:
+				RemoveFocusedLiterals ();
+				args.RetVal = true;
+				return;
+			}
+		}
+
+		private void ClickHandler (object o, ButtonPressEventArgs args)
+		{
+			args.RetVal = true;
+		
+			switch (args.Event.Type) {
+			case EventType.TwoButtonPress:
+				if (args.Event.Button == 1)
+					IsNegated = !IsNegated;
+				else
+					args.RetVal = false;
+				return;
+			
+			case EventType.ButtonPress:
+				Widget.GrabFocus ();
+
+				if (args.Event.Button == 1) {
+					// FIXME allow multiple selection of literals so they can be deleted, modified all at once
+					//if ((args.Event.State & ModifierType.ControlMask) != 0) {
+					//}
+				}
+				//else if (args.Event.Button == 3)
+				//{
+				//}
+
+				return;
+
+			default:
+				args.RetVal = false;
+				return;
+			}
+		}
+
+		void HandleDragDataGet (object sender, DragDataGetArgs args)
+		{
+			args.RetVal = true;
+			switch (args.Info) {
+			case (uint) MainWindow.TargetType.TagList:
+			case (uint) MainWindow.TargetType.TagQueryItem:
+				Byte [] data = Encoding.UTF8.GetBytes ("");
+				Atom [] targets = args.Context.Targets;
+			
+				args.SelectionData.Set (targets[0], 8, data, data.Length);
+
+				return;
+			default:
+				args.RetVal = false;
+				focusedLiterals = null;
+				break;
+			}
+		}
+
+		void HandleDragBegin (object sender, DragBeginArgs args)
+		{
+			Gtk.Drag.SetIconPixbuf (args.Context, image.Pixbuf, 0, 0);
+			focusedLiterals.Add (this);
+		}
+	
+		void HandleDragEnd (object sender, DragEndArgs args)
+		{
+			// Remove any literals still marked as focused, because
+			// the user is throwing them away.
+			RemoveFocusedLiterals ();
+
+			focusedLiterals = new ArrayList();
+			args.RetVal = true;
+		}
+		
+		private void HandleDragDataReceived (object o, EventArgs args)
+		{
+			// If focusedLiterals is not null, this is a drag of a tag that's already been placed
+			if (focusedLiterals.Count == 0)
+			{
+				if (TermAdded != null)
+					TermAdded (Parent, this);
+			}
+			else
+			{
+				if (! focusedLiterals.Contains(this))
+					if (LiteralsMoved != null)
+						LiteralsMoved (focusedLiterals, Parent, this);
+
+				// Unmark the literals as focused so they don't get nixed
+				focusedLiterals = null;
+			}
+		}
+
+		public void HandleToggleNegatedCommand (object o, EventArgs args)
+		{
+			IsNegated = !IsNegated;
+		}
+
+		public void HandleRemoveCommand (object o, EventArgs args)
+		{
+			RemoveSelf ();
+		}
+	
+		public void HandleAttachTagCommand (Tag t) 
+		{
+			if (AttachTag != null)
+				AttachTag (t, Parent, this);
+		}
+
+		private void HandleFocusIn (object o, EventArgs args)
+		{
+			//Console.WriteLine (tag.Name + " now has focus!");
+			//(Widget as Container).Children[0].GrabFocus ();
+		}
+		
+		public void HandleRequireTag (object sender, EventArgs args)
+		{
+			if (RequireTag != null)
+				RequireTag (new Tag [] {this.Tag});
+		}
+	
+		public void HandleUnRequireTag (object sender, EventArgs args)
+		{
+			if (UnRequireTag != null)
+				UnRequireTag (new Tag [] {this.Tag});
+		}
+		
+		// TODO bind this to be proportional to the icon_view thumbnail size?
+		private static int thumbnail_size = 30;
+
+		private static int overlay_size = (int) (.40 * thumbnail_size);
+		
+		private static TargetEntry [] tag_target_table = new TargetEntry [] {
+			new TargetEntry ("application/x-fspot-tag-query-item", 0, (uint) MainWindow.TargetType.TagQueryItem),
+		};
+
+		private static TargetEntry [] tag_dest_target_table = new TargetEntry [] {
+			new TargetEntry ("application/x-fspot-tags", 0, (uint) MainWindow.TargetType.TagList),
+			new TargetEntry ("application/x-fspot-tag-query-item", 0, (uint) MainWindow.TargetType.TagQueryItem),
+		};
+
+		private static ArrayList focusedLiterals = new ArrayList();
+		private Gtk.Image image;
+		
+		private Pixbuf normal_icon;
+		//private EventBox widget;
+		private Widget widget;
+		private Pixbuf negated_icon;
+		private static Pixbuf negated_overlay;
+		private static Tooltips tips;
+		private bool isNegated = false;
+		
+		public delegate void NegatedToggleHandler (Literal group); 
+		public event NegatedToggleHandler NegatedToggled;
+
+		public delegate void RemovingHandler (Literal group); 
+		public event RemovingHandler Removing;
+		
+		public delegate void RemovedHandler (Literal group); 
+		public event RemovedHandler Removed;
+		
+		public delegate void TermAddedHandler (LogicTerm parent, Literal after); 
+		public event TermAddedHandler TermAdded;
+		
+		public delegate void AttachTagHandler (Tag tag, LogicTerm parent, Literal after); 
+		public event AttachTagHandler AttachTag;
+		
+		public delegate void TagRequiredHandler (Tag [] tags); 
+		public event TagRequiredHandler RequireTag;
+		
+		public delegate void TagUnRequiredHandler (Tag [] tags); 
+		public event TagUnRequiredHandler UnRequireTag;
+		
+		public delegate void LiteralsMovedHandler (ArrayList literals, LogicTerm parent, Literal after); 
+		public event LiteralsMovedHandler LiteralsMoved;
+	}
+
+	public class LogicWidget : HBox {
+		public LogicWidget (PhotoQuery query, TagStore tag_store, TagSelectionWidget selector) : base ()
+		{
+			//SetFlag (WidgetFlags.NoWindow);
+			this.query = query;
+			this.tag_selection_widget = selector;
+
+			CanFocus = true;
+			Sensitive = true;
+			
+			Literal.Tips = tips;
+			
+			tips.Enable ();
+			
+			rootAdd = new Gtk.EventBox ();
+			rootAdd.CanFocus = true;
+			rootAdd.DragMotion		+= HandleDragMotion;
+			rootAdd.DragDataReceived	+= HandleDragDataReceived;
+			rootAdd.DragLeave		+= HandleLeave;
+			rootAdd.Show ();
+			tips.SetTip (rootAdd, Catalog.GetString ("Drop a tag here to include it"), null);
+
+			// TODO listen for tag edits, deletes
+			Init ();
+			
+			Gtk.Drag.DestSet (rootAdd, DestDefaults.All, tag_dest_target_table, 
+					  DragAction.Copy | DragAction.Move ); 
+			PackEnd (rootAdd, true, true, 0);
+
+			tag_store.TagChanged += HandleTagChanged;
+			tag_store.TagDeleted += HandleTagDeleted;
+
+			Show ();
+		}
+
+		private void Init ()
+		{
+			rootTerm = new OrTerm (null, null);
+		}
+
+		private void Preview ()
+		{
+			if (sepBox == null) {
+				sepBox = new HBox ();
+				Widget sep = rootTerm.SeparatorWidget ();
+				if (sep != null) {
+					sep.Show ();
+					sepBox.PackStart (sep, false, false, 0);
+					rootAdd.Add (sepBox);
+				}
+			}
+
+			sepBox.Show ();
+		}
+
+		/** Handlers **/
+
+		// When the user edits a tag (it's icon, name, etc) we get called
+		// and update the images/text in the query as needed to reflect the changes.
+		private void HandleTagChanged (Tag t)
+		{
+			foreach (Literal term in rootTerm.FindByTag (t)) {
+				term.Update ();
+			}
+		}
+		
+		// If the user deletes a tag that is in use in the query, remove it from the query too.
+		private void HandleTagDeleted (Tag t)
+		{
+			foreach (Literal term in rootTerm.FindByTag (t)) {
+				term.RemoveSelf ();
+			}
+		}
+
+		private void HandleDragMotion (object o, DragMotionArgs args)
+		{
+			if (!preview && rootTerm.Count > 0) {
+				Preview ();
+				preview = true;
+			}
+		}
+		
+		private void HandleLeave (object o, EventArgs args)
+		{
+			if (preview && Children.Length > 1) {
+				sepBox.Hide ();
+				preview = false;
+			}
+		}
+		
+		private void HandleLiteralsMoved (ArrayList literals, LogicTerm parent, Literal after)
+		{
+			preventUpdate = true;
+			foreach (Literal term in literals) {
+				Tag tag = term.Tag;
+
+				// Don't listen for it to be removed since we are
+				// moving it. We will update when we're done.
+				term.Removed -= HandleRemoved;
+				term.RemoveSelf ();
+
+				// Add it to where it was dropped
+				ArrayList groups = InsertTerm (new Tag[] {tag}, parent, after);
+
+				if (term.IsNegated)
+					foreach (Literal group in groups)
+						group.IsNegated = true;
+			}
+			preventUpdate = false;
+			UpdateQuery ();
+		}
+		
+		private void HandleTermAdded (LogicTerm parent, Literal after)
+		{
+			InsertTerm (parent, after);
+		}
+		
+		private void HandleAttachTag (Tag tag, LogicTerm parent, Literal after)
+		{
+			InsertTerm (new Tag [] {tag}, parent, after);
+		}
+		
+		private void HandleNegated (Literal group)
+		{
+			UpdateQuery ();
+		}
+		
+		private void HandleRemoving (Literal term)
+		{
+			// Remove separators as needed
+			if (term.Parent != null) {
+				if (term.Parent.Count > 1)
+				{
+					if (term == term.Parent.Last)
+						Remove (Children[WidgetPosition (term.Widget) - 1]);
+					else
+						Remove (Children[WidgetPosition (term.Widget) + 1]);
+				}
+				else if (term.Parent.Count == 1)
+				{
+					if (term.Parent.Parent != null) {
+						if (term.Parent.Parent.Count > 1) {
+							if (term.Parent == term.Parent.Parent.Last)
+								Remove (Children[WidgetPosition (term.Widget) - 1]);
+							else
+								Remove (Children[WidgetPosition (term.Widget) + 1]);
+						}
+					}
+				}
+			}
+
+			// Remove the term's widget
+			Remove (term.Widget);
+		}
+
+		private void HandleRemoved (Literal group)
+		{
+			UpdateQuery ();
+		}
+		
+		private void HandleDragDataReceived (object o, DragDataReceivedArgs args)
+		{
+			InsertTerm (rootTerm, null);
+
+			args.RetVal = true;
+		}
+		
+		/** Helper Functions **/
+		
+		public void PhotoTagsChanged (Tag [] tags)
+		{
+			bool refresh_required = false;
+				
+			foreach (Tag tag in tags) {
+				if ((rootTerm.FindByTag (tag)).Count > 0) {
+					refresh_required = true;
+					break;
+				}
+			}
+
+			if (refresh_required)
+				UpdateQuery ();
+		}
+		
+		// Inserts a widget into a Box at a certain index
+		private void InsertWidget (int index, Gtk.Widget widget) {
+			widget.Visible = true;
+			PackStart (widget, false, false, 0);
+			ReorderChild (widget, index);
+		}
+		
+		// Return the index position of a widget in this Box
+		private int WidgetPosition (Gtk.Widget widget)
+		{
+			for (int i = 0; i < Children.Length; i++)
+				if (Children[i] == widget)
+					return i;
+
+			return Children.Length - 1;
+		}
+				
+		public bool TagIncluded (Tag tag)
+		{
+			return rootTerm.TagIncluded (tag);
+		}
+		
+		public bool TagRequired (Tag tag)
+		{
+			return rootTerm.TagRequired (tag);
+		}
+		
+		// Add a tag to the rootTerm, at the end of the Box
+		public void Include (Tag [] tags)
+		{
+			ArrayList new_tags = new ArrayList(tags.Length);
+			foreach (Tag tag in tags) {
+				if (! rootTerm.TagIncluded (tag))
+					new_tags.Add (tag);
+			}
+
+			if (new_tags.Count == 0)
+				return;
+
+			tags = (Tag []) new_tags.ToArray (typeof (Tag));
+			
+			InsertTerm (tags, rootTerm, null);
+		}
+		
+		public void UnInclude (Tag [] tags)
+		{
+			ArrayList new_tags = new ArrayList(tags.Length);
+			foreach (Tag tag in tags) {
+				if (rootTerm.TagIncluded (tag))
+					new_tags.Add (tag);
+			}
+
+			if (new_tags.Count == 0)
+				return;
+
+			tags = (Tag []) new_tags.ToArray (typeof (Tag));
+			
+			bool needsUpdate = false;
+			preventUpdate = true;
+			foreach (LogicTerm parent in rootTerm.LiteralParents ()) {
+				if (parent.Count == 1) {
+					foreach (Tag tag in tags) {
+						if ((parent.Last as Literal).Tag == tag) {
+							(parent.Last as Literal).RemoveSelf ();
+							needsUpdate = true;
+							break;
+						}
+					}
+				}
+			}
+			preventUpdate = false;
+
+			if (needsUpdate)
+				UpdateQuery ();
+		}
+		
+		// AND this tag with all terms
+		public void Require (Tag [] tags)
+		{
+			// TODO it would be awesome if this was done by putting parentheses around
+			// OR terms and ANDing the result with this term.
+
+			// Trim out tags that are already required
+			ArrayList new_tags = new ArrayList(tags.Length);
+			foreach (Tag tag in tags) {
+				if (! rootTerm.TagRequired (tag))
+					new_tags.Add (tag);
+			}
+
+			if (new_tags.Count == 0)
+				return;
+
+			tags = (Tag []) new_tags.ToArray (typeof (Tag));
+
+			bool added = false;
+			preventUpdate = true;
+			foreach (LogicTerm parent in rootTerm.LiteralParents ()) {
+				// TODO logic could be broken if a term's SubTerms are a mixture
+				// of Literals and non-Literals
+				InsertTerm (tags, parent, parent.Last as Literal);
+				added = true;
+			}
+
+			// If there were no LiteralParents to add this tag to, then add it to the rootTerm
+			// TODO should add the first tag in the array,
+			// then add the others to the first's parent (so they will be ANDed together)
+			if (!added)
+				InsertTerm (tags, rootTerm, null);
+
+			preventUpdate = false;
+
+			UpdateQuery ();
+		}
+		
+		public void UnRequire (Tag [] tags)
+		{
+			// Trim out tags that are not required
+			ArrayList new_tags = new ArrayList(tags.Length);
+			foreach (Tag tag in tags) {
+				if (rootTerm.TagRequired (tag))
+					new_tags.Add (tag);
+			}
+
+			if (new_tags.Count == 0)
+				return;
+
+			tags = (Tag []) new_tags.ToArray (typeof (Tag));
+
+			preventUpdate = true;
+			foreach (LogicTerm parent in rootTerm.LiteralParents ()) {
+				// Don't remove if this tag is the only child of a term
+				if (parent.Count > 1) {
+					foreach (Tag tag in tags) {
+						((parent.FindByTag (tag))[0] as Literal).RemoveSelf ();
+					}
+				}
+			}
+
+			preventUpdate = false;
+
+			UpdateQuery ();
+		}
+
+		private void InsertTerm (LogicTerm parent, Literal after)
+		{
+			if (Literal.FocusedLiterals.Count != 0) {
+				HandleLiteralsMoved (Literal.FocusedLiterals, parent, after);
+
+				// Prevent them from being removed again
+				Literal.FocusedLiterals = null;
+			}
+			else
+				InsertTerm (tag_selection_widget.TagHighlight (), parent, after);
+		}
+		
+		public ArrayList InsertTerm (Tag [] tags, LogicTerm parent, Literal after)
+		{
+			int position;
+			if (after != null)
+				position = WidgetPosition (after.Widget) + 1;
+			else
+				position = Children.Length - 1;
+
+			ArrayList added = new ArrayList ();
+
+			foreach (Tag tag in tags) {
+				//Console.WriteLine ("Adding tag {0}", tag.Name);
+				
+				// Don't put a tag into a LogicTerm twice
+				if ((parent.FindByTag (tag, true)).Count > 0)
+					continue;
+
+				if (parent.Count > 0) {
+					Widget sep = parent.SeparatorWidget ();
+
+					//if (sep != null) {
+						InsertWidget (position, sep);
+						position++;
+					//}
+				}
+
+				// Encapsulate new OR terms within a new AND term of which they are the
+				// only member, so later other terms can be AND'd with them
+				//
+				// TODO should really see what type of term the parent is, and 
+				// encapsulate this term in a term of the opposite type. This will
+				// allow the query system to be expanded to work for multiple levels much easier.
+				if (parent == rootTerm) {
+					parent = new AndTerm (rootTerm, after);
+					after = null;
+				}
+
+				Literal term		= new Literal (parent, tag, after);
+				term.TermAdded		+= HandleTermAdded;
+				term.LiteralsMoved	+= HandleLiteralsMoved;
+				term.AttachTag		+= HandleAttachTag;
+				term.NegatedToggled	+= HandleNegated;
+				term.Removing		+= HandleRemoving;
+				term.Removed		+= HandleRemoved;
+				term.RequireTag		+= Require;
+				term.UnRequireTag	+= UnRequire;
+
+				added.Add (term);
+
+				// Insert this widget into the appropriate place in the hbox
+				InsertWidget (position, term.Widget);
+			}
+
+			UpdateQuery ();
+
+			return added;
+		}
+
+		public bool Cleared ()
+		{
+			return rootTerm.Count == 0;
+		}
+
+		// Update the query, which updates the icon_view
+		public void UpdateQuery ()
+		{
+			if (preventUpdate)
+				return;
+
+			if (rootTerm.Count == 0) {
+				query.ExtraCondition = null;
+			} else {
+				if (sepBox != null)
+					sepBox.Hide ();
+
+				query.ExtraCondition = rootTerm.ConditionString ();
+			}
+		}
+
+		// Clear out the query, starting afresh
+		public void Clear ()
+		{
+			// Don't remove the last widget, because it's the rootAdd widget
+			// which is useful for starting the next query
+			int last = Children.Length - 1;
+			int i = 0;
+			foreach (Widget widget in Children) {
+				if (i != last)
+					Remove (widget);
+				i++;
+			}
+
+			Init ();
+
+			UpdateQuery ();
+		}
+
+		private PhotoQuery query;
+		private TagSelectionWidget tag_selection_widget;
+		
+		private static Tooltips tips = new Tooltips ();
+		
+		private static LogicTerm rootTerm;
+		public static LogicTerm Root
+		{
+			get {
+				return rootTerm;
+			}
+		}
+
+		private EventBox rootAdd;
+		private HBox sepBox;
+
+		private bool preventUpdate = false;
+		private bool preview = false;
+
+		private ArrayList widgets = new ArrayList ();
+
+		// Drag and Drop
+		private static TargetEntry [] tag_dest_target_table = new TargetEntry [] {
+			new TargetEntry ("application/x-fspot-tags", 0, (uint) MainWindow.TargetType.TagList),
+			new TargetEntry ("application/x-fspot-tag-query-item", 0, (uint) MainWindow.TargetType.TagQueryItem),
+		};
+	}
+}
--- /dev/null	2005-11-08 00:39:39.504664232 -0600
+++ QueryWidget.cs	2005-11-10 01:28:23.000000000 -0600
@@ -0,0 +1,115 @@
+namespace FSpot {
+	public class QueryWidget : Gtk.VBox {
+		PhotoQuery query;
+		Tags.LogicWidget logic_widget;
+		Gtk.Label label;
+		Gtk.HBox warning_box;
+		Gtk.Button clear_button;
+		TagSelectionWidget selector;
+		Gtk.Tooltips tips = new Gtk.Tooltips ();
+
+		public QueryWidget (PhotoQuery query, Db db, TagSelectionWidget selector)
+		{
+			tips.Enable ();
+
+			this.query = query;
+			query.Changed += HandleChanged;
+			this.selector = selector;
+
+			Gtk.HSeparator sep = new Gtk.HSeparator ();
+			sep.Show ();
+			this.PackStart (sep, false, false, 0);
+			
+			Gtk.HBox hbox = new Gtk.HBox ();
+			hbox.Show ();
+			this.PackStart (hbox, false, false, 0);
+			
+			label = new Gtk.Label (Mono.Posix.Catalog.GetString ("Find: "));
+			label.Show ();
+			label.Ypad = 9;
+			hbox.PackStart (label, false, false, 0);
+			
+			logic_widget = new Tags.LogicWidget (query, db.Tags, selector);
+			logic_widget.Show ();
+			hbox.PackStart (logic_widget, true, true, 0);
+
+			warning_box = new Gtk.HBox ();
+			warning_box.PackStart (new Gtk.Label (""));
+			
+			Gtk.Image warning_image = new Gtk.Image ("gtk-dialog-warning", Gtk.IconSize.Button);
+			warning_image.Show ();
+			warning_box.PackStart (warning_image, false, false, 0);
+			
+			Gtk.Label warning = new Gtk.Label (Mono.Posix.Catalog.GetString ("No matching images found "));
+			warning_box.PackStart (warning, false, false, 0);
+			warning_box.ShowAll ();
+			warning_box.Spacing = 6;
+			warning_box.Visible = false;
+
+			hbox.PackStart (warning_box);				   
+			
+			clear_button = new Gtk.Button ();
+			clear_button.Add (new Gtk.Image ("gtk-stop", Gtk.IconSize.Button));
+			clear_button.Clicked += HandleClearButtonClicked;
+			clear_button.Relief = Gtk.ReliefStyle.None;
+			hbox.PackStart (clear_button, false, false, 0);
+			tips.SetTip (clear_button, Mono.Posix.Catalog.GetString("Clear Query"), null);
+
+			warning_box.Visible = false;
+		}
+		
+		public void HandleClearButtonClicked (object sender, System.EventArgs args)
+		{
+			logic_widget.Clear ();
+		}
+
+		public void HandleChanged (IBrowsableCollection collection) 
+		{
+			if (logic_widget.Cleared ()) {
+				this.Visible = false;
+				Hide ();
+			} else {
+				this.Visible = true;
+				Show ();
+			}
+
+			warning_box.Visible = (query.Count < 1);
+		}
+
+		public void PhotoTagsChanged (Tag[] tags)
+		{
+			System.Console.WriteLine ("QueryWidget - tags changed");
+			logic_widget.PhotoTagsChanged (tags);
+		}
+
+		public void Include (Tag [] tags)
+		{
+			logic_widget.Include (tags);
+		}
+		
+		public void UnInclude (Tag [] tags)
+		{
+			logic_widget.UnInclude (tags);
+		}
+		
+		public void Require (Tag [] tags)
+		{
+			logic_widget.Require (tags);
+		}
+		
+		public void UnRequire (Tag [] tags)
+		{
+			logic_widget.UnRequire (tags);
+		}
+		
+		public bool TagIncluded (Tag tag)
+		{
+			return logic_widget.TagIncluded (tag);
+		}
+		
+		public bool TagRequired (Tag tag)
+		{
+			return logic_widget.TagRequired (tag);
+		}
+	}
+}
--- /dev/null	2005-11-08 00:39:39.504664232 -0600
+++ PopupManager.cs	2005-11-10 02:25:20.000000000 -0600
@@ -0,0 +1,98 @@
+using System;
+
+public class PopupManager {
+	private AbstractPopup popup;
+
+	public PopupManager (AbstractPopup popup)
+	{
+		this.popup = popup;
+
+		// Listen for context-menu events
+		popup.EventSource.ButtonPressEvent += this.MousePopup;
+		popup.EventSource.PopupMenu += this.NonMousePopup;
+	}
+
+	public void MousePopup (object sender, Gtk.ButtonPressEventArgs args)
+	{
+		if (args.Event.Type == Gdk.EventType.ButtonPress && args.Event.Button == 3) {
+			popup.X = (int) args.Event.X;
+			popup.Y = (int) args.Event.Y;
+
+			args.RetVal = Popup (null);
+		}
+
+		args.RetVal = false;
+	}
+
+	public void NonMousePopup (object sender, Gtk.PopupMenuArgs args)
+	{
+		args.RetVal = Popup (popup.Positioner);
+	}
+
+	private bool Popup (Gtk.MenuPositionFunc positioner)
+	{
+		// FIXME this is a hack to handle the --view case for the time being.
+		if (MainWindow.Toplevel == null) {
+			return false;
+		}
+
+		Gtk.Menu popup_menu = new Gtk.Menu ();
+		popup.Populate (popup_menu);
+		popup_menu.Popup (null, null, positioner, IntPtr.Zero, 0, Gtk.Global.CurrentEventTime);
+
+		return true;
+	}
+
+	/*public void WidgetCentroidPosition (Gtk.Menu menu, out int x, out int y, out bool push_in)
+	{
+		// Position the menu in the middle of the focused widget
+		MainWindow.Toplevel.GetWidgetPosition(widget, out x, out y);
+
+		x += widget.Allocation.Width / 2;
+		y += widget.Allocation.Height / 2;
+
+		push_in = false;
+	}*/
+}
+
+public abstract class AbstractPopup {
+	private int x, y;
+	private Gtk.Widget event_source;
+
+	public AbstractPopup (Gtk.Widget event_source)
+	{
+		this.event_source = event_source;
+	}
+
+	public int X {
+		get { return x; }
+		set { x = value; }
+	}
+	
+	public int Y {
+		get { return y; }
+		set { y = value; }
+	}
+	
+	public Gtk.Widget EventSource {
+		get { return event_source; }
+		set { event_source = value; }
+	}
+
+	// Populate the popup menu
+	public abstract void Populate (Gtk.Menu menu);
+	
+	// Called by popup_menu.Popup to find out where to place the menu
+	public virtual void Positioner (Gtk.Menu menu, out int x, out int y, out bool push_in)
+	{
+		MainWindow.Toplevel.GetWidgetPosition(EventSource, out x, out y);
+
+		x += EventSource.Allocation.Width / 2;
+		y += EventSource.Allocation.Height / 2;
+
+		push_in = true;
+	}
+
+	// Gets the object associated with the currently saved x, y coordinates
+	protected abstract object GetObject ();
+}

Attachment: f-spot-not.png
Description: PNG image



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