Tag typing.



Hi guys,

For a while now I've wanted to tag my pictures without having to drag
and drop little icons.  Today I started on a patch that allows you to
tag pictures by typing tag names, like flickr.

The attached patch gives you the following behavior:

        - Select a photo or multiple photos and start typing a tag name.
        Focus will go to the little entry at the bottom of the window.
        Hitting enter will apply your changes.  If you want to type
        multiple tag names, comma-separate them.  If there were already
        tags on the photos that you selected, then you don't need to
        type a comma; just typing a letter will put the ", " in
        automatically for you.
        
        - When you select multiple photos, the entry will show the
        intersection of those photos' tags.  Deleting a tag from the
        entry and pushing the enter key on your keyboard will remove the
        deleted tag from the selected photos.
        
        - Hit Escape to cancel editing and hide the entry.
        
        - Typing a tag name later on will re-show and re-focus the
        entry.
        
        - There's a little X button that will hide the tag entry bar.
        
It works reasonably well.  Some things remain to be programmed in:

        - Tab completion.  There's a super-ghetto piece of source code
        logic programming in there right now to do this but it is
        incomplete.  We can't use the Gtk.Entry.Completion stuff, it's
        totally inadequate afaict.
        
        - We need to decide when to show the tagging bar.  It might make
        sense to hide it when the user isn't typing tags, but then
        again, it's really nice to be able to see what tags are applied
        to a picture (the little medallions aren't particularly
        evocative).
        
        - Tag merging.  Now that tags are easier to create, we need a
        way of merging them.  I think the way to implement this might be
        to make it so that when you select multiple tags in the tag
        sidebar and then right click, the popup menu that appears
        includes an entry called "Merge Tags" and it just merges them
        automatically into one tag, choosing the winning tag at random.
        The user can always rename it.  
        
        The thing is you need to replumb the popup menu because
        currently those popup actions only work on one tag at a time.
        But that's also probably a misfeature.
        
        - Smaller tag views.  I have lots of tags, and my sidebar on the
        left is pretty full, I have to scroll a lot to see all the tags.
        It'd be nice to have a mode where you can turn off the icons or
        make them as tall as the letter 'X' or something.  That could be
        a right-click thing too.

I've wanted this for a while now but a Linux Magazine article from last
week spurred me to JFDI.  Try it out, let me know what you think.  

Nat


        
Index: Db.cs
===================================================================
RCS file: /cvs/gnome/f-spot/src/Db.cs,v
retrieving revision 1.7
diff -u -r1.7 Db.cs
--- Db.cs	7 Oct 2005 21:35:39 -0000	1.7
+++ Db.cs	8 Nov 2005 04:37:03 -0000
@@ -24,7 +24,7 @@
 public abstract class DbStore {
 	// DbItem cache.
 
-	Hashtable item_cache;
+	protected Hashtable item_cache;
 	bool cache_is_immortal;
 
 	protected void AddToCache (DbItem item)
Index: MainWindow.cs
===================================================================
RCS file: /cvs/gnome/f-spot/src/MainWindow.cs,v
retrieving revision 1.228
diff -u -r1.228 MainWindow.cs
--- MainWindow.cs	2 Nov 2005 08:33:16 -0000	1.228
+++ MainWindow.cs	8 Nov 2005 04:37:04 -0000
@@ -83,6 +83,9 @@
 	[Glade.Widget] Gtk.Image near_image;
 	[Glade.Widget] Gtk.Image far_image;
 
+	[Glade.Widget] Gtk.HBox tagbar;
+	[Glade.Widget] Gtk.Entry tag_entry;
+
 	Gtk.Toolbar toolbar;
 
 	PhotoVersionMenu versions_submenu;
@@ -260,6 +263,12 @@
 		photo_view.UpdateStarted += HandlePhotoViewUpdateStarted;
 		photo_view.UpdateFinished += HandlePhotoViewUpdateFinished;
 
+		// Focus the tag entry if the user starts typing a tag
+		icon_view.KeyPressEvent += HandlePossibleTagTyping;
+		photo_view.KeyPressEvent += HandlePossibleTagTyping;
+		tag_entry.KeyPressEvent += HandleTagEntryKeyPressEvent;
+		tag_entry.FocusOutEvent += HandleTagEntryFocusOutEvent;
+
 		Gtk.Drag.DestSet (photo_view, DestDefaults.All, tag_target_table, 
 				  DragAction.Copy | DragAction.Move); 
 
@@ -455,7 +464,6 @@
 	private void RotateSelectedPictures (RotateCommand.Direction direction)
 	{
 		RotateCommand command = new RotateCommand (main_window);
-
 		
 		int [] selected_ids = SelectedIds ();
 		if (command.Execute (direction, SelectedPhotos (selected_ids))) {
@@ -626,6 +634,8 @@
 
 			break;
 		}
+
+		UpdateTagEntryFromSelection ();
 	}
 
 #if SHOW_CALENDAR
@@ -824,6 +834,8 @@
 					AttachTags (tag_selection_widget.TagHighlight (), SelectedIds());
 				else 
 					AttachTags (tag_selection_widget.TagHighlight (), new int [] {item});
+
+				UpdateTagEntryFromSelection ();
 			}
 			break;
 		case (uint)TargetType.UriList:
