Private
Public Access
1
0

Add double click gesture on image bubbles to open

This commit is contained in:
2025-06-18 18:07:59 -07:00
parent 0dece34012
commit 3379198940
5 changed files with 112 additions and 21 deletions

View File

@@ -29,6 +29,20 @@ public class AttachmentMetadata : Object {
} }
} }
public class AttachmentInfo : Object {
public string? path;
public string? preview_path;
public bool? downloaded;
public bool? preview_downloaded;
public AttachmentInfo(string? path, string? preview_path, bool? downloaded, bool? preview_downloaded) {
this.path = path;
this.preview_path = preview_path;
this.downloaded = downloaded;
this.preview_downloaded = preview_downloaded;
}
}
public class Attachment : Object { public class Attachment : Object {
public string guid; public string guid;
public string path; public string path;

View File

@@ -135,4 +135,13 @@ public class Repository : DBusServiceProxy {
return dbus_repository.upload_attachment(filename); return dbus_repository.upload_attachment(filename);
} }
public AttachmentInfo get_attachment_info(string attachment_guid) throws DBusServiceProxyError, GLib.Error {
if (dbus_repository == null) {
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
}
var info = dbus_repository.get_attachment_info(attachment_guid);
return new AttachmentInfo(info.attr1, info.attr2, info.attr3, info.attr4);
}
} }

View File

@@ -26,6 +26,7 @@ private class ImageBubbleLayout : BubbleLayout
{ {
public string image_path; public string image_path;
public bool is_downloaded; public bool is_downloaded;
public string? attachment_guid;
private Graphene.Size image_size; private Graphene.Size image_size;
private Gdk.Texture? cached_texture = null; private Gdk.Texture? cached_texture = null;

View File

@@ -16,8 +16,9 @@ private class TranscriptDrawingArea : Widget
} }
} }
public signal void on_text_bubble_hover(VisibleTextLayout? text_bubble); public signal void on_text_bubble_hover(VisibleLayout? text_bubble);
public signal void on_text_bubble_click(VisibleTextLayout? text_bubble); public signal void on_text_bubble_click(VisibleLayout? text_bubble);
public signal void on_image_bubble_activate(string attachment_guid);
private ArrayList<Message> _messages = new ArrayList<Message>(); private ArrayList<Message> _messages = new ArrayList<Message>();
private ArrayList<ChatItemLayout> _chat_items = new ArrayList<ChatItemLayout>(); private ArrayList<ChatItemLayout> _chat_items = new ArrayList<ChatItemLayout>();
@@ -29,7 +30,7 @@ private class TranscriptDrawingArea : Widget
private Gdk.Rectangle? _click_bounding_box = null; private Gdk.Rectangle? _click_bounding_box = null;
private EventControllerMotion _motion_controller = new EventControllerMotion(); private EventControllerMotion _motion_controller = new EventControllerMotion();
private ArrayList<VisibleTextLayout?> _visible_text_layouts = new ArrayList<VisibleTextLayout?>(); private ArrayList<VisibleLayout?> _visible_text_layouts = new ArrayList<VisibleLayout?>();
private const bool debug_viewport = false; private const bool debug_viewport = false;
private uint? _tick_callback_id = null; private uint? _tick_callback_id = null;
@@ -41,8 +42,8 @@ private class TranscriptDrawingArea : Widget
weak TranscriptDrawingArea self = this; weak TranscriptDrawingArea self = this;
_click_gesture.button = 0; _click_gesture.button = 0;
_click_gesture.begin.connect(() => { _click_gesture.pressed.connect((n_press, x, y) => {
self.on_click(self._click_gesture.get_current_button()); self.on_click(self._click_gesture.get_current_button(), n_press);
}); });
add_controller(_click_gesture); add_controller(_click_gesture);
@@ -157,8 +158,8 @@ private class TranscriptDrawingArea : Widget
// Skip drawing if this item is not in the viewport // Skip drawing if this item is not in the viewport
float height_offset = 0.0f; float height_offset = 0.0f;
if (viewport_rect.intersection(rect, null)) { if (viewport_rect.intersection(rect, null)) {
if (chat_item is TextBubbleLayout) { if (chat_item is BubbleLayout) {
_visible_text_layouts.add(VisibleTextLayout(chat_item as TextBubbleLayout, rect)); _visible_text_layouts.add(VisibleLayout(chat_item as BubbleLayout, rect));
} }
snapshot.save(); snapshot.save();
@@ -199,9 +200,10 @@ private class TranscriptDrawingArea : Widget
animation_tick(); animation_tick();
} }
private VisibleTextLayout? get_text_bubble_at(double x, double y) { private VisibleLayout? get_visible_layout_at(double x, double y) {
var point = Graphene.Point() { x = (float)x, y = (float)y };
foreach (var layout in _visible_text_layouts) { foreach (var layout in _visible_text_layouts) {
if (layout.rect.contains_point(Graphene.Point() { x = (float)x, y = (float)y })) { if (layout.rect.contains_point(point)) {
return layout; return layout;
} }
} }
@@ -209,16 +211,29 @@ private class TranscriptDrawingArea : Widget
return null; return null;
} }
private VisibleLayout? get_text_bubble_at(double x, double y) {
var layout = get_visible_layout_at(x, y);
if (layout != null && layout.bubble is TextBubbleLayout) {
return layout;
}
return null;
}
private void on_mouse_motion(double x, double y) { private void on_mouse_motion(double x, double y) {
VisibleTextLayout? hovered_text_bubble = get_text_bubble_at(x, y); VisibleLayout? hovered_text_bubble = get_text_bubble_at(x, y);
on_text_bubble_hover(hovered_text_bubble); on_text_bubble_hover(hovered_text_bubble);
} }
private void on_click(uint button) { private void on_click(uint button, int n_press) {
if (button == Gdk.BUTTON_SECONDARY) { if (button == Gdk.BUTTON_SECONDARY) {
on_right_click(); on_right_click();
} else if (button == Gdk.BUTTON_PRIMARY) { } else if (button == Gdk.BUTTON_PRIMARY) {
on_left_click(); on_left_click();
if (n_press == 2) {
on_double_click();
}
} }
} }
@@ -230,6 +245,17 @@ private class TranscriptDrawingArea : Widget
} }
} }
private void on_double_click() {
Gdk.Rectangle? bounding_box = null;
if (_click_gesture.get_bounding_box(out bounding_box)) {
var double_clicked_bubble = get_visible_layout_at(bounding_box.x, bounding_box.y);
if (double_clicked_bubble != null && double_clicked_bubble.bubble is ImageBubbleLayout) {
var image_bubble = double_clicked_bubble.bubble as ImageBubbleLayout;
on_image_bubble_activate(image_bubble.attachment_guid);
}
}
}
private void on_right_click() { private void on_right_click() {
var menu_model = new Menu(); var menu_model = new Menu();
menu_model.append("Copy", "transcript.copy"); menu_model.append("Copy", "transcript.copy");
@@ -346,6 +372,8 @@ private class TranscriptDrawingArea : Widget
var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size); var image_layout = new ImageBubbleLayout(attachment.preview_path, message.from_me, this, max_width, image_size);
image_layout.id = @"image-$(attachment.guid)"; image_layout.id = @"image-$(attachment.guid)";
image_layout.attachment_guid = attachment.guid;
if (animate) { if (animate) {
start_animation(image_layout.id); start_animation(image_layout.id);
} }
@@ -410,12 +438,12 @@ private class ChatItemAnimation
} }
} }
public struct VisibleTextLayout { public struct VisibleLayout {
public weak TextBubbleLayout text_bubble; public weak BubbleLayout bubble;
public Graphene.Rect rect; public Graphene.Rect rect;
public VisibleTextLayout(TextBubbleLayout text_bubble, Graphene.Rect rect) { public VisibleLayout(BubbleLayout bubble, Graphene.Rect rect) {
this.text_bubble = text_bubble; this.bubble = bubble;
this.rect = rect; this.rect = rect;
} }
} }

