271 lines
8.7 KiB
Diff
271 lines
8.7 KiB
Diff
From c185703eef79a48dead219f6b41a7874994c7273 Mon Sep 17 00:00:00 2001
|
|
From: Simon Gardling <titaniumtown@proton.me>
|
|
Date: Tue, 12 May 2026 12:03:33 -0400
|
|
Subject: [PATCH] feat(messages): Open image attachments in a dialog
|
|
|
|
Mirrors the official Signal desktop application by allowing the user
|
|
to click on an image attachment and see it expanded in an Adw.Dialog
|
|
overlay with built-in close button, Escape handling, and mobile
|
|
swipe-down support.
|
|
---
|
|
CHANGELOG.md | 4 ++
|
|
data/resources/meson.build | 1 +
|
|
data/resources/resources.gresource.xml.in | 1 +
|
|
.../ui/components/attachment_image.blp | 5 ++
|
|
data/resources/ui/image_viewer.blp | 15 +++++
|
|
src/backend/dummy.rs | 3 +-
|
|
src/gui/components/attachment_image.rs | 28 ++++++++-
|
|
src/gui/image_viewer.rs | 57 +++++++++++++++++++
|
|
src/gui/mod.rs | 1 +
|
|
src/gui/window.rs | 7 +++
|
|
10 files changed, 120 insertions(+), 2 deletions(-)
|
|
create mode 100644 data/resources/ui/image_viewer.blp
|
|
create mode 100644 src/gui/image_viewer.rs
|
|
|
|
diff --git a/CHANGELOG.md b/CHANGELOG.md
|
|
index 20dc578e..1e4649bc 100644
|
|
--- a/CHANGELOG.md
|
|
+++ b/CHANGELOG.md
|
|
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
|
## [Unreleased]
|
|
|
|
+### Added
|
|
+
|
|
+- Click an image attachment to open it in a fullscreen-capable viewer with a close button, fullscreen toggle, and `Escape`-to-dismiss.
|
|
+
|
|
## [0.20.4] - 2026-04-22
|
|
|
|
### Fixed
|
|
diff --git a/data/resources/meson.build b/data/resources/meson.build
|
|
index 7fc00b7c..efd789b7 100644
|
|
--- a/data/resources/meson.build
|
|
+++ b/data/resources/meson.build
|
|
@@ -11,6 +11,7 @@ blueprints = custom_target('blueprints',
|
|
'ui/dialog_clear_messages.blp',
|
|
'ui/dialog_unlink.blp',
|
|
'ui/error_dialog.blp',
|
|
+ 'ui/image_viewer.blp',
|
|
'ui/setup_window.blp',
|
|
'ui/linked_devices_window.blp',
|
|
'ui/device_info_item.blp',
|
|
diff --git a/data/resources/resources.gresource.xml.in b/data/resources/resources.gresource.xml.in
|
|
index 8604fe24..7c958d02 100644
|
|
--- a/data/resources/resources.gresource.xml.in
|
|
+++ b/data/resources/resources.gresource.xml.in
|
|
@@ -13,6 +13,7 @@
|
|
<file preprocess="xml-stripblanks">ui/linked_devices_window.ui</file>
|
|
<file preprocess="xml-stripblanks">ui/device_info_item.ui</file>
|
|
<file preprocess="xml-stripblanks">ui/error_dialog.ui</file>
|
|
+ <file preprocess="xml-stripblanks">ui/image_viewer.ui</file>
|
|
<file preprocess="xml-stripblanks">ui/attachment.ui</file>
|
|
<file preprocess="xml-stripblanks">ui/preferences_window.ui</file>
|
|
<file preprocess="xml-stripblanks">ui/shortcuts.ui</file>
|
|
diff --git a/data/resources/ui/components/attachment_image.blp b/data/resources/ui/components/attachment_image.blp
|
|
index ec303a72..bcbdabe1 100644
|
|
--- a/data/resources/ui/components/attachment_image.blp
|
|
+++ b/data/resources/ui/components/attachment_image.blp
|
|
@@ -2,6 +2,11 @@ using Gtk 4.0;
|
|
|
|
template $FlAttachmentImage: $FlAttachmentBase {
|
|
Picture picture {
|
|
+ GestureClick {
|
|
+ button: 1;
|
|
+ pressed => $on_clicked() swapped;
|
|
+ }
|
|
+
|
|
styles [
|
|
"photo",
|
|
]
|
|
diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp
|
|
new file mode 100644
|
|
index 00000000..19660f0b
|
|
--- /dev/null
|
|
+++ b/data/resources/ui/image_viewer.blp
|
|
@@ -0,0 +1,15 @@
|
|
+using Gtk 4.0;
|
|
+using Adw 1;
|
|
+
|
|
+template $FlImageViewer: Adw.Bin {
|
|
+ hexpand: true;
|
|
+ vexpand: true;
|
|
+
|
|
+ Picture picture {
|
|
+ paintable: bind template.attachment as <$FlAttachmentObject>.image;
|
|
+ content-fit: contain;
|
|
+ can-shrink: true;
|
|
+ hexpand: true;
|
|
+ vexpand: true;
|
|
+ }
|
|
+}
|
|
diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs
|
|
index e6824e80..1ebff6f6 100644
|
|
--- a/src/backend/dummy.rs
|
|
+++ b/src/backend/dummy.rs
|
|
@@ -382,6 +382,7 @@ impl super::Manager {
|
|
},
|
|
));
|
|
|
|
+
|
|
let msg_screenshot = msg!(self, "", 2, GROUP_ID, 19 + base_minute);
|
|
let screenshot_file = gtk::gio::File::for_uri("resource:///icon.svg");
|
|
let attachment = crate::backend::Attachment::from_file(screenshot_file, self);
|
|
@@ -393,7 +394,7 @@ impl super::Manager {
|
|
|
|
vec![
|
|
msg_replied,
|
|
- // msg_screenshot,
|
|
+ msg_screenshot,
|
|
msg_reply,
|
|
msg!(
|
|
self,
|
|
diff --git a/src/gui/components/attachment_image.rs b/src/gui/components/attachment_image.rs
|
|
index a91bf05b..9ca6e0c7 100644
|
|
--- a/src/gui/components/attachment_image.rs
|
|
+++ b/src/gui/components/attachment_image.rs
|
|
@@ -24,7 +24,10 @@ pub mod imp {
|
|
use glib::subclass::InitializingObject;
|
|
use gtk::{CompositeTemplate, Picture};
|
|
|
|
- use crate::gui::{attachment::Attachment, attachment::AttachmentImpl, utility::Utility};
|
|
+ use crate::gui::{
|
|
+ attachment::{Attachment, AttachmentImpl},
|
|
+ utility::Utility,
|
|
+ };
|
|
|
|
#[derive(CompositeTemplate, Default)]
|
|
#[template(resource = "/ui/components/attachment_image.ui")]
|
|
@@ -41,6 +44,7 @@ pub mod imp {
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
|
Self::bind_template(klass);
|
|
+ Self::bind_template_callbacks(klass);
|
|
Utility::bind_template_callbacks(klass);
|
|
}
|
|
|
|
@@ -49,6 +53,28 @@ pub mod imp {
|
|
}
|
|
}
|
|
|
|
+ #[gtk::template_callbacks]
|
|
+ impl AttachmentImage {
|
|
+ /// Open the image in a viewer presented inside the parent window.
|
|
+ /// Bails out silently if the attachment's texture hasn't loaded yet.
|
|
+ #[template_callback]
|
|
+ fn on_clicked(&self) {
|
|
+ let obj = self.obj();
|
|
+ let base = obj.upcast_ref::<Attachment>();
|
|
+ let attachment = base.attachment();
|
|
+ if attachment.image().is_none() {
|
|
+ return;
|
|
+ }
|
|
+ let Some(window) = obj
|
|
+ .root()
|
|
+ .and_then(|r| r.dynamic_cast::<crate::gui::Window>().ok())
|
|
+ else {
|
|
+ return;
|
|
+ };
|
|
+ window.present_image_viewer(&attachment);
|
|
+ }
|
|
+ }
|
|
+
|
|
impl ObjectImpl for AttachmentImage {
|
|
fn constructed(&self) {
|
|
self.parent_constructed();
|
|
diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs
|
|
new file mode 100644
|
|
index 00000000..a667a5a5
|
|
--- /dev/null
|
|
+++ b/src/gui/image_viewer.rs
|
|
@@ -0,0 +1,57 @@
|
|
+use crate::prelude::*;
|
|
+
|
|
+use crate::backend::Attachment;
|
|
+
|
|
+glib::wrapper! {
|
|
+ pub struct ImageViewer(ObjectSubclass<imp::ImageViewer>)
|
|
+ @extends adw::Bin, gtk::Widget,
|
|
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
|
+}
|
|
+
|
|
+impl ImageViewer {
|
|
+ pub fn new(attachment: &Attachment) -> Self {
|
|
+ Object::builder::<Self>()
|
|
+ .property("attachment", attachment)
|
|
+ .build()
|
|
+ }
|
|
+}
|
|
+
|
|
+pub mod imp {
|
|
+ use crate::prelude::*;
|
|
+
|
|
+ use gtk::{subclass::prelude::*, CompositeTemplate, Picture};
|
|
+
|
|
+ use crate::backend::Attachment;
|
|
+
|
|
+ #[derive(CompositeTemplate, Default, glib::Properties)]
|
|
+ #[properties(wrapper_type = super::ImageViewer)]
|
|
+ #[template(resource = "/ui/image_viewer.ui")]
|
|
+ pub struct ImageViewer {
|
|
+ #[property(get, set, construct_only)]
|
|
+ attachment: RefCell<Option<Attachment>>,
|
|
+
|
|
+ #[template_child]
|
|
+ pub(super) picture: TemplateChild<Picture>,
|
|
+ }
|
|
+
|
|
+ #[glib::object_subclass]
|
|
+ impl ObjectSubclass for ImageViewer {
|
|
+ const NAME: &'static str = "FlImageViewer";
|
|
+ type Type = super::ImageViewer;
|
|
+ type ParentType = adw::Bin;
|
|
+
|
|
+ fn class_init(klass: &mut Self::Class) {
|
|
+ Self::bind_template(klass);
|
|
+ }
|
|
+
|
|
+ fn instance_init(obj: &InitializingObject<Self>) {
|
|
+ obj.init_template();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ #[glib::derived_properties]
|
|
+ impl ObjectImpl for ImageViewer {}
|
|
+
|
|
+ impl WidgetImpl for ImageViewer {}
|
|
+ impl BinImpl for ImageViewer {}
|
|
+}
|
|
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
|
|
index 6b5cfd19..d2e997ee 100644
|
|
--- a/src/gui/mod.rs
|
|
+++ b/src/gui/mod.rs
|
|
@@ -8,6 +8,7 @@ mod channel_messages;
|
|
mod components;
|
|
mod device_info_item;
|
|
mod error_dialog;
|
|
+mod image_viewer;
|
|
mod linked_devices_window;
|
|
mod message_item;
|
|
mod new_channel_dialog;
|
|
diff --git a/src/gui/window.rs b/src/gui/window.rs
|
|
index 6335f3d6..4f75e360 100644
|
|
--- a/src/gui/window.rs
|
|
+++ b/src/gui/window.rs
|
|
@@ -103,6 +103,13 @@ impl Window {
|
|
pub(crate) fn settings(&self) -> gio::Settings {
|
|
self.imp().settings.clone()
|
|
}
|
|
+
|
|
+ pub fn present_image_viewer(&self, attachment: &crate::backend::Attachment) {
|
|
+ let viewer = crate::gui::image_viewer::ImageViewer::new(attachment);
|
|
+ let dialog = adw::Dialog::new();
|
|
+ dialog.set_child(Some(&viewer));
|
|
+ dialog.present(Some(self.upcast_ref::<gtk::Widget>()));
|
|
+ }
|
|
}
|
|
|
|
pub mod imp {
|
|
--
|
|
2.53.0
|
|
|