@@ -851,7 +863,9 @@
 		info_box.Photo = CurrentPhoto;
 		if (info_display != null)
 			info_display.Photo = CurrentPhoto;
+
 		UpdateMenus ();
+		UpdateTagEntryFromSelection ();
 	}
 
 	void HandleDoubleClicked (IconView icon_view, int clicked_item)
@@ -960,6 +974,8 @@
 		foreach (int num in SelectedIds ()) {
 			AddTagExtended (num, new Tag [] {t});
 		}
+
+		UpdateTagEntryFromSelection ();
 	}
 	
 	void HandleFindTagMenuSelected (Tag t)
@@ -973,6 +989,8 @@
 			query.Photos [num].RemoveTag (t);
 			query.Commit (num);
 		}
+
+		UpdateTagEntryFromSelection ();
 	}
 
 	//
@@ -1338,6 +1356,8 @@
 			query.Photos [num].RemoveTag (tags);
 			query.Commit (num);
 		}
+
+		UpdateTagEntryFromSelection ();
 	}
 
 	public void HandleEditSelectedTag (object obj, EventArgs args)
@@ -1988,5 +2008,180 @@
 		remove_tag_from_selection.Sensitive = tag_sensitive && active_selection;
 	}
 
+	// Typing in tags
+
+	private ArrayList selected_photos_tagnames;
+
+	private void UpdateTagEntryFromSelection ()
+	{
+		Hashtable taghash = new Hashtable ();
+
+		Photo [] sel = SelectedPhotos (SelectedIds ());
+		foreach (Photo p in sel) {
+			foreach (Tag t in p.Tags) {
+				int count = 1;
+				string tagname = t.Name;
+
+				if (taghash.Contains (tagname))
+					count = ((int) taghash [tagname]) + 1;
+
+				taghash [tagname] = count;
+			}
+		}
+
+		string taglist = "";
+		selected_photos_tagnames = new ArrayList ();
+		foreach (string tagname in taghash.Keys)
+			selected_photos_tagnames.Add (tagname);
+		selected_photos_tagnames.Sort ();
+		
+		foreach (string tagname in selected_photos_tagnames) {
+			if (((int) taghash [tagname]) != sel.Length)
+				continue;
+			
+			if (taglist == "")
+				taglist = tagname;
+			else
+				taglist = taglist + ", " + tagname;
+		}
+
+		tag_entry.Text = taglist;
+	}
+
+	public void HandlePossibleTagTyping (object sender, Gtk.KeyPressEventArgs args)
+	{
+		Console.WriteLine ("possible tag typing"); 
+
+		if (tagbar.Visible && tag_entry.HasFocus)
+			return;
+
+		char c = System.Convert.ToChar (Gdk.Keyval.ToUnicode ((uint) args.Event.Key));
+		if (! System.Char.IsLetter (c))
+			return;
+
+		if (! tagbar.Visible)
+			tagbar.Show ();
+		
+		if (tag_entry.Text.Length > 0)
+			tag_entry.Text += ", ";
+		tag_entry.Text += c;
+
+		tag_entry.GrabFocus ();
+		tag_entry.SelectRegion (-1, -1);
+	}
+
+	public void HandleTagEntryChanged (object sender, EventArgs args)
+	{
+	}
+
+	public void HandleTagEntryActivate (object sender, EventArgs args)
+	{
+		string [] tagnames = GetTypedTagNames ();
+
+		// Add any new tags to the selected photos
+		Category default_category = null;
+		Tag [] selection = tag_selection_widget.TagHighlight ();
+		if (selection.Length > 0) {
+			if (selection [0] is Category)
+				default_category = (Category) selection [0];
+			else
+				default_category = selection [0].Category;
+		}
+
+		foreach (string tagname in tagnames) {
+			if (tagname.Length == 0)
+				continue;
+			
+			Tag t = db.Tags.GetTagByName (tagname);
+			if (t == null) {
+				t = db.Tags.CreateTag (default_category, tagname);
+				db.Tags.Commit (t);
+			}
+
+			Tag [] tags = new Tag [1];
+			tags [0] = t;
+
+			foreach (int num in SelectedIds ())
+				AddTagExtended (num, tags);
+		}
+
+		// Remove any removed tags from the selected photos
+		foreach (string tagname in selected_photos_tagnames) {
+			if (! IsTagInList (tagnames, tagname)) {
+				
+				Tag tag = db.Tags.GetTagByName (tagname);
+
+				foreach (int num in SelectedIds ()) {
+					query.Photos [num].RemoveTag (tag);
+					query.Commit (num);
+				}
+			}
+		}
+
+		UpdateTagEntryFromSelection ();
+		icon_view.GrabFocus ();
+	}
+
+	private void HideTagBar ()
+	{
+		// Cancel any pending edits...
+		UpdateTagEntryFromSelection ();
+
+		tagbar.Hide ();
+
+		if (view_mode == ModeType.IconView)
+			icon_view.GrabFocus ();
+		else
+			photo_view.GrabFocus ();
+	}
+	
+
+	public void HandleTagBarCloseButtonPressed (object sender, EventArgs args)
+	{
+		HideTagBar ();
+	}
+
+	public void HandleTagEntryKeyPressEvent (object sender, Gtk.KeyPressEventArgs args)
+	{
+		if (args.Event.Key == Gdk.Key.Escape)
+			HideTagBar ();
+		else if (args.Event.Key == Gdk.Key.Tab) {
+			string [] names = GetTypedTagNames ();
+
+			Tag t = db.Tags.GetTagByNameStart (names [names.Length - 1]);
+			if (t != null) {
+				names [names.Length - 1] = t.Name;
+				tag_entry.Text = String.Join (", ", names);
+			}
+		}
+	}
+
+	public void HandleTagEntryFocusOutEvent (object sender, EventArgs args)
+	{
+		UpdateTagEntryFromSelection ();
+	}
+
+	private string [] GetTypedTagNames ()
+	{
+		string [] tagnames = tag_entry.Text.Split (new char [] {','});
+
+		ArrayList list = new ArrayList ();
+		for (int i = 0; i < tagnames.Length; i ++) {
+			string s = tagnames [i].Trim ();
+
+			if (s.Length > 0)
+				list.Add (s);
+		}
+
+		return (string []) (list.ToArray (typeof (string)));
+	}
+
+	private bool IsTagInList (string [] tags, string tag)
+	{
+		foreach (string t in tags)
+			if (t == tag)
+				return true;
+		return false;
+	}
 }
 
