1use std::cell::Cell;
2
3use adw::{prelude::*, subclass::prelude::*};
4use gtk::{gdk, gio, glib, glib::clone};
5use tracing::{error, warn};
6
7use crate::{
8 APP_ID, Application, PROFILE, SETTINGS_KEY_CURRENT_SESSION,
9 account_chooser_dialog::AccountChooserDialog,
10 account_settings::AccountSettings,
11 account_switcher::{AccountSwitcherButton, AccountSwitcherPopover},
12 components::OfflineBanner,
13 error_page::ErrorPage,
14 intent::SessionIntent,
15 login::Login,
16 prelude::*,
17 secret::SESSION_ID_LENGTH,
18 session::{Session, SessionState},
19 session_list::{FailedSession, SessionInfo},
20 session_view::SessionView,
21 toast,
22 utils::{FixedSelection, LoadingState},
23};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum WindowPage {
28 Loading,
30 Login,
32 Session,
34 Error,
36}
37
38impl WindowPage {
39 const fn name(self) -> &'static str {
41 match self {
42 Self::Loading => "loading",
43 Self::Login => "login",
44 Self::Session => "session",
45 Self::Error => "error",
46 }
47 }
48
49 fn from_name(name: &str) -> Self {
53 match name {
54 "loading" => Self::Loading,
55 "login" => Self::Login,
56 "session" => Self::Session,
57 "error" => Self::Error,
58 _ => panic!("Unknown WindowPage: {name}"),
59 }
60 }
61}
62
63mod imp {
64 use std::{cell::RefCell, rc::Rc};
65
66 use glib::subclass::InitializingObject;
67
68 use super::*;
69
70 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
71 #[template(resource = "/org/gnome/Fractal/ui/window.ui")]
72 #[properties(wrapper_type = super::Window)]
73 pub struct Window {
74 #[template_child]
75 main_stack: TemplateChild<gtk::Stack>,
76 #[template_child]
77 loading: TemplateChild<gtk::WindowHandle>,
78 #[template_child]
79 login: TemplateChild<Login>,
80 #[template_child]
81 error_page: TemplateChild<ErrorPage>,
82 #[template_child]
83 pub(super) session_view: TemplateChild<SessionView>,
84 #[template_child]
85 toast_overlay: TemplateChild<adw::ToastOverlay>,
86 #[property(get, set = Self::set_compact, explicit_notify)]
91 compact: Cell<bool>,
92 #[property(get)]
96 session_selection: FixedSelection,
97 pub(super) account_switcher: AccountSwitcherPopover,
99 }
100
101 #[glib::object_subclass]
102 impl ObjectSubclass for Window {
103 const NAME: &'static str = "Window";
104 type Type = super::Window;
105 type ParentType = adw::ApplicationWindow;
106
107 fn class_init(klass: &mut Self::Class) {
108 AccountSwitcherButton::ensure_type();
109 OfflineBanner::ensure_type();
110
111 Self::bind_template(klass);
112
113 klass.add_binding_action(gdk::Key::v, gdk::ModifierType::CONTROL_MASK, "win.paste");
114 klass.add_binding_action(gdk::Key::Insert, gdk::ModifierType::SHIFT_MASK, "win.paste");
115 klass.install_action("win.paste", None, |obj, _, _| {
116 obj.imp().session_view.handle_paste_action();
117 });
118
119 klass.install_action(
120 "win.open-account-settings",
121 Some(&String::static_variant_type()),
122 |obj, _, variant| {
123 if let Some(session_id) = variant.and_then(glib::Variant::get::<String>) {
124 obj.imp().open_account_settings(&session_id);
125 }
126 },
127 );
128
129 klass.install_action("win.new-session", None, |obj, _, _| {
130 obj.imp().set_visible_page(WindowPage::Login);
131 });
132 klass.install_action("win.show-session", None, |obj, _, _| {
133 obj.imp().show_session();
134 });
135
136 klass.install_action("win.toggle-fullscreen", None, |obj, _, _| {
137 if obj.is_fullscreen() {
138 obj.unfullscreen();
139 } else {
140 obj.fullscreen();
141 }
142 });
143 }
144
145 fn instance_init(obj: &InitializingObject<Self>) {
146 obj.init_template();
147 }
148 }
149
150 #[glib::derived_properties]
151 impl ObjectImpl for Window {
152 fn constructed(&self) {
153 self.parent_constructed();
154
155 if PROFILE.should_use_devel_class() {
157 self.obj().add_css_class("devel");
158 }
159
160 self.load_window_size();
161
162 self.main_stack.connect_transition_running_notify(clone!(
163 #[weak(rename_to = imp)]
164 self,
165 move |stack| if !stack.is_transition_running() {
166 imp.grab_focus();
168 }
169 ));
170
171 self.account_switcher
172 .set_session_selection(Some(self.session_selection.clone()));
173
174 self.session_selection.set_item_equivalence_fn(|lhs, rhs| {
175 let lhs = lhs
176 .downcast_ref::<SessionInfo>()
177 .expect("session selection item should be a SessionInfo");
178 let rhs = rhs
179 .downcast_ref::<SessionInfo>()
180 .expect("session selection item should be a SessionInfo");
181
182 lhs.session_id() == rhs.session_id()
183 });
184 self.session_selection.connect_selected_item_notify(clone!(
185 #[weak(rename_to = imp)]
186 self,
187 move |_| {
188 imp.update_selected_session();
189 }
190 ));
191 self.session_selection.connect_is_empty_notify(clone!(
192 #[weak(rename_to = imp)]
193 self,
194 move |session_selection| {
195 imp.obj()
196 .action_set_enabled("win.show-session", !session_selection.is_empty());
197 }
198 ));
199
200 let app = Application::default();
201 let session_list = app.session_list();
202
203 self.session_selection.set_model(Some(session_list.clone()));
204
205 if session_list.state() == LoadingState::Ready {
206 self.finish_session_selection_init();
207 } else {
208 session_list.connect_state_notify(clone!(
209 #[weak(rename_to = imp)]
210 self,
211 move |session_list| {
212 if session_list.state() == LoadingState::Ready {
213 imp.finish_session_selection_init();
214 }
215 }
216 ));
217 }
218 }
219 }
220
221 impl WindowImpl for Window {
222 fn close_request(&self) -> glib::Propagation {
223 if let Err(error) = self.save_window_size() {
224 warn!("Could not save window state: {error}");
225 }
226 if let Err(error) = self.save_current_visible_session() {
227 warn!("Could not save current session: {error}");
228 }
229
230 glib::Propagation::Proceed
231 }
232 }
233
234 impl WidgetImpl for Window {
235 fn grab_focus(&self) -> bool {
236 match self.visible_page() {
237 WindowPage::Loading => false,
238 WindowPage::Login => self.login.grab_focus(),
239 WindowPage::Session => self.session_view.grab_focus(),
240 WindowPage::Error => self.error_page.grab_focus(),
241 }
242 }
243 }
244
245 impl ApplicationWindowImpl for Window {}
246 impl AdwApplicationWindowImpl for Window {}
247
248 impl Window {
249 fn set_compact(&self, compact: bool) {
251 if compact == self.compact.get() {
252 return;
253 }
254
255 self.compact.set(compact);
256 self.obj().notify_compact();
257 }
258
259 fn finish_session_selection_init(&self) {
262 for item in self.session_selection.iter::<glib::Object>() {
263 if let Some(failed) = item.ok().and_downcast_ref::<FailedSession>() {
264 toast!(self.obj(), failed.error().to_user_facing());
265 }
266 }
267
268 self.restore_current_visible_session();
269
270 self.session_selection.connect_selected_notify(clone!(
271 #[weak(rename_to = imp)]
272 self,
273 move |session_selection| {
274 if session_selection.selected() == gtk::INVALID_LIST_POSITION {
275 imp.select_first_session();
276 }
277 }
278 ));
279
280 if self.session_selection.selected() == gtk::INVALID_LIST_POSITION {
281 self.select_first_session();
282 }
283 }
284
285 fn select_first_session(&self) {
289 let selected_session = self.session_selection.item(0);
291
292 if selected_session.is_none() {
293 self.set_visible_page(WindowPage::Login);
295 }
296
297 self.session_selection.set_selected_item(selected_session);
298 }
299
300 fn load_window_size(&self) {
302 let obj = self.obj();
303 let settings = Application::default().settings();
304
305 let width = settings.int("window-width");
306 let height = settings.int("window-height");
307 let is_maximized = settings.boolean("is-maximized");
308
309 obj.set_default_size(width, height);
310 obj.set_maximized(is_maximized);
311 }
312
313 fn save_window_size(&self) -> Result<(), glib::BoolError> {
315 let obj = self.obj();
316 let settings = Application::default().settings();
317
318 let size = obj.default_size();
319 settings.set_int("window-width", size.0)?;
320 settings.set_int("window-height", size.1)?;
321
322 settings.set_boolean("is-maximized", obj.is_maximized())?;
323
324 Ok(())
325 }
326
327 fn restore_current_visible_session(&self) {
329 let settings = Application::default().settings();
330 let mut current_session_setting =
331 settings.string(SETTINGS_KEY_CURRENT_SESSION).to_string();
332
333 if current_session_setting.len() > SESSION_ID_LENGTH {
335 current_session_setting.truncate(SESSION_ID_LENGTH);
336
337 if let Err(error) =
338 settings.set_string(SETTINGS_KEY_CURRENT_SESSION, ¤t_session_setting)
339 {
340 warn!("Could not save current session: {error}");
341 }
342 }
343
344 if let Some(session) = Application::default()
345 .session_list()
346 .get(¤t_session_setting)
347 {
348 self.session_selection.set_selected_item(Some(session));
349 }
350 }
351
352 fn save_current_visible_session(&self) -> Result<(), glib::BoolError> {
354 let settings = Application::default().settings();
355
356 settings.set_string(
357 SETTINGS_KEY_CURRENT_SESSION,
358 self.current_session_id().unwrap_or_default().as_str(),
359 )?;
360
361 Ok(())
362 }
363
364 pub(super) fn visible_page(&self) -> WindowPage {
366 WindowPage::from_name(
367 &self
368 .main_stack
369 .visible_child_name()
370 .expect("stack should always have a visible child name"),
371 )
372 }
373
374 pub(super) fn current_session_id(&self) -> Option<String> {
376 self.session_selection
377 .selected_item()
378 .and_downcast::<SessionInfo>()
379 .map(|s| s.session_id())
380 }
381
382 pub(super) fn set_current_session_by_id(&self, session_id: &str) -> bool {
386 let Some(index) = Application::default().session_list().index(session_id) else {
387 return false;
388 };
389
390 let index = index as u32;
391 let prev_selected = self.session_selection.selected();
392
393 if index == prev_selected {
394 self.show_session();
396 } else {
397 self.session_selection.set_selected(index);
398 }
399
400 true
401 }
402
403 fn update_selected_session(&self) {
405 let Some(selected_session) = self
406 .session_selection
407 .selected_item()
408 .and_downcast::<SessionInfo>()
409 else {
410 return;
411 };
412
413 let session = selected_session.downcast_ref::<Session>();
414 self.session_view.set_session(session);
415
416 if matches!(
419 self.visible_page(),
420 WindowPage::Session | WindowPage::Loading
421 ) {
422 self.show_session();
423 }
424 }
425
426 pub(super) fn show_session(&self) {
430 let Some(selected_session) = self
431 .session_selection
432 .selected_item()
433 .and_downcast::<SessionInfo>()
434 else {
435 return;
436 };
437
438 if let Some(session) = selected_session.downcast_ref::<Session>() {
439 if session.state() == SessionState::Ready {
440 self.set_visible_page(WindowPage::Session);
441 } else {
442 let ready_handler_cell: Rc<RefCell<Option<glib::SignalHandlerId>>> =
443 Rc::default();
444 let ready_handler = session.connect_ready(clone!(
445 #[weak(rename_to = imp)]
446 self,
447 #[strong]
448 ready_handler_cell,
449 move |session| {
450 if let Some(handler) = ready_handler_cell.take() {
451 session.disconnect(handler);
452 }
453
454 imp.update_selected_session();
455 }
456 ));
457 ready_handler_cell.replace(Some(ready_handler));
458
459 self.set_visible_page(WindowPage::Loading);
460 }
461
462 self.session_view.grab_focus();
464 } else if let Some(failed) = selected_session.downcast_ref::<FailedSession>() {
465 self.error_page
466 .display_session_error(&failed.error().to_user_facing());
467 self.set_visible_page(WindowPage::Error);
468 } else {
469 self.set_visible_page(WindowPage::Loading);
470 }
471 }
472
473 fn set_visible_page(&self, page: WindowPage) {
475 self.main_stack.set_visible_child_name(page.name());
476 }
477
478 pub(super) fn show_secret_error(&self, message: &str) {
480 self.error_page.display_secret_error(message);
481 self.set_visible_page(WindowPage::Error);
482 }
483
484 pub(super) fn add_toast(&self, toast: adw::Toast) {
486 self.toast_overlay.add_toast(toast);
487 }
488
489 fn open_account_settings(&self, session_id: &str) {
491 let Some(session) = Application::default()
492 .session_list()
493 .get(session_id)
494 .and_downcast::<Session>()
495 else {
496 error!("Tried to open account settings of unknown session with ID '{session_id}'");
497 return;
498 };
499
500 let dialog = AccountSettings::new(&session);
501 dialog.present(Some(&*self.obj()));
502 }
503 }
504}
505
506glib::wrapper! {
507 pub struct Window(ObjectSubclass<imp::Window>)
509 @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
510 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Root, gtk::Native,
511 gtk::ShortcutManager, gio::ActionMap, gio::ActionGroup;
512}
513
514impl Window {
515 pub fn new(app: &Application) -> Self {
516 glib::Object::builder()
517 .property("application", Some(app))
518 .property("icon-name", Some(APP_ID))
519 .build()
520 }
521
522 pub(crate) fn add_session(&self, session: Session) {
524 let index = Application::default().session_list().insert(session);
525 self.session_selection().set_selected(index as u32);
526 self.imp().show_session();
527 }
528
529 pub(crate) fn current_session_id(&self) -> Option<String> {
531 self.imp().current_session_id()
532 }
533
534 pub(crate) fn add_toast(&self, toast: adw::Toast) {
536 self.imp().add_toast(toast);
537 }
538
539 pub(crate) fn account_switcher(&self) -> &AccountSwitcherPopover {
541 &self.imp().account_switcher
542 }
543
544 pub(crate) fn session_view(&self) -> &SessionView {
546 &self.imp().session_view
547 }
548
549 pub(crate) fn show_secret_error(&self, message: &str) {
551 self.imp().show_secret_error(message);
552 }
553
554 pub(crate) async fn ask_session(&self) -> Option<String> {
560 let dialog = AccountChooserDialog::new(Application::default().session_list());
561 dialog.choose_account(self).await
562 }
563
564 pub(crate) fn process_session_intent(&self, session_id: &str, intent: SessionIntent) {
568 if !self.imp().set_current_session_by_id(session_id) {
569 error!("Cannot switch to unknown session with ID `{session_id}`");
570 return;
571 }
572
573 self.session_view().process_intent(intent);
574 }
575}