Skip to main content

fractal/session_view/
content.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::clone};
3
4use super::{Explore, Invite, InviteRequest, RoomHistory};
5use crate::{
6    identity_verification_view::IdentityVerificationView,
7    session::{
8        IdentityVerification, Room, RoomCategory, Session, SidebarIconItem, SidebarIconItemType,
9    },
10    utils::BoundObject,
11};
12
13/// A page of the content stack.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString, strum::AsRefStr)]
15#[strum(serialize_all = "kebab-case")]
16enum ContentPage {
17    /// The placeholder page when no content is presented.
18    Empty,
19    /// The history of the selected room.
20    RoomHistory,
21    /// The selected invite request.
22    InviteRequest,
23    /// The selected room invite.
24    Invite,
25    /// The explore page.
26    Explore,
27    /// The selected identity verification.
28    Verification,
29}
30
31mod imp {
32    use std::cell::{Cell, RefCell};
33
34    use glib::subclass::InitializingObject;
35
36    use super::*;
37
38    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
39    #[template(resource = "/org/gnome/Fractal/ui/session_view/content.ui")]
40    #[properties(wrapper_type = super::Content)]
41    pub struct Content {
42        #[template_child]
43        stack: TemplateChild<gtk::Stack>,
44        #[template_child]
45        room_history: TemplateChild<RoomHistory>,
46        #[template_child]
47        invite_request: TemplateChild<InviteRequest>,
48        #[template_child]
49        invite: TemplateChild<Invite>,
50        #[template_child]
51        explore: TemplateChild<Explore>,
52        #[template_child]
53        empty_page: TemplateChild<adw::ToolbarView>,
54        #[template_child]
55        empty_page_header_bar: TemplateChild<adw::HeaderBar>,
56        #[template_child]
57        verification_page: TemplateChild<adw::ToolbarView>,
58        #[template_child]
59        verification_page_header_bar: TemplateChild<adw::HeaderBar>,
60        #[template_child]
61        identity_verification_widget: TemplateChild<IdentityVerificationView>,
62        /// The current session.
63        #[property(get, set = Self::set_session, explicit_notify, nullable)]
64        session: glib::WeakRef<Session>,
65        /// Whether this is the only visible view, i.e. there is no sidebar.
66        #[property(get, set)]
67        only_view: Cell<bool>,
68        item_binding: RefCell<Option<glib::Binding>>,
69        /// The item currently displayed.
70        #[property(get, set = Self::set_item, explicit_notify, nullable)]
71        item: BoundObject<glib::Object>,
72    }
73
74    #[glib::object_subclass]
75    impl ObjectSubclass for Content {
76        const NAME: &'static str = "Content";
77        type Type = super::Content;
78        type ParentType = adw::NavigationPage;
79
80        fn class_init(klass: &mut Self::Class) {
81            Self::bind_template(klass);
82
83            klass.set_accessible_role(gtk::AccessibleRole::Group);
84        }
85
86        fn instance_init(obj: &InitializingObject<Self>) {
87            obj.init_template();
88        }
89    }
90
91    #[glib::derived_properties]
92    impl ObjectImpl for Content {
93        fn constructed(&self) {
94            self.parent_constructed();
95
96            self.stack.connect_visible_child_notify(clone!(
97                #[weak(rename_to = imp)]
98                self,
99                move |_| {
100                    if imp.visible_page() != ContentPage::Verification {
101                        imp.identity_verification_widget
102                            .set_verification(None::<IdentityVerification>);
103                    }
104                }
105            ));
106        }
107
108        fn dispose(&self) {
109            if let Some(binding) = self.item_binding.take() {
110                binding.unbind();
111            }
112        }
113    }
114
115    impl WidgetImpl for Content {}
116
117    impl NavigationPageImpl for Content {
118        fn hidden(&self) {
119            self.obj().set_item(None::<glib::Object>);
120        }
121    }
122
123    impl Content {
124        /// The visible page of the content.
125        pub(super) fn visible_page(&self) -> ContentPage {
126            self.stack
127                .visible_child_name()
128                .expect("stack should always have a visible child name")
129                .as_str()
130                .try_into()
131                .expect("stack child name should be convertible to a ContentPage")
132        }
133
134        /// Set the visible page of the content.
135        fn set_visible_page(&self, page: ContentPage) {
136            if self.visible_page() == page {
137                return;
138            }
139
140            self.stack.set_visible_child_name(page.as_ref());
141        }
142
143        /// Set the current session.
144        fn set_session(&self, session: Option<&Session>) {
145            if session == self.session.upgrade().as_ref() {
146                return;
147            }
148            let obj = self.obj();
149
150            if let Some(binding) = self.item_binding.take() {
151                binding.unbind();
152            }
153
154            if let Some(session) = session {
155                let item_binding = session
156                    .sidebar_list_model()
157                    .selection_model()
158                    .bind_property("selected-item", &*obj, "item")
159                    .sync_create()
160                    .bidirectional()
161                    .build();
162
163                self.item_binding.replace(Some(item_binding));
164            }
165
166            self.session.set(session);
167            obj.notify_session();
168        }
169
170        /// Set the item currently displayed.
171        fn set_item(&self, item: Option<glib::Object>) {
172            if self.item.obj() == item {
173                return;
174            }
175
176            self.item.disconnect_signals();
177
178            if let Some(item) = item {
179                let handler = if let Some(room) = item.downcast_ref::<Room>() {
180                    let category_handler = room.connect_category_notify(clone!(
181                        #[weak(rename_to = imp)]
182                        self,
183                        move |_| {
184                            imp.update_visible_child();
185                        }
186                    ));
187
188                    Some(category_handler)
189                } else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
190                    let dismiss_handler = verification.connect_dismiss(clone!(
191                        #[weak(rename_to = imp)]
192                        self,
193                        move |_| {
194                            imp.set_item(None);
195                        }
196                    ));
197
198                    Some(dismiss_handler)
199                } else {
200                    None
201                };
202
203                self.item.set(item, handler.into_iter().collect());
204            }
205
206            self.update_visible_child();
207            self.obj().notify_item();
208
209            if let Some(page) = self.stack.visible_child() {
210                page.grab_focus();
211            }
212        }
213
214        /// Update the visible child according to the current item.
215        fn update_visible_child(&self) {
216            let Some(item) = self.item.obj() else {
217                self.set_visible_page(ContentPage::Empty);
218                return;
219            };
220
221            if let Some(room) = item.downcast_ref::<Room>() {
222                match room.category() {
223                    RoomCategory::Knocked => {
224                        self.invite_request.set_room(Some(room.clone()));
225                        self.set_visible_page(ContentPage::InviteRequest);
226                    }
227                    RoomCategory::Invited => {
228                        self.invite.set_room(Some(room.clone()));
229                        self.set_visible_page(ContentPage::Invite);
230                    }
231                    _ => {
232                        self.room_history.set_timeline(Some(room.live_timeline()));
233                        self.set_visible_page(ContentPage::RoomHistory);
234                    }
235                }
236            } else if item
237                .downcast_ref::<SidebarIconItem>()
238                .is_some_and(|i| i.item_type() == SidebarIconItemType::Explore)
239            {
240                self.set_visible_page(ContentPage::Explore);
241            } else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
242                self.identity_verification_widget
243                    .set_verification(Some(verification.clone()));
244                self.set_visible_page(ContentPage::Verification);
245            }
246        }
247
248        /// Handle a paste action.
249        pub(super) fn handle_paste_action(&self) {
250            if self.visible_page() == ContentPage::RoomHistory {
251                self.room_history.handle_paste_action();
252            }
253        }
254
255        /// All the header bars of the children of the content.
256        pub(super) fn header_bars(&self) -> [&adw::HeaderBar; 6] {
257            [
258                &self.empty_page_header_bar,
259                self.room_history.header_bar(),
260                self.invite_request.header_bar(),
261                self.invite.header_bar(),
262                self.explore.header_bar(),
263                &self.verification_page_header_bar,
264            ]
265        }
266    }
267}
268
269glib::wrapper! {
270    /// A view displaying the selected content in the sidebar.
271    pub struct Content(ObjectSubclass<imp::Content>)
272        @extends gtk::Widget, adw::NavigationPage,
273        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
274}
275
276impl Content {
277    pub fn new(session: &Session) -> Self {
278        glib::Object::builder().property("session", session).build()
279    }
280
281    /// Handle a paste action.
282    pub(crate) fn handle_paste_action(&self) {
283        self.imp().handle_paste_action();
284    }
285
286    /// All the header bars of the children of the content.
287    pub(crate) fn header_bars(&self) -> [&adw::HeaderBar; 6] {
288        self.imp().header_bars()
289    }
290}