[shotwell] Implement linked tags
- From: Jens Georg <jensgeorg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [shotwell] Implement linked tags
- Date: Fri, 12 Aug 2016 10:25:26 +0000 (UTC)
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]