flare: update image patch
This commit is contained in:
@@ -89,7 +89,7 @@
|
|||||||
# alternative GTK signal client; carries local feature patches under
|
# alternative GTK signal client; carries local feature patches under
|
||||||
# patches/flare/ on top of upstream master (typing indicators, edited
|
# patches/flare/ on top of upstream master (typing indicators, edited
|
||||||
# messages, multi-select with delete-for-me, in-channel message search,
|
# messages, multi-select with delete-for-me, in-channel message search,
|
||||||
# the deleted-message placeholder, the image-viewer overlay, and the
|
# the deleted-message placeholder, the image-viewer dialog, and the
|
||||||
# not-yet-merged init_channels cache-miss fix).
|
# not-yet-merged init_channels cache-miss fix).
|
||||||
(pkgs.flare-signal.overrideAttrs (old: {
|
(pkgs.flare-signal.overrideAttrs (old: {
|
||||||
src = inputs.flare-upstream;
|
src = inputs.flare-upstream;
|
||||||
@@ -106,15 +106,6 @@
|
|||||||
../../patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch
|
../../patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch
|
||||||
../../patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch
|
../../patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch
|
||||||
];
|
];
|
||||||
# The image-viewer demo patch (0007) references
|
|
||||||
# data/screenshots/dummy_apple.jpg, but git binary diffs are not
|
|
||||||
# supported by the standard `patch` tool nixpkgs uses for the
|
|
||||||
# patchPhase. Ship the JPEG alongside the patches and copy it into
|
|
||||||
# the source tree before the build picks it up.
|
|
||||||
postPatch = (old.postPatch or "") + ''
|
|
||||||
install -m 0644 ${./../../patches/flare/dummy_apple.jpg} \
|
|
||||||
data/screenshots/dummy_apple.jpg
|
|
||||||
'';
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
# accounting
|
# accounting
|
||||||
|
|||||||
@@ -1,60 +1,42 @@
|
|||||||
From e13d8cbf15a2eea323397accb2183e2362dae106 Mon Sep 17 00:00:00 2001
|
From c185703eef79a48dead219f6b41a7874994c7273 Mon Sep 17 00:00:00 2001
|
||||||
From: Simon Gardling <titaniumtown@proton.me>
|
From: Simon Gardling <titaniumtown@proton.me>
|
||||||
Date: Wed, 6 May 2026 18:41:15 -0400
|
Date: Tue, 12 May 2026 12:03:33 -0400
|
||||||
Subject: [PATCH 7/7] feat(messages): Open image attachments in a
|
Subject: [PATCH] feat(messages): Open image attachments in a dialog
|
||||||
fullscreen-capable viewer
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain; charset=UTF-8
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
|
|
||||||
Clicking an image bubble currently does nothing — the existing
|
Mirrors the official Signal desktop application by allowing the user
|
||||||
"pressed" gesture is right-click-only and routes to the context menu.
|
to click on an image attachment and see it expanded in an Adw.Dialog
|
||||||
To actually look at an image at full resolution the user has to
|
overlay with built-in close button, Escape handling, and mobile
|
||||||
right-click and choose "Open", which dispatches the file through the
|
swipe-down support.
|
||||||
xdg-open portal to the system image viewer. That is the wrong
|
|
||||||
affordance for a chat client; every other Signal frontend opens the
|
|
||||||
image inline on tap.
|
|
||||||
|
|
||||||
Add an `ImageViewer` (Adw.Window subclass) that hosts the attachment's
|
|
||||||
texture in a `gtk::Picture` filling the window over a near-black
|
|
||||||
backdrop, with two `osd circular` buttons floating in the top-right
|
|
||||||
corner: a fullscreen toggle and a close button. Closing happens on
|
|
||||||
`Escape` or via the close button — clicks on the picture and on the
|
|
||||||
letterboxed area do nothing, leaving the gesture stack open for a
|
|
||||||
future pan/zoom implementation.
|
|
||||||
|
|
||||||
Wire `AttachmentImage` to open the viewer on left-click. Bails out
|
|
||||||
silently if the texture has not loaded yet or if the row is not
|
|
||||||
currently parented in a `gtk::Window`.
|
|
||||||
---
|
---
|
||||||
CHANGELOG.md | 1 +
|
CHANGELOG.md | 4 ++
|
||||||
data/resources/meson.build | 1 +
|
data/resources/meson.build | 1 +
|
||||||
data/resources/resources.gresource.xml.in | 5 +
|
data/resources/resources.gresource.xml.in | 1 +
|
||||||
data/resources/style.css | 8 +-
|
.../ui/components/attachment_image.blp | 5 ++
|
||||||
.../ui/components/attachment_image.blp | 5 +
|
data/resources/ui/image_viewer.blp | 15 +++++
|
||||||
data/resources/ui/image_viewer.blp | 64 +++++
|
src/backend/dummy.rs | 3 +-
|
||||||
data/resources/ui/window.blp | 224 +++++++++---------
|
src/gui/components/attachment_image.rs | 28 ++++++++-
|
||||||
src/backend/dummy.rs | 36 ++-
|
src/gui/image_viewer.rs | 57 +++++++++++++++++++
|
||||||
src/gui/components/attachment_image.rs | 33 ++-
|
|
||||||
src/gui/image_viewer.rs | 120 ++++++++++
|
|
||||||
src/gui/mod.rs | 1 +
|
src/gui/mod.rs | 1 +
|
||||||
src/gui/window.rs | 30 +++
|
src/gui/window.rs | 7 +++
|
||||||
13 files changed, 411 insertions(+), 117 deletions(-)
|
10 files changed, 120 insertions(+), 2 deletions(-)
|
||||||
create mode 100644 data/resources/ui/image_viewer.blp
|
create mode 100644 data/resources/ui/image_viewer.blp
|
||||||
create mode 100644 src/gui/image_viewer.rs
|
create mode 100644 src/gui/image_viewer.rs
|
||||||
|
|
||||||
diff --git a/CHANGELOG.md b/CHANGELOG.md
|
diff --git a/CHANGELOG.md b/CHANGELOG.md
|
||||||
index e221f555..8ebca71c 100644
|
index 20dc578e..1e4649bc 100644
|
||||||
--- a/CHANGELOG.md
|
--- a/CHANGELOG.md
|
||||||
+++ b/CHANGELOG.md
|
+++ b/CHANGELOG.md
|
||||||
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Display incoming edited messages with an `edited` indicator and edit your own sent messages from their context menu.
|
|
||||||
- Multi-select messages from their context menu and delete the selection locally with a single action.
|
|
||||||
- In-channel message search (Ctrl+Shift+F) over the loaded timeline with prev/next navigation and a match counter.
|
|
||||||
+- Click an image attachment to open it in a fullscreen-capable viewer with a close button, fullscreen toggle, and `Escape`-to-dismiss.
|
|
||||||
|
|
||||||
|
## [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
|
## [0.20.4] - 2026-04-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
diff --git a/data/resources/meson.build b/data/resources/meson.build
|
diff --git a/data/resources/meson.build b/data/resources/meson.build
|
||||||
index 7fc00b7c..efd789b7 100644
|
index 7fc00b7c..efd789b7 100644
|
||||||
--- a/data/resources/meson.build
|
--- a/data/resources/meson.build
|
||||||
@@ -68,21 +50,10 @@ index 7fc00b7c..efd789b7 100644
|
|||||||
'ui/linked_devices_window.blp',
|
'ui/linked_devices_window.blp',
|
||||||
'ui/device_info_item.blp',
|
'ui/device_info_item.blp',
|
||||||
diff --git a/data/resources/resources.gresource.xml.in b/data/resources/resources.gresource.xml.in
|
diff --git a/data/resources/resources.gresource.xml.in b/data/resources/resources.gresource.xml.in
|
||||||
index 8604fe24..863564b3 100644
|
index 8604fe24..7c958d02 100644
|
||||||
--- a/data/resources/resources.gresource.xml.in
|
--- a/data/resources/resources.gresource.xml.in
|
||||||
+++ b/data/resources/resources.gresource.xml.in
|
+++ b/data/resources/resources.gresource.xml.in
|
||||||
@@ -2,6 +2,10 @@
|
@@ -13,6 +13,7 @@
|
||||||
<gresources>
|
|
||||||
<gresource prefix="/">
|
|
||||||
<file alias="icon.svg">../icons/de.schmidhuberj.Flare.svg</file>
|
|
||||||
+ <!-- Photo bundled into the screenshot-mode build only as the demo image-->
|
|
||||||
+ <!-- attachment in `src/backend/dummy.rs`. "Big red apple.jpg" by-->
|
|
||||||
+ <!-- Paolo Neo, public domain, from Wikimedia Commons.-->
|
|
||||||
+ <file alias="dummy_apple.jpg">../screenshots/dummy_apple.jpg</file>
|
|
||||||
|
|
||||||
<file preprocess="xml-stripblanks">ui/window.ui</file>
|
|
||||||
<file preprocess="xml-stripblanks">ui/channel_list.ui</file>
|
|
||||||
@@ -13,6 +17,7 @@
|
|
||||||
<file preprocess="xml-stripblanks">ui/linked_devices_window.ui</file>
|
<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/device_info_item.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/error_dialog.ui</file>
|
<file preprocess="xml-stripblanks">ui/error_dialog.ui</file>
|
||||||
@@ -90,23 +61,6 @@ index 8604fe24..863564b3 100644
|
|||||||
<file preprocess="xml-stripblanks">ui/attachment.ui</file>
|
<file preprocess="xml-stripblanks">ui/attachment.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/preferences_window.ui</file>
|
<file preprocess="xml-stripblanks">ui/preferences_window.ui</file>
|
||||||
<file preprocess="xml-stripblanks">ui/shortcuts.ui</file>
|
<file preprocess="xml-stripblanks">ui/shortcuts.ui</file>
|
||||||
diff --git a/data/resources/style.css b/data/resources/style.css
|
|
||||||
index b3a517f9..0c95a55a 100644
|
|
||||||
--- a/data/resources/style.css
|
|
||||||
+++ b/data/resources/style.css
|
|
||||||
@@ -309,4 +309,10 @@ unread-indicator {
|
|
||||||
background: alpha(@accent_bg_color, 0.35);
|
|
||||||
outline: 2px solid alpha(@accent_color, 0.7);
|
|
||||||
outline-offset: -2px;
|
|
||||||
-}
|
|
||||||
\ No newline at end of file
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+/* Image attachment viewer: a near-black backdrop so the picture appears to
|
|
||||||
+ float and the floating OSD close button has enough contrast. */
|
|
||||||
+.image-viewer-backdrop {
|
|
||||||
+ background-color: rgb(15, 15, 15);
|
|
||||||
+}
|
|
||||||
diff --git a/data/resources/ui/components/attachment_image.blp b/data/resources/ui/components/attachment_image.blp
|
diff --git a/data/resources/ui/components/attachment_image.blp b/data/resources/ui/components/attachment_image.blp
|
||||||
index ec303a72..bcbdabe1 100644
|
index ec303a72..bcbdabe1 100644
|
||||||
--- a/data/resources/ui/components/attachment_image.blp
|
--- a/data/resources/ui/components/attachment_image.blp
|
||||||
@@ -125,44 +79,16 @@ index ec303a72..bcbdabe1 100644
|
|||||||
]
|
]
|
||||||
diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp
|
diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 00000000..1b27cc09
|
index 00000000..19660f0b
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/data/resources/ui/image_viewer.blp
|
+++ b/data/resources/ui/image_viewer.blp
|
||||||
@@ -0,0 +1,64 @@
|
@@ -0,0 +1,15 @@
|
||||||
+using Gtk 4.0;
|
+using Gtk 4.0;
|
||||||
+using Adw 1;
|
+using Adw 1;
|
||||||
+
|
+
|
||||||
+template $FlImageViewer: Adw.Bin {
|
+template $FlImageViewer: Adw.Bin {
|
||||||
+ hexpand: true;
|
+ hexpand: true;
|
||||||
+ vexpand: true;
|
+ vexpand: true;
|
||||||
+ focusable: true;
|
|
||||||
+
|
|
||||||
+ EventControllerKey {
|
|
||||||
+ key-pressed => $on_key_pressed() swapped;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ // Box paints the dark backdrop. Overlay does not draw a background, so
|
|
||||||
+ // the CSS class lives on the Box.
|
|
||||||
+ Box {
|
|
||||||
+ hexpand: true;
|
|
||||||
+ vexpand: true;
|
|
||||||
+
|
|
||||||
+ styles [
|
|
||||||
+ "image-viewer-backdrop",
|
|
||||||
+ ]
|
|
||||||
+
|
|
||||||
+ Overlay {
|
|
||||||
+ hexpand: true;
|
|
||||||
+ vexpand: true;
|
|
||||||
+
|
|
||||||
+ // Any left-click in the viewer dismisses — on the image, on the
|
|
||||||
+ // letterboxed dark area, anywhere. The close button absorbs its own
|
|
||||||
+ // click before it reaches us via standard GTK click-bubbling so the
|
|
||||||
+ // button still works.
|
|
||||||
+ GestureClick {
|
|
||||||
+ button: 1;
|
|
||||||
+ pressed => $on_backdrop_pressed() swapped;
|
|
||||||
+ }
|
|
||||||
+
|
+
|
||||||
+ Picture picture {
|
+ Picture picture {
|
||||||
+ paintable: bind template.attachment as <$FlAttachmentObject>.image;
|
+ paintable: bind template.attachment as <$FlAttachmentObject>.image;
|
||||||
@@ -171,340 +97,30 @@ index 00000000..1b27cc09
|
|||||||
+ hexpand: true;
|
+ hexpand: true;
|
||||||
+ vexpand: true;
|
+ vexpand: true;
|
||||||
+ }
|
+ }
|
||||||
+
|
|
||||||
+ [overlay]
|
|
||||||
+ Box {
|
|
||||||
+ halign: end;
|
|
||||||
+ valign: start;
|
|
||||||
+ margin-top: 12;
|
|
||||||
+ margin-end: 12;
|
|
||||||
+
|
|
||||||
+ Button close_button {
|
|
||||||
+ icon-name: "window-close-symbolic";
|
|
||||||
+ tooltip-text: _("Close");
|
|
||||||
+ clicked => $on_close_clicked() swapped;
|
|
||||||
+
|
|
||||||
+ styles [
|
|
||||||
+ "osd",
|
|
||||||
+ "circular",
|
|
||||||
+ ]
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+}
|
+}
|
||||||
diff --git a/data/resources/ui/window.blp b/data/resources/ui/window.blp
|
|
||||||
index 82de6af3..3706121a 100644
|
|
||||||
--- a/data/resources/ui/window.blp
|
|
||||||
+++ b/data/resources/ui/window.blp
|
|
||||||
@@ -52,130 +52,136 @@ template $FlWindow: Adw.ApplicationWindow {
|
|
||||||
Gtk.StackPage {
|
|
||||||
name: "main";
|
|
||||||
|
|
||||||
- child: Adw.NavigationSplitView split_view {
|
|
||||||
- min-sidebar-width: 300;
|
|
||||||
- max-sidebar-width: 400;
|
|
||||||
- sidebar-width-fraction: 0.25;
|
|
||||||
- notify::show-content => $handle_show_content() swapped;
|
|
||||||
-
|
|
||||||
- sidebar: Adw.NavigationPage {
|
|
||||||
- title: _("Channel list");
|
|
||||||
- tag: "channel-list";
|
|
||||||
-
|
|
||||||
- Adw.ToolbarView {
|
|
||||||
- [top]
|
|
||||||
- Adw.HeaderBar {
|
|
||||||
- title-widget: Adw.WindowTitle {
|
|
||||||
- title: "Flare";
|
|
||||||
- };
|
|
||||||
+ child: Gtk.Overlay image_viewer_overlay {
|
|
||||||
+ // Wraps the navigation split view so an image viewer pushed in via
|
|
||||||
+ // `Window::present_image_viewer` covers the entire window content
|
|
||||||
+ // (sidebar + chat) rather than being constrained to the split
|
|
||||||
+ // view's content pane like an Adw.Dialog would be.
|
|
||||||
+ Adw.NavigationSplitView split_view {
|
|
||||||
+ min-sidebar-width: 300;
|
|
||||||
+ max-sidebar-width: 400;
|
|
||||||
+ sidebar-width-fraction: 0.25;
|
|
||||||
+ notify::show-content => $handle_show_content() swapped;
|
|
||||||
+
|
|
||||||
+ sidebar: Adw.NavigationPage {
|
|
||||||
+ title: _("Channel list");
|
|
||||||
+ tag: "channel-list";
|
|
||||||
+
|
|
||||||
+ Adw.ToolbarView {
|
|
||||||
+ [top]
|
|
||||||
+ Adw.HeaderBar {
|
|
||||||
+ title-widget: Adw.WindowTitle {
|
|
||||||
+ title: "Flare";
|
|
||||||
+ };
|
|
||||||
+
|
|
||||||
+ [start]
|
|
||||||
+ Gtk.Button {
|
|
||||||
+ accessibility {
|
|
||||||
+ label: C_("accessibility", "Add Conversation");
|
|
||||||
+ }
|
|
||||||
|
|
||||||
- [start]
|
|
||||||
- Gtk.Button {
|
|
||||||
- accessibility {
|
|
||||||
- label: C_("accessibility", "Add Conversation");
|
|
||||||
+ tooltip-text: "Add Conversation";
|
|
||||||
+ icon-name: "chat-message-new-symbolic";
|
|
||||||
+ clicked => $handle_add_conversation_clicked() swapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
- tooltip-text: "Add Conversation";
|
|
||||||
- icon-name: "chat-message-new-symbolic";
|
|
||||||
- clicked => $handle_add_conversation_clicked() swapped;
|
|
||||||
- }
|
|
||||||
+ [end]
|
|
||||||
+ MenuButton {
|
|
||||||
+ accessibility {
|
|
||||||
+ label: C_("accessibility", "Menu");
|
|
||||||
+ }
|
|
||||||
|
|
||||||
- [end]
|
|
||||||
- MenuButton {
|
|
||||||
- accessibility {
|
|
||||||
- label: C_("accessibility", "Menu");
|
|
||||||
+ tooltip-text: "Menu";
|
|
||||||
+ menu-model: menubar;
|
|
||||||
+ primary: true;
|
|
||||||
+ icon-name: "open-menu-symbolic";
|
|
||||||
}
|
|
||||||
|
|
||||||
- tooltip-text: "Menu";
|
|
||||||
- menu-model: menubar;
|
|
||||||
- primary: true;
|
|
||||||
- icon-name: "open-menu-symbolic";
|
|
||||||
- }
|
|
||||||
+ [end]
|
|
||||||
+ ToggleButton {
|
|
||||||
+ accessibility {
|
|
||||||
+ label: C_("accessibility", "Search");
|
|
||||||
+ }
|
|
||||||
|
|
||||||
- [end]
|
|
||||||
- ToggleButton {
|
|
||||||
- accessibility {
|
|
||||||
- label: C_("accessibility", "Search");
|
|
||||||
+ tooltip-text: "Search";
|
|
||||||
+ icon-name: "system-search-symbolic";
|
|
||||||
+ active: bind channel_list.search-enabled no-sync-create;
|
|
||||||
+ clicked => $handle_search_clicked() swapped;
|
|
||||||
}
|
|
||||||
-
|
|
||||||
- tooltip-text: "Search";
|
|
||||||
- icon-name: "system-search-symbolic";
|
|
||||||
- active: bind channel_list.search-enabled no-sync-create;
|
|
||||||
- clicked => $handle_search_clicked() swapped;
|
|
||||||
}
|
|
||||||
- }
|
|
||||||
|
|
||||||
- content: $FlChannelList channel_list {
|
|
||||||
- notify::active-channel => $handle_go_forward() swapped;
|
|
||||||
- manager: bind template.manager;
|
|
||||||
- };
|
|
||||||
- }
|
|
||||||
- };
|
|
||||||
-
|
|
||||||
- content: Adw.NavigationPage {
|
|
||||||
- title: _("Chat");
|
|
||||||
- tag: "chat";
|
|
||||||
-
|
|
||||||
- Adw.ToolbarView {
|
|
||||||
- [top]
|
|
||||||
- Adw.HeaderBar {
|
|
||||||
- title-widget: Button {
|
|
||||||
- styles [
|
|
||||||
- "flat",
|
|
||||||
- ]
|
|
||||||
-
|
|
||||||
- sensitive: bind $is_some(channel_list.active-channel) as <bool>;
|
|
||||||
- visible: bind $is_some(channel_list.active-channel) as <bool>;
|
|
||||||
- action-name: "win.channel-information";
|
|
||||||
-
|
|
||||||
- child: Box {
|
|
||||||
- name: "room_title";
|
|
||||||
- orientation: vertical;
|
|
||||||
- halign: center;
|
|
||||||
- valign: center;
|
|
||||||
-
|
|
||||||
- Label title_label {
|
|
||||||
- focusable: true;
|
|
||||||
- ellipsize: end;
|
|
||||||
- halign: center;
|
|
||||||
- wrap: false;
|
|
||||||
- single-line-mode: true;
|
|
||||||
- use-markup: false;
|
|
||||||
- width-chars: 5;
|
|
||||||
- label: bind channel_list.active-channel as <$FlChannel>.title;
|
|
||||||
-
|
|
||||||
- styles [
|
|
||||||
- "title",
|
|
||||||
- ]
|
|
||||||
- }
|
|
||||||
+ content: $FlChannelList channel_list {
|
|
||||||
+ notify::active-channel => $handle_go_forward() swapped;
|
|
||||||
+ manager: bind template.manager;
|
|
||||||
+ };
|
|
||||||
+ }
|
|
||||||
+ };
|
|
||||||
|
|
||||||
- Label subtitle_label {
|
|
||||||
- focusable: true;
|
|
||||||
- ellipsize: end;
|
|
||||||
+ content: Adw.NavigationPage {
|
|
||||||
+ title: _("Chat");
|
|
||||||
+ tag: "chat";
|
|
||||||
+
|
|
||||||
+ Adw.ToolbarView {
|
|
||||||
+ [top]
|
|
||||||
+ Adw.HeaderBar {
|
|
||||||
+ title-widget: Button {
|
|
||||||
+ styles [
|
|
||||||
+ "flat",
|
|
||||||
+ ]
|
|
||||||
+
|
|
||||||
+ sensitive: bind $is_some(channel_list.active-channel) as <bool>;
|
|
||||||
+ visible: bind $is_some(channel_list.active-channel) as <bool>;
|
|
||||||
+ action-name: "win.channel-information";
|
|
||||||
+
|
|
||||||
+ child: Box {
|
|
||||||
+ name: "room_title";
|
|
||||||
+ orientation: vertical;
|
|
||||||
halign: center;
|
|
||||||
- wrap: false;
|
|
||||||
- single-line-mode: true;
|
|
||||||
- use-markup: true;
|
|
||||||
- visible: bind channel_list.active-channel as <$FlChannel>.is-typing;
|
|
||||||
- label: bind channel_list.active-channel as <$FlChannel>.typing-label;
|
|
||||||
- tooltip-markup: bind channel_list.active-channel as <$FlChannel>.description;
|
|
||||||
-
|
|
||||||
- styles [
|
|
||||||
- "subtitle-room",
|
|
||||||
- "accent",
|
|
||||||
- ]
|
|
||||||
- }
|
|
||||||
+ valign: center;
|
|
||||||
+
|
|
||||||
+ Label title_label {
|
|
||||||
+ focusable: true;
|
|
||||||
+ ellipsize: end;
|
|
||||||
+ halign: center;
|
|
||||||
+ wrap: false;
|
|
||||||
+ single-line-mode: true;
|
|
||||||
+ use-markup: false;
|
|
||||||
+ width-chars: 5;
|
|
||||||
+ label: bind channel_list.active-channel as <$FlChannel>.title;
|
|
||||||
+
|
|
||||||
+ styles [
|
|
||||||
+ "title",
|
|
||||||
+ ]
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ Label subtitle_label {
|
|
||||||
+ focusable: true;
|
|
||||||
+ ellipsize: end;
|
|
||||||
+ halign: center;
|
|
||||||
+ wrap: false;
|
|
||||||
+ single-line-mode: true;
|
|
||||||
+ use-markup: true;
|
|
||||||
+ visible: bind channel_list.active-channel as <$FlChannel>.is-typing;
|
|
||||||
+ label: bind channel_list.active-channel as <$FlChannel>.typing-label;
|
|
||||||
+ tooltip-markup: bind channel_list.active-channel as <$FlChannel>.description;
|
|
||||||
+
|
|
||||||
+ styles [
|
|
||||||
+ "subtitle-room",
|
|
||||||
+ "accent",
|
|
||||||
+ ]
|
|
||||||
+ }
|
|
||||||
+ };
|
|
||||||
};
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ content: $FlChannelMessages channel_messages {
|
|
||||||
+ manager: bind template.manager;
|
|
||||||
+ active-channel: bind channel_list.active-channel;
|
|
||||||
+ has-channels: bind channel_list.has-channels;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-
|
|
||||||
- content: $FlChannelMessages channel_messages {
|
|
||||||
- manager: bind template.manager;
|
|
||||||
- active-channel: bind channel_list.active-channel;
|
|
||||||
- has-channels: bind channel_list.has-channels;
|
|
||||||
- };
|
|
||||||
- }
|
|
||||||
- };
|
|
||||||
+ };
|
|
||||||
+ }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs
|
diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs
|
||||||
index e6824e80..c1aae764 100644
|
index e6824e80..1ebff6f6 100644
|
||||||
--- a/src/backend/dummy.rs
|
--- a/src/backend/dummy.rs
|
||||||
+++ b/src/backend/dummy.rs
|
+++ b/src/backend/dummy.rs
|
||||||
@@ -382,18 +382,40 @@ impl super::Manager {
|
@@ -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 msg_screenshot = msg!(self, "", 2, GROUP_ID, 19 + base_minute);
|
||||||
- let attachment = crate::backend::Attachment::from_file(screenshot_file, self);
|
let screenshot_file = gtk::gio::File::for_uri("resource:///icon.svg");
|
||||||
- msg_screenshot
|
let attachment = crate::backend::Attachment::from_file(screenshot_file, self);
|
||||||
+ // A short question-and-answer in the 1-on-1 chat with contact 2
|
@@ -393,7 +394,7 @@ impl super::Manager {
|
||||||
+ // ("Developer") demonstrating the click-to-open image viewer
|
|
||||||
+ // (`gui::image_viewer`). The contact sends a photo of an apple
|
|
||||||
+ // (bundled into the screenshot-mode build via
|
|
||||||
+ // `data/resources/resources.gresource.xml.in` as `dummy_apple.jpg`,
|
|
||||||
+ // public domain "Big red apple.jpg" by Paolo Neo via Wikimedia
|
|
||||||
+ // Commons) along with a
|
|
||||||
+ // text question, and the local user answers with the obvious
|
|
||||||
+ // one-word reply.
|
|
||||||
+ let msg_fruit_question = msg!(
|
|
||||||
+ self,
|
|
||||||
+ "what is the name of this fruit?",
|
|
||||||
+ 2,
|
|
||||||
+ 2,
|
|
||||||
+ 30 + base_minute
|
|
||||||
+ );
|
|
||||||
+ // `Attachment::from_file` only loads images via filesystem paths; for
|
|
||||||
+ // `resource://` URIs `GFile::path()` returns None, so the image
|
|
||||||
+ // would never reach the Picture. Load the bytes directly out of the
|
|
||||||
+ // gresource bundle into a Texture instead, and build the attachment
|
|
||||||
+ // from that.
|
|
||||||
+ let fruit_pixbuf = gtk::gdk_pixbuf::Pixbuf::from_resource("/dummy_apple.jpg")
|
|
||||||
+ .expect("apple photo resource to load");
|
|
||||||
+ let fruit_texture = gtk::gdk::Texture::for_pixbuf(&fruit_pixbuf);
|
|
||||||
+ let fruit_photo = crate::backend::Attachment::from_texture(fruit_texture, self);
|
|
||||||
+ msg_fruit_question
|
|
||||||
.clone()
|
|
||||||
.downcast::<TextMessage>()
|
|
||||||
.unwrap()
|
|
||||||
- .add_attachment(attachment);
|
|
||||||
+ .add_attachment(fruit_photo);
|
|
||||||
+ let msg_fruit_answer = msg!(self, "apple", 0, 2, 32 + base_minute);
|
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
msg_replied,
|
msg_replied,
|
||||||
- // msg_screenshot,
|
- // msg_screenshot,
|
||||||
|
+ msg_screenshot,
|
||||||
msg_reply,
|
msg_reply,
|
||||||
msg!(
|
msg!(
|
||||||
self,
|
self,
|
||||||
@@ -405,6 +427,8 @@ impl super::Manager {
|
|
||||||
msg!(self, "Glad you like it.", 2, GROUP_ID, 25 + base_minute),
|
|
||||||
msg!(self, "YAY!", 0, GROUP_ID, 27 + base_minute),
|
|
||||||
msg!(self, "Greetings", 2, 2, base_minute - 100),
|
|
||||||
+ msg_fruit_question,
|
|
||||||
+ msg_fruit_answer,
|
|
||||||
msg!(self, "It's a trap!", 3, 3, 1 + base_minute),
|
|
||||||
msg!(self, "Hello, Mr. Anderson", 4, 4, 2 + base_minute),
|
|
||||||
]
|
|
||||||
diff --git a/src/gui/components/attachment_image.rs b/src/gui/components/attachment_image.rs
|
diff --git a/src/gui/components/attachment_image.rs b/src/gui/components/attachment_image.rs
|
||||||
index a91bf05b..4ddd29b6 100644
|
index a91bf05b..9ca6e0c7 100644
|
||||||
--- a/src/gui/components/attachment_image.rs
|
--- a/src/gui/components/attachment_image.rs
|
||||||
+++ b/src/gui/components/attachment_image.rs
|
+++ b/src/gui/components/attachment_image.rs
|
||||||
@@ -24,7 +24,10 @@ pub mod imp {
|
@@ -24,7 +24,10 @@ pub mod imp {
|
||||||
@@ -527,7 +143,7 @@ index a91bf05b..4ddd29b6 100644
|
|||||||
Utility::bind_template_callbacks(klass);
|
Utility::bind_template_callbacks(klass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +53,33 @@ pub mod imp {
|
@@ -49,6 +53,28 @@ pub mod imp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,11 +159,6 @@ index a91bf05b..4ddd29b6 100644
|
|||||||
+ if attachment.image().is_none() {
|
+ if attachment.image().is_none() {
|
||||||
+ return;
|
+ return;
|
||||||
+ }
|
+ }
|
||||||
+ // Walk up to the top-level `crate::gui::Window` and ask it to
|
|
||||||
+ // host the viewer in its window-level overlay so the viewer
|
|
||||||
+ // covers the entire window content (sidebar + chat) rather
|
|
||||||
+ // than being constrained to the navigation split view's
|
|
||||||
+ // content pane like an Adw.Dialog would be.
|
|
||||||
+ let Some(window) = obj
|
+ let Some(window) = obj
|
||||||
+ .root()
|
+ .root()
|
||||||
+ .and_then(|r| r.dynamic_cast::<crate::gui::Window>().ok())
|
+ .and_then(|r| r.dynamic_cast::<crate::gui::Window>().ok())
|
||||||
@@ -563,25 +174,15 @@ index a91bf05b..4ddd29b6 100644
|
|||||||
self.parent_constructed();
|
self.parent_constructed();
|
||||||
diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs
|
diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 00000000..b3e9d720
|
index 00000000..a667a5a5
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/src/gui/image_viewer.rs
|
+++ b/src/gui/image_viewer.rs
|
||||||
@@ -0,0 +1,120 @@
|
@@ -0,0 +1,57 @@
|
||||||
+use crate::prelude::*;
|
+use crate::prelude::*;
|
||||||
+
|
+
|
||||||
+use crate::backend::Attachment;
|
+use crate::backend::Attachment;
|
||||||
+
|
+
|
||||||
+glib::wrapper! {
|
+glib::wrapper! {
|
||||||
+ /// A widget that displays an image attachment over a dark backdrop. It is
|
|
||||||
+ /// designed to be inserted into a top-level [`gtk::Overlay`] in
|
|
||||||
+ /// [`crate::gui::Window`] (see [`crate::gui::Window::present_image_viewer`])
|
|
||||||
+ /// so that it covers every other widget in the window — sidebar, chat
|
|
||||||
+ /// pane, header bar — rather than being constrained to the content area
|
|
||||||
+ /// of the surrounding navigation split view (which is what `Adw.Dialog`
|
|
||||||
+ /// would force).
|
|
||||||
+ ///
|
|
||||||
+ /// Emits `closed` when the user dismisses the viewer. Consumers should
|
|
||||||
+ /// remove the widget from the overlay in response.
|
|
||||||
+ pub struct ImageViewer(ObjectSubclass<imp::ImageViewer>)
|
+ pub struct ImageViewer(ObjectSubclass<imp::ImageViewer>)
|
||||||
+ @extends adw::Bin, gtk::Widget,
|
+ @extends adw::Bin, gtk::Widget,
|
||||||
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
||||||
@@ -589,7 +190,6 @@ index 00000000..b3e9d720
|
|||||||
+
|
+
|
||||||
+impl ImageViewer {
|
+impl ImageViewer {
|
||||||
+ pub fn new(attachment: &Attachment) -> Self {
|
+ pub fn new(attachment: &Attachment) -> Self {
|
||||||
+ log::trace!("Initializing `ImageViewer`");
|
|
||||||
+ Object::builder::<Self>()
|
+ Object::builder::<Self>()
|
||||||
+ .property("attachment", attachment)
|
+ .property("attachment", attachment)
|
||||||
+ .build()
|
+ .build()
|
||||||
@@ -599,8 +199,7 @@ index 00000000..b3e9d720
|
|||||||
+pub mod imp {
|
+pub mod imp {
|
||||||
+ use crate::prelude::*;
|
+ use crate::prelude::*;
|
||||||
+
|
+
|
||||||
+ use glib::subclass::{InitializingObject, Signal};
|
+ use gtk::{subclass::prelude::*, CompositeTemplate, Picture};
|
||||||
+ use gtk::{CompositeTemplate, Picture};
|
|
||||||
+
|
+
|
||||||
+ use crate::backend::Attachment;
|
+ use crate::backend::Attachment;
|
||||||
+
|
+
|
||||||
@@ -615,41 +214,6 @@ index 00000000..b3e9d720
|
|||||||
+ pub(super) picture: TemplateChild<Picture>,
|
+ pub(super) picture: TemplateChild<Picture>,
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ #[gtk::template_callbacks]
|
|
||||||
+ impl ImageViewer {
|
|
||||||
+ /// Click anywhere in the viewer (image or backdrop) closes. The
|
|
||||||
+ /// close button stops its own click before it reaches us via the
|
|
||||||
+ /// usual GTK click-bubbling, so the button still works.
|
|
||||||
+ #[template_callback]
|
|
||||||
+ fn on_backdrop_pressed(&self) {
|
|
||||||
+ self.obj().emit_by_name::<()>("closed", &[]);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ #[template_callback]
|
|
||||||
+ fn on_close_clicked(&self) {
|
|
||||||
+ self.obj().emit_by_name::<()>("closed", &[]);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ /// Close on `Escape`. The viewer grabs focus on construction so the
|
|
||||||
+ /// key controller actually sees keystrokes; without that, focus
|
|
||||||
+ /// would still be on whatever widget the user clicked on (typically
|
|
||||||
+ /// the message row) and `Escape` would route there instead.
|
|
||||||
+ #[template_callback]
|
|
||||||
+ fn on_key_pressed(
|
|
||||||
+ &self,
|
|
||||||
+ keyval: gdk::Key,
|
|
||||||
+ _keycode: u32,
|
|
||||||
+ _state: gdk::ModifierType,
|
|
||||||
+ ) -> glib::Propagation {
|
|
||||||
+ if keyval == gdk::Key::Escape {
|
|
||||||
+ self.obj().emit_by_name::<()>("closed", &[]);
|
|
||||||
+ glib::Propagation::Stop
|
|
||||||
+ } else {
|
|
||||||
+ glib::Propagation::Proceed
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ #[glib::object_subclass]
|
+ #[glib::object_subclass]
|
||||||
+ impl ObjectSubclass for ImageViewer {
|
+ impl ObjectSubclass for ImageViewer {
|
||||||
+ const NAME: &'static str = "FlImageViewer";
|
+ const NAME: &'static str = "FlImageViewer";
|
||||||
@@ -658,7 +222,6 @@ index 00000000..b3e9d720
|
|||||||
+
|
+
|
||||||
+ fn class_init(klass: &mut Self::Class) {
|
+ fn class_init(klass: &mut Self::Class) {
|
||||||
+ Self::bind_template(klass);
|
+ Self::bind_template(klass);
|
||||||
+ Self::bind_template_callbacks(klass);
|
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ fn instance_init(obj: &InitializingObject<Self>) {
|
+ fn instance_init(obj: &InitializingObject<Self>) {
|
||||||
@@ -667,22 +230,7 @@ index 00000000..b3e9d720
|
|||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
+ #[glib::derived_properties]
|
+ #[glib::derived_properties]
|
||||||
+ impl ObjectImpl for ImageViewer {
|
+ impl ObjectImpl for ImageViewer {}
|
||||||
+ fn signals() -> &'static [Signal] {
|
|
||||||
+ static SIGNALS: Lazy<Vec<Signal>> =
|
|
||||||
+ Lazy::new(|| vec![Signal::builder("closed").build()]);
|
|
||||||
+ SIGNALS.as_ref()
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ fn constructed(&self) {
|
|
||||||
+ self.parent_constructed();
|
|
||||||
+ // Make the widget focusable so the key controller below is
|
|
||||||
+ // actually allowed to receive keystrokes; Adw.Bin defaults to
|
|
||||||
+ // not-focusable.
|
|
||||||
+ self.obj().set_focusable(true);
|
|
||||||
+ self.obj().set_can_focus(true);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+
|
+
|
||||||
+ impl WidgetImpl for ImageViewer {}
|
+ impl WidgetImpl for ImageViewer {}
|
||||||
+ impl BinImpl for ImageViewer {}
|
+ impl BinImpl for ImageViewer {}
|
||||||
@@ -700,60 +248,23 @@ index 6b5cfd19..d2e997ee 100644
|
|||||||
mod message_item;
|
mod message_item;
|
||||||
mod new_channel_dialog;
|
mod new_channel_dialog;
|
||||||
diff --git a/src/gui/window.rs b/src/gui/window.rs
|
diff --git a/src/gui/window.rs b/src/gui/window.rs
|
||||||
index ce097ce1..bd9474e2 100644
|
index 6335f3d6..4f75e360 100644
|
||||||
--- a/src/gui/window.rs
|
--- a/src/gui/window.rs
|
||||||
+++ b/src/gui/window.rs
|
+++ b/src/gui/window.rs
|
||||||
@@ -104,6 +104,33 @@ impl Window {
|
@@ -103,6 +103,13 @@ impl Window {
|
||||||
pub(crate) fn settings(&self) -> gio::Settings {
|
pub(crate) fn settings(&self) -> gio::Settings {
|
||||||
self.imp().settings.clone()
|
self.imp().settings.clone()
|
||||||
}
|
}
|
||||||
+
|
+
|
||||||
+ /// Show an image viewer overlay covering the whole window content area
|
|
||||||
+ /// for `attachment`. Removes itself from the overlay when the viewer
|
|
||||||
+ /// emits `closed` (on `Escape`, click anywhere inside the viewer, or
|
|
||||||
+ /// the close button).
|
|
||||||
+ pub fn present_image_viewer(&self, attachment: &crate::backend::Attachment) {
|
+ pub fn present_image_viewer(&self, attachment: &crate::backend::Attachment) {
|
||||||
+ use crate::gui::image_viewer::ImageViewer;
|
+ let viewer = crate::gui::image_viewer::ImageViewer::new(attachment);
|
||||||
+ let viewer = ImageViewer::new(attachment);
|
+ let dialog = adw::Dialog::new();
|
||||||
+ let overlay = self.imp().image_viewer_overlay.clone();
|
+ dialog.set_child(Some(&viewer));
|
||||||
+ overlay.add_overlay(&viewer);
|
+ dialog.present(Some(self.upcast_ref::<gtk::Widget>()));
|
||||||
+ viewer.grab_focus();
|
|
||||||
+ viewer.connect_local(
|
|
||||||
+ "closed",
|
|
||||||
+ false,
|
|
||||||
+ clone!(
|
|
||||||
+ #[weak]
|
|
||||||
+ overlay,
|
|
||||||
+ #[weak]
|
|
||||||
+ viewer,
|
|
||||||
+ #[upgrade_or_default]
|
|
||||||
+ move |_| {
|
|
||||||
+ overlay.remove_overlay(&viewer);
|
|
||||||
+ None
|
|
||||||
+ }
|
|
||||||
+ ),
|
|
||||||
+ );
|
|
||||||
+ }
|
+ }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod imp {
|
pub mod imp {
|
||||||
@@ -142,6 +169,8 @@ pub mod imp {
|
|
||||||
#[template_child]
|
|
||||||
split_view: TemplateChild<adw::NavigationSplitView>,
|
|
||||||
#[template_child]
|
|
||||||
+ pub(super) image_viewer_overlay: TemplateChild<gtk::Overlay>,
|
|
||||||
+ #[template_child]
|
|
||||||
pub(super) channel_list: TemplateChild<ChannelList>,
|
|
||||||
#[template_child]
|
|
||||||
subtitle_label: TemplateChild<gtk::Label>,
|
|
||||||
@@ -165,6 +194,7 @@ pub mod imp {
|
|
||||||
channel_list: Default::default(),
|
|
||||||
subtitle_label: Default::default(),
|
|
||||||
channel_messages: Default::default(),
|
|
||||||
+ image_viewer_overlay: Default::default(),
|
|
||||||
new_channel_dialog: Default::default(),
|
|
||||||
manager: Default::default(),
|
|
||||||
settings: Settings::new(BASE_ID),
|
|
||||||
--
|
--
|
||||||
2.53.0
|
2.53.0
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 276 KiB |
Reference in New Issue
Block a user