Set up the settings user interface #225

Merged
savanni merged 5 commits from kifu/main-menu into main 2024-03-21 21:11:03 +00:00
5 changed files with 119 additions and 25 deletions
Showing only changes of commit c3c144e035 - Show all commits

View File

@ -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<RwLock<Config>>,
config: Arc<RwLock<Config>>,
// state: Arc<RwLock<AppState>>,
library: Arc<RwLock<Option<Database>>>,
subscribers: Arc<RwLock<Vec<Sender<CoreNotification>>>>,
}
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<Database>> {
self.library.read().unwrap()
}

View File

@ -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 = "*" }

View File

@ -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);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<gtk::Window>, 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 }
});
}
}),
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<gtk::Window>,
on_library_chosen: Rc<impl Fn(ConfigOption) + 'static>,
) -> adw::ActionRow {
let dialog = gtk::FileDialog::builder().build();
let dialog_button = gtk::Button::builder()
.child(&gtk::Label::new(Some("Select Library")))
.build();
let parent = parent.clone();
dialog_button.connect_clicked(move |_| {
let no_parent: Option<&gtk::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<SettingsPrivate>) @extends gtk::Frame, gtk::Widget, @implements gtk::Accessible, gtk::Buildable, gtk::Orientable;
pub struct SettingsView(ObjectSubclass<SettingsPrivate>) @extends gtk::Frame, gtk::Widget, @implements gtk::Accessible, gtk::Orientable;
}
impl SettingsView {
pub fn new() -> Self {
pub fn new(
parent: &impl IsA<gtk::Window>,
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(&gtk::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"]);