[shotwell] Implement linked tags



commit 644f056255e903d56eff4a152911da86ea164803
Author: Andreas Brauchli <a brauchli elementarea net>
Date:   Thu Aug 11 15:08:10 2016 +0200

    Implement linked tags
    
    The tag list underneath the thumbnails is now interactive:
    * Hover over a tag to highlight it by underlining the tag text,
      unless dragging is active.
    * Click a tag to switch to the tag page, as if the was selected in the
      sidebar.
    
    Tag class changes:
    * make_tag_string() now demands a sorted list of terminal tags. This
      ensures that the sort order is maintained when generating the string
      representation of the list.
      The new make_user_visible_tag_list() can be used to filter and sort a Tag
      Collection accordingly.
    
    * Fix a bug in Tag.make_tag_string:
      A list of seen tags is kept to not show tags with the same string
      representation (but potentially different tag-path) twice. However,
      tags were never added to the list, such that it always remained empty
      and new items were thus compared against the empty list.
    
    Fixes https://bugzilla.gnome.org/show_bug.cgi?id=717523

 src/CheckerboardLayout.vala |  124 +++++++++++++++++++++++++++++++++++++++++--
 src/Page.vala               |   10 +++-
 src/Tag.vala                |   67 ++++++++++++++---------
 src/Thumbnail.vala          |    6 +--
 4 files changed, 171 insertions(+), 36 deletions(-)
