From b98e0bdcead96cc02297671cf2edc7252af4c5c3 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 15 Mar 2024 10:49:25 -0400 Subject: [PATCH 1/5] Strip kifu down even further and revamp the app_window structure --- kifu/core/src/api.rs | 15 +- kifu/core/src/lib.rs | 8 +- kifu/core/src/types.rs | 10 +- kifu/core/src/ui/configuration.rs | 22 --- kifu/gtk/Cargo.toml | 2 +- ...om.luminescent-dreams.kifu.dev.gschema.xml | 4 +- .../com.luminescent-dreams.kifu.gschema.xml | 4 +- kifu/gtk/src/app_window.rs | 175 ++++++++++++++++++ kifu/gtk/src/lib.rs | 23 ++- kifu/gtk/src/main.rs | 41 +++- kifu/gtk/src/ui/mod.rs | 105 ++--------- .../src/view_models/settings_view_model.rs | 15 +- kifu/gtk/src/views/mod.rs | 2 + kifu/gtk/src/views/settings.rs | 46 +++++ 14 files changed, 327 insertions(+), 145 deletions(-) delete mode 100644 kifu/core/src/ui/configuration.rs create mode 100644 kifu/gtk/src/app_window.rs create mode 100644 kifu/gtk/src/views/settings.rs diff --git a/kifu/core/src/api.rs b/kifu/core/src/api.rs index e656707..101c6fc 100644 --- a/kifu/core/src/api.rs +++ b/kifu/core/src/api.rs @@ -1,13 +1,12 @@ use crate::{ database::Database, - types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank}, - ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView}, + types::{AppState, Config, ConfigOption, LibraryPath, GameState, Player, Rank}, }; use async_std::channel::{Receiver, Sender}; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, - sync::{Arc, RwLock}, + sync::{Arc, RwLock, RwLockReadGuard}, }; pub trait Observable { @@ -62,6 +61,7 @@ impl From for Player { } } +/* #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreResponse { ConfigurationView(ConfigurationView), @@ -69,6 +69,7 @@ pub enum CoreResponse { PlayingFieldView(PlayingFieldView), UpdatedConfigurationView(ConfigurationView), } +*/ #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreNotification { @@ -79,7 +80,7 @@ pub enum CoreNotification { pub struct Core { // config: Arc>, // state: Arc>, - database: Arc>>, + library: Arc>>, subscribers: Arc>>>, } @@ -92,11 +93,15 @@ impl Core { Self { // 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 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..f3facad 100644 --- a/kifu/gtk/Cargo.toml +++ b/kifu/gtk/Cargo.toml @@ -9,7 +9,7 @@ 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" } 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/src/app_window.rs b/kifu/gtk/src/app_window.rs new file mode 100644 index 0000000..788e3ee --- /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 crate::view_models::SettingsViewModel; + +enum AppView { + Settings(SettingsViewModel), + Home, +} + +// 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 +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, +} + +impl AppWindow { + pub fn new(app: &adw::Application, core: Core) -> Self { + /* + + 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 = setup_header(); + + let current_view = match config.get::() { + Some(_) => AppView::Home, + None => AppView::Config(SettingsViewModel::new(core.clone())), + }; + + let content = adw::Bin::builder().css_classes(vec!["content"]).build(); + content.set_child(Some( + )); + + + window.set_content(Some(&layout)); + */ + + 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, + } + } + + 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_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); + 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); + + /* + 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..f8c2e5c 100644 --- a/kifu/gtk/src/main.rs +++ b/kifu/gtk/src/main.rs @@ -1,8 +1,10 @@ use adw::prelude::*; -use kifu_core::{Config, ConfigOption, Core, CoreRequest, CoreResponse, DatabasePath}; +use gio::ActionEntry; +use kifu_core::{Config, ConfigOption, Core}; use kifu_gtk::{ perftrace, - ui::{AppWindow, ConfigurationPage, Home, PlayingField}, + // ui::{ConfigurationPage, Home, PlayingField}, + AppWindow, CoreApi, }; use std::sync::{Arc, RwLock}; @@ -12,6 +14,7 @@ const APP_ID_PROD: &str = "com.luminescent-dreams.kifu-gtk"; const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/kifu-gtk/"; +/* fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) { let playing_field = Arc::new(RwLock::new(None)); match message { @@ -48,6 +51,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 db_path: String = settings.string("database-path").into(); + let mut config = Config::new(); + config.set(ConfigOption::LibraryPath(db_path.into())); + config +} + +fn setup_app_configuration_action(app: &adw::Application) { + println!("setup_app_configuration_action"); + let action = ActionEntry::builder("show_config") + .activate(|_app: &adw::Application, _, _| { + println!("show configuration window"); + }) + .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 +82,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() @@ -101,16 +121,20 @@ fn main() { .resource_base_path("/com/luminescent-dreams/kifu-gtk") .build(); + app.connect_activate({ let runtime = runtime.clone(); move |app| { - let app_window = AppWindow::new(app); + let app_window = AppWindow::new(app, core.clone()); + + setup_app_configuration_action(app); 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 +143,7 @@ fn main() { } }); app.add_action(&action_config); + */ app_window.window.present(); @@ -134,7 +159,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/settings_view_model.rs b/kifu/gtk/src/view_models/settings_view_model.rs index 2f13cd8..1e2bfcb 100644 --- a/kifu/gtk/src/view_models/settings_view_model.rs +++ b/kifu/gtk/src/view_models/settings_view_model.rs @@ -14,17 +14,24 @@ 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 crate::{views, LocalObserver, views::SettingsView}; use kifu_core::{Core, CoreNotification}; +/// 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. pub struct SettingsViewModel { core: Core, + // 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: LocalObserver, - widget: gtk::Box, + pub widget: views::SettingsView, } impl SettingsViewModel { - fn new(core: Core) -> Self { + pub fn new(core: Core) -> Self { let notification_observer = LocalObserver::new(&core, |msg| { println!("SettingsViewModel called with message: {:?}", msg) }); @@ -32,7 +39,7 @@ impl SettingsViewModel { Self { core, notification_observer, - widget: gtk::Box::new(gtk::Orientation::Horizontal, 0), + widget: SettingsView::new(), } } } 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..f80bccd --- /dev/null +++ b/kifu/gtk/src/views/settings.rs @@ -0,0 +1,46 @@ +/* +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 glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +pub struct SettingsPrivate {} + +#[glib::object_subclass] +impl ObjectSubclass for SettingsPrivate { + const NAME: &'static str = "Settings"; + type Type = SettingsView; + type ParentType = gtk::Box; + + fn new() -> Self { + Self {} + } +} + +impl ObjectImpl for SettingsPrivate {} +impl WidgetImpl for SettingsPrivate {} +impl BoxImpl for SettingsPrivate {} + +glib::wrapper! { + pub struct SettingsView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl SettingsView { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + s + } +} -- 2.44.1 From 05a6dcf3afc24753dc371d0df00357367aad083c Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 15 Mar 2024 14:07:55 -0400 Subject: [PATCH 2/5] Set up the initial settings screen --- kifu/gtk/resources/style.css | 5 ++ kifu/gtk/src/app_window.rs | 48 ++++++------------- kifu/gtk/src/main.rs | 23 +++++---- .../src/view_models/settings_view_model.rs | 6 ++- kifu/gtk/src/views/settings.rs | 25 ++++++++-- 5 files changed, 61 insertions(+), 46 deletions(-) diff --git a/kifu/gtk/resources/style.css b/kifu/gtk/resources/style.css index 4282dcd..e3c8988 100644 --- a/kifu/gtk/resources/style.css +++ b/kifu/gtk/resources/style.css @@ -1,3 +1,8 @@ .content { padding: 8px; } + +.settings-view { + margin: 8px; + background-color: @view_bg_color; +} diff --git a/kifu/gtk/src/app_window.rs b/kifu/gtk/src/app_window.rs index 788e3ee..3fdf750 100644 --- a/kifu/gtk/src/app_window.rs +++ b/kifu/gtk/src/app_window.rs @@ -19,9 +19,11 @@ 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::SettingsViewModel; +#[derive(Clone)] enum AppView { Settings(SettingsViewModel), Home, @@ -31,6 +33,7 @@ enum AppView { // - 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, @@ -48,42 +51,14 @@ pub struct AppWindow { // 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 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 = setup_header(); - - let current_view = match config.get::() { - Some(_) => AppView::Home, - None => AppView::Config(SettingsViewModel::new(core.clone())), - }; - - let content = adw::Bin::builder().css_classes(vec!["content"]).build(); - content.set_child(Some( - )); - - - window.set_content(Some(&layout)); - */ - let window = Self::setup_window(app); let header = Self::setup_header(); let panel_overlay = Self::setup_panel_overlay(); @@ -105,9 +80,16 @@ impl AppWindow { content, panel_overlay, core, + settings_view_model: Default::default(), } } + pub fn open_settings(&self) { + let view_model = SettingsViewModel::new(self.core.clone()); + self.panel_overlay.add_overlay(&view_model.widget); + *self.settings_view_model.write().unwrap() = Some(view_model); + } + fn setup_window(app: &adw::Application) -> adw::ApplicationWindow { let window = adw::ApplicationWindow::builder() .application(app) @@ -124,7 +106,7 @@ impl AppWindow { .build(); let app_menu = gio::Menu::new(); - let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show_config")); + let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show_settings")); app_menu.append_item(&menu_item); let hamburger = gtk::MenuButton::builder() diff --git a/kifu/gtk/src/main.rs b/kifu/gtk/src/main.rs index f8c2e5c..2b6da10 100644 --- a/kifu/gtk/src/main.rs +++ b/kifu/gtk/src/main.rs @@ -55,17 +55,17 @@ 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 db_path: String = settings.string("database-path").into(); + let lib_path: String = settings.string("library-path").into(); let mut config = Config::new(); - config.set(ConfigOption::LibraryPath(db_path.into())); + config.set(ConfigOption::LibraryPath(lib_path.into())); config } -fn setup_app_configuration_action(app: &adw::Application) { +fn setup_app_configuration_action(app: &adw::Application, app_window: AppWindow) { println!("setup_app_configuration_action"); - let action = ActionEntry::builder("show_config") - .activate(|_app: &adw::Application, _, _| { - println!("show configuration window"); + let action = ActionEntry::builder("show_settings") + .activate(move |_app: &adw::Application, _, _| { + app_window.open_settings(); }) .build(); app.add_action_entries([action]); @@ -125,14 +125,21 @@ fn main() { app.connect_activate({ let runtime = runtime.clone(); move |app| { - let app_window = AppWindow::new(app, core.clone()); + let mut app_window = AppWindow::new(app, core.clone()); - setup_app_configuration_action(app); + 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); diff --git a/kifu/gtk/src/view_models/settings_view_model.rs b/kifu/gtk/src/view_models/settings_view_model.rs index 1e2bfcb..57a66e9 100644 --- a/kifu/gtk/src/view_models/settings_view_model.rs +++ b/kifu/gtk/src/view_models/settings_view_model.rs @@ -16,17 +16,19 @@ You should have received a copy of the GNU General Public License along with Kif use crate::{views, LocalObserver, views::SettingsView}; use kifu_core::{Core, CoreNotification}; +use std::sync::Arc; /// 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, // 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: LocalObserver, + notification_observer: Arc>, pub widget: views::SettingsView, } @@ -38,7 +40,7 @@ impl SettingsViewModel { Self { core, - notification_observer, + notification_observer: Arc::new(notification_observer), widget: SettingsView::new(), } } diff --git a/kifu/gtk/src/views/settings.rs b/kifu/gtk/src/views/settings.rs index f80bccd..7ed6b56 100644 --- a/kifu/gtk/src/views/settings.rs +++ b/kifu/gtk/src/views/settings.rs @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with Kif use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; +use adw::prelude::*; pub struct SettingsPrivate {} @@ -23,7 +24,7 @@ pub struct SettingsPrivate {} impl ObjectSubclass for SettingsPrivate { const NAME: &'static str = "Settings"; type Type = SettingsView; - type ParentType = gtk::Box; + type ParentType = gtk::Frame; fn new() -> Self { Self {} @@ -32,15 +33,33 @@ impl ObjectSubclass for SettingsPrivate { impl ObjectImpl for SettingsPrivate {} impl WidgetImpl for SettingsPrivate {} -impl BoxImpl for SettingsPrivate {} +#[allow(deprecated)] +impl FrameImpl for SettingsPrivate {} glib::wrapper! { - pub struct SettingsView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; + pub struct SettingsView(ObjectSubclass) @extends gtk::Frame, gtk::Widget, @implements gtk::Accessible, gtk::Buildable, gtk::Orientable; } impl SettingsView { pub fn new() -> Self { let s: Self = Object::builder().build(); + + let group = adw::PreferencesGroup::builder().build(); + let library_row = adw::PreferencesRow::builder() + .title("Library Path") + .child(>k::Label::builder().label("Library Path").build()) + .build(); + group.add(&library_row); + + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .vexpand(true) + .build(); + layout.append(&group); + + s.set_child(Some(&layout)); + s.set_css_classes(&["settings-view"]); + s } } -- 2.44.1 From c3c144e0359de7546c37385aa27853b4921bcb1f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 19 Mar 2024 10:08:43 -0400 Subject: [PATCH 3/5] Set up the preferences UI with the path to the games library --- kifu/core/src/api.rs | 41 +++++++--- kifu/gtk/Cargo.toml | 2 +- kifu/gtk/src/app_window.rs | 4 +- .../src/view_models/settings_view_model.rs | 21 +++-- kifu/gtk/src/views/settings.rs | 76 +++++++++++++++++-- 5 files changed, 119 insertions(+), 25 deletions(-) diff --git a/kifu/core/src/api.rs b/kifu/core/src/api.rs index 101c6fc..9c01419 100644 --- a/kifu/core/src/api.rs +++ b/kifu/core/src/api.rs @@ -1,10 +1,15 @@ use crate::{ database::Database, - types::{AppState, Config, ConfigOption, LibraryPath, GameState, Player, Rank}, + 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, RwLockReadGuard}, }; @@ -71,33 +76,49 @@ pub enum CoreResponse { } */ -#[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>, 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, 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() } diff --git a/kifu/gtk/Cargo.toml b/kifu/gtk/Cargo.toml index f3facad..ed32044 100644 --- a/kifu/gtk/Cargo.toml +++ b/kifu/gtk/Cargo.toml @@ -15,7 +15,7 @@ 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/src/app_window.rs b/kifu/gtk/src/app_window.rs index 3fdf750..05914e8 100644 --- a/kifu/gtk/src/app_window.rs +++ b/kifu/gtk/src/app_window.rs @@ -15,9 +15,11 @@ You should have received a copy of the GNU General Public License along with Kif */ 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}; @@ -85,7 +87,7 @@ impl AppWindow { } pub fn open_settings(&self) { - let view_model = SettingsViewModel::new(self.core.clone()); + let view_model = SettingsViewModel::new(&self.window, self.core.clone()); self.panel_overlay.add_overlay(&view_model.widget); *self.settings_view_model.write().unwrap() = Some(view_model); } diff --git a/kifu/gtk/src/view_models/settings_view_model.rs b/kifu/gtk/src/view_models/settings_view_model.rs index 57a66e9..2fe8d78 100644 --- a/kifu/gtk/src/view_models/settings_view_model.rs +++ b/kifu/gtk/src/view_models/settings_view_model.rs @@ -14,8 +14,10 @@ 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::{views, LocalObserver, views::SettingsView}; -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; /// SettingsViewModel @@ -33,15 +35,24 @@ pub struct SettingsViewModel { } impl SettingsViewModel { - pub fn new(core: Core) -> Self { + pub fn new(parent: &impl IsA, core: Core) -> Self { let notification_observer = LocalObserver::new(&core, |msg| { println!("SettingsViewModel called with message: {:?}", msg) }); + let config = core.get_config(); + Self { - core, + core: core.clone(), notification_observer: Arc::new(notification_observer), - widget: SettingsView::new(), + widget: SettingsView::new(parent, config, { + &|new_config| { + spawn({ + let core = core.clone(); + async move { core.set_config(new_config).await } + }); + } + }), } } } diff --git a/kifu/gtk/src/views/settings.rs b/kifu/gtk/src/views/settings.rs index 7ed6b56..f591d69 100644 --- a/kifu/gtk/src/views/settings.rs +++ b/kifu/gtk/src/views/settings.rs @@ -14,9 +14,47 @@ 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}; + +use adw::prelude::*; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use adw::prelude::*; +use kifu_core::{Config, ConfigOption, LibraryPath}; + +fn library_chooser_row( + parent: &impl IsA, + 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"))) + .build(); + + let parent = parent.clone(); + dialog_button.connect_clicked(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, move |result| match result { + Ok(path) => { + on_library_chosen(ConfigOption::LibraryPath(LibraryPath(path.path().unwrap()))) + } + Err(err) => println!("Error choosing a library: {:?}", err), + }); + }); + + let library_row = adw::ActionRow::builder() + .title("Library Path") + .subtitle("No library set") + // .child(&library_row) + .build(); + + library_row.add_suffix(&dialog_button); + library_row.connect_activate(|_| println!("library row activated")); + + library_row +} pub struct SettingsPrivate {} @@ -37,25 +75,47 @@ impl WidgetImpl for SettingsPrivate {} impl FrameImpl for SettingsPrivate {} glib::wrapper! { - pub struct SettingsView(ObjectSubclass) @extends gtk::Frame, gtk::Widget, @implements gtk::Accessible, gtk::Buildable, gtk::Orientable; + pub struct SettingsView(ObjectSubclass) @extends gtk::Frame, gtk::Widget, @implements gtk::Accessible, gtk::Orientable; } impl SettingsView { - pub fn new() -> Self { + pub fn new( + parent: &impl IsA, + config: Config, + on_save: &impl FnOnce(Config), + ) -> Self { let s: Self = Object::builder().build(); + let config = Rc::new(RefCell::new(config)); - let group = adw::PreferencesGroup::builder().build(); - let library_row = adw::PreferencesRow::builder() - .title("Library Path") - .child(>k::Label::builder().label("Library Path").build()) - .build(); + let group = adw::PreferencesGroup::builder().vexpand(true).build(); + + let library_row = library_chooser_row( + parent, + Rc::new({ + let config = config.clone(); + move |library_path| { + config.borrow_mut().set(library_path); + println!("library path changed, need to update the UI"); + } + }), + ); group.add(&library_row); + let cancel_button = gtk::Button::builder().label("Cancel").build(); + let save_button = gtk::Button::builder().label("Save").build(); + let action_row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .halign(gtk::Align::End) + .build(); + action_row.append(&cancel_button); + action_row.append(&save_button); + let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .vexpand(true) .build(); layout.append(&group); + layout.append(&action_row); s.set_child(Some(&layout)); s.set_css_classes(&["settings-view"]); -- 2.44.1 From 5612c89a613f1ac2935bbd9eafbbe338928b2d37 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 21 Mar 2024 08:43:08 -0400 Subject: [PATCH 4/5] Thread signals through all of the settings dialog --- kifu/gtk/resources/style.css | 9 +++ kifu/gtk/src/app_window.rs | 24 ++++++-- kifu/gtk/src/view_models/home_view_model.rs | 10 ++-- .../src/view_models/settings_view_model.rs | 35 +++++++---- kifu/gtk/src/views/settings.rs | 60 +++++++++++++------ 5 files changed, 102 insertions(+), 36 deletions(-) diff --git a/kifu/gtk/resources/style.css b/kifu/gtk/resources/style.css index e3c8988..713aadc 100644 --- a/kifu/gtk/resources/style.css +++ b/kifu/gtk/resources/style.css @@ -4,5 +4,14 @@ .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 index 05914e8..020d72c 100644 --- a/kifu/gtk/src/app_window.rs +++ b/kifu/gtk/src/app_window.rs @@ -23,12 +23,12 @@ use gtk::STYLE_PROVIDER_PRIORITY_USER; use kifu_core::{Config, Core}; use std::sync::{Arc, RwLock}; -use crate::view_models::SettingsViewModel; +use crate::{view_models::HomeViewModel, view_models::SettingsViewModel}; #[derive(Clone)] enum AppView { Settings(SettingsViewModel), - Home, + Home(HomeViewModel), } // An application window should generally contain @@ -87,11 +87,27 @@ impl AppWindow { } pub fn open_settings(&self) { - let view_model = SettingsViewModel::new(&self.window, self.core.clone()); + 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) @@ -136,7 +152,7 @@ impl AppWindow { .child(¬hing_page) .build(), ); - content.push(AppView::Home); + content.push(AppView::Home(HomeViewModel::new(core.clone()))); /* match *core.library() { 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 2fe8d78..e505a11 100644 --- a/kifu/gtk/src/view_models/settings_view_model.rs +++ b/kifu/gtk/src/view_models/settings_view_model.rs @@ -18,7 +18,7 @@ use crate::{views, views::SettingsView, LocalObserver}; use async_std::task::spawn; use gtk::prelude::*; use kifu_core::{Config, Core, CoreNotification}; -use std::sync::Arc; +use std::{sync::Arc, rc::Rc}; /// SettingsViewModel /// @@ -35,24 +35,39 @@ pub struct SettingsViewModel { } impl SettingsViewModel { - pub fn new(parent: &impl IsA, 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: core.clone(), notification_observer: Arc::new(notification_observer), - widget: SettingsView::new(parent, config, { - &|new_config| { - spawn({ - let core = core.clone(); - async move { core.set_config(new_config).await } - }); - } - }), + widget, } } } diff --git a/kifu/gtk/src/views/settings.rs b/kifu/gtk/src/views/settings.rs index f591d69..9b6338b 100644 --- a/kifu/gtk/src/views/settings.rs +++ b/kifu/gtk/src/views/settings.rs @@ -29,29 +29,41 @@ fn library_chooser_row( let dialog_button = gtk::Button::builder() .child(>k::Label::new(Some("Select Library"))) + .valign(gtk::Align::Center) .build(); let parent = parent.clone(); - dialog_button.connect_clicked(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, move |result| match result { - Ok(path) => { - on_library_chosen(ConfigOption::LibraryPath(LibraryPath(path.path().unwrap()))) - } - Err(err) => println!("Error choosing a library: {:?}", err), - }); - }); let library_row = adw::ActionRow::builder() .title("Library Path") .subtitle("No library set") - // .child(&library_row) + .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.connect_activate(|_| println!("library row activated")); library_row } @@ -82,12 +94,13 @@ impl SettingsView { pub fn new( parent: &impl IsA, config: Config, - on_save: &impl FnOnce(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().vexpand(true).build(); + let group = adw::PreferencesGroup::builder().build(); let library_row = library_chooser_row( parent, @@ -95,26 +108,37 @@ impl SettingsView { let config = config.clone(); move |library_path| { config.borrow_mut().set(library_path); - println!("library path changed, need to update the UI"); } }), ); 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) - .vexpand(true) + .spacing(8) .build(); - layout.append(&group); + layout.append(&preferences_box); layout.append(&action_row); s.set_child(Some(&layout)); -- 2.44.1 From de54ec676f9eea35d520cb82cd0ef5fafeb68ded Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 21 Mar 2024 17:01:40 -0400 Subject: [PATCH 5/5] Make sure the settings get saved --- kifu/gtk/src/main.rs | 36 +++++++++++++++++++++++++++++++--- kifu/gtk/src/views/settings.rs | 6 ++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/kifu/gtk/src/main.rs b/kifu/gtk/src/main.rs index 2b6da10..0eaf729 100644 --- a/kifu/gtk/src/main.rs +++ b/kifu/gtk/src/main.rs @@ -1,6 +1,8 @@ use adw::prelude::*; +use async_std::channel::Receiver; +use async_std::task::spawn; use gio::ActionEntry; -use kifu_core::{Config, ConfigOption, Core}; +use kifu_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable}; use kifu_gtk::{ perftrace, // ui::{ConfigurationPage, Home, PlayingField}, @@ -14,6 +16,29 @@ 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)); @@ -107,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(); @@ -121,14 +152,13 @@ fn main() { .resource_base_path("/com/luminescent-dreams/kifu-gtk") .build(); - app.connect_activate({ let runtime = runtime.clone(); move |app| { let mut app_window = AppWindow::new(app, core.clone()); match *core.library() { - Some(_) => {}, + Some(_) => {} None => app_window.open_settings(), } diff --git a/kifu/gtk/src/views/settings.rs b/kifu/gtk/src/views/settings.rs index 9b6338b..4244bed 100644 --- a/kifu/gtk/src/views/settings.rs +++ b/kifu/gtk/src/views/settings.rs @@ -14,7 +14,7 @@ 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}; +use std::{cell::RefCell, path::Path, rc::Rc, borrow::Cow}; use adw::prelude::*; use glib::Object; @@ -23,6 +23,7 @@ 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(); @@ -36,7 +37,7 @@ fn library_chooser_row( let library_row = adw::ActionRow::builder() .title("Library Path") - .subtitle("No library set") + .subtitle(library_path.map(|LibraryPath(path)| path.to_string_lossy().into_owned()).unwrap_or("No library set".to_owned())) .css_classes(["preference-item"]) .build(); @@ -104,6 +105,7 @@ impl SettingsView { let library_row = library_chooser_row( parent, + config.borrow().get(), Rc::new({ let config = config.clone(); move |library_path| { -- 2.44.1