use std::{cell::RefCell, fmt, rc::Rc};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{gio, glib, glib::clone};
use tracing::{debug, error, info, warn};
use crate::{
config, intent,
session::model::{Session, SessionState},
session_list::{FailedSession, SessionInfo, SessionList},
spawn,
system_settings::SystemSettings,
toast,
utils::{matrix::MatrixIdUri, BoundObjectWeakRef, LoadingState},
Window,
};
pub const SETTINGS_KEY_CURRENT_SESSION: &str = "current-session";
mod imp {
use super::*;
#[derive(Debug)]
pub struct Application {
pub settings: gio::Settings,
pub system_settings: SystemSettings,
pub session_list: SessionList,
pub intent_handler: BoundObjectWeakRef<glib::Object>,
}
impl Default for Application {
fn default() -> Self {
Self {
settings: gio::Settings::new(config::APP_ID),
system_settings: Default::default(),
session_list: Default::default(),
intent_handler: Default::default(),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for Application {
const NAME: &'static str = "Application";
type Type = super::Application;
type ParentType = adw::Application;
}
impl ObjectImpl for Application {
fn constructed(&self) {
self.parent_constructed();
let app = self.obj();
app.set_up_gactions();
app.set_up_accels();
self.session_list.connect_error_notify(clone!(
#[weak]
app,
move |session_list| {
if let Some(message) = session_list.error() {
let window = app.present_main_window();
window.show_secret_error(&message);
}
}
));
spawn!(clone!(
#[weak(rename_to = session_list)]
self.session_list,
async move {
session_list.restore_sessions().await;
}
));
}
}
impl ApplicationImpl for Application {
fn activate(&self) {
debug!("Application::activate");
self.obj().present_main_window();
}
fn open(&self, files: &[gio::File], _hint: &str) {
debug!("Application::open");
self.obj().present_main_window();
if files.len() > 1 {
warn!("Trying to open several URIs, only the first one will be processed");
}
if let Some(uri) = files.first().map(|f| f.uri()) {
self.obj().process_uri(&uri);
} else {
debug!("No URI to open");
}
}
}
impl GtkApplicationImpl for Application {}
impl AdwApplicationImpl for Application {}
}
glib::wrapper! {
pub struct Application(ObjectSubclass<imp::Application>)
@extends gio::Application, gtk::Application, adw::Application,
@implements gio::ActionMap, gio::ActionGroup;
}
impl Application {
pub fn new() -> Self {
glib::Object::builder()
.property("application-id", Some(config::APP_ID))
.property("flags", gio::ApplicationFlags::HANDLES_OPEN)
.property("resource-base-path", Some("/org/gnome/Fractal/"))
.build()
}
fn present_main_window(&self) -> Window {
let window = if let Some(window) = self.active_window().and_downcast() {
window
} else {
Window::new(self)
};
window.present();
window
}
pub fn settings(&self) -> gio::Settings {
self.imp().settings.clone()
}
pub fn system_settings(&self) -> SystemSettings {
self.imp().system_settings.clone()
}
pub fn session_list(&self) -> &SessionList {
&self.imp().session_list
}
fn set_up_gactions(&self) {
self.add_action_entries([
gio::ActionEntry::builder("quit")
.activate(|app: &Application, _, _| {
if let Some(window) = app.active_window() {
window.close();
}
app.quit();
})
.build(),
gio::ActionEntry::builder("about")
.activate(|app: &Application, _, _| {
app.show_about_dialog();
})
.build(),
gio::ActionEntry::builder("show-room")
.parameter_type(Some(&intent::ShowRoomPayload::static_variant_type()))
.activate(|app: &Application, _, v| {
let Some(payload) = v.and_then(|v| v.get::<intent::ShowRoomPayload>()) else {
error!("Triggered `show-room` action without the proper payload");
return;
};
app.process_intent(intent::SessionIntent::ShowRoom(payload));
})
.build(),
gio::ActionEntry::builder("show-identity-verification")
.parameter_type(Some(&intent::ShowIdentityVerificationPayload::static_variant_type()))
.activate(|app: &Application, _, v| {
let Some(payload) = v.and_then(|v| v.get::<intent::ShowIdentityVerificationPayload>()) else {
error!("Triggered `show-identity-verification` action without the proper payload");
return;
};
app.process_intent(intent::SessionIntent::ShowIdentityVerification(payload));
})
.build(),
]);
}
fn set_up_accels(&self) {
self.set_accels_for_action("app.quit", &["<Control>q"]);
self.set_accels_for_action("win.show-help-overlay", &["<Control>question"]);
}
fn show_about_dialog(&self) {
let dialog = adw::AboutDialog::builder()
.application_name("Fractal")
.application_icon(config::APP_ID)
.developer_name(gettext("The Fractal Team"))
.license_type(gtk::License::Gpl30)
.website("https://gitlab.gnome.org/World/fractal/")
.issue_url("https://gitlab.gnome.org/World/fractal/-/issues")
.support_url("https://matrix.to/#/#fractal:gnome.org")
.version(config::VERSION)
.copyright(gettext("© 2017-2024 The Fractal Team"))
.developers(vec![
"Alejandro Domínguez".to_string(),
"Alexandre Franke".to_string(),
"Bilal Elmoussaoui".to_string(),
"Christopher Davis".to_string(),
"Daniel García Moreno".to_string(),
"Eisha Chen-yen-su".to_string(),
"Jordan Petridis".to_string(),
"Julian Sparber".to_string(),
"Kévin Commaille".to_string(),
"Saurav Sachidanand".to_string(),
])
.designers(vec!["Tobias Bernard".to_string()])
.translator_credits(gettext("translator-credits"))
.build();
dialog.add_credit_section(Some(&gettext("Name by")), &["Regina Bíró"]);
dialog.connect_activate_link(clone!(
#[weak(rename_to = obj)]
self,
#[weak]
dialog,
#[upgrade_or]
false,
move |_, uri| {
if uri == "https://matrix.to/#/#fractal:gnome.org"
&& obj.session_list().has_session_ready()
{
obj.process_uri(uri);
dialog.close();
return true;
}
false
}
));
dialog.present(Some(&self.present_main_window()));
}
fn process_uri(&self, uri: &str) {
match MatrixIdUri::parse(uri) {
Ok(matrix_id) => self.process_intent(matrix_id),
Err(error) => warn!("Invalid Matrix URI: {error}"),
}
}
fn process_intent(&self, intent: impl Into<intent::AppIntent>) {
let intent = intent.into();
debug!("Processing intent {intent:?}");
self.imp().intent_handler.disconnect_signals();
let session_list = self.session_list();
if session_list.state() == LoadingState::Ready {
match intent {
intent::AppIntent::WithSession(session_intent) => {
self.process_session_intent(session_intent);
}
intent::AppIntent::ShowMatrixId(matrix_uri) => match session_list.n_items() {
0 => {
warn!("Cannot open URI with no logged in session");
}
1 => {
let session = session_list.first().expect("There should be one session");
let session_intent = intent::SessionIntent::with_matrix_uri(
session.session_id(),
matrix_uri,
);
self.process_session_intent(session_intent);
}
_ => {
spawn!(clone!(
#[weak(rename_to = obj)]
self,
async move {
obj.choose_session_for_uri(matrix_uri).await;
}
));
}
},
}
} else {
let cell = Rc::new(RefCell::new(Some(intent)));
let handler = session_list.connect_state_notify(clone!(
#[weak(rename_to = obj)]
self,
#[strong]
cell,
move |session_list| {
if session_list.state() == LoadingState::Ready {
obj.imp().intent_handler.disconnect_signals();
if let Some(intent) = cell.take() {
obj.process_intent(intent);
}
}
}
));
self.imp()
.intent_handler
.set(session_list.upcast_ref(), vec![handler]);
}
}
async fn choose_session_for_uri(&self, matrix_uri: MatrixIdUri) {
let main_window = self.present_main_window();
let Some(session_id) = main_window.choose_session_for_uri().await else {
warn!("No session selected to show URI");
return;
};
let session_intent = intent::SessionIntent::with_matrix_uri(session_id, matrix_uri);
self.process_session_intent(session_intent);
}
fn process_session_intent(&self, intent: intent::SessionIntent) {
let Some(session_info) = self.session_list().get(intent.session_id()) else {
warn!("Could not find session to process intent {intent:?}");
toast!(self.present_main_window(), gettext("Session not found"));
return;
};
if session_info.is::<FailedSession>() {
warn!("Could not process intent {intent:?} for failed session");
} else if let Some(session) = session_info.downcast_ref::<Session>() {
if session.state() == SessionState::Ready {
self.present_main_window()
.process_session_intent_ready(intent);
} else {
let cell = Rc::new(RefCell::new(Some(intent)));
let handler = session.connect_state_notify(clone!(
#[weak(rename_to = obj)]
self,
#[strong]
cell,
move |session| {
if session.state() == SessionState::Ready {
obj.imp().intent_handler.disconnect_signals();
if let Some(intent) = cell.take() {
obj.present_main_window()
.process_session_intent_ready(intent);
}
}
}
));
self.imp()
.intent_handler
.set(session.upcast_ref(), vec![handler]);
}
} else {
let session_list = self.session_list();
let cell = Rc::new(RefCell::new(Some(intent)));
let handler = session_list.connect_items_changed(clone!(
#[weak(rename_to = obj)]
self,
#[strong]
cell,
move |session_list, pos, _, added| {
if added == 0 {
return;
}
let Some(session_id) =
cell.borrow().as_ref().map(|i| i.session_id().to_owned())
else {
return;
};
for i in pos..pos + added {
let Some(session_info) = session_list.item(i).and_downcast::<SessionInfo>()
else {
break;
};
if session_info.session_id() == session_id {
obj.imp().intent_handler.disconnect_signals();
if let Some(intent) = cell.take() {
obj.process_session_intent(intent);
}
break;
}
}
}
));
self.imp()
.intent_handler
.set(session_list.upcast_ref(), vec![handler]);
}
}
pub fn run(&self) {
info!("Fractal ({})", config::APP_ID);
info!("Version: {} ({})", config::VERSION, config::PROFILE);
info!("Datadir: {}", config::PKGDATADIR);
ApplicationExtManual::run(self);
}
}
impl Default for Application {
fn default() -> Self {
gio::Application::default()
.and_downcast::<Application>()
.unwrap()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum AppProfile {
Stable,
Beta,
Devel,
}
impl AppProfile {
pub fn as_str(&self) -> &str {
match self {
Self::Stable => "stable",
Self::Beta => "beta",
Self::Devel => "devel",
}
}
pub fn should_use_devel_class(&self) -> bool {
matches!(self, Self::Devel)
}
}
impl fmt::Display for AppProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}