From e35df88d4a00c3a34f2b4d9fb7f10bb5d877bd29 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 24 Jan 2023 18:57:04 +0100 Subject: [PATCH] Fix UI for libadwaita --- libdino/src/plugin/interfaces.vala | 1 + main/CMakeLists.txt | 7 +- main/data/unified_main_content.ui | 78 ++++++---- .../conversation_item_skeleton.vala | 2 +- .../conversation_view.vala | 106 +++++++++++--- .../file_default_widget.vala | 9 -- .../file_image_widget.vala | 55 +++++-- .../file_widget.vala | 134 ++++++++++-------- .../message_widget.vala | 3 + main/src/ui/global_search.vala | 2 + main/src/ui/main_window.vala | 8 +- main/src/ui/main_window_controller.vala | 20 +-- main/src/ui/util/scaling_image.vala | 131 ----------------- main/src/ui/widgets/fixed_ratio_picture.vala | 88 ++++++++++++ .../src/ui/widgets/natural_size_increase.vala | 59 ++++++++ 15 files changed, 422 insertions(+), 281 deletions(-) delete mode 100644 main/src/ui/util/scaling_image.vala create mode 100644 main/src/ui/widgets/fixed_ratio_picture.vala create mode 100644 main/src/ui/widgets/natural_size_increase.vala diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index 6a30f6dc..0fef0134 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -154,6 +154,7 @@ public interface ConversationItemWidgetInterface: Object { public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget); public class MessageAction : Object { public string icon_name; + public string? tooltip; public Object? popover; public MessageActionEvoked? callback; } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 363b4185..b8f9d942 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -112,6 +112,9 @@ set(MAIN_DEFINITIONS) if(GTK4_VERSION VERSION_GREATER_EQUAL "4.6") set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} GTK_4_6) endif() +if(GTK4_VERSION VERSION_GREATER_EQUAL "4.8") + set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} GTK_4_8) +endif() if(Adwaita_VERSION VERSION_GREATER_EQUAL "1.2") set(MAIN_DEFINITIONS ${MAIN_DEFINITIONS} Adw_1_2) endif() @@ -204,7 +207,9 @@ SOURCES src/ui/util/label_hybrid.vala src/ui/util/sizing_bin.vala src/ui/util/size_request_box.vala - src/ui/util/scaling_image.vala + + src/ui/widgets/fixed_ratio_picture.vala + src/ui/widgets/natural_size_increase.vala CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi ${CMAKE_BINARY_DIR}/exports/qlite.vapi diff --git a/main/data/unified_main_content.ui b/main/data/unified_main_content.ui index a66275de..d8ba93be 100644 --- a/main/data/unified_main_content.ui +++ b/main/data/unified_main_content.ui @@ -54,44 +54,60 @@ vertical - - - + + end + true + true + false + natural + true + + + 600 - - content - - + + false + + + content + + + + - - - - - - placeholder - - - im.dino.Dino-symbolic - True - True + + + + placeholder + + + im.dino.Dino-symbolic + True + True + + - + - - - - end - slide-left - - - - 400 + + + + + + + false + 400 + 400 + + + true + - + 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; - - widget.clicked.connect(on_clicked); - widget.open_file.connect(open_file); - widget.save_file_as.connect(save_file); - widget.cancel_download.connect(cancel_download); - } - - public void set_file_transfer(FileTransfer file_transfer, StreamInteractor stream_interactor) { + this.ref(); + this.widget.weak_ref(() => { + this.widget = null; + this.unref(); + }); 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); + 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); } 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); diff --git a/main/src/ui/global_search.vala b/main/src/ui/global_search.vala index aaf41b08..d206220d 100644 --- a/main/src/ui/global_search.vala +++ b/main/src/ui/global_search.vala @@ -151,6 +151,8 @@ public class GlobalSearch { } private void clear_search() { + // Scroll to top + results_scrolled.vadjustment.value = 0; foreach (Widget widget in results_box_children) { results_box.remove(widget); } diff --git a/main/src/ui/main_window.vala b/main/src/ui/main_window.vala index d892d9ab..9121b91e 100644 --- a/main/src/ui/main_window.vala +++ b/main/src/ui/main_window.vala @@ -23,7 +23,7 @@ public class MainWindow : Adw.Window { public Adw.Leaflet leaflet; public Box left_box; public Box right_box; - public Revealer search_revealer; + public Adw.Flap search_flap; public GlobalSearch global_search; private Stack stack = new Stack(); private Stack left_stack; @@ -35,7 +35,7 @@ public class MainWindow : Adw.Window { class construct { var shortcut = new Shortcut(new KeyvalTrigger(Key.F, ModifierType.CONTROL_MASK), new CallbackAction((widget, args) => { - ((MainWindow) widget).search_revealer.reveal_child = true; + ((MainWindow) widget).search_flap.reveal_flap = true; return false; })); add_shortcut(shortcut); @@ -67,11 +67,11 @@ public class MainWindow : Adw.Window { left_stack = (Stack) builder.get_object("left_stack"); right_stack = (Stack) builder.get_object("right_stack"); conversation_view = (ConversationView) builder.get_object("conversation_view"); - search_revealer = (Revealer) builder.get_object("search_revealer"); + search_flap = (Adw.Flap) builder.get_object("search_flap"); conversation_selector = ((ConversationSelector) builder.get_object("conversation_list")).init(stream_interactor); conversation_selector.conversation_selected.connect_after(() => leaflet.navigate(Adw.NavigationDirection.FORWARD)); - Frame search_frame = (Frame) builder.get_object("search_frame"); + Adw.Bin search_frame = (Adw.Bin) builder.get_object("search_frame"); global_search = new GlobalSearch(stream_interactor); search_frame.set_child(global_search.get_widget()); } diff --git a/main/src/ui/main_window_controller.vala b/main/src/ui/main_window_controller.vala index 9e7e8ce7..7a3ebcb2 100644 --- a/main/src/ui/main_window_controller.vala +++ b/main/src/ui/main_window_controller.vala @@ -45,10 +45,10 @@ public class MainWindowController : Object { this.conversation_view_controller = new ConversationViewController(window.conversation_view, window.conversation_titlebar, stream_interactor); - conversation_view_controller.search_menu_entry.button.bind_property("active", window.search_revealer, "reveal_child", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + conversation_view_controller.search_menu_entry.button.bind_property("active", window.search_flap, "reveal-flap", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); - window.search_revealer.notify["child-revealed"].connect(() => { - if (window.search_revealer.child_revealed) { + window.search_flap.notify["reveal-flap"].connect(() => { + if (window.search_flap.reveal_flap) { if (window.conversation_view.conversation_frame.conversation != null && window.global_search.search_entry.text == "") { reset_search_entry(); } @@ -59,7 +59,9 @@ public class MainWindowController : Object { window.global_search.selected_item.connect((item) => { select_conversation(item.conversation, false, false); window.conversation_view.conversation_frame.initialize_around_message(item.conversation, item); - close_search(); + if (window.search_flap.folded) { + close_search(); + } }); window.welcome_placeholder.primary_button.clicked.connect(() => { @@ -91,16 +93,6 @@ public class MainWindowController : Object { Widget window_widget = ((Widget) window); - GestureClick gesture_click_controller = new GestureClick(); - window_widget.add_controller(gesture_click_controller); - gesture_click_controller.pressed.connect((n_press, click_x, click_y) => { - double search_x, search_y; - bool ret = window.search_revealer.translate_coordinates(window, 0, 0, out search_x, out search_y); - if (ret && click_x < search_x) { - close_search(); - } - }); - EventControllerKey key_event_controller = new EventControllerKey(); window_widget.add_controller(key_event_controller); // TODO GTK4: Why doesn't this work with key_pressed signal diff --git a/main/src/ui/util/scaling_image.vala b/main/src/ui/util/scaling_image.vala deleted file mode 100644 index d6ca31fd..00000000 --- a/main/src/ui/util/scaling_image.vala +++ /dev/null @@ -1,131 +0,0 @@ -using Gdk; -using Gtk; - -namespace Dino.Ui { - -class FixedRatioLayout : Gtk.LayoutManager { - public int min_width { get; set; default = 0; } - public int target_width { get; set; default = -1; } - public int max_width { get; set; default = int.MAX; } - public int min_height { get; set; default = 0; } - public int target_height { get; set; default = -1; } - public int max_height { get; set; default = int.MAX; } - - public FixedRatioLayout() { - this.notify.connect(layout_changed); - } - - private void measure_target_size(Gtk.Widget widget, out int width, out int height) { - if (target_width != -1 && target_height != -1) { - width = target_width; - height = target_height; - return; - } - Widget child; - width = min_width; - height = min_height; - - child = widget.get_first_child(); - while (child != null) { - if (child.should_layout()) { - int child_min = 0; - int child_nat = 0; - int child_min_baseline = -1; - int child_nat_baseline = -1; - child.measure(Orientation.HORIZONTAL, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline); - width = int.max(child_nat, width); - } - child = child.get_next_sibling(); - } - width = int.min(width, max_width); - - child = widget.get_first_child(); - while (child != null) { - if (child.should_layout()) { - int child_min = 0; - int child_nat = 0; - int child_min_baseline = -1; - int child_nat_baseline = -1; - child.measure(Orientation.VERTICAL, width, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline); - height = int.max(child_nat, height); - } - child = child.get_next_sibling(); - } - - if (height > max_height) { - height = max_height; - width = min_width; - - child = widget.get_first_child(); - while (child != null) { - if (child.should_layout()) { - int child_min = 0; - int child_nat = 0; - int child_min_baseline = -1; - int child_nat_baseline = -1; - child.measure(Orientation.HORIZONTAL, max_height, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline); - width = int.max(child_nat, width); - } - child = child.get_next_sibling(); - } - width = int.min(width, max_width); - } - } - - public override void measure(Gtk.Widget widget, Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { - minimum_baseline = -1; - natural_baseline = -1; - int width, height; - measure_target_size(widget, out width, out height); - if (orientation == Orientation.HORIZONTAL) { - minimum = min_width; - natural = width; - } else if (for_size == -1) { - minimum = min_height; - natural = height; - } else { - minimum = natural = height * for_size / width; - } - } - - public override void allocate(Gtk.Widget widget, int width, int height, int baseline) { - Widget child = widget.get_first_child(); - while (child != null) { - if (child.should_layout()) { - child.allocate(width, height, baseline, null); - } - child = child.get_next_sibling(); - } - } - - public override SizeRequestMode get_request_mode(Gtk.Widget widget) { - return SizeRequestMode.HEIGHT_FOR_WIDTH; - } -} - -class FixedRatioPicture : Gtk.Widget { - public int min_width { get { return layout.min_width; } set { layout.min_width = value; } } - public int target_width { get { return layout.target_width; } set { layout.target_width = value; } } - public int max_width { get { return layout.max_width; } set { layout.max_width = value; } } - public int min_height { get { return layout.min_height; } set { layout.min_height = value; } } - public int target_height { get { return layout.target_height; } set { layout.target_height = value; } } - public int max_height { get { return layout.max_height; } set { layout.max_height = value; } } - public File file { get { return inner.file; } set { inner.file = value; } } - public Gdk.Paintable paintable { get { return inner.paintable; } set { inner.paintable = value; } } -#if GTK_4_8 && VALA_0_58 - public Gtk.ContentFit content_fit { get { return inner.content_fit; } set { inner.content_fit = value; } } -#endif - private Gtk.Picture inner = new Gtk.Picture(); - private FixedRatioLayout layout = new FixedRatioLayout(); - - public FixedRatioPicture() { - layout_manager = layout; - inner.insert_after(this, null); - } - - public override void dispose() { - inner.unparent(); - base.dispose(); - } -} -} \ No newline at end of file diff --git a/main/src/ui/widgets/fixed_ratio_picture.vala b/main/src/ui/widgets/fixed_ratio_picture.vala new file mode 100644 index 00000000..79c60141 --- /dev/null +++ b/main/src/ui/widgets/fixed_ratio_picture.vala @@ -0,0 +1,88 @@ +using Gdk; +using Gtk; + +class Dino.Ui.FixedRatioPicture : Gtk.Widget { + public int min_width { get; set; default = -1; } + public int max_width { get; set; default = int.MAX; } + public int min_height { get; set; default = -1; } + public int max_height { get; set; default = int.MAX; } + public File file { get { return inner.file; } set { inner.file = value; } } + public Gdk.Paintable paintable { get { return inner.paintable; } set { inner.paintable = value; } } +#if GTK_4_8 && VALA_0_58 + public Gtk.ContentFit content_fit { get { return inner.content_fit; } set { inner.content_fit = value; } } +#endif + private Gtk.Picture inner = new Gtk.Picture(); + + construct { + set_css_name("picture"); + add_css_class("fixed-ratio"); + inner.insert_after(this, null); + this.notify.connect(queue_resize); + } + + private void measure_target_size(out int width, out int height) { + if (width_request != -1 && height_request != -1) { + width = width_request; + height = height_request; + return; + } + width = min_width; + height = min_height; + + if (inner.should_layout()) { + int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1; + inner.measure(Orientation.HORIZONTAL, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline); + width = int.max(child_nat, width); + } + width = int.min(width, max_width); + + if (inner.should_layout()) { + int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1; + inner.measure(Orientation.VERTICAL, width, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline); + height = int.max(child_nat, height); + } + + if (height > max_height) { + height = max_height; + width = min_width; + + if (inner.should_layout()) { + int child_min = 0, child_nat = 0, child_min_baseline = -1, child_nat_baseline = -1; + inner.measure(Orientation.HORIZONTAL, max_height, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline); + width = int.max(child_nat, width); + } + width = int.min(width, max_width); + } + } + + public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { + minimum_baseline = -1; + natural_baseline = -1; + int width, height; + measure_target_size(out width, out height); + if (orientation == Orientation.HORIZONTAL) { + minimum = min_width; + natural = width; + } else if (for_size == -1) { + minimum = min_height; + natural = height; + } else { + minimum = natural = height * for_size / width; + } + } + + public override void size_allocate(int width, int height, int baseline) { + if (inner.should_layout()) { + inner.allocate(width, height, baseline, null); + } + } + + public override SizeRequestMode get_request_mode() { + return SizeRequestMode.HEIGHT_FOR_WIDTH; + } + + public override void dispose() { + inner.unparent(); + base.dispose(); + } +} \ No newline at end of file diff --git a/main/src/ui/widgets/natural_size_increase.vala b/main/src/ui/widgets/natural_size_increase.vala new file mode 100644 index 00000000..2b04d748 --- /dev/null +++ b/main/src/ui/widgets/natural_size_increase.vala @@ -0,0 +1,59 @@ +using Gtk; + +public class Dino.Ui.NaturalSizeIncrease : Gtk.Widget { + public int min_natural_height { get; set; default = -1; } + public int min_natural_width { get; set; default = -1; } + + construct { + this.notify.connect(queue_resize); + } + + public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { + minimum = 0; + if (orientation == Orientation.HORIZONTAL) { + natural = min_natural_width; + } else { + natural = min_natural_height; + } + natural = int.max(0, natural); + minimum_baseline = -1; + natural_baseline = -1; + Widget child = get_first_child(); + while (child != null) { + if (child.should_layout()) { + int child_min = 0; + int child_nat = 0; + int child_min_baseline = -1; + int child_nat_baseline = -1; + child.measure(orientation, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline); + minimum = int.max(minimum, child_min); + natural = int.max(natural, child_nat); + if (child_min_baseline > 0) { + minimum_baseline = int.max(minimum_baseline, child_min_baseline); + } + if (child_nat_baseline > 0) { + natural_baseline = int.max(natural_baseline, child_nat_baseline); + } + } + child = child.get_next_sibling(); + } + } + + public override void size_allocate(int width, int height, int baseline) { + Widget child = get_first_child(); + while (child != null) { + if (child.should_layout()) { + child.allocate(width, height, baseline, null); + } + child = child.get_next_sibling(); + } + } + + public override SizeRequestMode get_request_mode() { + Widget child = get_first_child(); + if (child != null) { + return child.get_request_mode(); + } + return SizeRequestMode.CONSTANT_SIZE; + } +} \ No newline at end of file