using Adw; using Gtk; using Gee; public class TranscriptView : Adw.Bin { public MessageListModel? model { get { return _model; } set { if (_model != null) { _model.disconnect(messages_changed_handler_id); _model.unwatch_updates(); } _model = value; if (value != null) { reset_for_conversation_change(); weak TranscriptView self = this; messages_changed_handler_id = value.messages_changed.connect(() => { self.reload_messages(); }); value.load_messages(); value.watch_updates(); } else { transcript_drawing_area.set_messages(new ArrayList()); } } } public string title { get { return title_label.label; } set { title_label.label = value; } } private MessageListModel? _model = null; private Adw.ToolbarView container; private Label title_label = new Label("Messages"); private Overlay overlay = new Overlay(); private TranscriptDrawingArea transcript_drawing_area = new TranscriptDrawingArea(); private ScrolledWindow scrolled_window = new ScrolledWindow(); private ulong messages_changed_handler_id = 0; private bool needs_reload = false; private Graphene.Point _hovering_text_view_origin = Graphene.Point() { x = 0, y = 0 }; private TextView _hovering_text_view = new TextView(); private VisibleTextLayout? hovered_text_bubble = null; private VisibleTextLayout? locked_text_bubble = null; public TranscriptView() { container = new Adw.ToolbarView(); set_child(container); // Set minimum width for the transcript view set_size_request(330, -1); overlay.set_child(transcript_drawing_area); weak TranscriptView self = this; overlay.get_child_position.connect((child, out allocation) => { allocation = Gtk.Allocation() { x = 0, y = 0, width = 0, height = 0 }; if (self.hovered_text_bubble != null) { var rect = self.hovered_text_bubble.rect; allocation.x = (int)(rect.origin.x + self._hovering_text_view_origin.x); allocation.y = (int)(rect.origin.y - self._hovering_text_view_origin.y); allocation.width = (int)rect.size.width; allocation.height = (int)rect.size.height; return true; } return true; }); scrolled_window.set_child(overlay); scrolled_window.add_css_class("flipped-y-axis"); transcript_drawing_area.viewport = scrolled_window.vadjustment; container.set_content(scrolled_window); // Connect to the adjustment's value_changed signal scrolled_window.vadjustment.value_changed.connect(() => { transcript_drawing_area.viewport = scrolled_window.vadjustment; }); var header_bar = new Adw.HeaderBar(); title_label.single_line_mode = true; title_label.ellipsize = Pango.EllipsizeMode.END; header_bar.set_title_widget(title_label); container.add_top_bar(header_bar); // This is an invisible text view that's used to handle selection. _hovering_text_view.add_css_class("hovering-text-view"); _hovering_text_view.add_css_class("flipped-y-axis"); _hovering_text_view.set_editable(false); _hovering_text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR); overlay.add_overlay(_hovering_text_view); // When the selection changes, lock the text bubble so that if the cursor moves to another bubble we don't clear it. _hovering_text_view.buffer.mark_set.connect((location, end) => { self.lock_text_bubble(self.hovered_text_bubble); }); // When the mouse hovers over a text bubble, configure the hovering text view to show the text of the bubble. transcript_drawing_area.on_text_bubble_hover.connect((visible_text_layout) => { if (visible_text_layout != null) { configure_hovering_text_view(visible_text_layout); } }); // This is triggered when another bubble is currently locked, and the user clicks on a new bubble. transcript_drawing_area.on_text_bubble_click.connect((visible_text_layout) => { if (visible_text_layout != null) { locked_text_bubble = null; configure_hovering_text_view(visible_text_layout); } }); Repository.get_instance().attachment_downloaded.connect((attachment_guid) => { debug("Attachment downloaded: %s", attachment_guid); // See if this attachment is part of this transcript. bool contains_attachment = false; foreach (var message in _model.messages) { foreach (var attachment in message.attachments) { if (attachment.guid == attachment_guid) { contains_attachment = true; break; } } } if (contains_attachment && !needs_reload) { debug("Queueing reload of messages for attachment download"); needs_reload = true; GLib.Idle.add(() => { if (needs_reload) { debug("Reloading messages for attachment download"); model.load_messages(); needs_reload = false; } return false; }, GLib.Priority.HIGH); } }); } private void reset_for_conversation_change() { locked_text_bubble = null; hovered_text_bubble = null; _hovering_text_view.buffer.text = ""; overlay.queue_allocate(); // Reset scroll position by updating the existing adjustment scrolled_window.vadjustment.value = 0; scrolled_window.vadjustment.upper = 0; scrolled_window.vadjustment.page_size = 0; } private void lock_text_bubble(VisibleTextLayout? visible_text_layout) { if (visible_text_layout != null) { locked_text_bubble = visible_text_layout; configure_hovering_text_view(visible_text_layout); } } private void configure_hovering_text_view(VisibleTextLayout? visible_text_layout) { hovered_text_bubble = visible_text_layout; if (locked_text_bubble == null) { _hovering_text_view_origin = visible_text_layout.text_bubble.get_text_origin(); _hovering_text_view.buffer.text = visible_text_layout.text_bubble.message.text; overlay.queue_allocate(); } } private void reload_messages() { transcript_drawing_area.show_sender = _model.is_group_chat; transcript_drawing_area.set_messages(_model.messages); } }