View File

@@ -51,8 +51,8 @@ public class TranscriptView : Adw.Bin
private TextView _hovering_text_view = new TextView(); private TextView _hovering_text_view = new TextView();
private bool _queued_url_open = false; private bool _queued_url_open = false;
private VisibleTextLayout? hovered_text_bubble = null; private VisibleLayout? hovered_text_bubble = null;
private VisibleTextLayout? locked_text_bubble = null; private VisibleLayout? locked_text_bubble = null;
public TranscriptView() { public TranscriptView() {
container = new Adw.ToolbarView(); container = new Adw.ToolbarView();
@@ -123,6 +123,10 @@ public class TranscriptView : Adw.Bin
} }
}); });
transcript_drawing_area.on_image_bubble_activate.connect((attachment_guid) => {
self.open_attachment(attachment_guid);
});
Repository.get_instance().attachment_downloaded.connect((attachment_guid) => { Repository.get_instance().attachment_downloaded.connect((attachment_guid) => {
debug("Attachment downloaded: %s", attachment_guid); debug("Attachment downloaded: %s", attachment_guid);
@@ -154,6 +158,40 @@ public class TranscriptView : Adw.Bin
}); });
} }
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) { private void on_hovering_text_view_clicked(TextIter location, TextMark end) {
lock_text_bubble(hovered_text_bubble); lock_text_bubble(hovered_text_bubble);
@@ -213,23 +251,24 @@ public class TranscriptView : Adw.Bin
scrolled_window.vadjustment.page_size = 0; scrolled_window.vadjustment.page_size = 0;
} }
private void lock_text_bubble(VisibleTextLayout? visible_text_layout) { private void lock_text_bubble(VisibleLayout? visible_text_layout) {
if (visible_text_layout != null) { if (visible_text_layout != null) {
locked_text_bubble = visible_text_layout; locked_text_bubble = visible_text_layout;
configure_hovering_text_view(visible_text_layout); configure_hovering_text_view(visible_text_layout);
} }
} }
private void configure_hovering_text_view(VisibleTextLayout? visible_text_layout) { private void configure_hovering_text_view(VisibleLayout? visible_text_layout) {
hovered_text_bubble = visible_text_layout; hovered_text_bubble = visible_text_layout;
if (locked_text_bubble == null) { if (locked_text_bubble == null) {
_hovering_text_view_origin = visible_text_layout.text_bubble.get_text_origin(); TextBubbleLayout text_bubble = visible_text_layout.bubble as TextBubbleLayout;
_hovering_text_view_origin = text_bubble.get_text_origin();
_hovering_text_view.buffer.text = ""; _hovering_text_view.buffer.text = "";
Gtk.TextIter start_iter; Gtk.TextIter start_iter;
_hovering_text_view.buffer.get_start_iter(out start_iter); _hovering_text_view.buffer.get_start_iter(out start_iter);
_hovering_text_view.buffer.insert_markup(ref start_iter, visible_text_layout.text_bubble.message.markup, -1); _hovering_text_view.buffer.insert_markup(ref start_iter, text_bubble.message.markup, -1);
overlay.queue_allocate(); overlay.queue_allocate();
} }
} }