diff --git a/kifu/core/src/api.rs b/kifu/core/src/api.rs index e656707..9c01419 100644 --- a/kifu/core/src/api.rs +++ b/kifu/core/src/api.rs @@ -1,13 +1,17 @@ use crate::{ database::Database, - types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank}, - ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView}, + types::{AppState, Config, ConfigOption, GameState, LibraryPath, Player, Rank}, +}; +use async_std::{ + channel::{Receiver, Sender}, + stream, + task::spawn, }; -use async_std::channel::{Receiver, Sender}; use serde::{Deserialize, Serialize}; use std::{ + future::Future, path::PathBuf, - sync::{Arc, RwLock}, + sync::{Arc, RwLock, RwLockReadGuard}, }; pub trait Observable { @@ -62,6 +66,7 @@ impl From for Player { } } +/* #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreResponse { ConfigurationView(ConfigurationView), @@ -69,34 +74,55 @@ pub enum CoreResponse { PlayingFieldView(PlayingFieldView), UpdatedConfigurationView(ConfigurationView), } +*/ -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug)] pub enum CoreNotification { + ConfigurationUpdated(Config), BoardUpdated, } #[derive(Clone, Debug)] pub struct Core { - // config: Arc>, + config: Arc>, // state: Arc>, - database: Arc>>, + library: Arc>>, subscribers: Arc>>>, } impl Core { - pub fn new(_config: Config) -> Self { - // let config = Config::from_path(config_path).expect("configuration to open"); - - // let state = Arc::new(RwLock::new(AppState::new(db_path))); - + pub fn new(config: Config) -> Self { Self { - // config: Arc::new(RwLock::new(config)), + config: Arc::new(RwLock::new(config)), // state, - database: Arc::new(RwLock::new(None)), + library: Arc::new(RwLock::new(None)), subscribers: Arc::new(RwLock::new(vec![])), } } + pub fn get_config(&self) -> Config { + self.config.read().unwrap().clone() + } + + /// Change the configuration of the Core. This function will update any relevant core + /// functions, especially the contents of the library, and it will notify any subscribed objects + /// that the configuration has changed. + /// + /// It will not handle persisting the new configuration, as the backing store for the + /// configuration is not a decision for the core library. + pub async fn set_config(&self, config: Config) { + *self.config.write().unwrap() = config.clone(); + let subscribers = self.subscribers.read().unwrap().clone(); + for subscriber in subscribers { + let subscriber = subscriber.clone(); + let _ = subscriber.send(CoreNotification::ConfigurationUpdated(config.clone())).await; + } + } + + pub fn library<'a>(&'a self) -> RwLockReadGuard<'_, Option> { + self.library.read().unwrap() + } + /* pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse { match request { diff --git a/kifu/core/src/lib.rs b/kifu/core/src/lib.rs index 855fe82..8a7f40f 100644 --- a/kifu/core/src/lib.rs +++ b/kifu/core/src/lib.rs @@ -1,10 +1,7 @@ extern crate config_derive; mod api; -pub use api::{ - ChangeSettingRequest, Core, CoreNotification, CoreRequest, CoreResponse, CreateGameRequest, - HotseatPlayerRequest, Observable, PlayerInfoRequest, -}; +pub use api::{Core, CoreNotification, Observable}; mod board; pub use board::*; @@ -12,6 +9,5 @@ pub use board::*; mod database; mod types; -pub use types::{BoardError, Color, Config, ConfigOption, DatabasePath, Player, Rank, Size}; +pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size}; -pub mod ui; diff --git a/kifu/core/src/types.rs b/kifu/core/src/types.rs index a443b99..7f39a8b 100644 --- a/kifu/core/src/types.rs +++ b/kifu/core/src/types.rs @@ -10,21 +10,21 @@ use std::{path::PathBuf, time::Duration}; use thiserror::Error; define_config! { - DatabasePath(DatabasePath), + LibraryPath(LibraryPath), Me(Me), } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)] -pub struct DatabasePath(pub PathBuf); +pub struct LibraryPath(pub PathBuf); -impl std::ops::Deref for DatabasePath { +impl std::ops::Deref for LibraryPath { type Target = PathBuf; fn deref(&self) -> &Self::Target { &self.0 } } -impl From for DatabasePath { +impl From for LibraryPath { fn from(s: String) -> Self { Self(PathBuf::from(s)) } @@ -78,7 +78,7 @@ pub struct AppState { } impl AppState { - pub fn new(database_path: DatabasePath) -> Self { + pub fn new(database_path: LibraryPath) -> Self { Self { game: Some(GameState::default()), database: Database::open_path(database_path.to_path_buf()).unwrap(), diff --git a/kifu/core/src/ui/configuration.rs b/kifu/core/src/ui/configuration.rs deleted file mode 100644 index 4596374..0000000 --- a/kifu/core/src/ui/configuration.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{ - types::{Config, DatabasePath}, - ui::Field, -}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ConfigurationView { - pub library: Field<()>, -} - -pub fn configuration(config: &Config) -> ConfigurationView { - let path: Option = config.get(); - ConfigurationView { - library: Field { - id: "library-path-field".to_owned(), - label: "Library".to_owned(), - value: path.map(|path| path.to_string_lossy().into_owned()), - action: (), - }, - } -} diff --git a/kifu/gtk/Cargo.toml b/kifu/gtk/Cargo.toml index 4167479..ed32044 100644 --- a/kifu/gtk/Cargo.toml +++ b/kifu/gtk/Cargo.toml @@ -9,13 +9,13 @@ screenplay = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] } +adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] } async-channel = { version = "2" } async-std = { version = "1" } cairo-rs = { version = "0.18" } gio = { version = "0.18" } glib = { version = "0.18" } -gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] } +gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] } image = { version = "0.24" } kifu-core = { path = "../core" } pango = { version = "*" } diff --git a/kifu/gtk/resources/com.luminescent-dreams.kifu.dev.gschema.xml b/kifu/gtk/resources/com.luminescent-dreams.kifu.dev.gschema.xml index 9918738..bb93e16 100644 --- a/kifu/gtk/resources/com.luminescent-dreams.kifu.dev.gschema.xml +++ b/kifu/gtk/resources/com.luminescent-dreams.kifu.dev.gschema.xml @@ -1,7 +1,7 @@ - + "" Path to the directory of games @@ -10,4 +10,4 @@ Language override, use system settings if empty - \ No newline at end of file + diff --git a/kifu/gtk/resources/com.luminescent-dreams.kifu.gschema.xml b/kifu/gtk/resources/com.luminescent-dreams.kifu.gschema.xml index bed47e9..0569b5b 100644 --- a/kifu/gtk/resources/com.luminescent-dreams.kifu.gschema.xml +++ b/kifu/gtk/resources/com.luminescent-dreams.kifu.gschema.xml @@ -1,7 +1,7 @@ - + "" Path to the directory of games @@ -10,4 +10,4 @@ Language override, use system settings if empty - \ No newline at end of file + diff --git a/kifu/gtk/resources/style.css b/kifu/gtk/resources/style.css index 4282dcd..713aadc 100644 --- a/kifu/gtk/resources/style.css +++ b/kifu/gtk/resources/style.css @@ -1,3 +1,17 @@ .content { padding: 8px; } + +.settings-view { + margin: 8px; + padding: 4px; + background-color: @view_bg_color; +} + +.settings-view { + padding: 8px; +} + +.preference-item > suffixes { + margin: 4px; +} diff --git a/kifu/gtk/src/app_window.rs b/kifu/gtk/src/app_window.rs new file mode 100644 index 0000000..020d72c --- /dev/null +++ b/kifu/gtk/src/app_window.rs @@ -0,0 +1,175 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of Kifu. + +Kifu is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +Kifu is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with Kifu. If not, see . +*/ + +use adw::prelude::*; +/* +use gio::resources_lookup_data; +use glib::IsA; +use gtk::STYLE_PROVIDER_PRIORITY_USER; +*/ +use kifu_core::{Config, Core}; +use std::sync::{Arc, RwLock}; + +use crate::{view_models::HomeViewModel, view_models::SettingsViewModel}; + +#[derive(Clone)] +enum AppView { + Settings(SettingsViewModel), + Home(HomeViewModel), +} + +// An application window should generally contain +// - an overlay widget +// - the main content in a stack on the bottom panel of the overlay +// - the settings and the about page in bins atop the overlay +#[derive(Clone)] +pub struct AppWindow { + pub window: adw::ApplicationWindow, + header: adw::HeaderBar, + + // content is a stack which contains the view models for the application. These are the main + // elements that users want to interact with: the home page, the game library, a review, a game + // itself, perhaps also chat rooms and player lists on other networks. stack contains the + // widgets that need to be rendered. The two of these work together in order to ensure that + // we can maintain the state of previous views. Since the two of these work together, they are + // a candidate for extraction into a new widget or a new struct. + stack: adw::NavigationView, + content: Vec, + + // Overlays are for transient content, such as about and settings, which can be accessed from + // anywhere but shouldn't be part of the main application flow. + panel_overlay: gtk::Overlay, + core: Core, + + // Not liking this, but I have to keep track of the settings view model separately from + // anything else. I'll have to look into this later. + settings_view_model: Arc>>, +} + +impl AppWindow { + pub fn new(app: &adw::Application, core: Core) -> Self { + let window = Self::setup_window(app); + let header = Self::setup_header(); + let panel_overlay = Self::setup_panel_overlay(); + let (stack, content) = Self::setup_content(core.clone()); + + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + layout.append(&header); + layout.append(&panel_overlay); + panel_overlay.set_child(Some(&stack)); + + window.set_content(Some(&layout)); + + Self { + window, + header, + stack, + content, + panel_overlay, + core, + settings_view_model: Default::default(), + } + } + + pub fn open_settings(&self) { + let view_model = SettingsViewModel::new(&self.window, self.core.clone(), { + let s = self.clone(); + move || { + s.close_overlay(); + } + }); + self.panel_overlay.add_overlay(&view_model.widget); + *self.settings_view_model.write().unwrap() = Some(view_model); + } + + pub fn close_overlay(&self) { + let mut vm = self.settings_view_model.write().unwrap(); + match *vm { + Some(ref mut view_model) => { + self.panel_overlay.remove_overlay(&view_model.widget); + *vm = None; + } + None => {} + } + } + + fn setup_window(app: &adw::Application) -> adw::ApplicationWindow { + let window = adw::ApplicationWindow::builder() + .application(app) + .width_request(800) + .height_request(500) + .build(); + + window + } + + fn setup_header() -> adw::HeaderBar { + let header = adw::HeaderBar::builder() + .title_widget(>k::Label::new(Some("Kifu"))) + .build(); + + let app_menu = gio::Menu::new(); + let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show_settings")); + app_menu.append_item(&menu_item); + + let hamburger = gtk::MenuButton::builder() + .icon_name("open-menu-symbolic") + .build(); + hamburger.set_menu_model(Some(&app_menu)); + + header.pack_end(&hamburger); + header + } + + fn setup_panel_overlay() -> gtk::Overlay { + gtk::Overlay::new() + } + + fn setup_content(core: Core) -> (adw::NavigationView, Vec) { + let stack = adw::NavigationView::new(); + let mut content = Vec::new(); + + let nothing_page = adw::StatusPage::builder().title("Nothing here").build(); + let _ = stack.push( + &adw::NavigationPage::builder() + .can_pop(false) + .title("Kifu") + .child(¬hing_page) + .build(), + ); + content.push(AppView::Home(HomeViewModel::new(core.clone()))); + + /* + match *core.library() { + Some(_) => { + } + None => { + let settings_vm = SettingsViewModel::new(core.clone()); + let _ = stack.push(&adw::NavigationPage::new(&settings_vm.widget, "Settings")); + content.push(AppView::Settings(settings_vm)); + } + } + */ + + (stack, content) + } + + // pub fn set_content(content: &impl IsA) -> adw::ViewStack { + // self.content.set_child(Some(content)); + // } +} diff --git a/kifu/gtk/src/lib.rs b/kifu/gtk/src/lib.rs index ef7508c..ca2ab98 100644 --- a/kifu/gtk/src/lib.rs +++ b/kifu/gtk/src/lib.rs @@ -1,10 +1,29 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of Kifu. + +Kifu is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +Kifu is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with Kifu. If not, see . +*/ + pub mod ui; +mod app_window; +pub use app_window::AppWindow; + mod view_models; mod views; use async_std::task::yield_now; -use kifu_core::{Core, CoreRequest, CoreResponse, Observable}; +use kifu_core::{Core, Observable}; use std::{rc::Rc, sync::Arc}; use tokio::runtime::Runtime; @@ -15,6 +34,7 @@ pub struct CoreApi { } impl CoreApi { + /* pub fn dispatch(&self, request: CoreRequest) { /* spawn({ @@ -26,6 +46,7 @@ impl CoreApi { }); */ } +*/ } pub fn perftrace(trace_name: &str, f: F) -> A diff --git a/kifu/gtk/src/main.rs b/kifu/gtk/src/main.rs index 4601569..0eaf729 100644 --- a/kifu/gtk/src/main.rs +++ b/kifu/gtk/src/main.rs @@ -1,8 +1,12 @@ use adw::prelude::*; -use kifu_core::{Config, ConfigOption, Core, CoreRequest, CoreResponse, DatabasePath}; +use async_std::channel::Receiver; +use async_std::task::spawn; +use gio::ActionEntry; +use kifu_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable}; use kifu_gtk::{ perftrace, - ui::{AppWindow, ConfigurationPage, Home, PlayingField}, + // ui::{ConfigurationPage, Home, PlayingField}, + AppWindow, CoreApi, }; use std::sync::{Arc, RwLock}; @@ -12,6 +16,30 @@ const APP_ID_PROD: &str = "com.luminescent-dreams.kifu-gtk"; const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/kifu-gtk/"; +async fn handler(notifications: Receiver, app_id: String) { + loop { + let msg = notifications.recv().await; + match msg { + Ok(CoreNotification::ConfigurationUpdated(cfg)) => { + println!("commiting configuration"); + let settings = gio::Settings::new(&app_id); + if let Some(LibraryPath(library_path)) = cfg.get() { + let _ = settings.set_string( + "library-path", + &library_path.into_os_string().into_string().unwrap(), + ); + } + } + Ok(_) => println!("discarding message"), + Err(err) => { + println!("shutting down handler with error: {:?}", err); + return; + } + } + } +} + +/* fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) { let playing_field = Arc::new(RwLock::new(None)); match message { @@ -48,6 +76,26 @@ fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) }), } } +*/ + +fn load_config(app_id: &str) -> Config { + let settings = gio::Settings::new(app_id); + let lib_path: String = settings.string("library-path").into(); + let mut config = Config::new(); + config.set(ConfigOption::LibraryPath(lib_path.into())); + config +} + +fn setup_app_configuration_action(app: &adw::Application, app_window: AppWindow) { + println!("setup_app_configuration_action"); + let action = ActionEntry::builder("show_settings") + .activate(move |_app: &adw::Application, _, _| { + app_window.open_settings(); + }) + .build(); + app.add_action_entries([action]); + println!("setup_app_configuration_action complete"); +} fn main() { gio::resources_register_include!("com.luminescent-dreams.kifu-gtk.gresource") @@ -59,10 +107,7 @@ fn main() { APP_ID_PROD }; - let settings = gio::Settings::new(app_id); - let db_path: String = settings.string("database-path").into(); - let mut config = Config::new(); - config.set(ConfigOption::DatabasePath(db_path.into())); + let config = load_config(&app_id); let runtime = Arc::new( tokio::runtime::Builder::new_multi_thread() @@ -87,6 +132,12 @@ fn main() { let core = Core::new(config); + spawn({ + let notifier = core.subscribe(); + let app_id = app_id.to_owned(); + handler(notifier, app_id) + }); + /* let core_handle = runtime.spawn({ let core = core.clone(); @@ -104,13 +155,23 @@ fn main() { app.connect_activate({ let runtime = runtime.clone(); move |app| { - let app_window = AppWindow::new(app); + let mut app_window = AppWindow::new(app, core.clone()); + match *core.library() { + Some(_) => {} + None => app_window.open_settings(), + } + + setup_app_configuration_action(app, app_window.clone()); + + /* let api = CoreApi { rt: runtime.clone(), core: core.clone(), }; + */ + /* let action_config = gio::SimpleAction::new("show-config", None); action_config.connect_activate({ let api = api.clone(); @@ -119,6 +180,7 @@ fn main() { } }); app.add_action(&action_config); + */ app_window.window.present(); @@ -134,7 +196,7 @@ fn main() { }); */ - api.dispatch(CoreRequest::Home); + // api.dispatch(CoreRequest::Home); } }); diff --git a/kifu/gtk/src/ui/mod.rs b/kifu/gtk/src/ui/mod.rs index 00b5cbd..43718f0 100644 --- a/kifu/gtk/src/ui/mod.rs +++ b/kifu/gtk/src/ui/mod.rs @@ -1,100 +1,27 @@ -use adw::prelude::*; -use gio::resources_lookup_data; -use glib::IsA; -use gtk::STYLE_PROVIDER_PRIORITY_USER; +// mod chat; +// pub use chat::Chat; -mod chat; -pub use chat::Chat; +// mod config; +// pub use config::ConfigurationPage; -mod config; -pub use config::ConfigurationPage; +// mod game_preview; +// pub use game_preview::GamePreview; -mod game_preview; -pub use game_preview::GamePreview; +// mod library; +// pub use library::Library; -mod library; -pub use library::Library; +// mod player_card; +// pub use player_card::PlayerCard; -mod player_card; -pub use player_card::PlayerCard; +// mod playing_field; +// pub use playing_field::PlayingField; -mod playing_field; -pub use playing_field::PlayingField; +// mod home; +// pub use home::Home; -mod home; -pub use home::Home; - -mod board; -pub use board::Board; +// mod board; +// pub use board::Board; #[cfg(feature = "screenplay")] pub use playing_field::playing_field_view; -pub struct AppWindow { - pub window: adw::ApplicationWindow, - pub header: adw::HeaderBar, - pub content: adw::Bin, -} - -impl AppWindow { - pub fn new(app: &adw::Application) -> Self { - let window = adw::ApplicationWindow::builder() - .application(app) - .width_request(800) - .height_request(500) - .build(); - - let stylesheet = String::from_utf8( - resources_lookup_data( - "/com/luminescent-dreams/kifu-gtk/style.css", - gio::ResourceLookupFlags::NONE, - ) - .expect("stylesheet should just be available") - .to_vec(), - ) - .expect("to parse stylesheet"); - - let provider = gtk::CssProvider::new(); - provider.load_from_data(&stylesheet); - let context = window.style_context(); - context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER); - - let header = adw::HeaderBar::builder() - .title_widget(>k::Label::new(Some("Kifu"))) - .build(); - - let app_menu = gio::Menu::new(); - let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show-config")); - app_menu.append_item(&menu_item); - - let hamburger = gtk::MenuButton::builder() - .icon_name("open-menu-symbolic") - .build(); - hamburger.set_menu_model(Some(&app_menu)); - - header.pack_end(&hamburger); - - let content = adw::Bin::builder().css_classes(vec!["content"]).build(); - content.set_child(Some( - &adw::StatusPage::builder().title("Nothing here").build(), - )); - - let layout = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - layout.append(&header); - layout.append(&content); - - window.set_content(Some(&layout)); - - Self { - window, - header, - content, - } - } - - pub fn set_content(&self, content: &impl IsA) { - self.content.set_child(Some(content)); - } -} diff --git a/kifu/gtk/src/view_models/home_view_model.rs b/kifu/gtk/src/view_models/home_view_model.rs index effe97a..5af979a 100644 --- a/kifu/gtk/src/view_models/home_view_model.rs +++ b/kifu/gtk/src/view_models/home_view_model.rs @@ -16,23 +16,25 @@ You should have received a copy of the GNU General Public License along with Kif use crate::LocalObserver; use kifu_core::{Core, CoreNotification}; +use std::sync::Arc; /// Home controls the view that the user sees when starting the application if there are no games in progress. It provides a window into the database, showing a list of recently recorded games. It also provides the UI for starting a new game. This will render an empty database view if the user hasn't configured a database yet. +#[derive(Clone)] pub struct HomeViewModel { core: Core, - notification_observer: LocalObserver, + notification_observer: Arc>, widget: gtk::Box, } impl HomeViewModel { - fn new(core: Core) -> Self { + pub fn new(core: Core) -> Self { let notification_observer = LocalObserver::new(&core, |msg| { - println!("DatabaseViewModelHandler called with message: {:?}", msg) + println!("HomeViewModel handler called with message: {:?}", msg) }); Self { core, - notification_observer, + notification_observer: Arc::new(notification_observer), widget: gtk::Box::new(gtk::Orientation::Horizontal, 0), } } diff --git a/kifu/gtk/src/view_models/settings_view_model.rs b/kifu/gtk/src/view_models/settings_view_model.rs index 2f13cd8..e505a11 100644 --- a/kifu/gtk/src/view_models/settings_view_model.rs +++ b/kifu/gtk/src/view_models/settings_view_model.rs @@ -14,25 +14,60 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with Kifu. If not, see . */ -use crate::LocalObserver; -use kifu_core::{Core, CoreNotification}; +use crate::{views, views::SettingsView, LocalObserver}; +use async_std::task::spawn; +use gtk::prelude::*; +use kifu_core::{Config, Core, CoreNotification}; +use std::{sync::Arc, rc::Rc}; +/// SettingsViewModel +/// +/// Listens for messages from the core, and serves as intermediary between the Settings UI and the +/// core. Because it needs to respond to events from the core, it owns the widget, which allows it +/// to tell the widget to update after certain events. +#[derive(Clone)] pub struct SettingsViewModel { core: Core, - notification_observer: LocalObserver, - widget: gtk::Box, + // Technically, Settings doesn't care about any events from Core. We will keep this around for + // now as reference, until something which does care shows up. + notification_observer: Arc>, + pub widget: views::SettingsView, } impl SettingsViewModel { - fn new(core: Core) -> Self { + pub fn new(parent: &impl IsA, core: Core, on_close: impl Fn() + 'static) -> Self { + let on_close = Arc::new(on_close); + let notification_observer = LocalObserver::new(&core, |msg| { println!("SettingsViewModel called with message: {:?}", msg) }); + let config = core.get_config(); + + let widget = SettingsView::new( + parent, + config, + { + let core = core.clone(); + let on_close = on_close.clone(); + move |new_config| { + spawn({ + let core = core.clone(); + on_close(); + async move { + println!("running set_config in the background"); + core.set_config(new_config).await; + } + }); + } + }, + move || on_close(), + ); + Self { - core, - notification_observer, - widget: gtk::Box::new(gtk::Orientation::Horizontal, 0), + core: core.clone(), + notification_observer: Arc::new(notification_observer), + widget, } } } diff --git a/kifu/gtk/src/views/mod.rs b/kifu/gtk/src/views/mod.rs index e69de29..e95b387 100644 --- a/kifu/gtk/src/views/mod.rs +++ b/kifu/gtk/src/views/mod.rs @@ -0,0 +1,2 @@ +mod settings; +pub use settings::SettingsView; diff --git a/kifu/gtk/src/views/settings.rs b/kifu/gtk/src/views/settings.rs new file mode 100644 index 0000000..4244bed --- /dev/null +++ b/kifu/gtk/src/views/settings.rs @@ -0,0 +1,151 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of Kifu. + +Kifu is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +Kifu is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with Kifu. If not, see . +*/ + +use std::{cell::RefCell, path::Path, rc::Rc, borrow::Cow}; + +use adw::prelude::*; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use kifu_core::{Config, ConfigOption, LibraryPath}; + +fn library_chooser_row( + parent: &impl IsA, + library_path: Option, + on_library_chosen: Rc, +) -> adw::ActionRow { + let dialog = gtk::FileDialog::builder().build(); + + let dialog_button = gtk::Button::builder() + .child(>k::Label::new(Some("Select Library"))) + .valign(gtk::Align::Center) + .build(); + + let parent = parent.clone(); + + let library_row = adw::ActionRow::builder() + .title("Library Path") + .subtitle(library_path.map(|LibraryPath(path)| path.to_string_lossy().into_owned()).unwrap_or("No library set".to_owned())) + .css_classes(["preference-item"]) + .build(); + + dialog_button.connect_clicked({ + let library_row = library_row.clone(); + move |_| { + let no_parent: Option<>k::Window> = None; + let not_cancellable: Option<&gio::Cancellable> = None; + let on_library_chosen = on_library_chosen.clone(); + dialog.select_folder(no_parent, not_cancellable, { + let library_row = library_row.clone(); + move |result| match result { + Ok(path) => { + let path_str: String = + path.path().unwrap().into_os_string().into_string().unwrap(); + library_row.set_subtitle(&path_str); + on_library_chosen(ConfigOption::LibraryPath(LibraryPath( + path.path().unwrap(), + ))) + } + Err(err) => println!("Error choosing a library: {:?}", err), + } + }); + } + }); + + library_row.add_suffix(&dialog_button); + + library_row +} + +pub struct SettingsPrivate {} + +#[glib::object_subclass] +impl ObjectSubclass for SettingsPrivate { + const NAME: &'static str = "Settings"; + type Type = SettingsView; + type ParentType = gtk::Frame; + + fn new() -> Self { + Self {} + } +} + +impl ObjectImpl for SettingsPrivate {} +impl WidgetImpl for SettingsPrivate {} +#[allow(deprecated)] +impl FrameImpl for SettingsPrivate {} + +glib::wrapper! { + pub struct SettingsView(ObjectSubclass) @extends gtk::Frame, gtk::Widget, @implements gtk::Accessible, gtk::Orientable; +} + +impl SettingsView { + pub fn new( + parent: &impl IsA, + config: Config, + on_save: impl Fn(Config) + 'static, + on_cancel: impl Fn() + 'static, + ) -> Self { + let s: Self = Object::builder().build(); + let config = Rc::new(RefCell::new(config)); + + let group = adw::PreferencesGroup::builder().build(); + + let library_row = library_chooser_row( + parent, + config.borrow().get(), + Rc::new({ + let config = config.clone(); + move |library_path| { + config.borrow_mut().set(library_path); + } + }), + ); + group.add(&library_row); + + let cancel_button = gtk::Button::builder().label("Cancel").build(); + cancel_button.connect_clicked(move |_| on_cancel()); + let save_button = gtk::Button::builder().label("Save").build(); + save_button.connect_clicked({ + let config = config.clone(); + move |_| on_save(config.borrow().clone()) + }); + + let action_row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .halign(gtk::Align::End) + .valign(gtk::Align::End) + .build(); + action_row.append(&cancel_button); + action_row.append(&save_button); + + let preferences_box = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + preferences_box.append(&group); + + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(8) + .build(); + layout.append(&preferences_box); + layout.append(&action_row); + + s.set_child(Some(&layout)); + s.set_css_classes(&["settings-view"]); + + s + } +}