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