1use std::cell::Cell;
2
3use adw::{prelude::*, subclass::prelude::*};
4use gtk::{gdk, gio, glib, glib::clone, CompositeTemplate};
5use tracing::{error, warn};
6
7use crate::{
8 account_chooser_dialog::AccountChooserDialog,
9 account_switcher::{AccountSwitcherButton, AccountSwitcherPopover},
10 components::OfflineBanner,
11 error_page::ErrorPage,
12 intent::SessionIntent,
13 login::Login,
14 prelude::*,
15 secret::SESSION_ID_LENGTH,
16 session::{
17 model::{Session, SessionState},
18 view::{AccountSettings, SessionView},
19 },
20 session_list::{FailedSession, SessionInfo},
21 toast,
22 utils::LoadingState,
23 Application, APP_ID, PROFILE, SETTINGS_KEY_CURRENT_SESSION,
24};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString, strum::AsRefStr)]
28#[strum(serialize_all = "kebab-case")]
29enum WindowPage {
30 Loading,
32 Login,
34 Session,
36 Error,
38}
39
40mod imp {
41 use glib::subclass::InitializingObject;
42
43 use super::*;
44
45 #[derive(Debug, CompositeTemplate, Default, glib::Properties)]
46 #[template(resource = "/org/gnome/Fractal/ui/window.ui")]
47 #[properties(wrapper_type = super::Window)]
48 pub struct Window {
49 #[template_child]
50 main_stack: TemplateChild<gtk::Stack>,
51 #[template_child]
52 loading: TemplateChild<gtk::WindowHandle>,
53 #[template_child]
54 login: TemplateChild<Login>,
55 #[template_child]
56 error_page: TemplateChild<ErrorPage>,
57 #[template_child]
58 pub(super) session_view: TemplateChild<SessionView>,
59 #[template_child]
60 toast_overlay: TemplateChild<adw::ToastOverlay>,
61 #[property(get, set = Self::set_compact, explicit_notify)]
66 compact: Cell<bool>,
67 #[property(get)]
71 session_selection: gtk::SingleSelection,
72 pub(super) account_switcher: AccountSwitcherPopover,
74 }
75
76 #[glib::object_subclass]
77 impl ObjectSubclass for Window {
78 const NAME: &'static str = "Window";
79 type Type = super::Window;
80 type ParentType = adw::ApplicationWindow;
81
82 fn class_init(klass: &mut Self::Class) {
83 AccountSwitcherButton::ensure_type();
84 OfflineBanner::ensure_type();
85
86 Self::bind_template(klass);
87
88 klass.add_binding_action(gdk::Key::v, gdk::ModifierType::CONTROL_MASK, "win.paste");
89 klass.add_binding_action(gdk::Key::Insert, gdk::ModifierType::SHIFT_MASK, "win.paste");
90 klass.install_action("win.paste", None, |obj, _, _| {
91 obj.imp().session_view.handle_paste_action();
92 });
93
94 klass.install_action(
95 "win.open-account-settings",
96 Some(&String::static_variant_type()),
97 |obj, _, variant| {
98 if let Some(session_id) = variant.and_then(glib::Variant::get::<String>) {
99 obj.imp().open_account_settings(&session_id);
100 }
101 },
102 );
103
104 klass.install_action("win.new-session", None, |obj, _, _| {
105 obj.imp().set_visible_page(WindowPage::Login);
106 });
107 klass.install_action("win.show-session", None, |obj, _, _| {
108 obj.imp().show_selected_session();
109 });
110
111 klass.install_action("win.toggle-fullscreen", None, |obj, _, _| {
112 if obj.is_fullscreen() {
113 obj.unfullscreen();
114 } else {
115 obj.fullscreen();
116 }
117 });
118 }
119
120 fn instance_init(obj: &InitializingObject<Self>) {
121 obj.init_template();
122 }
123 }
124
125 #[glib::derived_properties]
126 impl ObjectImpl for Window {
127 fn constructed(&self) {
128 self.parent_constructed();
129 let obj = self.obj();
130
131 let builder = gtk::Builder::from_resource("/org/gnome/Fractal/ui/shortcuts.ui");
132 let shortcuts = builder
133 .object("shortcuts")
134 .expect("shortcuts object should exist in UI template");
135 obj.set_help_overlay(Some(&shortcuts));
136
137 if PROFILE.should_use_devel_class() {
139 obj.add_css_class("devel");
140 }
141
142 self.load_window_size();
143
144 self.main_stack.connect_transition_running_notify(clone!(
145 #[weak(rename_to = imp)]
146 self,
147 move |stack| if !stack.is_transition_running() {
148 imp.grab_focus();
150 }
151 ));
152
153 self.account_switcher
154 .set_session_selection(Some(self.session_selection.clone()));
155
156 self.session_selection.connect_selected_item_notify(clone!(
157 #[weak(rename_to = imp)]
158 self,
159 move |_| {
160 imp.show_selected_session();
161 }
162 ));
163 self.session_selection.connect_items_changed(clone!(
164 #[weak(rename_to = imp)]
165 self,
166 move |session_selection, pos, removed, added| {
167 let obj = imp.obj();
168
169 let n_items = session_selection.n_items();
170 obj.action_set_enabled("win.show-session", n_items > 0);
171
172 if removed > 0 && n_items == 0 {
173 imp.set_visible_page(WindowPage::Login);
175 return;
176 }
177
178 if added == 0 {
179 return;
180 }
181
182 let settings = Application::default().settings();
183 let mut current_session_setting =
184 settings.string(SETTINGS_KEY_CURRENT_SESSION).to_string();
185
186 if current_session_setting.len() > SESSION_ID_LENGTH {
188 current_session_setting.truncate(SESSION_ID_LENGTH);
189
190 if let Err(error) = settings
191 .set_string(SETTINGS_KEY_CURRENT_SESSION, ¤t_session_setting)
192 {
193 warn!("Could not save current session: {error}");
194 }
195 }
196
197 for i in pos..pos + added {
198 let Some(session) = session_selection.item(i).and_downcast::<SessionInfo>()
199 else {
200 continue;
201 };
202
203 if let Some(failed) = session.downcast_ref::<FailedSession>() {
204 toast!(obj, failed.error().to_user_facing());
205 }
206
207 if session.session_id() == current_session_setting {
208 session_selection.set_selected(i);
209 }
210 }
211 }
212 ));
213
214 let app = Application::default();
215 let session_list = app.session_list();
216
217 self.session_selection.set_model(Some(session_list));
218
219 if session_list.state() == LoadingState::Ready {
220 if session_list.is_empty() {
221 self.set_visible_page(WindowPage::Login);
222 }
223 } else {
224 session_list.connect_state_notify(clone!(
225 #[weak(rename_to=imp)]
226 self,
227 move |session_list| {
228 if session_list.state() == LoadingState::Ready && session_list.is_empty() {
229 imp.set_visible_page(WindowPage::Login);
230 }
231 }
232 ));
233 }
234 }
235 }
236
237 impl WindowImpl for Window {
238 fn close_request(&self) -> glib::Propagation {
239 if let Err(error) = self.save_window_size() {
240 warn!("Could not save window state: {error}");
241 }
242 if let Err(error) = self.save_current_visible_session() {
243 warn!("Could not save current session: {error}");
244 }
245
246 glib::Propagation::Proceed
247 }
248 }
249
250 impl WidgetImpl for Window {
251 fn grab_focus(&self) -> bool {
252 match self.visible_page() {
253 WindowPage::Loading => false,
254 WindowPage::Login => self.login.grab_focus(),
255 WindowPage::Session => self.session_view.grab_focus(),
256 WindowPage::Error => self.error_page.grab_focus(),
257 }
258 }
259 }
260
261 impl ApplicationWindowImpl for Window {}
262 impl AdwApplicationWindowImpl for Window {}
263
264 impl Window {
265 fn set_compact(&self, compact: bool) {
267 if compact == self.compact.get() {
268 return;
269 }
270
271 self.compact.set(compact);
272 self.obj().notify_compact();
273 }
274
275 fn load_window_size(&self) {
277 let obj = self.obj();
278 let settings = Application::default().settings();
279
280 let width = settings.int("window-width");
281 let height = settings.int("window-height");
282 let is_maximized = settings.boolean("is-maximized");
283
284 obj.set_default_size(width, height);
285 obj.set_maximized(is_maximized);
286 }
287
288 fn save_window_size(&self) -> Result<(), glib::BoolError> {
290 let obj = self.obj();
291 let settings = Application::default().settings();
292
293 let size = obj.default_size();
294 settings.set_int("window-width", size.0)?;
295 settings.set_int("window-height", size.1)?;
296
297 settings.set_boolean("is-maximized", obj.is_maximized())?;
298
299 Ok(())
300 }
301
302 fn save_current_visible_session(&self) -> Result<(), glib::BoolError> {
304 let settings = Application::default().settings();
305
306 settings.set_string(
307 SETTINGS_KEY_CURRENT_SESSION,
308 self.current_session_id().unwrap_or_default().as_str(),
309 )?;
310
311 Ok(())
312 }
313
314 pub(super) fn visible_page(&self) -> WindowPage {
316 self.main_stack
317 .visible_child_name()
318 .expect("stack should always have a visible child name")
319 .as_str()
320 .try_into()
321 .expect("stack child name should be convertible to a WindowPage")
322 }
323
324 pub(super) fn current_session_id(&self) -> Option<String> {
326 self.session_selection
327 .selected_item()
328 .and_downcast::<SessionInfo>()
329 .map(|s| s.session_id())
330 }
331
332 pub(super) fn set_current_session_by_id(&self, session_id: &str) -> bool {
336 let Some(index) = Application::default().session_list().index(session_id) else {
337 return false;
338 };
339
340 let index = index as u32;
341 let prev_selected = self.session_selection.selected();
342
343 if index == prev_selected {
344 self.show_selected_session();
346 } else {
347 self.session_selection.set_selected(index);
348 }
349
350 true
351 }
352
353 fn show_selected_session(&self) {
357 let Some(session) = self
358 .session_selection
359 .selected_item()
360 .and_downcast::<SessionInfo>()
361 else {
362 return;
363 };
364
365 if let Some(session) = session.downcast_ref::<Session>() {
366 self.session_view.set_session(Some(session));
367
368 if session.state() == SessionState::Ready {
369 self.set_visible_page(WindowPage::Session);
370 } else {
371 session.connect_ready(clone!(
372 #[weak(rename_to = imp)]
373 self,
374 move |_| {
375 imp.set_visible_page(WindowPage::Session);
376 }
377 ));
378 self.set_visible_page(WindowPage::Loading);
379 }
380
381 self.session_view.grab_focus();
383
384 return;
385 }
386
387 if let Some(failed) = session.downcast_ref::<FailedSession>() {
388 self.error_page
389 .display_session_error(&failed.error().to_user_facing());
390 self.set_visible_page(WindowPage::Error);
391 } else {
392 self.set_visible_page(WindowPage::Loading);
393 }
394
395 self.session_view.set_session(None::<Session>);
396 }
397
398 fn set_visible_page(&self, name: WindowPage) {
400 self.main_stack.set_visible_child_name(name.as_ref());
401 }
402
403 pub(super) fn show_secret_error(&self, message: &str) {
405 self.error_page.display_secret_error(message);
406 self.set_visible_page(WindowPage::Error);
407 }
408
409 pub(super) fn add_toast(&self, toast: adw::Toast) {
411 self.toast_overlay.add_toast(toast);
412 }
413
414 fn open_account_settings(&self, session_id: &str) {
416 let Some(session) = Application::default()
417 .session_list()
418 .get(session_id)
419 .and_downcast::<Session>()
420 else {
421 error!("Tried to open account settings of unknown session with ID '{session_id}'");
422 return;
423 };
424
425 let dialog = AccountSettings::new(&session);
426 dialog.present(Some(&*self.obj()));
427 }
428 }
429}
430
431glib::wrapper! {
432 pub struct Window(ObjectSubclass<imp::Window>)
434 @extends gtk::Widget, gtk::Window, gtk::Root, gtk::ApplicationWindow, adw::ApplicationWindow,
435 @implements gtk::Accessible, gio::ActionMap, gio::ActionGroup;
436}
437
438impl Window {
439 pub fn new(app: &Application) -> Self {
440 glib::Object::builder()
441 .property("application", Some(app))
442 .property("icon-name", Some(APP_ID))
443 .build()
444 }
445
446 pub(crate) fn add_session(&self, session: Session) {
448 let index = Application::default().session_list().insert(session);
449 self.session_selection().set_selected(index as u32);
450 }
451
452 pub(crate) fn current_session_id(&self) -> Option<String> {
454 self.imp().current_session_id()
455 }
456
457 pub(crate) fn add_toast(&self, toast: adw::Toast) {
459 self.imp().add_toast(toast);
460 }
461
462 pub(crate) fn account_switcher(&self) -> &AccountSwitcherPopover {
464 &self.imp().account_switcher
465 }
466
467 pub(crate) fn session_view(&self) -> &SessionView {
469 &self.imp().session_view
470 }
471
472 pub(crate) fn show_secret_error(&self, message: &str) {
474 self.imp().show_secret_error(message);
475 }
476
477 pub(crate) async fn ask_session(&self) -> Option<String> {
483 let dialog = AccountChooserDialog::new(Application::default().session_list());
484 dialog.choose_account(self).await
485 }
486
487 pub(crate) fn process_session_intent(&self, session_id: &str, intent: SessionIntent) {
491 if !self.imp().set_current_session_by_id(session_id) {
492 error!("Cannot switch to unknown session with ID `{session_id}`");
493 return;
494 }
495
496 self.session_view().process_intent(intent);
497 }
498}