From 7e7dcedaf31ee35499875491c9f569c575d28435 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Mon, 14 Feb 2022 14:55:59 +0100 Subject: Port from GTK3 to GTK4 --- .../ui/conversation_content_view/call_widget.vala | 37 +-- .../chat_state_populator.vala | 6 +- .../conversation_item_skeleton.vala | 265 ++++++++------------- .../conversation_view.vala | 133 +++++------ .../date_separator_populator.vala | 8 +- .../file_default_widget.vala | 78 +++--- .../file_image_widget.vala | 25 +- .../ui/conversation_content_view/file_widget.vala | 45 ++-- .../message_item_widget.vala | 229 ++++++++++++++++++ .../conversation_content_view/message_widget.vala | 217 +++++++---------- .../subscription_notification.vala | 10 +- 11 files changed, 589 insertions(+), 464 deletions(-) create mode 100644 main/src/ui/conversation_content_view/message_item_widget.vala (limited to 'main/src/ui/conversation_content_view') diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala index e45792e2..645c31c1 100644 --- a/main/src/ui/conversation_content_view/call_widget.vala +++ b/main/src/ui/conversation_content_view/call_widget.vala @@ -17,7 +17,7 @@ namespace Dino.Ui { this.stream_interactor = stream_interactor; } - public override Object? get_widget(Plugins.WidgetType type) { + public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { CallItem call_item = content_item as CallItem; CallState? call_state = stream_interactor.get_module(Calls.IDENTITY).call_states[call_item.call]; return new CallWidget(stream_interactor, call_item.call, call_state, call_item.conversation) { visible=true }; @@ -45,6 +45,7 @@ namespace Dino.Ui { private Conversation conversation; public Call.State call_state { get; set; } // needs to be public for binding private uint time_update_handler_id = 0; + private ArrayList multiparty_peer_widgets = new ArrayList(); construct { margin_top = 4; @@ -58,11 +59,11 @@ namespace Dino.Ui { this.call = call; this.conversation = conversation; - size_allocate.connect((allocation) => { - if (allocation.height > parent.get_allocated_height()) { - Idle.add(() => { parent.queue_resize(); return false; }); - } - }); +// size_allocate.connect((allocation) => { +// if (allocation.height > parent.get_allocated_height()) { +// Idle.add(() => { parent.queue_resize(); return false; }); +// } +// }); call.bind_property("state", this, "call-state"); this.notify["call-state"].connect(update_call_state); @@ -88,16 +89,20 @@ namespace Dino.Ui { if (call.state != Call.State.IN_PROGRESS && call.state != Call.State.ENDED) return; if (call.counterparts.size <= 1 && conversation.type_ == Conversation.Type.CHAT) return; - multiparty_peer_box.foreach((widget) => { multiparty_peer_box.remove(widget); }); + foreach (Widget peer_widget in multiparty_peer_widgets) { + multiparty_peer_box.remove(peer_widget); + } foreach (Jid counterpart in call.counterparts) { AvatarImage image = new AvatarImage() { force_gray=true, margin_top=2, visible=true }; image.set_conversation_participant(stream_interactor, conversation, counterpart.bare_jid); - multiparty_peer_box.add(image); + multiparty_peer_box.append(image); + multiparty_peer_widgets.add(image); } AvatarImage image2 = new AvatarImage() { force_gray=true, margin_top=2, visible=true }; image2.set_conversation_participant(stream_interactor, conversation, call.account.bare_jid); - multiparty_peer_box.add(image2); + multiparty_peer_box.append(image2); + multiparty_peer_widgets.add(image2); outer_additional_box.get_style_context().add_class("multiparty-participants"); @@ -121,7 +126,7 @@ namespace Dino.Ui { switch (relevant_state) { case Call.State.RINGING: - image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR); + image.set_from_icon_name("dino-phone-ring-symbolic"); if (call.direction == Call.DIRECTION_INCOMING) { bool video = call_manager.should_we_send_video(); @@ -146,7 +151,7 @@ namespace Dino.Ui { break; case Call.State.ESTABLISHING: case Call.State.IN_PROGRESS: - image.set_from_icon_name("dino-phone-in-talk-symbolic", IconSize.LARGE_TOOLBAR); + image.set_from_icon_name("dino-phone-in-talk-symbolic"); title_label.label = _("Call started"); string duration = get_duration_string((new DateTime.now_utc()).difference(call.local_time)); subtitle_label.label = _("Started %s ago").printf(duration); @@ -162,13 +167,13 @@ namespace Dino.Ui { break; case Call.State.OTHER_DEVICE: - image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR); + image.set_from_icon_name("dino-phone-hangup-symbolic"); title_label.label = call.direction == Call.DIRECTION_INCOMING ? _("Incoming call") : _("Outgoing call"); subtitle_label.label = _("You handled this call on another device"); break; case Call.State.ENDED: - image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR); + image.set_from_icon_name("dino-phone-hangup-symbolic"); title_label.label = _("Call ended"); string formated_end = Util.format_time(call.end_time, _("%H∶%M"), _("%l∶%M %p")); string duration = get_duration_string(call.end_time.difference(call.local_time)); @@ -177,7 +182,7 @@ namespace Dino.Ui { _("Lasted %s").printf(duration); break; case Call.State.MISSED: - image.set_from_icon_name("dino-phone-missed-symbolic", IconSize.LARGE_TOOLBAR); + image.set_from_icon_name("dino-phone-missed-symbolic"); title_label.label = _("Call missed"); if (call.direction == Call.DIRECTION_INCOMING) { subtitle_label.label = _("You missed this call"); @@ -187,7 +192,7 @@ namespace Dino.Ui { } break; case Call.State.DECLINED: - image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR); + image.set_from_icon_name("dino-phone-hangup-symbolic"); title_label.label = _("Call declined"); if (call.direction == Call.DIRECTION_INCOMING) { subtitle_label.label = _("You declined this call"); @@ -197,7 +202,7 @@ namespace Dino.Ui { } break; case Call.State.FAILED: - image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR); + image.set_from_icon_name("dino-phone-hangup-symbolic"); title_label.label = _("Call failed"); subtitle_label.label = "Call failed to establish"; break; diff --git a/main/src/ui/conversation_content_view/chat_state_populator.vala b/main/src/ui/conversation_content_view/chat_state_populator.vala index 247c83fe..0665caac 100644 --- a/main/src/ui/conversation_content_view/chat_state_populator.vala +++ b/main/src/ui/conversation_content_view/chat_state_populator.vala @@ -76,14 +76,14 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { this.jids = jids; } - public override Object? get_widget(Plugins.WidgetType widget_type) { + public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType widget_type) { label = new Label("") { xalign=0, vexpand=true, visible=true }; label.get_style_context().add_class("dim-label"); image = new AvatarImage() { margin_top=2, valign=Align.START, visible=true }; Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true }; - image_content_box.add(image); - image_content_box.add(label); + image_content_box.append(image); + image_content_box.append(label); update(); return image_content_box; 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 077440d6..3e4ce88b 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -7,7 +7,16 @@ using Dino.Entities; namespace Dino.Ui.ConversationSummary { -public class ConversationItemSkeleton : EventBox { +public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, Object { + + public Grid main_grid { get; set; } + public Label name_label { get; set; } + public Label time_label { get; set; } + public AvatarImage avatar_image { get; set; } + public Image encryption_image { get; set; } + public Image received_image { get; set; } + + public Widget? content_widget = null; public bool show_skeleton { get; set; default=false; } public bool last_group_item { get; set; default=true; } @@ -20,188 +29,130 @@ public class ConversationItemSkeleton : EventBox { public ContentMetaItem? content_meta_item = null; public Widget? widget = null; - private Box image_content_box = new Box(Orientation.HORIZONTAL, 8) { visible=true }; - private Box header_content_box = new Box(Orientation.VERTICAL, 0) { visible=true }; - private ItemMetaDataHeader? metadata_header = null; - private AvatarImage? image = null; + 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) { this.stream_interactor = stream_interactor; this.conversation = conversation; this.item = item; this.content_meta_item = item as ContentMetaItem; - this.get_style_context().add_class("message-box"); - - item.bind_property("in-edit-mode", this, "item-in-edit-mode"); - this.notify["item-in-edit-mode"].connect(update_edit_mode); - item.bind_property("mark", this, "item-mark", BindingFlags.SYNC_CREATE); - this.notify["item-mark"].connect(update_error_mode); - update_error_mode(); + Builder builder = new Builder.from_resource("/im/dino/Dino/conversation_item_widget.ui"); + main_grid = (Grid) builder.get_object("main_grid"); + main_grid.get_style_context().add_class("message-box"); + name_label = (Label) builder.get_object("name_label"); + time_label = (Label) builder.get_object("time_label"); + avatar_image = (AvatarImage) builder.get_object("avatar_image"); + encryption_image = (Image) builder.get_object("encrypted_image"); + received_image = (Image) builder.get_object("marked_image"); - widget = item.get_widget(Plugins.WidgetType.GTK) as Widget; + widget = item.get_widget(this, Plugins.WidgetType.GTK4) as Widget; if (widget != null) { widget.valign = Align.END; - header_content_box.add(widget); + set_widget(widget, Plugins.WidgetType.GTK4); } - image_content_box.add(header_content_box); - - if (initial_item) { - this.add(image_content_box); - } else { - Revealer revealer = new Revealer() { transition_duration=200, transition_type=RevealerTransitionType.SLIDE_UP, reveal_child=false, visible=true }; - revealer.add_with_properties(image_content_box); - this.add(revealer); - revealer.reveal_child = true; + if (item.requires_header) { + avatar_image.set_conversation_participant(stream_interactor, conversation, item.jid); } - this.notify["show-skeleton"].connect(update_margin); this.notify["last-group-item"].connect(update_margin); + this.notify["show-skeleton"].connect(set_header); update_margin(); } - private void update_margin() { - if (item.requires_header && show_skeleton && metadata_header == null) { - metadata_header = new ItemMetaDataHeader(stream_interactor, conversation, item) { visible=true }; - header_content_box.add(metadata_header); - header_content_box.reorder_child(metadata_header, 0); - } - if (item.requires_avatar && show_skeleton && image == null) { - image = new AvatarImage() { margin_top=2, valign=Align.START, visible=true, allow_gray = false }; - image.set_conversation_participant(stream_interactor, conversation, item.jid); - image_content_box.add(image); - image_content_box.reorder_child(image, 0); - } + private void set_header() { + if (!show_skeleton || !item.requires_header) return; + + update_name_label(); +// name_label.style_updated.connect(update_name_label); + updated_roster_handler_id = stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect((account, jid, roster_item) => { + if (this.conversation.account.equals(account) && this.conversation.counterpart.equals(jid)) { + update_name_label(); + } + }); + + item.notify["encryption"].connect(update_encryption_icon); + update_encryption_icon(); - if (image != null) { - image.visible = this.show_skeleton; + if (item.time != null) { + update_time(); } - if (metadata_header != null) { - metadata_header.visible = this.show_skeleton; + + item.bind_property("mark", this, "item-mark", BindingFlags.SYNC_CREATE); + this.notify["item-mark"].connect_after(update_received_mark); + update_received_mark(); + } + + public void set_widget(Object object, Plugins.WidgetType type) { + if (content_widget != null) content_widget.unparent(); + + Widget widget = (Widget) object; + content_widget = widget; + main_grid.attach(widget, 1, 1, 4, 1); + } + + private void update_margin() { + avatar_image.visible = show_skeleton; + name_label.visible = show_skeleton; + time_label.visible = show_skeleton; + encryption_image.visible = show_skeleton; + received_image.visible = show_skeleton; + + if (show_skeleton) { + main_grid.get_style_context().add_class("has-skeleton"); } - image_content_box.margin_start = this.show_skeleton ? 15 : 58; - image_content_box.margin_end = 15; - if (this.show_skeleton && this.last_group_item) { - image_content_box.margin_top = 8; - image_content_box.margin_bottom = 8; - } else { - image_content_box.margin_top = 4; - image_content_box.margin_bottom = 4; + if (last_group_item) { + main_grid.get_style_context().add_class("last-group-item"); } } private void update_edit_mode() { if (item.in_edit_mode) { - this.get_style_context().add_class("edit-mode"); + main_grid.get_style_context().add_class("edit-mode"); } else { - this.get_style_context().remove_class("edit-mode"); + main_grid.get_style_context().remove_class("edit-mode"); } } private void update_error_mode() { if (item_mark == Message.Marked.ERROR) { - this.get_style_context().add_class("error"); + main_grid.get_style_context().add_class("error"); } else { - this.get_style_context().remove_class("error"); - } - } -} - -[GtkTemplate (ui = "/im/dino/Dino/conversation_content_view/item_metadata_header.ui")] -public class ItemMetaDataHeader : Box { - [GtkChild] public unowned Label name_label; - [GtkChild] public unowned Label time_label; - public Image received_image = new Image() { opacity=0.4 }; - public Widget? encryption_image = null; - - public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12); - - private StreamInteractor stream_interactor; - private Conversation conversation; - private Plugins.MetaConversationItem item; - public Entities.Message.Marked item_mark { get; set; } - private ArrayList items = new ArrayList(); - private uint time_update_timeout = 0; - private ulong updated_roster_handler_id = 0; - - public ItemMetaDataHeader(StreamInteractor stream_interactor, Conversation conversation, Plugins.MetaConversationItem item) { - this.stream_interactor = stream_interactor; - this.conversation = conversation; - this.item = item; - items.add(item); - - update_name_label(); - name_label.style_updated.connect(update_name_label); - updated_roster_handler_id = stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect((account, jid, roster_item) => { - if (this.conversation.account.equals(account) && this.conversation.counterpart.equals(jid)) { - update_name_label(); - } - }); - - conversation.notify["encryption"].connect(update_unencrypted_icon); - item.notify["encryption"].connect(update_encryption_icon); - update_encryption_icon(); - - this.add(received_image); - - if (item.time != null) { - update_time(); + main_grid.get_style_context().remove_class("error"); } - - item.bind_property("mark", this, "item-mark"); - this.notify["item-mark"].connect_after(update_received_mark); - update_received_mark(); } private void update_encryption_icon() { + encryption_image.visible = true; + Application app = GLib.Application.get_default() as Application; ContentMetaItem ci = item as ContentMetaItem; if (item.encryption != Encryption.NONE && item.encryption != Encryption.UNKNOWN && ci != null) { - Widget? widget = null; + string? icon_name = null; foreach(var e in app.plugin_registry.encryption_list_entries) { if (e.encryption == item.encryption) { - widget = e.get_encryption_icon(conversation, ci.content_item) as Widget; + icon_name = e.get_encryption_icon_name(conversation, ci.content_item); break; } } - if (widget == null) { - widget = new Image.from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER) { opacity=0.4, visible = true }; - } - update_encryption_image(widget); - } - if (item.encryption == Encryption.NONE) { - update_unencrypted_icon(); + encryption_image.icon_name = icon_name ?? "dino-changes-prevent-symbolic"; } - } - - private void update_unencrypted_icon() { - if (item.encryption != Encryption.NONE) return; - - if (conversation.encryption != Encryption.NONE && encryption_image == null) { - Image image = new Image() { opacity=0.4, visible = true }; - image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER); - image.tooltip_text = _("Unencrypted"); - update_encryption_image(image); - Util.force_error_color(image); - } else if (conversation.encryption == Encryption.NONE && encryption_image != null) { - update_encryption_image(null); - } - } - private void update_encryption_image(Widget? widget) { - if (encryption_image != null) { - this.remove(encryption_image); - encryption_image = null; - } - if (widget != null) { - this.add(widget); - this.reorder_child(widget, 3); - encryption_image = widget; + if (item.encryption == Encryption.NONE) { + if (conversation.encryption != Encryption.NONE) { + encryption_image.icon_name = "dino-changes-allowed-symbolic"; + encryption_image.tooltip_text = _("Unencrypted"); + Util.force_error_color(encryption_image); + } else if (conversation.encryption == Encryption.NONE) { + encryption_image.icon_name = null; + encryption_image.visible = false; + } } } @@ -209,7 +160,7 @@ public class ItemMetaDataHeader : Box { time_label.label = get_relative_time(item.time.to_local()).to_string(); time_update_timeout = Timeout.add_seconds((int) get_next_time_change(), () => { - if (this.parent == null) return false; + if (this.main_grid.parent == null) return false; update_time(); return false; }); @@ -220,41 +171,15 @@ public class ItemMetaDataHeader : Box { } private void update_received_mark() { - bool all_received = true; - bool all_read = true; - bool all_sent = true; - foreach (Plugins.MetaConversationItem item in items) { - if (item.mark == Message.Marked.WONTSEND) { - received_image.visible = true; - received_image.set_from_icon_name("dialog-warning-symbolic", ICON_SIZE_HEADER); - Util.force_error_color(received_image); - Util.force_error_color(time_label); - string error_text = _("Unable to send message"); - received_image.tooltip_text = error_text; - time_label.tooltip_text = error_text; - return; - } else if (item.mark != Message.Marked.READ) { - all_read = false; - if (item.mark != Message.Marked.RECEIVED) { - all_received = false; - if (item.mark == Message.Marked.UNSENT) { - all_sent = false; - } - } - } - } - if (all_read) { - received_image.visible = true; - received_image.set_from_icon_name("dino-double-tick-symbolic", ICON_SIZE_HEADER); - } else if (all_received) { - received_image.visible = true; - received_image.set_from_icon_name("dino-tick-symbolic", ICON_SIZE_HEADER); - } else if (!all_sent) { - received_image.visible = true; - received_image.set_from_icon_name("image-loading-symbolic", ICON_SIZE_HEADER); - } else if (received_image.visible) { - received_image.set_from_icon_name("image-loading-symbolic", ICON_SIZE_HEADER); - + switch (content_meta_item.mark) { + case Message.Marked.RECEIVED: received_image.icon_name = "dino-tick-symbolic"; break; + case Message.Marked.READ: received_image.icon_name = "dino-double-tick-symbolic"; break; + case Message.Marked.WONTSEND: + received_image.icon_name = "dialog-warning-symbolic"; + received_image.icon_name = _("Unable to send message"); + // TODO error color on marked icon and time + break; + default: received_image.icon_name = null; break; } } @@ -311,6 +236,10 @@ public class ItemMetaDataHeader : Box { } } + public Widget get_widget() { + return main_grid; + } + public override void dispose() { if (time_update_timeout != 0) { Source.remove(time_update_timeout); diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index d6cbed62..8d46281f 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -8,7 +8,7 @@ using Dino.Entities; namespace Dino.Ui.ConversationSummary { [GtkTemplate (ui = "/im/dino/Dino/conversation_content_view/view.ui")] -public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins.NotificationCollection { +public class ConversationView : Widget, Plugins.ConversationItemCollection, Plugins.NotificationCollection { public Conversation? conversation { get; private set; } @@ -19,8 +19,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins [GtkChild] private unowned Image button1_icon; [GtkChild] private unowned Box notifications; [GtkChild] private unowned Box main; - [GtkChild] private unowned EventBox main_event_box; - [GtkChild] private unowned EventBox main_wrap_event_box; + [GtkChild] private unowned Box main_wrap_box; [GtkChild] private unowned Stack stack; private StreamInteractor stream_interactor; @@ -41,9 +40,13 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins private bool firstLoad = true; private bool at_current_content = true; private bool reload_messages = true; - ConversationItemSkeleton currently_highlighted = null; + Widget currently_highlighted = null; ContentMetaItem? current_meta_item = null; - int last_y_root = -1; + double last_y = -1; + + construct { + this.layout_manager = new BinLayout(); + } public ConversationView init(StreamInteractor stream_interactor) { this.stream_interactor = stream_interactor; @@ -64,20 +67,21 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins // we connect to the parent event box that also wraps the overlaying message_menu_box. // This eliminates the unwanted leave events emitted on the main_event_box when hovering // the overlaying menu buttons. - main_wrap_event_box.events = EventMask.ENTER_NOTIFY_MASK; - main_wrap_event_box.events = EventMask.LEAVE_NOTIFY_MASK; - main_wrap_event_box.leave_notify_event.connect(on_leave_notify_event); - main_wrap_event_box.enter_notify_event.connect(on_enter_notify_event); + EventControllerMotion main_wrap_motion_events = new EventControllerMotion(); + main_wrap_box.add_controller(main_wrap_motion_events); + main_wrap_motion_events.leave.connect(on_leave_notify_event); + main_wrap_motion_events.enter.connect(update_highlight); // The buttons of the overlaying message_menu_box may partially overlap the adjacent // conversation items. We connect to the main_event_box directly to avoid emitting // the pointer motion events as long as the pointer is above the message menu. // This ensures that the currently highlighted item remains unchanged when the pointer // reaches the overlapping part of a button. - main_event_box.events = EventMask.POINTER_MOTION_MASK; - main_event_box.motion_notify_event.connect(on_motion_notify_event); + EventControllerMotion main_motion_events = new EventControllerMotion(); + main.add_controller(main_motion_events); + main_motion_events.motion.connect(update_highlight); button1.clicked.connect(() => { - current_meta_item.get_item_actions(Plugins.WidgetType.GTK)[0].callback(button1, current_meta_item, currently_highlighted.widget); + current_meta_item.get_item_actions(Plugins.WidgetType.GTK4)[0].callback(button1, current_meta_item, currently_highlighted); update_message_menu(); }); @@ -102,66 +106,51 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins } } - private bool on_enter_notify_event(Gdk.EventCrossing event) { - update_highlight((int)event.x_root, (int)event.y_root); - return false; - } - - private bool on_leave_notify_event(Gdk.EventCrossing event) { + private void on_leave_notify_event() { if (currently_highlighted != null) { - currently_highlighted.unset_state_flags(StateFlags.PRELIGHT); + currently_highlighted.get_style_context().remove_class("highlight"); currently_highlighted = null; } message_menu_box.visible = false; - return false; } - private bool on_motion_notify_event(Gdk.EventMotion event) { - update_highlight((int)event.x_root, (int)event.y_root); - return false; - } - - private void update_highlight(int x_root, int y_root) { - if (currently_highlighted != null && (last_y_root - y_root).abs() <= 2) { + private void update_highlight(double x, double y) { + if (currently_highlighted != null && (last_y - y).abs() <= 2) { return; } - last_y_root = y_root; - - int toplevel_window_pos_x, toplevel_window_pos_y, dest_x, dest_y; - Widget toplevel_widget = this.get_toplevel(); - // Obtain the position of the main application window relative to the root window - toplevel_widget.get_window().get_origin(out toplevel_window_pos_x, out toplevel_window_pos_y); - // Get the pointer location relative to the `main` box - toplevel_widget.translate_coordinates(main, x_root - toplevel_window_pos_x, y_root - toplevel_window_pos_y, out dest_x, out dest_y); + last_y = y; // Get widget under pointer int h = 0; - ConversationItemSkeleton? w = null; - foreach (Widget widget in main.get_children()) { - h += widget.get_allocated_height(); - if (h >= dest_y) { - w = widget as ConversationItemSkeleton; + Widget? w = null; + Plugins.MetaConversationItem? meta_item = null; + foreach (Plugins.MetaConversationItem item in meta_items) { + Widget widget = widgets[item]; + h += widget.get_allocated_height() + widget.margin_top + widget.margin_bottom; + if (h >= y) { + w = widget; break; } }; - if (currently_highlighted != null) currently_highlighted.unset_state_flags(StateFlags.PRELIGHT); + if (currently_highlighted != null) currently_highlighted.get_style_context().remove_class("highlight"); + + currently_highlighted = null; + current_meta_item = null; if (w == null) { - currently_highlighted = null; - current_meta_item = null; update_message_menu(); return; } // Get widget coordinates in main - int widget_x, widget_y; + double widget_x, widget_y; w.translate_coordinates(main, 0, 0, out widget_x, out widget_y); // Get MessageItem foreach (Plugins.MetaConversationItem item in item_item_skeletons.keys) { - if (item_item_skeletons[item] == w) { + if (item_item_skeletons[item].get_widget() == w) { current_meta_item = item as ContentMetaItem; } } @@ -170,11 +159,11 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins if (current_meta_item != null) { // Highlight widget - w.set_state_flags(StateFlags.PRELIGHT, true); currently_highlighted = w; + currently_highlighted.get_style_context().add_class("highlight"); // Move message menu - message_menu_box.margin_top = widget_y - 10; + message_menu_box.margin_top = (int)(widget_y - 10); } } @@ -184,11 +173,11 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins return; } - var actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK); + var actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK4); message_menu_box.visible = actions != null && actions.size > 0; if (actions != null && actions.size == 1) { button1.visible = true; - button1_icon.set_from_icon_name(actions[0].icon_name, IconSize.SMALL_TOOLBAR); + button1_icon.set_from_icon_name(actions[0].icon_name); } } @@ -235,15 +224,14 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins reload_messages = false; Timeout.add(700, () => { int h = 0, i = 0; - bool @break = false; - main.@foreach((widget) => { - if (widget == w || @break) { - @break = true; - return; + 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.get_style_context().add_class("highlight-once"); reload_messages = true; @@ -270,9 +258,9 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins // Init for new conversation foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_addition_populators) { - populator.init(conversation, this, Plugins.WidgetType.GTK); + populator.init(conversation, this, Plugins.WidgetType.GTK4); } - content_populator.init(this, conversation, Plugins.WidgetType.GTK); + content_populator.init(this, conversation, Plugins.WidgetType.GTK4); subscription_notification.init(conversation, this); animate = false; @@ -286,7 +274,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins } Application app = GLib.Application.get_default() as Application; foreach (Plugins.NotificationPopulator populator in app.plugin_registry.notification_populators) { - populator.init(conversation, this, Plugins.WidgetType.GTK); + populator.init(conversation, this, Plugins.WidgetType.GTK4); } Idle.add(() => { on_value_notify(); return false; }); } @@ -318,7 +306,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins private void remove_item(Plugins.MetaConversationItem item) { ConversationItemSkeleton? skeleton = item_item_skeletons[item]; if (skeleton != null) { - main.remove(skeleton); + main.remove(skeleton.get_widget()); widgets.unset(item); item_skeletons.remove(skeleton); item_item_skeletons.unset(item); @@ -331,21 +319,21 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins } public void on_add_meta_notification(Plugins.MetaConversationNotification notification) { - Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK); + Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK4); if (widget != null) { add_notification(widget); } } public void on_remove_meta_notification(Plugins.MetaConversationNotification notification){ - Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK); + Widget? widget = (Widget) notification.get_widget(Plugins.WidgetType.GTK4); if (widget != null) { remove_notification(widget); } } public void add_notification(Widget widget) { - notifications.add(widget); + notifications.append(widget); Timeout.add(20, () => { notification_revealer.transition_duration = 200; notification_revealer.reveal_child = true; @@ -362,15 +350,14 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins Plugins.MetaConversationItem? lower_item = meta_items.lower(item); // Fill datastructure - ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item, !animate) { visible=true }; + ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item, !animate); item_item_skeletons[item] = item_skeleton; int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0; item_skeletons.insert(index, item_skeleton); // Insert widget - widgets[item] = item_skeleton; - main.add(item_skeleton); - main.reorder_child(item_skeleton, index); + widgets[item] = item_skeleton.get_widget(); + widgets[item].insert_after(main, item_item_skeletons.has_key(lower_item) ? item_item_skeletons[lower_item].get_widget() : null); if (lower_item != null) { if (can_merge(item, lower_item)) { @@ -405,7 +392,7 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins } } } - return item_skeleton; + return item_skeleton.get_widget(); } private bool can_merge(Plugins.MetaConversationItem upper_item /*more recent, displayed below*/, Plugins.MetaConversationItem lower_item /*less recent, displayed above*/) { @@ -420,7 +407,11 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins private void on_upper_notify() { if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size if (at_current_content) { - scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down + Idle.add(() => { + // If we do this directly without Idle.add, scrolling down doesn't work properly + scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down + return false; + }); } } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) { scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content @@ -482,12 +473,14 @@ public class ConversationView : Box, Plugins.ConversationItemCollection, Plugins meta_items.clear(); item_skeletons.clear(); item_item_skeletons.clear(); + foreach (Widget widget in widgets.values) { + main.remove(widget); + } widgets.clear(); - main.@foreach((widget) => { main.remove(widget); }); } private void clear_notifications() { - notifications.@foreach((widget) => { notifications.remove(widget); }); +// notifications.@foreach((widget) => { notifications.remove(widget); }); notification_revealer.transition_duration = 0; notification_revealer.set_reveal_child(false); } diff --git a/main/src/ui/conversation_content_view/date_separator_populator.vala b/main/src/ui/conversation_content_view/date_separator_populator.vala index 5f6838e5..40bf0693 100644 --- a/main/src/ui/conversation_content_view/date_separator_populator.vala +++ b/main/src/ui/conversation_content_view/date_separator_populator.vala @@ -61,7 +61,7 @@ public class MetaDateItem : Plugins.MetaConversationItem { this.time = date; } - public override Object? get_widget(Plugins.WidgetType widget_type) { + public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType widget_type) { return new DateSeparatorWidget(date); } @@ -86,9 +86,9 @@ public class DateSeparatorWidget : Box { label = new Label("") { use_markup=true, halign=Align.CENTER, hexpand=false, visible=true }; label.get_style_context().add_class("dim-label"); - this.add(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true }); - this.add(label); - this.add(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true }); + this.append(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true }); + this.append(label); + this.append(new Separator(Orientation.HORIZONTAL) { valign=Align.CENTER, hexpand=true, visible=true }); update_time(); } 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 638dab15..3bd5842f 100644 --- a/main/src/ui/conversation_content_view/file_default_widget.vala +++ b/main/src/ui/conversation_content_view/file_default_widget.vala @@ -7,34 +7,49 @@ using Dino.Entities; namespace Dino.Ui { [GtkTemplate (ui = "/im/dino/Dino/file_default_widget.ui")] -public class FileDefaultWidget : EventBox { +public class FileDefaultWidget : Box { + + 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; [GtkChild] public unowned Label mime_label; [GtkChild] public unowned Image content_type_image; [GtkChild] public unowned Spinner spinner; - [GtkChild] public unowned EventBox stack_event_box; [GtkChild] public unowned MenuButton file_menu; - public ModelButton file_open_button; - public ModelButton file_save_button; - public ModelButton cancel_button; - 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() { - this.enter_notify_event.connect(on_pointer_entered_event); - this.leave_notify_event.connect(on_pointer_left_event); - file_open_button = new ModelButton() { text=_("Open"), visible=true }; - file_save_button = new ModelButton() { text=_("Save as…"), visible=true }; - cancel_button = new ModelButton() { text=_("Cancel"), visible=true }; + EventControllerMotion this_motion_events = new EventControllerMotion(); + this.add_controller(this_motion_events); + this_motion_events.enter.connect(on_pointer_entered_event); + this_motion_events.leave.connect(on_pointer_left_event); + + GestureClick gesture_click_controller = new GestureClick(); + this.add_controller(gesture_click_controller); + gesture_click_controller.pressed.connect((n_press, x, y) => { + // Check whether the click was inside the file menu. Otherwise, open the file. + double x_button, y_button; + this.translate_coordinates(file_menu, x, y, out x_button, out y_button); + if (file_menu.contains(x_button, y_button)) return; + + this.open_file(); + }); } public void update_file_info(string? mime_type, FileTransfer.State state, long size) { this.state = state; - spinner.active = false; // A hidden spinning spinner still uses CPU. Deactivate asap + spinner.stop(); // A hidden spinning spinner still uses CPU. Deactivate asap content_type_image.icon_name = get_file_icon_name(mime_type); string? mime_description = mime_type != null ? ContentType.get_description(mime_type) : null; @@ -45,33 +60,23 @@ public class FileDefaultWidget : EventBox { image_stack.set_visible_child_name("content_type_image"); // Create a menu - Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu(); - Box file_menu_box = new Box(Orientation.VERTICAL, 0) { margin=10, visible=true }; - file_menu_box.add(file_open_button); - file_menu_box.add(file_save_button); - popover_menu.add(file_menu_box); + 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); file_menu.popover = popover_menu; - file_menu.button_release_event.connect(() => { - popover_menu.visible = true; - return true; - }); popover_menu.closed.connect(on_pointer_left); break; case FileTransfer.State.IN_PROGRESS: mime_label.label = _("Downloading %s…").printf(get_size_string(size)); - spinner.active = true; + spinner.start(); image_stack.set_visible_child_name("spinner"); // Create a menu - Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu(); - Box file_menu_box = new Box(Orientation.VERTICAL, 0) { margin=10, visible=true }; - file_menu_box.add(cancel_button); - popover_menu.add(file_menu_box); + Menu menu_model = new Menu(); + menu_model.append(_("Cancel"), "file.cancel_download"); + Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model); file_menu.popover = popover_menu; - file_menu.button_release_event.connect(() => { - popover_menu.visible = true; - return true; - }); popover_menu.closed.connect(on_pointer_left); break; case FileTransfer.State.NOT_STARTED: @@ -92,8 +97,8 @@ public class FileDefaultWidget : EventBox { } } - private bool on_pointer_entered_event(Gdk.EventCrossing event) { - event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.HAND2)); + private void on_pointer_entered_event() { + this.set_cursor_from_name("pointer"); content_type_image.opacity = 0.7; if (state == FileTransfer.State.NOT_STARTED) { image_stack.set_visible_child_name("download_image"); @@ -101,16 +106,13 @@ public class FileDefaultWidget : EventBox { if (state == FileTransfer.State.COMPLETE || state == FileTransfer.State.IN_PROGRESS) { file_menu.opacity = 1; } - return false; } - private bool on_pointer_left_event(Gdk.EventCrossing event) { - if (event.detail == Gdk.NotifyType.INFERIOR) return false; - if (file_menu.popover != null && file_menu.popover.visible) return false; + private void on_pointer_left_event() { + if (file_menu.popover != null && file_menu.popover.visible) return; - event.get_window().set_cursor(new Cursor.for_display(Gdk.Display.get_default(), CursorType.XTERM)); + this.set_cursor(null); on_pointer_left(); - return false; } private void on_pointer_left() { 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 91eddd93..aad220e8 100644 --- a/main/src/ui/conversation_content_view/file_image_widget.vala +++ b/main/src/ui/conversation_content_view/file_image_widget.vala @@ -6,7 +6,7 @@ using Dino.Entities; namespace Dino.Ui { -public class FileImageWidget : EventBox { +public class FileImageWidget : Box { private ScalingImage image; FileDefaultWidget file_default_widget; @@ -14,7 +14,6 @@ public class FileImageWidget : EventBox { public FileImageWidget() { this.halign = Align.START; - this.events = EventMask.POINTER_MOTION_MASK; this.get_style_context().add_class("file-image-widget"); } @@ -36,6 +35,7 @@ public class FileImageWidget : EventBox { pixbuf = pixbuf.apply_embedded_orientation(); image.load(pixbuf); + Picture picture = new Picture.for_pixbuf(pixbuf) { can_shrink=true, keep_aspect_ratio=true, halign=Align.START }; Idle.add(load_from_file.callback); return image; @@ -47,28 +47,29 @@ public class FileImageWidget : EventBox { 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 }; - file_default_widget.stack_event_box.visible = false; + 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); Overlay overlay = new Overlay() { visible=true }; - overlay.add(image); + overlay.set_child(image); overlay.add_overlay(file_default_widget); + overlay.set_measure_overlay(image, true); + overlay.set_clip_overlay(file_default_widget, true); - this.enter_notify_event.connect((event) => { + EventControllerMotion this_motion_events = new EventControllerMotion(); + this.add_controller(this_motion_events); + this_motion_events.enter.connect(() => { file_default_widget.visible = true; - return false; }); - this.leave_notify_event.connect((event) => { - if (event.detail == Gdk.NotifyType.INFERIOR) return false; - if (file_default_widget.file_menu.popover != null && file_default_widget.file_menu.popover.visible) return false; + this_motion_events.leave.connect(() => { + if (file_default_widget.file_menu.popover != null && file_default_widget.file_menu.popover.visible) return; file_default_widget.visible = false; - return false; }); - this.add(overlay); + 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 b63195dc..6378c298 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -16,7 +16,7 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem { this.stream_interactor = stream_interactor; } - public override Object? get_widget(Plugins.WidgetType type) { + public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { FileItem file_item = content_item as FileItem; FileTransfer transfer = file_item.file_transfer; return new FileWidget(stream_interactor, transfer) { visible=true }; @@ -51,11 +51,11 @@ public class FileWidget : SizeRequestBox { this.file_transfer = file_transfer; update_widget.begin(); - size_allocate.connect((allocation) => { - if (allocation.height > parent.get_allocated_height()) { - Idle.add(() => { parent.queue_resize(); return false; }); - } - }); +// size_allocate.connect((allocation) => { +// if (allocation.height > parent.get_allocated_height()) { +// Idle.add(() => { parent.queue_resize(); return false; }); +// } +// }); file_transfer.bind_property("state", this, "file-transfer-state"); file_transfer.bind_property("mime-type", this, "file-transfer-mime-type"); @@ -79,7 +79,7 @@ public class FileWidget : SizeRequestBox { if (content != null) this.remove(content); content = file_image_widget; state = State.IMAGE; - this.add(content); + this.append(content); return; } catch (Error e) { } } @@ -91,7 +91,7 @@ public class FileWidget : SizeRequestBox { default_widget_controller.set_file_transfer(file_transfer, stream_interactor); content = default_file_widget; this.state = State.DEFAULT; - this.add(content); + this.append(content); } } @@ -128,10 +128,15 @@ public class FileDefaultWidgetController : Object { public FileDefaultWidgetController(FileDefaultWidget widget) { this.widget = widget; - widget.button_release_event.connect(on_clicked); - widget.file_open_button.clicked.connect(open_file); - widget.file_save_button.clicked.connect(save_file); - widget.cancel_button.clicked.connect(cancel_download); + + widget.open_file.connect(open_file); + widget.save_file_as.connect(save_file); + widget.cancel_download.connect(cancel_download); + + var gesture_controller = new GestureClick(); + gesture_controller.set_button(1); // listen for left clicks + gesture_controller.released.connect(on_clicked); + widget.add_controller(gesture_controller); } public void set_file_transfer(FileTransfer file_transfer, StreamInteractor stream_interactor) { @@ -173,30 +178,29 @@ public class FileDefaultWidgetController : Object { } private void save_file() { - var save_dialog = new FileChooserNative(_("Save as…"), widget.get_toplevel() as Gtk.Window, FileChooserAction.SAVE, null, null); - save_dialog.set_do_overwrite_confirmation(true); + 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); - if (save_dialog.run() == Gtk.ResponseType.ACCEPT) { + save_dialog.response.connect(() => { try{ GLib.File.new_for_uri(file_uri).copy(save_dialog.get_file(), GLib.FileCopyFlags.OVERWRITE, null); } catch (Error err) { warning("Failed copy file %s - %s", file_uri, err.message); } - } + }); + + save_dialog.show(); } private void cancel_download() { file_transfer.cancellable.cancel(); } - private bool on_clicked(EventButton event_button) { + private void on_clicked() { switch (state) { case FileTransfer.State.COMPLETE: - if (event_button.button == 1) { - open_file(); - } + open_file(); break; case FileTransfer.State.NOT_STARTED: assert(stream_interactor != null && file_transfer != null); @@ -206,7 +210,6 @@ public class FileDefaultWidgetController : Object { // Clicking doesn't do anything in FAILED and IN_PROGRESS states break; } - return false; } } diff --git a/main/src/ui/conversation_content_view/message_item_widget.vala b/main/src/ui/conversation_content_view/message_item_widget.vala new file mode 100644 index 00000000..23a499d9 --- /dev/null +++ b/main/src/ui/conversation_content_view/message_item_widget.vala @@ -0,0 +1,229 @@ +using Dino.Entities; +using Gtk; + +namespace Dino.Ui { + public class MessageItemWidget : SizeRequestBin { + + public signal void edit_cancelled(); + public signal void edit_sent(string text); + + enum AdditionalInfo { + NONE, + PENDING, + DELIVERY_FAILED + } + + StreamInteractor stream_interactor; + public ContentItem content_item; + public Message.Marked marked { get; set; } + + Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true }; + MessageItemEditMode? edit_mode = null; + ChatTextViewController? controller = null; + AdditionalInfo additional_info = AdditionalInfo.NONE; + + ulong realize_id = -1; + ulong style_updated_id = -1; + ulong marked_notify_handler_id = -1; + + construct { + this.append(label); + label.activate_link.connect(on_label_activate_link); + this.size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH; + } + + public MessageItemWidget(StreamInteractor stream_interactor, ContentItem content_item) { + this.stream_interactor = stream_interactor; + this.content_item = content_item; + + Message message = ((MessageItem) content_item).message; + if (message.direction == Message.DIRECTION_SENT && !(message.marked in Message.MARKED_RECEIVED)) { + var binding = message.bind_property("marked", this, "marked"); + marked_notify_handler_id = this.notify["marked"].connect(() => { + // Currently "pending", but not anymore + if (additional_info == AdditionalInfo.PENDING && + message.marked != Message.Marked.SENDING && message.marked != Message.Marked.UNSENT) { + update_label(); + } + + // Currently "error", but not anymore + if (additional_info == AdditionalInfo.DELIVERY_FAILED && message.marked != Message.Marked.ERROR) { + update_label(); + } + + // Currently not error, but should be + if (additional_info != AdditionalInfo.DELIVERY_FAILED && message.marked == Message.Marked.ERROR) { + update_label(); + } + + // Nothing bad can happen anymore + if (message.marked in Message.MARKED_RECEIVED) { + binding.unbind(); + this.disconnect(marked_notify_handler_id); + } + }); + } + + update_label(); + } + + public void set_edit_mode() { + + MessageItem message_item = content_item as MessageItem; + Message message = message_item.message; + + if (edit_mode == null) { + edit_mode = new MessageItemEditMode(); + controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor); + Conversation conversation = message_item.conversation; + controller.initialize_for_conversation(conversation); + + edit_mode.cancelled.connect(() => { + edit_cancelled(); + unset_edit_mode(); + }); + edit_mode.send.connect(() => { + if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) { + edit_sent(edit_mode.chat_text_view.text_view.buffer.text); + } else { + edit_cancelled(); + } + unset_edit_mode(); + }); + } + + edit_mode.chat_text_view.text_view.buffer.text = message.body; + + this.remove(label); + this.append(edit_mode); + + edit_mode.chat_text_view.text_view.grab_focus(); + } + + public void unset_edit_mode() { + this.remove(edit_mode); + this.append(label); + label.grab_focus(); + label.selectable = false; + label.selectable = true; + } + + public void update_label() { + label.label = generate_markup_text(content_item); + } + + private string generate_markup_text(ContentItem item) { + MessageItem message_item = item as MessageItem; + Conversation conversation = message_item.conversation; + Message message = message_item.message; + + bool theme_dependent = false; + + string markup_text = message.body; + if (markup_text.length > 10000) { + markup_text = markup_text.substring(0, 10000) + " [" + _("Message too long") + "]"; + } + if (message.body.has_prefix("/me ")) { + markup_text = markup_text.substring(4); + } + + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + markup_text = Util.parse_add_markup_theme(markup_text, conversation.nickname, true, true, true, Util.is_dark_theme(this), ref theme_dependent); + } else { + markup_text = Util.parse_add_markup_theme(markup_text, null, true, true, true, Util.is_dark_theme(this), ref theme_dependent); + } + + if (message.body.has_prefix("/me ")) { + string display_name = Util.get_participant_display_name(stream_interactor, conversation, message.from); + markup_text = @"$(Markup.escape_text(display_name)) " + markup_text + ""; + } + + int only_emoji_count = Util.get_only_emoji_count(markup_text); + if (only_emoji_count != -1) { + string size_str = only_emoji_count < 5 ? "xx-large" : "large"; + markup_text = @"" + markup_text + ""; + } + + string dim_color = Util.is_dark_theme(this) ? "#BDBDBD" : "#707070"; + + if (message.edit_to != null) { + markup_text += @" (%s)".printf(_("edited")); + theme_dependent = true; + } + + // Append message status info + additional_info = AdditionalInfo.NONE; + if (message.direction == Message.DIRECTION_SENT && (message.marked == Message.Marked.SENDING || message.marked == Message.Marked.UNSENT)) { + // Append "pending..." iff message has not been sent yet + if (message.time.compare(new DateTime.now_utc().add_seconds(-10)) < 0) { + markup_text += @" %s".printf(_("pending…")); + theme_dependent = true; + additional_info = AdditionalInfo.PENDING; + } else { + int time_diff = (- (int) message.time.difference(new DateTime.now_utc()) / 1000); + Timeout.add(10000 - time_diff, () => { + update_label(); + return false; + }); + } + } else if (message.direction == Message.DIRECTION_SENT && message.marked == Message.Marked.ERROR) { + // Append "delivery failed" if there was a server error + string error_color = Util.rgba_to_hex(Util.get_label_pango_color(label, "@error_color")); + markup_text += " %s".printf(error_color, _("delivery failed")); + theme_dependent = true; + additional_info = AdditionalInfo.DELIVERY_FAILED; + } + + if (theme_dependent && realize_id == -1) { + realize_id = label.realize.connect(update_label); + // style_updated_id = label.style_updated.connect(update_label); + } else if (!theme_dependent && realize_id != -1) { + label.disconnect(realize_id); + label.disconnect(style_updated_id); + } + return markup_text; + } + + public static bool on_label_activate_link(string uri) { + // Always handle xmpp URIs with Dino + if (!uri.has_prefix("xmpp:")) return false; + File file = File.new_for_uri(uri); + Dino.Application.get_default().open(new File[]{file}, ""); + return true; + } + } + + [GtkTemplate (ui = "/im/dino/Dino/message_item_widget_edit_mode.ui")] + public class MessageItemEditMode : Box { + + public signal void cancelled(); + public signal void send(); + + [GtkChild] public unowned MenuButton emoji_button; + [GtkChild] public unowned ChatTextView chat_text_view; + [GtkChild] public unowned Button cancel_button; + [GtkChild] public unowned Button send_button; + [GtkChild] public unowned Frame frame; + + construct { + Util.force_css(frame, "* { border-radius: 3px; }"); + + EmojiChooser chooser = new EmojiChooser(); + chooser.emoji_picked.connect((emoji) => { + chat_text_view.text_view.buffer.insert_at_cursor(emoji, emoji.data.length); + }); + emoji_button.set_popover(chooser); + + chat_text_view.text_view.buffer.changed.connect_after(on_text_view_changed); + + cancel_button.clicked.connect(() => cancelled()); + send_button.clicked.connect(() => send()); + chat_text_view.cancel_input.connect(() => cancelled()); + chat_text_view.send_text.connect(() => send()); + } + + private void on_text_view_changed() { + send_button.sensitive = chat_text_view.text_view.buffer.text != ""; + } + } +} \ No newline at end of file diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index e7bd1282..3ebce0ee 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -10,84 +10,16 @@ namespace Dino.Ui.ConversationSummary { public class MessageMetaItem : ContentMetaItem { - private StreamInteractor stream_interactor; - private MessageItemWidget message_item_widget; - private MessageItem message_item; - - public MessageMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { - base(content_item); - message_item = content_item as MessageItem; - this.stream_interactor = stream_interactor; - } - - public override Object? get_widget(Plugins.WidgetType type) { - message_item_widget = new MessageItemWidget(stream_interactor, content_item) { visible=true }; - - message_item_widget.edit_cancelled.connect(() => { this.in_edit_mode = false; }); - message_item_widget.edit_sent.connect(on_edit_send); - - stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction); - - this.notify["in-edit-mode"].connect(() => { - if (in_edit_mode == false) return; - bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); - if (allowed) { - message_item_widget.set_edit_mode(); - } else { - this.in_edit_mode = false; - } - }); - - return message_item_widget; - } - - public override Gee.List? get_item_actions(Plugins.WidgetType type) { - if (content_item as FileItem != null) return null; - - bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); - Gee.List actions = new ArrayList(); - if (allowed && !in_edit_mode) { - Plugins.MessageAction action1 = new Plugins.MessageAction(); - action1.icon_name = "document-edit-symbolic"; - action1.callback = (button, content_meta_item_activated, widget) => { - this.in_edit_mode = true; - }; - actions.add(action1); - } - return actions; - } - - private void on_edit_send(string text) { - stream_interactor.get_module(MessageCorrection.IDENTITY).send_correction(message_item.conversation, message_item.message, text); - this.in_edit_mode = false; - } - - private void on_received_correction(ContentItem content_item) { - if (this.content_item.id == content_item.id) { - this.content_item = content_item; - message_item = content_item as MessageItem; - message_item_widget.content_item = content_item; - message_item_widget.update_label(); - } - } -} - -public class MessageItemWidget : SizeRequestBin { - - public signal void edit_cancelled(); - public signal void edit_sent(string text); - enum AdditionalInfo { NONE, PENDING, DELIVERY_FAILED } - StreamInteractor stream_interactor; - public ContentItem content_item; + private StreamInteractor stream_interactor; + private MessageItem message_item; public Message.Marked marked { get; set; } - Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true, visible=true }; MessageItemEditMode? edit_mode = null; ChatTextViewController? controller = null; AdditionalInfo additional_info = AdditionalInfo.NONE; @@ -96,15 +28,14 @@ public class MessageItemWidget : SizeRequestBin { ulong style_updated_id = -1; ulong marked_notify_handler_id = -1; - construct { - this.add(label); - label.activate_link.connect(on_label_activate_link); - this.size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH; - } + public Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, vexpand=true, can_focus=false }; - public MessageItemWidget(StreamInteractor stream_interactor, ContentItem content_item) { + public MessageMetaItem(ContentItem content_item, StreamInteractor stream_interactor) { + base(content_item); + message_item = content_item as MessageItem; this.stream_interactor = stream_interactor; - this.content_item = content_item; + + label.activate_link.connect(on_label_activate_link); Message message = ((MessageItem) content_item).message; if (message.direction == Message.DIRECTION_SENT && !(message.marked in Message.MARKED_RECEIVED)) { @@ -137,51 +68,6 @@ public class MessageItemWidget : SizeRequestBin { update_label(); } - public void set_edit_mode() { - - MessageItem message_item = content_item as MessageItem; - Message message = message_item.message; - - if (edit_mode == null) { - edit_mode = new MessageItemEditMode(); - controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor); - Conversation conversation = message_item.conversation; - controller.initialize_for_conversation(conversation); - - edit_mode.cancelled.connect(() => { - edit_cancelled(); - unset_edit_mode(); - }); - edit_mode.send.connect(() => { - if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) { - edit_sent(edit_mode.chat_text_view.text_view.buffer.text); - } else { - edit_cancelled(); - } - unset_edit_mode(); - }); - } - - edit_mode.chat_text_view.text_view.buffer.text = message.body; - - this.remove(label); - this.add(edit_mode); - - edit_mode.chat_text_view.text_view.grab_focus(); - } - - public void unset_edit_mode() { - this.remove(edit_mode); - this.add(label); - label.grab_focus(); - label.selectable = false; - label.selectable = true; - } - - public void update_label() { - label.label = generate_markup_text(content_item); - } - private string generate_markup_text(ContentItem item) { MessageItem message_item = item as MessageItem; Conversation conversation = message_item.conversation; @@ -198,9 +84,9 @@ public class MessageItemWidget : SizeRequestBin { } if (conversation.type_ == Conversation.Type.GROUPCHAT) { - markup_text = Util.parse_add_markup_theme(markup_text, conversation.nickname, true, true, true, Util.is_dark_theme(this), ref theme_dependent); + markup_text = Util.parse_add_markup_theme(markup_text, conversation.nickname, true, true, true, Util.is_dark_theme(this.label), ref theme_dependent); } else { - markup_text = Util.parse_add_markup_theme(markup_text, null, true, true, true, Util.is_dark_theme(this), ref theme_dependent); + markup_text = Util.parse_add_markup_theme(markup_text, null, true, true, true, Util.is_dark_theme(this.label), ref theme_dependent); } if (message.body.has_prefix("/me ")) { @@ -214,7 +100,7 @@ public class MessageItemWidget : SizeRequestBin { markup_text = @"" + markup_text + ""; } - string dim_color = Util.is_dark_theme(this) ? "#BDBDBD" : "#707070"; + string dim_color = Util.is_dark_theme(this.label) ? "#BDBDBD" : "#707070"; if (message.edit_to != null) { markup_text += @" (%s)".printf(_("edited")); @@ -246,7 +132,7 @@ public class MessageItemWidget : SizeRequestBin { if (theme_dependent && realize_id == -1) { realize_id = label.realize.connect(update_label); - style_updated_id = label.style_updated.connect(update_label); +// style_updated_id = label.style_updated.connect(update_label); } else if (!theme_dependent && realize_id != -1) { label.disconnect(realize_id); label.disconnect(style_updated_id); @@ -254,6 +140,83 @@ public class MessageItemWidget : SizeRequestBin { return markup_text; } + public void update_label() { + label.label = generate_markup_text(content_item); + } + + public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { + + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction); + + this.notify["in-edit-mode"].connect(() => { + if (in_edit_mode == false) return; + bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); + if (allowed) { + MessageItem message_item = content_item as MessageItem; + Message message = message_item.message; + + edit_mode = new MessageItemEditMode(); + controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor); + Conversation conversation = message_item.conversation; + controller.initialize_for_conversation(conversation); + + edit_mode.cancelled.connect(() => { + in_edit_mode = false; + outer.set_widget(label, Plugins.WidgetType.GTK4); + label.grab_focus(); + }); + edit_mode.send.connect(() => { + if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) { + on_edit_send(edit_mode.chat_text_view.text_view.buffer.text); + } else { +// edit_cancelled(); + } + in_edit_mode = false; + outer.set_widget(label, Plugins.WidgetType.GTK4); + label.grab_focus(); + }); + + edit_mode.chat_text_view.text_view.buffer.text = message.body; + + outer.set_widget(edit_mode, Plugins.WidgetType.GTK4); + edit_mode.chat_text_view.text_view.grab_focus(); + } else { + this.in_edit_mode = false; + } + }); + + return label; + } + + public override Gee.List? get_item_actions(Plugins.WidgetType type) { + if (content_item as FileItem != null) return null; + + bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); + Gee.List actions = new ArrayList(); + if (allowed && !in_edit_mode) { + Plugins.MessageAction action1 = new Plugins.MessageAction(); + action1.icon_name = "document-edit-symbolic"; + action1.callback = (button, content_meta_item_activated, widget) => { + this.in_edit_mode = true; + }; + actions.add(action1); + } + return actions; + } + + private void on_edit_send(string text) { + stream_interactor.get_module(MessageCorrection.IDENTITY).send_correction(message_item.conversation, message_item.message, text); + this.in_edit_mode = false; + } + + private void on_received_correction(ContentItem content_item) { + if (this.content_item.id == content_item.id) { + this.content_item = content_item; + message_item = content_item as MessageItem; + update_label(); + } + } + public static bool on_label_activate_link(string uri) { // Always handle xmpp URIs with Dino if (!uri.has_prefix("xmpp:")) return false; @@ -276,7 +239,7 @@ public class MessageItemEditMode : Box { [GtkChild] public unowned Frame frame; construct { - Util.force_css(frame, "* { border-radius: 3px; }"); + Util.force_css(frame, "* { border-radius: 3px; padding: 5px 7px; }"); EmojiChooser chooser = new EmojiChooser(); chooser.emoji_picked.connect((emoji) => { diff --git a/main/src/ui/conversation_content_view/subscription_notification.vala b/main/src/ui/conversation_content_view/subscription_notification.vala index d493ff78..1f0f39d8 100644 --- a/main/src/ui/conversation_content_view/subscription_notification.vala +++ b/main/src/ui/conversation_content_view/subscription_notification.vala @@ -34,8 +34,8 @@ public class SubscriptionNotitication : Object { private void show_notification() { Box box = new Box(Orientation.HORIZONTAL, 5) { visible=true }; - Button accept_button = new Button() { label=_("Accept"), visible=true }; - Button deny_button = new Button() { label=_("Deny"), visible=true }; + Button accept_button = new Button.with_label(_("Accept")) { visible=true }; + Button deny_button = new Button.with_label(_("Deny")) { visible=true }; GLib.Application app = GLib.Application.get_default(); accept_button.clicked.connect(() => { app.activate_action("accept-subscription", conversation.id); @@ -45,9 +45,9 @@ public class SubscriptionNotitication : Object { app.activate_action("deny-subscription", conversation.id); conversation_view.remove_notification(box); }); - box.add(new Label(_("This contact would like to add you to their contact list")) { margin_end=10, visible=true }); - box.add(accept_button); - box.add(deny_button); + box.append(new Label(_("This contact would like to add you to their contact list")) { margin_end=10, visible=true }); + box.append(accept_button); + box.append(deny_button); conversation_view.add_notification(box); } } -- cgit v1.2.3-54-g00ecf