Index: PhotoStore.cs
===================================================================
RCS file: /cvs/gnome/f-spot/src/PhotoStore.cs,v
retrieving revision 1.72
diff -u -r1.72 PhotoStore.cs
--- PhotoStore.cs	2 Nov 2005 08:33:15 -0000	1.72
+++ PhotoStore.cs	8 Nov 2005 04:37:04 -0000
@@ -1273,7 +1273,7 @@
 		db.Photos.Commit (me_in_sf);
 
 		me_in_sf.RemoveTag (favorites_tag);
-		me_in_sf.Description = "Myself and the SF skyline";
+     		me_in_sf.Description = "Myself and the SF skyline";
 		me_in_sf.CreateVersion ("cropped", Photo.OriginalVersionId);
 		me_in_sf.CreateVersion ("UM-ed", Photo.OriginalVersionId);
 		db.Photos.Commit (me_in_sf);
Index: PhotoView.cs
===================================================================
RCS file: /cvs/gnome/f-spot/src/PhotoView.cs,v
retrieving revision 1.68
diff -u -r1.68 PhotoView.cs
--- PhotoView.cs	2 Nov 2005 08:33:15 -0000	1.68
+++ PhotoView.cs	8 Nov 2005 04:37:04 -0000
@@ -253,7 +253,6 @@
 		}
 
 		Photo photo = (Photo)Item.Current;
