From e35df88d4a00c3a34f2b4d9fb7f10bb5d877bd29 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 24 Jan 2023 18:57:04 +0100 Subject: Fix UI for libadwaita --- .../conversation_item_skeleton.vala | 2 +- .../conversation_view.vala | 106 +++++++++++++---- .../file_default_widget.vala | 9 -- .../file_image_widget.vala | 55 +++++++-- .../ui/conversation_content_view/file_widget.vala | 132 ++++++++++++--------- .../conversation_content_view/message_widget.vala | 3 + 6 files changed, 208 insertions(+), 99 deletions(-) (limited to 'main/src/ui/conversation_content_view') diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index 00c88db3..3a68c9dc 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -37,7 +37,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, private uint time_update_timeout = 0; private ulong updated_roster_handler_id = 0; - public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item, bool initial_item) { + public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) { this.stream_interactor = stream_interactor; this.conversation = conversation; this.item = item; diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index 5481cfc5..4d978132 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -24,7 +24,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug private Gee.List? message_actions = null; private StreamInteractor stream_interactor; - private Gee.TreeSet content_items = new Gee.TreeSet(compare_meta_items); + private Gee.TreeSet content_items = new Gee.TreeSet(compare_content_meta_items); private Gee.TreeSet meta_items = new TreeSet(compare_meta_items); private Gee.HashMap item_item_skeletons = new Gee.HashMap(); private Gee.HashMap widgets = new Gee.HashMap(); @@ -37,7 +37,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug private double? was_page_size; private Mutex reloading_mutex = Mutex(); - private bool animate = false; private bool firstLoad = true; private bool at_current_content = true; private bool reload_messages = true; @@ -82,6 +81,15 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug main.add_controller(main_motion_events); main_motion_events.motion.connect(update_highlight); + // Process touch events and capture phase to allow highlighting a message without cursor + GestureClick click_controller = new GestureClick(); + click_controller.touch_only = true; + click_controller.propagation_phase = Gtk.PropagationPhase.CAPTURE; + main_wrap_box.add_controller(click_controller); + click_controller.pressed.connect_after((n, x, y) => { + update_highlight(x, y); + }); + return this; } @@ -200,6 +208,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug MenuButton button = new MenuButton(); button.icon_name = message_actions[i].icon_name; button.set_popover(message_actions[i].popover as Popover); + button.tooltip_text = Util.string_if_tooltips_active(message_actions[i].tooltip); action_buttons.add(button); } @@ -210,6 +219,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug button.clicked.connect(() => { message_action.callback(button, current_meta_item, currently_highlighted); }); + button.tooltip_text = Util.string_if_tooltips_active(message_actions[i].tooltip); action_buttons.add(button); } } @@ -232,12 +242,71 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug }); firstLoad = false; } + if (conversation == this.conversation && at_current_content) { + // Just make sure we are scrolled down + if (scrolled.vadjustment.value != scrolled.vadjustment.upper) { + scroll_animation(scrolled.vadjustment.upper).play(); + } + return; + } clear(); initialize_for_conversation_(conversation); display_latest(); + at_current_content = true; + // Scroll to end + scrolled.vadjustment.value = scrolled.vadjustment.upper; + } + + private void scroll_and_highlight_item(Plugins.MetaConversationItem target, uint duration = 500) { + Widget widget = null; + int h = 0; + foreach (Plugins.MetaConversationItem item in meta_items) { + widget = widgets[item]; + if (target == item) { + break; + } + h += widget.get_allocated_height(); + } + if (widget != widgets[target]) { + warning("Target item widget not reached"); + return; + } + double target_height = h - scrolled.vadjustment.page_size * 1/3; + Adw.Animation animation = scroll_animation(target_height); + animation.done.connect(() => { + widget.remove_css_class("highlight-once"); + widget.add_css_class("highlight-once"); + Timeout.add(5000, () => { + widget.remove_css_class("highlight-once"); + return false; + }); + }); + animation.play(); + } + + private Adw.Animation scroll_animation(double target) { +#if ADW_1_2 + return new Adw.TimedAnimation(scrolled, scrolled.vadjustment.value, target, 500, + new Adw.PropertyAnimationTarget(scrolled.vadjustment, "value") + ); +#else + return new Adw.TimedAnimation(scrolled, scrolled.vadjustment.value, target, 500, + new Adw.CallbackAnimationTarget(value => { + scrolled.vadjustment.value = value; + }) + ); +#endif + } public void initialize_around_message(Conversation conversation, ContentItem content_item) { + if (conversation == this.conversation) { + ContentMetaItem? matching_item = content_items.first_match(it => it.content_item.id == content_item.id); + if (matching_item != null) { + scroll_and_highlight_item(matching_item); + return; + } + } clear(); initialize_for_conversation_(conversation); Gee.List before_items = content_populator.populate_before(conversation, content_item, 40); @@ -245,7 +314,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug do_insert_item(item); } ContentMetaItem meta_item = content_populator.get_content_meta_item(content_item); - meta_item.can_merge = false; Widget w = insert_new(meta_item); content_items.add(meta_item); meta_items.add(meta_item); @@ -261,23 +329,16 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug // Compute where to jump to for centered message, jump, highlight. reload_messages = false; Timeout.add(700, () => { - int h = 0, i = 0; - foreach (Plugins.MetaConversationItem item in meta_items) { - Widget widget = widgets[item]; - if (widget == w) { - break; - } - h += widget.get_allocated_height(); - i++; - } - scrolled.vadjustment.value = h - scrolled.vadjustment.page_size * 1/3; - w.add_css_class("highlight-once"); + scroll_and_highlight_item(meta_item, 300); reload_messages = true; return false; }); } private void initialize_for_conversation_(Conversation? conversation) { + if (this.conversation == conversation) { + print("Re-initialized for %s\n", conversation.counterpart.bare_jid.to_string()); + } // Deinitialize old conversation Dino.Application app = Dino.Application.get_default(); if (this.conversation != null) { @@ -299,9 +360,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug } content_populator.init(this, conversation, Plugins.WidgetType.GTK4); subscription_notification.init(conversation, this); - - animate = false; - Timeout.add(20, () => { animate = true; return false; }); } private void display_latest() { @@ -331,8 +389,8 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug public void do_insert_item(Plugins.MetaConversationItem item) { lock (meta_items) { insert_new(item); - if (item as ContentMetaItem != null) { - content_items.add(item); + if (item is ContentMetaItem) { + content_items.add((ContentMetaItem)item); } meta_items.add(item); } @@ -348,7 +406,9 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug widget_order.remove(skeleton.get_widget()); item_item_skeletons.unset(item); - content_items.remove(item); + if (item is ContentMetaItem) { + content_items.remove((ContentMetaItem)item); + } meta_items.remove(item); } @@ -387,7 +447,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug Plugins.MetaConversationItem? lower_item = meta_items.lower(item); // Fill datastructure - ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item, !animate); + ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item); item_item_skeletons[item] = item_skeleton; int index = lower_item != null ? widget_order.index_of(item_item_skeletons[lower_item].get_widget()) + 1 : 0; widget_order.insert(index, item_skeleton.get_widget()); @@ -503,6 +563,10 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug } } + private static int compare_content_meta_items(ContentMetaItem a, ContentMetaItem b) { + return compare_meta_items(a, b); + } + private static int compare_meta_items(Plugins.MetaConversationItem a, Plugins.MetaConversationItem b) { int cmp1 = a.time.compare(b.time); if (cmp1 != 0) return cmp1; diff --git a/main/src/ui/conversation_content_view/file_default_widget.vala b/main/src/ui/conversation_content_view/file_default_widget.vala index 9efc130f..352c8d7a 100644 --- a/main/src/ui/conversation_content_view/file_default_widget.vala +++ b/main/src/ui/conversation_content_view/file_default_widget.vala @@ -10,9 +10,6 @@ namespace Dino.Ui { public class FileDefaultWidget : Box { public signal void clicked(); - public signal void open_file(); - public signal void save_file_as(); - public signal void cancel_download(); [GtkChild] public unowned Stack image_stack; [GtkChild] public unowned Label name_label; @@ -23,12 +20,6 @@ public class FileDefaultWidget : Box { private FileTransfer.State state; - class construct { - install_action("file.open", null, (widget, action_name) => { ((FileDefaultWidget) widget).open_file(); }); - install_action("file.save_as", null, (widget, action_name) => { ((FileDefaultWidget) widget).save_file_as(); }); - install_action("file.cancel", null, (widget, action_name) => { ((FileDefaultWidget) widget).cancel_download(); }); - } - public FileDefaultWidget() { EventControllerMotion this_motion_events = new EventControllerMotion(); this.add_controller(this_motion_events); diff --git a/main/src/ui/conversation_content_view/file_image_widget.vala b/main/src/ui/conversation_content_view/file_image_widget.vala index ec8481b7..505c46a0 100644 --- a/main/src/ui/conversation_content_view/file_image_widget.vala +++ b/main/src/ui/conversation_content_view/file_image_widget.vala @@ -8,41 +8,70 @@ namespace Dino.Ui { public class FileImageWidget : Box { - FileDefaultWidget file_default_widget; - FileDefaultWidgetController file_default_widget_controller; - public FileImageWidget() { this.halign = Align.START; this.add_css_class("file-image-widget"); + this.set_cursor_from_name("zoom-in"); } public async void load_from_file(File file, string file_name, int MAX_WIDTH=600, int MAX_HEIGHT=300) throws GLib.Error { - FixedRatioPicture image = new FixedRatioPicture() { min_width = 100, min_height = 100, max_width = MAX_WIDTH, max_height = MAX_HEIGHT, file = file }; + Gtk.Box image_overlay_toolbar = new Gtk.Box(Orientation.HORIZONTAL, 0) { halign=Gtk.Align.END, valign=Gtk.Align.START, margin_top=10, margin_start=10, margin_end=10, margin_bottom=10, vexpand=false, visible=false }; + image_overlay_toolbar.add_css_class("card"); + image_overlay_toolbar.add_css_class("toolbar"); + image_overlay_toolbar.add_css_class("overlay-toolbar"); + image_overlay_toolbar.set_cursor_from_name("default"); + + FixedRatioPicture image = new FixedRatioPicture() { min_width=100, min_height=100, max_width=MAX_WIDTH, max_height=MAX_HEIGHT, file=file }; + GestureClick gesture_click_controller = new GestureClick(); + gesture_click_controller.button = 1; // listen for left clicks + gesture_click_controller.released.connect((n_press, x, y) => { + switch (gesture_click_controller.get_device().source) { + case Gdk.InputSource.TOUCHSCREEN: + case Gdk.InputSource.PEN: + if (n_press == 1) { + image_overlay_toolbar.visible = !image_overlay_toolbar.visible; + } else if (n_press == 2) { + this.activate_action("file.open", null); + image_overlay_toolbar.visible = false; + } + break; + default: + this.activate_action("file.open", null); + image_overlay_toolbar.visible = false; + break; + } + }); + image.add_controller(gesture_click_controller); FileInfo file_info = file.query_info("*", FileQueryInfoFlags.NONE); string? mime_type = file_info.get_content_type(); - file_default_widget = new FileDefaultWidget() { valign=Align.END, vexpand=false, visible=false }; - file_default_widget.image_stack.visible = false; - file_default_widget_controller = new FileDefaultWidgetController(file_default_widget); - file_default_widget_controller.set_file(file, file_name, mime_type); + MenuButton button = new MenuButton(); + button.icon_name = "open-menu"; + Menu menu_model = new Menu(); + menu_model.append(_("Open"), "file.open"); + menu_model.append(_("Save as…"), "file.save_as"); + Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model); + button.popover = popover_menu; + + image_overlay_toolbar.append(button); Overlay overlay = new Overlay(); overlay.set_child(image); - overlay.add_overlay(file_default_widget); + overlay.add_overlay(image_overlay_toolbar); overlay.set_measure_overlay(image, true); - overlay.set_clip_overlay(file_default_widget, true); + overlay.set_clip_overlay(image_overlay_toolbar, true); EventControllerMotion this_motion_events = new EventControllerMotion(); this.add_controller(this_motion_events); this_motion_events.enter.connect(() => { - file_default_widget.visible = true; + image_overlay_toolbar.visible = true; }); this_motion_events.leave.connect(() => { - if (file_default_widget.file_menu.popover != null && file_default_widget.file_menu.popover.visible) return; + if (button.popover != null && button.popover.visible) return; - file_default_widget.visible = false; + image_overlay_toolbar.visible = false; }); this.append(overlay); diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index 52a26f33..8c36475a 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -21,7 +21,9 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem { } public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { - return new FileWidget(stream_interactor, file_transfer); + FileWidget widget = new FileWidget(file_transfer); + FileWidgetController widget_controller = new FileWidgetController(widget, file_transfer, stream_interactor); + return widget; } public override Gee.List? get_item_actions(Plugins.WidgetType type) { @@ -57,7 +59,6 @@ public class FileWidget : SizeRequestBox { DEFAULT } - private StreamInteractor stream_interactor; private FileTransfer file_transfer; public FileTransfer.State file_transfer_state { get; set; } public string file_transfer_mime_type { get; set; } @@ -66,13 +67,24 @@ public class FileWidget : SizeRequestBox { private FileDefaultWidgetController default_widget_controller; private Widget? content = null; + public signal void open_file(); + public signal void save_file_as(); + public signal void start_download(); + public signal void cancel_download(); + + class construct { + install_action("file.open", null, (widget, action_name) => { ((FileWidget) widget).open_file(); }); + install_action("file.save_as", null, (widget, action_name) => { ((FileWidget) widget).save_file_as(); }); + install_action("file.download", null, (widget, action_name) => { ((FileWidget) widget).start_download(); }); + install_action("file.cancel", null, (widget, action_name) => { ((FileWidget) widget).cancel_download(); }); + } + construct { margin_top = 4; size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH; } - public FileWidget(StreamInteractor stream_interactor, FileTransfer file_transfer) { - this.stream_interactor = stream_interactor; + public FileWidget(FileTransfer file_transfer) { this.file_transfer = file_transfer; update_widget.begin(); @@ -113,7 +125,7 @@ public class FileWidget : SizeRequestBox { if (content != null) this.remove(content); FileDefaultWidget default_file_widget = new FileDefaultWidget(); default_widget_controller = new FileDefaultWidgetController(default_file_widget); - default_widget_controller.set_file_transfer(file_transfer, stream_interactor); + default_widget_controller.set_file_transfer(file_transfer); content = default_file_widget; this.state = State.DEFAULT; this.append(content); @@ -138,94 +150,104 @@ public class FileWidget : SizeRequestBox { } } -public class FileDefaultWidgetController : Object { - - private FileDefaultWidget widget; - private FileTransfer? file_transfer; - public string file_transfer_path { get; set; } - public string file_transfer_state { get; set; } - public string file_transfer_mime_type { get; set; } +public class FileWidgetController : Object { + private weak Widget widget; + private FileTransfer file_transfer; private StreamInteractor? stream_interactor; - private string file_uri; - private string file_name; - private FileTransfer.State state; - public FileDefaultWidgetController(FileDefaultWidget widget) { + public FileWidgetController(FileWidget widget, FileTransfer file_transfer, StreamInteractor? stream_interactor = null) { this.widget = widget; + this.ref(); + this.widget.weak_ref(() => { + this.widget = null; + this.unref(); + }); + this.file_transfer = file_transfer; + this.stream_interactor = stream_interactor; - widget.clicked.connect(on_clicked); widget.open_file.connect(open_file); widget.save_file_as.connect(save_file); + widget.start_download.connect(start_download); widget.cancel_download.connect(cancel_download); } - public void set_file_transfer(FileTransfer file_transfer, StreamInteractor stream_interactor) { - this.file_transfer = file_transfer; - this.stream_interactor = stream_interactor; - - widget.name_label.label = file_name = file_transfer.file_name; - - file_transfer.bind_property("path", this, "file-transfer-path"); - file_transfer.bind_property("state", this, "file-transfer-state"); - file_transfer.bind_property("mime-type", this, "file-transfer-mime-type"); - - this.notify["file-transfer-path"].connect(update_file_info); - this.notify["file-transfer-state"].connect(update_file_info); - this.notify["file-transfer-mime-type"].connect(update_file_info); - - update_file_info(); - } - - public void set_file(File file, string file_name, string? mime_type) { - file_uri = file.get_uri(); - state = FileTransfer.State.COMPLETE; - widget.name_label.label = this.file_name = file_name; - widget.update_file_info(mime_type, state, -1); - } - - private void update_file_info() { - file_uri = file_transfer.get_file().get_uri(); - state = file_transfer.state; - widget.update_file_info(file_transfer.mime_type, file_transfer.state, file_transfer.size); - } - private void open_file() { try{ - AppInfo.launch_default_for_uri(file_uri, null); + AppInfo.launch_default_for_uri(file_transfer.get_file().get_uri(), null); } catch (Error err) { - warning("Failed to open %s - %s", file_uri, err.message); + warning("Failed to open %s - %s", file_transfer.get_file().get_uri(), err.message); } } private void save_file() { var save_dialog = new FileChooserNative(_("Save as…"), widget.get_root() as Gtk.Window, FileChooserAction.SAVE, null, null); save_dialog.set_modal(true); - save_dialog.set_current_name(file_name); + save_dialog.set_current_name(file_transfer.file_name); save_dialog.response.connect(() => { try{ - GLib.File.new_for_uri(file_uri).copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null); + GLib.File.new_for_uri(file_transfer.get_file().get_uri()).copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null); } catch (Error err) { - warning("Failed copy file %s - %s", file_uri, err.message); + warning("Failed copy file %s - %s", file_transfer.get_file().get_uri(), err.message); } }); save_dialog.show(); } + private void start_download() { + if (stream_interactor != null) { + stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer); + } + } + private void cancel_download() { file_transfer.cancellable.cancel(); } +} + +public class FileDefaultWidgetController : Object { + + private FileDefaultWidget widget; + private FileTransfer? file_transfer; + public string file_transfer_state { get; set; } + public string file_transfer_mime_type { get; set; } + + private FileTransfer.State state; + + public FileDefaultWidgetController(FileDefaultWidget widget) { + this.widget = widget; + + widget.clicked.connect(on_clicked); + + this.notify["file-transfer-state"].connect(update_file_info); + this.notify["file-transfer-mime-type"].connect(update_file_info); + } + + public void set_file_transfer(FileTransfer file_transfer) { + this.file_transfer = file_transfer; + + widget.name_label.label = file_transfer.file_name; + + file_transfer.bind_property("state", this, "file-transfer-state"); + file_transfer.bind_property("mime-type", this, "file-transfer-mime-type"); + + update_file_info(); + } + + private void update_file_info() { + state = file_transfer.state; + widget.update_file_info(file_transfer.mime_type, file_transfer.state, file_transfer.size); + } private void on_clicked() { switch (state) { case FileTransfer.State.COMPLETE: - open_file(); + widget.activate_action("file.open", null); break; case FileTransfer.State.NOT_STARTED: - assert(stream_interactor != null && file_transfer != null); - stream_interactor.get_module(FileManager.IDENTITY).download_file.begin(file_transfer); + widget.activate_action("file.download", null); break; default: // Clicking doesn't do anything in FAILED and IN_PROGRESS states diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index fb4ba162..900525fe 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -217,6 +217,7 @@ public class MessageMetaItem : ContentMetaItem { if (correction_allowed) { Plugins.MessageAction action1 = new Plugins.MessageAction(); action1.icon_name = "document-edit-symbolic"; + action1.tooltip = _("Edit message"); action1.callback = (button, content_meta_item_activated, widget) => { this.in_edit_mode = true; }; @@ -225,6 +226,7 @@ public class MessageMetaItem : ContentMetaItem { Plugins.MessageAction reply_action = new Plugins.MessageAction(); reply_action.icon_name = "mail-reply-sender-symbolic"; + reply_action.tooltip = _("Reply"); reply_action.callback = (button, content_meta_item_activated, widget) => { GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(message_item.conversation.id), new GLib.Variant.int32(content_item.id) })); }; @@ -233,6 +235,7 @@ public class MessageMetaItem : ContentMetaItem { if (supports_reaction) { Plugins.MessageAction action2 = new Plugins.MessageAction(); action2.icon_name = "dino-emoticon-add-symbolic"; + action2.tooltip = _("Add reaction"); EmojiChooser chooser = new EmojiChooser(); chooser.emoji_picked.connect((emoji) => { stream_interactor.get_module(Reactions.IDENTITY).add_reaction(message_item.conversation, message_item, emoji); -- cgit v1.2.3-70-g09d2