491 lines
16 KiB
Rust
491 lines
16 KiB
Rust
use std::cell::Cell;
|
|
|
|
use adw::subclass::prelude::AdwApplicationWindowImpl;
|
|
use gettextrs::gettext;
|
|
use gtk::{self, gdk, gio, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
|
|
use ruma::{RoomId, UserId};
|
|
use tracing::{error, warn};
|
|
|
|
use crate::{
|
|
account_switcher::{AccountSwitcherButton, AccountSwitcherPopover},
|
|
components::Spinner,
|
|
error_page::ErrorPage,
|
|
greeter::Greeter,
|
|
login::Login,
|
|
prelude::*,
|
|
session::{
|
|
model::{Session, SessionState},
|
|
view::{AccountSettings, SessionView},
|
|
},
|
|
session_list::{FailedSession, SessionInfo},
|
|
toast,
|
|
utils::LoadingState,
|
|
Application, APP_ID, PROFILE,
|
|
};
|
|
|
|
mod imp {
|
|
use glib::subclass::InitializingObject;
|
|
|
|
use super::*;
|
|
|
|
#[derive(Debug, CompositeTemplate, Default, glib::Properties)]
|
|
#[template(resource = "/org/gnome/Fractal/ui/window.ui")]
|
|
#[properties(wrapper_type = super::Window)]
|
|
pub struct Window {
|
|
#[template_child]
|
|
pub main_stack: TemplateChild<gtk::Stack>,
|
|
#[template_child]
|
|
pub loading: TemplateChild<gtk::WindowHandle>,
|
|
#[template_child]
|
|
pub greeter: TemplateChild<Greeter>,
|
|
#[template_child]
|
|
pub login: TemplateChild<Login>,
|
|
#[template_child]
|
|
pub error_page: TemplateChild<ErrorPage>,
|
|
#[template_child]
|
|
pub session: TemplateChild<SessionView>,
|
|
#[template_child]
|
|
pub toast_overlay: TemplateChild<adw::ToastOverlay>,
|
|
#[template_child]
|
|
pub offline_banner: TemplateChild<adw::Banner>,
|
|
#[template_child]
|
|
pub spinner: TemplateChild<Spinner>,
|
|
/// Whether the window should be in compact view.
|
|
///
|
|
/// It means that the horizontal size is not large enough to hold all
|
|
/// the content.
|
|
#[property(get, set = Self::set_compact, explicit_notify)]
|
|
pub compact: Cell<bool>,
|
|
/// The selection of the logged-in sessions.
|
|
///
|
|
/// The one that is selected being the one that is visible.
|
|
#[property(get)]
|
|
pub session_selection: gtk::SingleSelection,
|
|
pub account_switcher: AccountSwitcherPopover,
|
|
}
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for Window {
|
|
const NAME: &'static str = "Window";
|
|
type Type = super::Window;
|
|
type ParentType = adw::ApplicationWindow;
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
|
AccountSwitcherButton::static_type();
|
|
|
|
Self::bind_template(klass);
|
|
|
|
klass.add_binding_action(
|
|
gdk::Key::v,
|
|
gdk::ModifierType::CONTROL_MASK,
|
|
"win.paste",
|
|
None,
|
|
);
|
|
klass.add_binding_action(
|
|
gdk::Key::Insert,
|
|
gdk::ModifierType::SHIFT_MASK,
|
|
"win.paste",
|
|
None,
|
|
);
|
|
klass.install_action("win.paste", None, move |obj, _, _| {
|
|
obj.imp().session.handle_paste_action();
|
|
});
|
|
|
|
klass.install_action(
|
|
"win.open-account-settings",
|
|
Some("s"),
|
|
move |obj, _, variant| {
|
|
if let Some(session_id) = variant.and_then(|v| v.get::<String>()) {
|
|
obj.open_account_settings(&session_id);
|
|
}
|
|
},
|
|
);
|
|
|
|
klass.install_action("win.new-session", None, |obj, _, _| {
|
|
obj.switch_to_greeter_page();
|
|
});
|
|
klass.install_action("win.show-login", None, |obj, _, _| {
|
|
obj.switch_to_login_page();
|
|
});
|
|
klass.install_action("win.show-session", None, |obj, _, _| {
|
|
obj.show_selected_session();
|
|
});
|
|
|
|
klass.install_action("win.toggle-fullscreen", None, |obj, _, _| {
|
|
if obj.is_fullscreened() {
|
|
obj.unfullscreen();
|
|
} else {
|
|
obj.fullscreen();
|
|
}
|
|
});
|
|
}
|
|
|
|
fn instance_init(obj: &InitializingObject<Self>) {
|
|
obj.init_template();
|
|
}
|
|
}
|
|
|
|
#[glib::derived_properties]
|
|
impl ObjectImpl for Window {
|
|
fn constructed(&self) {
|
|
self.parent_constructed();
|
|
let obj = self.obj();
|
|
|
|
let builder = gtk::Builder::from_resource("/org/gnome/Fractal/ui/shortcuts.ui");
|
|
let shortcuts = builder.object("shortcuts").unwrap();
|
|
obj.set_help_overlay(Some(&shortcuts));
|
|
|
|
// Development Profile
|
|
if PROFILE.should_use_devel_class() {
|
|
obj.add_css_class("devel");
|
|
}
|
|
|
|
obj.load_window_size();
|
|
|
|
self.main_stack.connect_visible_child_notify(
|
|
clone!(@weak obj => move |_| obj.set_default_by_child()),
|
|
);
|
|
obj.set_default_by_child();
|
|
|
|
self.account_switcher
|
|
.set_session_selection(Some(self.session_selection.clone()));
|
|
|
|
self.session_selection
|
|
.connect_selected_item_notify(clone!(@weak obj => move |_| {
|
|
obj.show_selected_session();
|
|
}));
|
|
self.session_selection.connect_items_changed(
|
|
clone!(@weak obj => move |session_selection, pos, removed, added| {
|
|
let n_items = session_selection.n_items();
|
|
obj.action_set_enabled("win.show-session", n_items > 0);
|
|
|
|
if removed > 0 && n_items == 0 {
|
|
obj.switch_to_greeter_page();
|
|
return;
|
|
}
|
|
|
|
if added == 0 {
|
|
return;
|
|
}
|
|
|
|
for i in pos..pos + added {
|
|
let Some(session) = session_selection.item(i).and_downcast::<SessionInfo>() else {
|
|
continue;
|
|
};
|
|
|
|
if let Some(failed) = session.downcast_ref::<FailedSession>() {
|
|
toast!(obj, failed.error().to_user_facing());
|
|
}
|
|
|
|
let settings = Application::default().settings();
|
|
if session.session_id() == settings.string("current-session")
|
|
{
|
|
session_selection.set_selected(i);
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
|
|
let app = Application::default();
|
|
let session_list = app.session_list();
|
|
|
|
self.session_selection.set_model(Some(session_list));
|
|
|
|
if session_list.state() == LoadingState::Ready {
|
|
if session_list.is_empty() {
|
|
obj.switch_to_greeter_page();
|
|
}
|
|
} else {
|
|
session_list.connect_notify_local(
|
|
Some("state"),
|
|
clone!(@weak obj => move |session_list, _| {
|
|
if session_list.state() == LoadingState::Ready && session_list.is_empty() {
|
|
obj.switch_to_greeter_page();
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
let monitor = gio::NetworkMonitor::default();
|
|
monitor.connect_network_changed(clone!(@weak obj => move |_, _| {
|
|
obj.update_network_state();
|
|
}));
|
|
|
|
obj.update_network_state();
|
|
}
|
|
}
|
|
|
|
impl WindowImpl for Window {
|
|
// save window state on delete event
|
|
fn close_request(&self) -> glib::Propagation {
|
|
let obj = self.obj();
|
|
|
|
if let Err(error) = obj.save_window_size() {
|
|
warn!("Failed to save window state: {error}");
|
|
}
|
|
if let Err(error) = obj.save_current_visible_session() {
|
|
warn!("Failed to save current session: {error}");
|
|
}
|
|
|
|
glib::Propagation::Proceed
|
|
}
|
|
}
|
|
|
|
impl WidgetImpl for Window {}
|
|
impl ApplicationWindowImpl for Window {}
|
|
impl AdwApplicationWindowImpl for Window {}
|
|
|
|
impl Window {
|
|
/// Set whether the window should be in compact view.
|
|
fn set_compact(&self, compact: bool) {
|
|
if compact == self.compact.get() {
|
|
return;
|
|
}
|
|
|
|
self.compact.set(compact);
|
|
self.obj().notify_compact();
|
|
}
|
|
}
|
|
}
|
|
|
|
glib::wrapper! {
|
|
/// The main window.
|
|
pub struct Window(ObjectSubclass<imp::Window>)
|
|
@extends gtk::Widget, gtk::Window, gtk::Root, gtk::ApplicationWindow, adw::ApplicationWindow, @implements gtk::Accessible, gio::ActionMap, gio::ActionGroup;
|
|
}
|
|
|
|
impl Window {
|
|
pub fn new(app: &Application) -> Self {
|
|
glib::Object::builder()
|
|
.property("application", Some(app))
|
|
.property("icon-name", Some(APP_ID))
|
|
.build()
|
|
}
|
|
|
|
/// Add the given session to the session list and select it.
|
|
pub fn add_session(&self, session: Session) {
|
|
let index = Application::default().session_list().insert(session);
|
|
self.session_selection().set_selected(index as u32);
|
|
}
|
|
|
|
/// The ID of the currently visible session, if any.
|
|
pub fn current_session_id(&self) -> Option<String> {
|
|
Some(
|
|
self.imp()
|
|
.session_selection
|
|
.selected_item()
|
|
.and_downcast::<SessionInfo>()?
|
|
.session_id()
|
|
.to_owned(),
|
|
)
|
|
}
|
|
|
|
/// Set the current session by its ID.
|
|
///
|
|
/// Returns `true` if the session was set as the current session.
|
|
pub fn set_current_session_by_id(&self, session_id: &str) -> bool {
|
|
let imp = self.imp();
|
|
|
|
let Some(index) = Application::default().session_list().index(session_id) else {
|
|
return false;
|
|
};
|
|
|
|
let index = index as u32;
|
|
let prev_selected = imp.session_selection.selected();
|
|
|
|
if index == prev_selected {
|
|
// Make sure the session is displayed;
|
|
self.show_selected_session();
|
|
} else {
|
|
imp.session_selection.set_selected(index);
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Show the selected session.
|
|
///
|
|
/// The displayed view will change according to the current session.
|
|
pub fn show_selected_session(&self) {
|
|
let imp = self.imp();
|
|
|
|
let Some(session) = imp
|
|
.session_selection
|
|
.selected_item()
|
|
.and_downcast::<SessionInfo>()
|
|
else {
|
|
return;
|
|
};
|
|
|
|
if let Some(session) = session.downcast_ref::<Session>() {
|
|
imp.session.set_session(Some(session));
|
|
|
|
if session.state() == SessionState::Ready {
|
|
imp.main_stack.set_visible_child(&*imp.session);
|
|
} else {
|
|
session.connect_ready(clone!(@weak imp => move |_| {
|
|
imp.main_stack.set_visible_child(&*imp.session);
|
|
}));
|
|
self.switch_to_loading_page();
|
|
}
|
|
|
|
// We need to grab the focus so that keyboard shortcuts work.
|
|
imp.session.grab_focus();
|
|
|
|
return;
|
|
}
|
|
|
|
if let Some(failed) = session.downcast_ref::<FailedSession>() {
|
|
imp.error_page
|
|
.display_session_error(&failed.error().to_user_facing());
|
|
imp.main_stack.set_visible_child(&*imp.error_page);
|
|
} else {
|
|
self.switch_to_loading_page();
|
|
}
|
|
|
|
imp.session.set_session(None::<Session>);
|
|
}
|
|
|
|
pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
|
|
let settings = Application::default().settings();
|
|
|
|
let size = self.default_size();
|
|
|
|
settings.set_int("window-width", size.0)?;
|
|
settings.set_int("window-height", size.1)?;
|
|
|
|
settings.set_boolean("is-maximized", self.is_maximized())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn load_window_size(&self) {
|
|
let settings = Application::default().settings();
|
|
|
|
let width = settings.int("window-width");
|
|
let height = settings.int("window-height");
|
|
let is_maximized = settings.boolean("is-maximized");
|
|
|
|
self.set_default_size(width, height);
|
|
self.set_property("maximized", is_maximized);
|
|
}
|
|
|
|
/// Change the default widget of the window based on the visible child.
|
|
///
|
|
/// These are the default widgets:
|
|
/// - `Greeter` screen => `Login` button.
|
|
fn set_default_by_child(&self) {
|
|
let imp = self.imp();
|
|
|
|
if imp.main_stack.visible_child() == Some(imp.greeter.get().upcast()) {
|
|
self.set_default_widget(Some(&imp.greeter.default_widget()));
|
|
} else {
|
|
self.set_default_widget(gtk::Widget::NONE);
|
|
}
|
|
}
|
|
|
|
pub fn switch_to_loading_page(&self) {
|
|
let imp = self.imp();
|
|
imp.main_stack.set_visible_child(&*imp.loading);
|
|
}
|
|
|
|
pub fn switch_to_login_page(&self) {
|
|
let imp = self.imp();
|
|
imp.main_stack.set_visible_child(&*imp.login);
|
|
imp.login.focus_default();
|
|
}
|
|
|
|
pub fn switch_to_greeter_page(&self) {
|
|
let imp = self.imp();
|
|
imp.main_stack.set_visible_child(&*imp.greeter);
|
|
}
|
|
|
|
/// This appends a new toast to the list
|
|
pub fn add_toast(&self, toast: adw::Toast) {
|
|
self.imp().toast_overlay.add_toast(toast);
|
|
}
|
|
|
|
pub fn account_switcher(&self) -> &AccountSwitcherPopover {
|
|
&self.imp().account_switcher
|
|
}
|
|
|
|
/// The `SessionView` of this window.
|
|
pub fn session_view(&self) -> &SessionView {
|
|
&self.imp().session
|
|
}
|
|
|
|
fn update_network_state(&self) {
|
|
let imp = self.imp();
|
|
let monitor = gio::NetworkMonitor::default();
|
|
|
|
let is_network_available = monitor.is_network_available();
|
|
self.action_set_enabled("win.show-login", is_network_available);
|
|
|
|
if !is_network_available {
|
|
imp.offline_banner
|
|
.set_title(&gettext("No network connection"));
|
|
imp.offline_banner.set_revealed(true);
|
|
} else if monitor.connectivity() < gio::NetworkConnectivity::Full {
|
|
imp.offline_banner
|
|
.set_title(&gettext("No Internet connection"));
|
|
imp.offline_banner.set_revealed(true);
|
|
} else {
|
|
imp.offline_banner.set_revealed(false);
|
|
}
|
|
}
|
|
|
|
/// Show the given room for the given session.
|
|
pub fn show_room(&self, session_id: &str, room_id: &RoomId) {
|
|
if self.set_current_session_by_id(session_id) {
|
|
self.imp().session.select_room_by_id(room_id);
|
|
|
|
self.present();
|
|
}
|
|
}
|
|
|
|
pub fn save_current_visible_session(&self) -> Result<(), glib::BoolError> {
|
|
let settings = Application::default().settings();
|
|
|
|
settings.set_string(
|
|
"current-session",
|
|
self.current_session_id().unwrap_or_default().as_str(),
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Open the account settings for the session with the given ID.
|
|
pub fn open_account_settings(&self, session_id: &str) {
|
|
let Some(session) = Application::default()
|
|
.session_list()
|
|
.get(session_id)
|
|
.and_downcast::<Session>()
|
|
else {
|
|
error!("Tried to open account settings of unknown session with ID '{session_id}'");
|
|
return;
|
|
};
|
|
|
|
let window = AccountSettings::new(Some(self), &session);
|
|
window.present();
|
|
}
|
|
|
|
/// Open the error page and display the given secret error message.
|
|
pub fn show_secret_error(&self, message: &str) {
|
|
let imp = self.imp();
|
|
imp.error_page.display_secret_error(message);
|
|
imp.main_stack.set_visible_child(&*imp.error_page);
|
|
}
|
|
|
|
/// Show the verification with the given flow ID for the user with the given
|
|
/// ID for the given session.
|
|
pub fn show_verification(&self, session_id: &str, user_id: &UserId, flow_id: &str) {
|
|
if self.set_current_session_by_id(session_id) {
|
|
self.imp()
|
|
.session
|
|
.select_verification_by_id(user_id, flow_id);
|
|
|
|
self.present();
|
|
}
|
|
}
|
|
}
|