-		Exif.ExifData exif_data = new Exif.ExifData (photo.DefaultVersionPath);
 
 		Pixbuf edited;
 		if (redeye) {
@@ -273,7 +272,6 @@
 		// be fixed there.
 		photo_view.Pixbuf = edited;
 		photo_view.UnsetSelection ();
-		bool version = false;
 
 		try {
 			bool create_version = photo.DefaultVersionId == Photo.OriginalVersionId;
Index: TagSelectionWidget.cs
===================================================================
RCS file: /cvs/gnome/f-spot/src/TagSelectionWidget.cs,v
retrieving revision 1.21
diff -u -r1.21 TagSelectionWidget.cs
--- TagSelectionWidget.cs	2 Nov 2005 05:35:57 -0000	1.21
+++ TagSelectionWidget.cs	8 Nov 2005 04:37:04 -0000
@@ -290,7 +290,7 @@
 	{
 		TreeIter root = TreeIter.Zero;
 		iter = TreeIter.Zero;
-	
+
 		bool valid = Model.GetIterFirst (out root);
 		
 		while (valid) {
@@ -399,12 +399,14 @@
 	
 	private void HandleTagCreated (Tag tag) 
 	{
-		TreeIter iter;
+		TreeIter iter = TreeIter.Zero;
 
-		if (TreeIterForTag (tag.Category, out iter)) {
-			// create dialog doesn't let you create a top level tag...
-			InsertInOrder (iter, false, tag);
-		}
+		if (tag.Category != tag_store.RootCategory)
+			TreeIterForTag (tag.Category, out iter);
+
+		InsertInOrder (iter,
+			       tag.Category.Name == tag_store.RootCategory.Name,
+			       tag);
 	}
 	
 	private void HandleTagChanged (Tag tag) 
Index: TagStore.cs
===================================================================
RCS file: /cvs/gnome/f-spot/src/TagStore.cs,v
retrieving revision 1.20
diff -u -r1.20 TagStore.cs
--- TagStore.cs	28 Oct 2005 19:52:30 -0000	1.20
+++ TagStore.cs	8 Nov 2005 04:37:04 -0000
@@ -268,6 +268,26 @@
 		}
 	}
 
+	public Tag GetTagByName (string name)
+	{
+		foreach (Tag t in this.item_cache.Values) {
+			if (t.Name == name)
+				return t;
+		}
+
+		return null;
+	}
+
+	public Tag GetTagByNameStart (string s)
+	{
+		foreach (Tag t in this.item_cache.Values) {
+			if (t.Name.StartsWith (s))
+				return t;
+		}
+
+		return null;
+	}
+
 	// In this store we keep all the items (i.e. the tags) in memory at all times.  This is
 	// mostly to simplify handling of the parent relationship between tags, but it also makes it
 	// a little bit faster.  We achieve this by passing "true" as the cache_is_immortal to our
Index: f-spot.glade
===================================================================
RCS file: /cvs/gnome/f-spot/src/f-spot.glade,v
retrieving revision 1.115
diff -u -r1.115 f-spot.glade
--- f-spot.glade	7 Nov 2005 20:06:48 -0000	1.115
+++ f-spot.glade	8 Nov 2005 04:37:05 -0000
@@ -7405,7 +7405,91 @@
 		  <property name="spacing">0</property>
 
 		  <child>
-		    <placeholder/>
+		    <widget class="GtkHBox" id="tagbar">
+		      <property name="border_width">2</property>
+		      <property name="homogeneous">False</property>
+		      <property name="spacing">1</property>
+
+		      <child>
+			<widget class="GtkLabel" id="label160">
+			  <property name="visible">True</property>
+			  <property name="label" translatable="yes">Tags: </property>
+			  <property name="use_underline">False</property>
+			  <property name="use_markup">False</property>
+			  <property name="justify">GTK_JUSTIFY_LEFT</property>
+			  <property name="wrap">False</property>
+			  <property name="selectable">False</property>
+			  <property name="xalign">0.5</property>
+			  <property name="yalign">0.5</property>
+			  <property name="xpad">0</property>
+			  <property name="ypad">0</property>
+			  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+			  <property name="width_chars">-1</property>
+			  <property name="single_line_mode">False</property>
+			  <property name="angle">0</property>
+			</widget>
+			<packing>
+			  <property name="padding">0</property>
+			  <property name="expand">False</property>
+			  <property name="fill">False</property>
+			</packing>
+		      </child>
+
+		      <child>
+			<widget class="GtkEntry" id="tag_entry">
+			  <property name="visible">True</property>
+			  <property name="can_focus">True</property>
+			  <property name="has_focus">True</property>
+			  <property name="editable">True</property>
+			  <property name="visibility">True</property>
+			  <property name="max_length">0</property>
+			  <property name="text" translatable="yes"></property>
+			  <property name="has_frame">True</property>
+			  <property name="invisible_char">*</property>
+			  <property name="activates_default">False</property>
+			  <signal name="changed" handler="HandleTagEntryChanged" last_modification_time="Mon, 07 Nov 2005 20:15:50 GMT"/>
+			  <signal name="activate" handler="HandleTagEntryActivate" last_modification_time="Mon, 07 Nov 2005 20:18:18 GMT"/>
+			</widget>
+			<packing>
+			  <property name="padding">0</property>
+			  <property name="expand">True</property>
+			  <property name="fill">True</property>
+			</packing>
+		      </child>
+
+		      <child>
+			<widget class="GtkButton" id="tag_close_button">
+			  <property name="visible">True</property>
+			  <property name="can_focus">True</property>
+			  <property name="relief">GTK_RELIEF_NONE</property>
+			  <property name="focus_on_click">True</property>
+			  <signal name="pressed" handler="HandleTagBarCloseButtonPressed" last_modification_time="Tue, 08 Nov 2005 01:22:39 GMT"/>
+
+			  <child>
+			    <widget class="GtkImage" id="image23">
+			      <property name="visible">True</property>
+			      <property name="stock">gtk-close</property>
+			      <property name="icon_size">4</property>
+			      <property name="xalign">0.5</property>
+			      <property name="yalign">0.5</property>
+			      <property name="xpad">0</property>
+			      <property name="ypad">0</property>
+			    </widget>
+			  </child>
+			</widget>
+			<packing>
+			  <property name="padding">0</property>
+			  <property name="expand">False</property>
+			  <property name="fill">False</property>
+			</packing>
+		      </child>
+		    </widget>
+		    <packing>
+		      <property name="padding">0</property>
+		      <property name="expand">False</property>
+		      <property name="fill">True</property>
+		      <property name="pack_type">GTK_PACK_END</property>
+		    </packing>
 		  </child>
 
 		  <child>
@@ -7671,6 +7755,7 @@
 	  <property name="padding">0</property>
 	  <property name="expand">False</property>
 	  <property name="fill">False</property>
+	  <property name="pack_type">GTK_PACK_END</property>
 	</packing>
       </child>
     </widget>


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