From 97d11454532c7e022efa96c4f4cac82780495c49 Mon Sep 17 00:00:00 2001 From: GeopJr Date: Sun, 18 Jun 2023 14:40:24 +0300 Subject: [PATCH] feat(Composer): custom emoji picker (#308) * feat(Composer): custom emoji picker * feat(Widgets.Emoji): request paintable on idle * feat: increase default image cache to 5m * feat: move cechooser to its own widget * feat: do not use linked * feat: change title if only other category exists * fix: margins * chore: remove unused code --- data/gresource.xml | 1 + .../scalable/actions/tuba-cat-symbolic.svg | 2 + src/API/Emoji.vala | 7 + src/Application.vala | 4 +- .../Completion/CompletionProvider.vala | 7 - src/Dialogs/Composer/EditorPage.vala | 10 + src/Views/MediaViewer.vala | 5 - src/Widgets/CustomEmojiChooser.vala | 197 ++++++++++++++++++ src/Widgets/Emoji.vala | 20 +- src/Widgets/meson.build | 1 + 10 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 data/icons/scalable/actions/tuba-cat-symbolic.svg create mode 100644 src/Widgets/CustomEmojiChooser.vala diff --git a/data/gresource.xml b/data/gresource.xml index 2c15f6543..98c960f7d 100644 --- a/data/gresource.xml +++ b/data/gresource.xml @@ -61,6 +61,7 @@ icons/scalable/actions/tuba-newspaper-symbolic.svg icons/scalable/actions/tuba-explore2-large-symbolic.svg icons/scalable/actions/tuba-text-justify-left-symbolic.svg + icons/scalable/actions/tuba-cat-symbolic.svg gtk/dropdown/icon.ui diff --git a/data/icons/scalable/actions/tuba-cat-symbolic.svg b/data/icons/scalable/actions/tuba-cat-symbolic.svg new file mode 100644 index 000000000..fa81255c8 --- /dev/null +++ b/data/icons/scalable/actions/tuba-cat-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/src/API/Emoji.vala b/src/API/Emoji.vala index b788eeee2..da25bf240 100644 --- a/src/API/Emoji.vala +++ b/src/API/Emoji.vala @@ -1,4 +1,11 @@ public class Tuba.API.Emoji : Entity { public string shortcode { get; set; } public string url { get; set; } + public string category { get; set; default=_("Other"); } + public bool visible_in_picker { get; set; default=true; } + public bool is_other { + get { + return category == _("Other"); + } + } } diff --git a/src/Application.vala b/src/Application.vala index 5ac21a95e..ae689326d 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -127,7 +127,9 @@ namespace Tuba { streams = new Streams (); network = new Network (); entity_cache = new EntityCache (); - image_cache = new ImageCache (); + image_cache = new ImageCache () { + maintenance_secs = 60 * 5 + }; accounts = new SecretAccountStore(); accounts.init (); diff --git a/src/Dialogs/Composer/Completion/CompletionProvider.vala b/src/Dialogs/Composer/Completion/CompletionProvider.vala index c38d5efef..70ceaff3d 100644 --- a/src/Dialogs/Composer/Completion/CompletionProvider.vala +++ b/src/Dialogs/Composer/Completion/CompletionProvider.vala @@ -7,13 +7,6 @@ public abstract class Tuba.CompletionProvider: Object, GtkSource.CompletionProvi public string? trigger_char { get; construct; } protected bool is_capturing_input { get; set; default = false; } protected int empty_triggers = 0; - protected ImageCache image_cache; - - construct { - image_cache = new ImageCache () { - maintenance_secs = 60 - }; - } public virtual bool is_trigger (Gtk.TextIter iter, unichar ch) { if (this.trigger_char == null) { diff --git a/src/Dialogs/Composer/EditorPage.vala b/src/Dialogs/Composer/EditorPage.vala index 6d146ad37..6ccc1d617 100644 --- a/src/Dialogs/Composer/EditorPage.vala +++ b/src/Dialogs/Composer/EditorPage.vala @@ -26,6 +26,7 @@ public class Tuba.EditorPage : ComposerPage { install_languages (status.language); add_button (new Gtk.Separator (Orientation.VERTICAL)); install_cw (status.spoiler_text); + add_button (new Gtk.Separator (Orientation.VERTICAL)); install_emoji_picker(); validate (); @@ -199,9 +200,18 @@ public class Tuba.EditorPage : ComposerPage { tooltip_text = _("Emoji Picker") }; + var custom_emoji_picker = new Widgets.CustomEmojiChooser (); + var custom_emoji_button = new MenuButton () { + icon_name = "tuba-cat-symbolic", + popover = custom_emoji_picker, + tooltip_text = _("Custom Emoji Picker") + }; + add_button(emoji_button); + add_button(custom_emoji_button); emoji_picker.emoji_picked.connect(on_emoji_picked); + custom_emoji_picker.emoji_picked.connect(on_emoji_picked); } protected void on_emoji_picked(string emoji_unicode) { diff --git a/src/Views/MediaViewer.vala b/src/Views/MediaViewer.vala index 111dbb15d..a05326ff4 100644 --- a/src/Views/MediaViewer.vala +++ b/src/Views/MediaViewer.vala @@ -201,7 +201,6 @@ public class Tuba.Views.MediaViewer : Gtk.Box { private Gee.ArrayList items = new Gee.ArrayList (); protected Gtk.Button fullscreen_btn; protected Adw.HeaderBar headerbar; - protected ImageCache image_cache; private Adw.Carousel carousel; private Adw.CarouselIndicatorDots carousel_dots; @@ -224,10 +223,6 @@ public class Tuba.Views.MediaViewer : Gtk.Box { overlay.add_overlay (generate_media_buttons ()); overlay.child = carousel; - image_cache = new ImageCache () { - maintenance_secs = 60 * 5 - }; - var drag = new Gtk.GestureDrag (); drag.drag_begin.connect(on_drag_begin); drag.drag_update.connect(on_drag_update); diff --git a/src/Widgets/CustomEmojiChooser.vala b/src/Widgets/CustomEmojiChooser.vala new file mode 100644 index 000000000..687b70854 --- /dev/null +++ b/src/Widgets/CustomEmojiChooser.vala @@ -0,0 +1,197 @@ +public class Tuba.Widgets.CustomEmojiChooser : Gtk.Popover { + public string query { get; set; default = ""; } + public signal void emoji_picked (string shortcode); + public bool is_populated { get; protected set; default=false; } + + private Gee.HashMap> gen_emojis_cat_map () { + var res = new Gee.HashMap>(); + var emojis = accounts.active.instance_emojis; + + if (emojis != null && emojis.size > 0) { + emojis.@foreach (e => { + if (!e.visible_in_picker) return true; + + if (res.has_key(e.category)) { + var array = res.get (e.category); + array.add (e); + } else { + var array = new Gee.ArrayList (); + array.add (e); + res.set(e.category, array); + } + + return true; + }); + } + + return res; + } + + private Gtk.Box custom_emojis_box; + private Gtk.SearchEntry entry; + private Gtk.FlowBox results; + private Gtk.Label results_label; + private Gtk.ScrolledWindow custom_emojis_scrolled; + private GLib.ListStore list_store = new GLib.ListStore (typeof (API.Emoji)); + construct { + this.add_css_class ("emoji-picker"); + custom_emojis_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6) { + margin_end = 6, + margin_bottom = 6, + margin_start = 6 + }; + custom_emojis_scrolled = new Gtk.ScrolledWindow () { + hscrollbar_policy = Gtk.PolicyType.NEVER, + height_request = 360 + }; + var content_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + content_box.append (custom_emojis_scrolled); + + this.child = content_box; + custom_emojis_scrolled.child = custom_emojis_box; + + results_label = create_category_label (_("Results")); + results_label.visible = false; + custom_emojis_box.append (results_label); + + results = create_emoji_box (); + custom_emojis_box.append (results); + + results.bind_model (list_store, model => { + var emoji = model as API.Emoji; + if (emoji == null) Process.exit (0); + return create_emoji_button (emoji); + }); + + entry = new Gtk.SearchEntry () { + text = query, + hexpand = true + }; + + var entry_bin = new Adw.Bin () { + css_classes = { "emoji-searchbar" }, + child = entry + }; + content_box.prepend (entry_bin); + + entry.activate.connect (search); + entry.search_changed.connect (search); + entry.stop_search.connect (search); + } + + protected void search () { + query = entry.text.chug ().chomp ().down ().replace (":", ""); + list_store.remove_all (); + + if (query == "") { + results_label.visible = false; + return; + } + + var emojis = accounts.active.instance_emojis; + + if (emojis != null && emojis.size > 0) { + var at_least_one = false; + emojis.@foreach (e => { + if (!e.visible_in_picker) return true; + if (query in e.shortcode) { + at_least_one = true; + list_store.append (e); + }; + + return true; + }); + + if (at_least_one) { + results_label.label = _("Results"); + custom_emojis_scrolled.scroll_child (Gtk.ScrollType.START, false); + } else { + results_label.label = _("No Results"); + } + + results_label.visible = true; + } + } + + protected void on_custom_emoji_picked (Gtk.Button emoji_btn) { + var emoji = emoji_btn.child as Emoji; + if (emoji != null) { + on_close (); + emoji_picked (@":$(emoji.shortcode):"); + } + } + + protected void on_close () { + this.popdown (); + } + + public override void show () { + base.show (); + + GLib.Idle.add (() => { + if (!is_populated) populate_chooser (); + entry.grab_focus (); + return GLib.Source.REMOVE; + }); + } + + protected void populate_chooser () { + var categorized_custom_emojis = gen_emojis_cat_map (); + + categorized_custom_emojis.@foreach (e => { + if (e.key == _("Other")) return true; + create_category (e.key, e.value); + + return true; + }); + + if (categorized_custom_emojis.has_key (_("Other"))) + create_category (categorized_custom_emojis.size > 1 ? _("Other") : _("Custom Emojis"), categorized_custom_emojis.get(_("Other"))); + + is_populated = true; + } + + protected Gtk.Button create_emoji_button (API.Emoji emoji) { + var emoji_btn = new Gtk.Button () { + css_classes = { "flat" }, + child = new Widgets.Emoji (emoji.url, emoji.shortcode) + }; + emoji_btn.set_css_name ("emoji"); + + emoji_btn.clicked.connect (on_custom_emoji_picked); + return emoji_btn; + } + + protected void create_category (string key, Gee.ArrayList value) { + custom_emojis_box.append (create_category_label (key)); + + var emojis_flowbox = create_emoji_box (); + value.@foreach (emoji => { + emojis_flowbox.append (create_emoji_button (emoji)); + + return true; + }); + + custom_emojis_box.append (emojis_flowbox); + } + + protected Gtk.FlowBox create_emoji_box () { + return new Gtk.FlowBox () { + homogeneous = true, + column_spacing = 6, + row_spacing = 6, + max_children_per_line = 6, + min_children_per_line = 6, + selection_mode = Gtk.SelectionMode.NONE + }; + } + + protected Gtk.Label create_category_label (string label) { + return new Gtk.Label (label) { + wrap = true, + wrap_mode = Pango.WrapMode.WORD_CHAR, + halign = Gtk.Align.START, + margin_top = 3 + }; + } +} diff --git a/src/Widgets/Emoji.vala b/src/Widgets/Emoji.vala index 81387c47a..4a494041d 100644 --- a/src/Widgets/Emoji.vala +++ b/src/Widgets/Emoji.vala @@ -4,21 +4,27 @@ using Gdk; public class Tuba.Widgets.Emoji : Adw.Bin { protected Image image; + public string? shortcode { get; set; } construct { image = new Gtk.Image (); child = image; } - public Emoji (string emoji_url, string? shortcode = null) { - if (shortcode != null) - image.tooltip_text = shortcode; - image_cache.request_paintable (emoji_url, on_cache_response); + public Emoji (string emoji_url, string? t_shortcode = null) { + if (t_shortcode != null) { + image.tooltip_text = t_shortcode; + shortcode = t_shortcode; + } + + GLib.Idle.add (() => { + image_cache.request_paintable (emoji_url, on_cache_response); + return GLib.Source.REMOVE; + }); } void on_cache_response (bool is_loaded, owned Paintable? data) { - var image_widget = (child as Image); - if (child != null && image_widget != null) - image_widget.paintable = data; + if (image != null) + image.paintable = data; } } diff --git a/src/Widgets/meson.build b/src/Widgets/meson.build index c11ccfc9d..b571a9caa 100644 --- a/src/Widgets/meson.build +++ b/src/Widgets/meson.build @@ -3,6 +3,7 @@ sources += files( 'Background.vala', 'BookWyrmPage.vala', 'Conversation.vala', + 'CustomEmojiChooser.vala', 'Emoji.vala', 'EmojiLabel.vala', 'LabelWithWidgets.vala',