fractal/session/view/content/room_history/message_toolbar/
attachment_dialog.rs

1use adw::{prelude::*, subclass::prelude::*};
2use futures_channel::oneshot;
3use gtk::{gdk, gio, glib, glib::clone, CompositeTemplate};
4use tracing::error;
5
6use crate::{components::MediaContentViewer, spawn};
7
8mod imp {
9    use std::cell::RefCell;
10
11    use super::*;
12
13    #[derive(Debug, Default, CompositeTemplate)]
14    #[template(
15        resource = "/org/gnome/Fractal/ui/session/view/content/room_history/message_toolbar/attachment_dialog.ui"
16    )]
17    pub struct AttachmentDialog {
18        #[template_child]
19        cancel_button: TemplateChild<gtk::Button>,
20        #[template_child]
21        send_button: TemplateChild<gtk::Button>,
22        #[template_child]
23        media: TemplateChild<MediaContentViewer>,
24        sender: RefCell<Option<oneshot::Sender<gtk::ResponseType>>>,
25    }
26
27    #[glib::object_subclass]
28    impl ObjectSubclass for AttachmentDialog {
29        const NAME: &'static str = "AttachmentDialog";
30        type Type = super::AttachmentDialog;
31        type ParentType = adw::Dialog;
32
33        fn class_init(klass: &mut Self::Class) {
34            Self::bind_template(klass);
35            Self::bind_template_callbacks(klass);
36        }
37
38        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
39            obj.init_template();
40        }
41    }
42
43    impl ObjectImpl for AttachmentDialog {
44        fn constructed(&self) {
45            self.parent_constructed();
46
47            self.set_loading(true);
48        }
49    }
50
51    impl WidgetImpl for AttachmentDialog {
52        fn grab_focus(&self) -> bool {
53            let loading = !self.send_button.is_sensitive();
54
55            if loading {
56                self.cancel_button.grab_focus()
57            } else {
58                self.send_button.grab_focus()
59            }
60        }
61    }
62
63    impl AdwDialogImpl for AttachmentDialog {
64        fn closed(&self) {
65            self.send_response(gtk::ResponseType::Cancel);
66        }
67    }
68
69    #[gtk::template_callbacks]
70    impl AttachmentDialog {
71        /// Set whether this dialog is loading.
72        fn set_loading(&self, loading: bool) {
73            self.send_button.set_sensitive(!loading);
74            self.grab_focus();
75        }
76
77        /// Sent the given response.
78        fn send_response(&self, response: gtk::ResponseType) {
79            if let Some(sender) = self.sender.take() {
80                if sender.send(response).is_err() {
81                    error!("Could not send attachment dialog response {response:?}");
82                }
83            }
84        }
85
86        /// Set the image to preview.
87        pub(super) fn set_image(&self, image: &gdk::Texture) {
88            self.media.view_image(image);
89            self.set_loading(false);
90        }
91
92        /// Set the file to preview.
93        pub(super) async fn set_file(&self, file: gio::File) {
94            self.media.view_file(file.into(), None).await;
95            self.set_loading(false);
96        }
97
98        /// Set the location to preview.
99        pub(super) fn set_location(&self, geo_uri: &geo_uri::GeoUri) {
100            self.media.view_location(geo_uri);
101            self.set_loading(false);
102        }
103
104        /// Emit the signal that the user wants to send the attachment.
105        #[template_callback]
106        fn send(&self) {
107            self.send_response(gtk::ResponseType::Ok);
108            self.obj().close();
109        }
110
111        /// Present the dialog and wait for the user to select a response.
112        ///
113        /// The response is [`gtk::ResponseType::Ok`] if the user clicked on
114        /// send, otherwise it is [`gtk::ResponseType::Cancel`].
115        pub(super) async fn response_future(&self, parent: &gtk::Widget) -> gtk::ResponseType {
116            let (sender, receiver) = oneshot::channel();
117            self.sender.replace(Some(sender));
118
119            self.obj().present(Some(parent));
120
121            receiver.await.unwrap_or(gtk::ResponseType::Cancel)
122        }
123    }
124}
125
126glib::wrapper! {
127    /// A dialog to preview an attachment before sending it.
128    pub struct AttachmentDialog(ObjectSubclass<imp::AttachmentDialog>)
129        @extends gtk::Widget, adw::Dialog;
130}
131
132impl AttachmentDialog {
133    /// Create an attachment dialog with the given title.
134    ///
135    /// Its initial state is loading.
136    pub fn new(title: &str) -> Self {
137        glib::Object::builder().property("title", title).build()
138    }
139
140    /// Set the image to preview.
141    pub(crate) fn set_image(&self, image: &gdk::Texture) {
142        self.imp().set_image(image);
143    }
144
145    /// Set the file to preview.
146    pub(crate) fn set_file(&self, file: gio::File) {
147        let imp = self.imp();
148
149        spawn!(clone!(
150            #[weak]
151            imp,
152            async move {
153                imp.set_file(file).await;
154            }
155        ));
156    }
157
158    /// Create an attachment dialog to preview and send a location.
159    pub(crate) fn set_location(&self, geo_uri: &geo_uri::GeoUri) {
160        self.imp().set_location(geo_uri);
161    }
162
163    /// Present the dialog and wait for the user to select a response.
164    ///
165    /// The response is [`gtk::ResponseType::Ok`] if the user clicked on send,
166    /// otherwise it is [`gtk::ResponseType::Cancel`].
167    pub(crate) async fn response_future(
168        &self,
169        parent: &impl IsA<gtk::Widget>,
170    ) -> gtk::ResponseType {
171        self.imp().response_future(parent.upcast_ref()).await
172    }
173}