---
diff --git a/src/CheckerboardLayout.vala b/src/CheckerboardLayout.vala
index 032235f..f2acc30 100644
--- a/src/CheckerboardLayout.vala
+++ b/src/CheckerboardLayout.vala
@@ -127,6 +127,9 @@ public abstract class CheckerboardItem : ThumbnailView {
     private CheckerboardItemText? subtitle = null;
     private bool subtitle_visible = false;
     private bool is_cursor = false;
+    private Pango.Alignment tag_alignment = Pango.Alignment.LEFT;
+    private Gee.List<Tag>? user_visible_tag_list = null;
+    private Gee.Collection<Tag> tags;
     private Gdk.Pixbuf pixbuf = null;
     private Gdk.Pixbuf display_pixbuf = null;
     private Gdk.Pixbuf brightened = null;
@@ -154,7 +157,9 @@ public abstract class CheckerboardItem : ThumbnailView {
         // (notify_membership_changed) and calculate when the collection's property settings
         // are known
     }
-    
+
+    public bool has_tags { get; private set; }
+
     public override string get_name() {
         return (title != null) ? title.get_text() : base.get_name();
     }
@@ -236,8 +241,58 @@ public abstract class CheckerboardItem : ThumbnailView {
         recalc_size("set_comment_visible");
         notify_view_altered();
     }
-    
-    
+
+    public void set_tags(Gee.Collection<Tag>? tags,
+            Pango.Alignment alignment = Pango.Alignment.LEFT) {
+        has_tags = (tags != null && tags.size > 0);
+        tag_alignment = alignment;
+        string text;
+        if (has_tags) {
+            this.tags = tags;
+            user_visible_tag_list = Tag.make_user_visible_tag_list(tags);
+            text = Tag.make_tag_markup_string(user_visible_tag_list);
+        } else {
+            text = "<small>.</small>";
+        }
+
+        if (subtitle != null && subtitle.is_set_to(text, true, alignment))
+            return;
+        subtitle = new CheckerboardItemText(text, alignment, true);
+
+        if (subtitle_visible) {
+            recalc_size("set_subtitle");
+            notify_view_altered();
+        }
+    }
+
+    public void clear_tags() {
+        clear_subtitle();
+        has_tags = false;
+        user_visible_tag_list = null;
+    }
+
+    public void highlight_user_visible_tag(int index)
+            requires (user_visible_tag_list != null) {
+        string text = Tag.make_tag_markup_string(user_visible_tag_list, index);
+        subtitle = new CheckerboardItemText(text, tag_alignment, true);
+
+        if (subtitle_visible)
+            notify_view_altered();
+    }
+
+    public Tag get_user_visible_tag(int index)
+            requires (index >= 0 && index < user_visible_tag_list.size) {
+        return user_visible_tag_list.get(index);
+    }
+
+    public Pango.Layout? get_tag_list_layout() {
+        return has_tags ? subtitle.get_pango_layout() : null;
+    }
+
+    public Gdk.Rectangle get_subtitle_allocation() {
+        return subtitle.allocation;
+    }
+
     public string get_subtitle() {
         return (subtitle != null) ? subtitle.get_text() : "";
     }
@@ -1182,7 +1237,68 @@ public class CheckerboardLayout : Gtk.DrawingArea {
 
         return null;
     }
-    
+
+    public static int get_tag_index_at_pos(string tag_list, int pos) {
+        int sep_len = Tag.TAG_LIST_SEPARATOR_STRING.length;
+        assert (sep_len > 0);
+        int len = tag_list.length;
+        if (pos < 0 || pos >= len)
+            return -1;
+
+        // check if we're hovering on a separator
+        for (int i = 0; i < sep_len; ++i) {
+            if (tag_list[pos] == Tag.TAG_LIST_SEPARATOR_STRING[i] && pos >= i) {
+                if (tag_list.substring(pos - i, sep_len) == Tag.TAG_LIST_SEPARATOR_STRING)
+                    return -1;
+            }
+        }
+
+        // Determine the tag index by counting the number of separators before
+        // the requested position. This only works if the separator string
+        // contains the delimiter used to delimit tags (i.e. the comma `,'.)
+        int index = 0;
+        for (int i = 0; i < pos; ++i) {
+            if (tag_list[i] == Tag.TAG_LIST_SEPARATOR_STRING[0] &&
+                    i + sep_len <= len &&
+                    tag_list.substring(i, sep_len) == Tag.TAG_LIST_SEPARATOR_STRING) {
+                ++index;
+                i += sep_len - 1;
+            }
+        }
+        return index;
+    }
+
+    private int internal_handle_tag_mouse_event(CheckerboardItem item, int x, int y) {
+        Pango.Layout? layout = item.get_tag_list_layout();
+        if (layout == null)
+            return -1;
+        Gdk.Rectangle rect = item.get_subtitle_allocation();
+        int index, trailing;
+        int px = (x - rect.x) * Pango.SCALE;
+        int py = (y - rect.y) * Pango.SCALE;
+        if (layout.xy_to_index(px, py, out index, out trailing))
+            return get_tag_index_at_pos(layout.get_text(), index);
+        return -1;
+    }
+
+    public bool handle_mouse_motion(CheckerboardItem item, int x, int y, Gdk.ModifierType mask) {
+        if (!item.has_tags || is_drag_select_active())
+            return false;
+        int tag_index = internal_handle_tag_mouse_event(item, x, y);
+        item.highlight_user_visible_tag(tag_index);
+        return (tag_index >= 0);
+    }
+
+    public bool handle_left_click(CheckerboardItem item, double xd, double yd, Gdk.ModifierType mask) {
+        int tag_index = internal_handle_tag_mouse_event(item, (int)Math.round(xd), (int)Math.round(yd));
+        if (tag_index >= 0) {
+            Tag tag = item.get_user_visible_tag(tag_index);
+            LibraryWindow.get_app().switch_to_tag(tag);
+            return true;
+        }
+        return false;
+    }
+
     public Gee.List<CheckerboardItem> get_visible_items() {
         return intersection(visible_page);
     }
diff --git a/src/Page.vala b/src/Page.vala
index 081017c..4877a47 100644
--- a/src/Page.vala
+++ b/src/Page.vala
@@ -1471,9 +1471,14 @@ public abstract class CheckerboardPage : Page {
         uint state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK);
         
         // use clicks for multiple selection and activation only; single selects are handled by
-        // button release, to allow for multiple items to be selected then dragged
+        // button release, to allow for multiple items to be selected then dragged ...
         CheckerboardItem item = get_item_at_pixel(event.x, event.y);
         if (item != null) {
+            // ... however, there is no dragging if the user clicks on an interactive part of the
+            // CheckerboardItem (e.g. a tag)
+            if (layout.handle_left_click(item, event.x, event.y, event.state))
+                return true;
+
             switch (state) {
                 case Gdk.ModifierType.CONTROL_MASK:
                     // with only Ctrl pressed, multiple selections are possible ... chosen item
@@ -1644,6 +1649,9 @@ public abstract class CheckerboardPage : Page {
     }
     
     protected virtual bool on_mouse_over(CheckerboardItem? item, int x, int y, Gdk.ModifierType mask) {
+        if (item != null)
+            layout.handle_mouse_motion(item, x, y, mask);
+
         // if hovering over the last hovered item, or both are null (nothing highlighted and
         // hovering over empty space), do nothing
         if (item == highlighted)
diff --git a/src/Tag.vala b/src/Tag.vala
index cf41c0e..31b1e40 100644
--- a/src/Tag.vala
+++ b/src/Tag.vala
@@ -269,6 +269,7 @@ public class TagSourceCollection : ContainerSourceCollection {
 public class Tag : DataSource, ContainerSource, Proxyable, Indexable {
     public const string TYPENAME = "tag";
     public const string PATH_SEPARATOR_STRING = "/";
+    public const string TAG_LIST_SEPARATOR_STRING = ", ";
     
     private class TagSnapshot : SourceSnapshot {
         private TagRow row;
@@ -533,7 +534,12 @@ public class Tag : DataSource, ContainerSource, Proxyable, Indexable {
         return String.precollated_compare(a.get_name(), a.get_name_collation_key(), b.get_name(),
             b.get_name_collation_key());
     }
-    
+
+    public static int compare_user_visible_names(Tag a, Tag b) {
+        return String.precollated_compare(a.get_user_visible_name(), a.get_name_collation_key(),
+                                          b.get_user_visible_name(), b.get_name_collation_key());
+    }
+
     public static uint hash_name_string(string a) {
         return String.collated_hash(a);
     }
@@ -601,40 +607,47 @@ public class Tag : DataSource, ContainerSource, Proxyable, Indexable {
         
         return result;
     }
-    
-    public static string make_tag_string(Gee.Collection<Tag> tags, string? start = null, 
-        string separator = ", ", string? end = null, bool escape = false) {
-        StringBuilder builder = new StringBuilder(start ?? "");
+
+    // Creates a sorted list of terminal tags, unique by user-visible-name
+    public static Gee.List<Tag> make_user_visible_tag_list(Gee.Collection<Tag> tags) {
         Gee.HashSet<string> seen_tags = new Gee.HashSet<string>();
         Gee.Collection<Tag> terminal_tags = get_terminal_tags(tags);
-        Gee.ArrayList<string> sorted_tags = new Gee.ArrayList<string>();
+        Gee.ArrayList<Tag> sorted_tags = new Gee.ArrayList<Tag>();
         foreach (Tag tag in terminal_tags) {
-            string user_visible_name = escape ? guarded_markup_escape_text(
-                tag.get_user_visible_name()) : tag.get_user_visible_name();
-
-            if (!seen_tags.contains(user_visible_name))
-                sorted_tags.add(user_visible_name);
+            string user_visible_name = tag.get_user_visible_name();
+            if (!seen_tags.contains(user_visible_name)) {
+                sorted_tags.add(tag);
+                seen_tags.add(user_visible_name);
+            }
         }
-        
-        sorted_tags.sort();
-        Gee.Iterator<string> iter = sorted_tags.iterator();
-        while(iter.next()) {
-            builder.append(iter.get());
-            builder.append(separator);
+        sorted_tags.sort(Tag.compare_user_visible_names);
+        return sorted_tags;
+    }
+
+    public static string make_tag_markup_string(Gee.List<Tag> tags, int highlight_index = -1) {
+        StringBuilder builder = new StringBuilder("<small>");
+        int i = 0;
+        bool first = true;
+        foreach(Tag tag in tags) {
+            string tag_name = tag.get_user_visible_name();
+            string esc_tag_name = guarded_markup_escape_text(tag_name);
+            if (first)
+                first = false;
+            else
+                builder.append(TAG_LIST_SEPARATOR_STRING);
+            if (highlight_index == i)
+                builder.append("<u>");
+            builder.append(esc_tag_name);
+            if (highlight_index == i)
+                builder.append("</u>");
+            ++i;
         }
-        
+
+        builder.append("</small>");
         string built = builder.str;
-        
-        if (built.length >= separator.length)
-            if (built.substring(built.length - separator.length, separator.length) == separator)
-                built = built.substring(0, built.length - separator.length);
-        
-        if (end != null)
-            built += end;
-        
         return built;
     }
-    
+
     // Utility function to cleanup a tag name that comes from user input and prepare it for use
     // in the system and storage in the database.  Returns null if the name is unacceptable.
     public static string? prep_tag_name(string name) {
diff --git a/src/Thumbnail.vala b/src/Thumbnail.vala
index 983ab75..2fe28e8 100644
--- a/src/Thumbnail.vala
+++ b/src/Thumbnail.vala
@@ -80,11 +80,9 @@ public class Thumbnail : MediaSourceItem {
     private void update_tags(bool init = false) {
         Gee.Collection<Tag>? tags = Tag.global.fetch_sorted_for_source(media);
         if (tags == null || tags.size == 0)
-            clear_subtitle();
-        else if (!init)
-            set_subtitle(Tag.make_tag_string(tags, "<small>", ", ", "</small>", true), true);
+            clear_tags();
         else
-            set_subtitle("<small>.</small>", true);
+            set_tags(tags);
     }
     
     private void on_tag_contents_altered(ContainerSource container, Gee.Collection<DataSource>? added,


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