Private
Public Access
1
0
Files
Kordophone/src/transcript/transcript-view.vala

281 lines
10 KiB
Vala
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Message>());
}
}
}
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 bool _queued_url_open = false;
private VisibleLayout? hovered_text_bubble = null;
private VisibleLayout? 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) => {
if (location.get_offset() == 0) { return; }
self.on_hovering_text_view_clicked(location, end);
});
// 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);
}
});
transcript_drawing_area.on_image_bubble_activate.connect((attachment_guid) => {
self.open_attachment(attachment_guid);
});
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);
}
});
}
delegate void OpenPath(string path);
private ulong attachment_downloaded_handler_id = 0;
private void open_attachment(string attachment_guid) {
OpenPath open_path = (path) => {
try {
GLib.AppInfo.launch_default_for_uri("file://" + path, null);
} catch (GLib.Error e) {
warning("Failed to open image %s: %s", path, e.message);
}
};
try {
var attachment_info = Repository.get_instance().get_attachment_info(attachment_guid);
if (attachment_info.downloaded == true) {
// We already have it, so open it.
open_path(attachment_info.path);
} else {
// We need to download this, then open it once the downloaded signal is emitted.
Repository.get_instance().download_attachment(attachment_guid, false);
// TODO: Should probably indicate progress here.
attachment_downloaded_handler_id = Repository.get_instance().attachment_downloaded.connect((guid) => {
if (guid == attachment_guid) {
open_path(attachment_info.path);
Repository.get_instance().disconnect(attachment_downloaded_handler_id);
}
});
}
} catch (GLib.Error e) {
warning("Failed to get attachment info: %s", e.message);
}
}
private void on_hovering_text_view_clicked(TextIter location, TextMark end) {
lock_text_bubble(hovered_text_bubble);
if (!_queued_url_open) {
_queued_url_open = true;
// 100ms timeout to let the selection state settle. (this is a workaround)
GLib.Timeout.add(100, () => {
open_url_at_location(location);
_queued_url_open = false;
return false;
}, GLib.Priority.HIGH);
}
}
private void open_url_at_location(TextIter location) {
Gtk.TextTag? underline_tag = null;
foreach (unowned Gtk.TextTag tag in location.get_tags()) {
if (tag.underline != Pango.Underline.NONE) {
underline_tag = tag;
break;
}
}
if (underline_tag == null) {
return; // Click wasn't on an underlined (i.e. link) region
}
// Determine the full extent (start/end iters) of this underlined region
Gtk.TextIter start_iter = location;
Gtk.TextIter end_iter = location;
start_iter.backward_to_tag_toggle(underline_tag);
end_iter.forward_to_tag_toggle(underline_tag);
string url = _hovering_text_view.buffer.get_text(start_iter, end_iter, false);
// Try to open the URL guard against malformed data
if (url != null && url.strip().length > 0) {
try {
GLib.AppInfo.launch_default_for_uri(url.strip(), null);
} catch (GLib.Error e) {
warning("Failed to open URL %s: %s", url, e.message);
}
}
}
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(VisibleLayout? 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(VisibleLayout? visible_text_layout) {
hovered_text_bubble = visible_text_layout;
if (locked_text_bubble == null) {
TextBubbleLayout text_bubble = visible_text_layout.bubble as TextBubbleLayout;
_hovering_text_view_origin = text_bubble.get_text_origin();
_hovering_text_view.buffer.text = "";
Gtk.TextIter start_iter;
_hovering_text_view.buffer.get_start_iter(out start_iter);
_hovering_text_view.buffer.insert_markup(ref start_iter, text_bubble.message.markup, -1);
overlay.queue_allocate();
}
}
private void reload_messages() {
transcript_drawing_area.show_sender = _model.is_group_chat;
transcript_drawing_area.set_messages(_model.messages);
}
}