Set up the settings user interface #225
|
@ -1,10 +1,15 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
database::Database,
|
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 serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
future::Future,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, RwLock, RwLockReadGuard},
|
sync::{Arc, RwLock, RwLockReadGuard},
|
||||||
};
|
};
|
||||||
|
@ -71,33 +76,49 @@ pub enum CoreResponse {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum CoreNotification {
|
pub enum CoreNotification {
|
||||||
|
ConfigurationUpdated(Config),
|
||||||
BoardUpdated,
|
BoardUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Core {
|
pub struct Core {
|
||||||
// config: Arc<RwLock<Config>>,
|
config: Arc<RwLock<Config>>,
|
||||||
// state: Arc<RwLock<AppState>>,
|
// state: Arc<RwLock<AppState>>,
|
||||||
library: Arc<RwLock<Option<Database>>>,
|
library: Arc<RwLock<Option<Database>>>,
|
||||||
subscribers: Arc<RwLock<Vec<Sender<CoreNotification>>>>,
|
subscribers: Arc<RwLock<Vec<Sender<CoreNotification>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Core {
|
impl Core {
|
||||||
pub fn new(_config: Config) -> Self {
|
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)));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
// config: Arc::new(RwLock::new(config)),
|
config: Arc::new(RwLock::new(config)),
|
||||||
// state,
|
// state,
|
||||||
library: Arc::new(RwLock::new(None)),
|
library: Arc::new(RwLock::new(None)),
|
||||||
subscribers: Arc::new(RwLock::new(vec![])),
|
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>> {
|
pub fn library<'a>(&'a self) -> RwLockReadGuard<'_, Option<Database>> {
|
||||||
self.library.read().unwrap()
|
self.library.read().unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ async-std = { version = "1" }
|
||||||
cairo-rs = { version = "0.18" }
|
cairo-rs = { version = "0.18" }
|
||||||
gio = { version = "0.18" }
|
gio = { version = "0.18" }
|
||||||
glib = { 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" }
|
image = { version = "0.24" }
|
||||||
kifu-core = { path = "../core" }
|
kifu-core = { path = "../core" }
|
||||||
pango = { version = "*" }
|
pango = { version = "*" }
|
||||||
|
|
|
@ -15,9 +15,11 @@ You should have received a copy of the GNU General Public License along with Kif
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
/*
|
||||||
use gio::resources_lookup_data;
|
use gio::resources_lookup_data;
|
||||||
use glib::IsA;
|
use glib::IsA;
|
||||||
use gtk::STYLE_PROVIDER_PRIORITY_USER;
|
use gtk::STYLE_PROVIDER_PRIORITY_USER;
|
||||||
|
*/
|
||||||
use kifu_core::{Config, Core};
|
use kifu_core::{Config, Core};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
@ -85,7 +87,7 @@ impl AppWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_settings(&self) {
|
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.panel_overlay.add_overlay(&view_model.widget);
|
||||||
*self.settings_view_model.write().unwrap() = Some(view_model);
|
*self.settings_view_model.write().unwrap() = Some(view_model);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/>.
|
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 crate::{views, views::SettingsView, LocalObserver};
|
||||||
use kifu_core::{Core, CoreNotification};
|
use async_std::task::spawn;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use kifu_core::{Config, Core, CoreNotification};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// SettingsViewModel
|
/// SettingsViewModel
|
||||||
|
@ -33,15 +35,24 @@ pub struct SettingsViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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| {
|
let notification_observer = LocalObserver::new(&core, |msg| {
|
||||||
println!("SettingsViewModel called with message: {:?}", msg)
|
println!("SettingsViewModel called with message: {:?}", msg)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let config = core.get_config();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
core,
|
core: core.clone(),
|
||||||
notification_observer: Arc::new(notification_observer),
|
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 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/>.
|
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 glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
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(>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 {}
|
pub struct SettingsPrivate {}
|
||||||
|
|
||||||
|
@ -37,25 +75,47 @@ impl WidgetImpl for SettingsPrivate {}
|
||||||
impl FrameImpl for SettingsPrivate {}
|
impl FrameImpl for SettingsPrivate {}
|
||||||
|
|
||||||
glib::wrapper! {
|
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 {
|
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 s: Self = Object::builder().build();
|
||||||
|
let config = Rc::new(RefCell::new(config));
|
||||||
|
|
||||||
let group = adw::PreferencesGroup::builder().build();
|
let group = adw::PreferencesGroup::builder().vexpand(true).build();
|
||||||
let library_row = adw::PreferencesRow::builder()
|
|
||||||
.title("Library Path")
|
let library_row = library_chooser_row(
|
||||||
.child(>k::Label::builder().label("Library Path").build())
|
parent,
|
||||||
.build();
|
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);
|
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()
|
let layout = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Vertical)
|
.orientation(gtk::Orientation::Vertical)
|
||||||
.vexpand(true)
|
.vexpand(true)
|
||||||
.build();
|
.build();
|
||||||
layout.append(&group);
|
layout.append(&group);
|
||||||
|
layout.append(&action_row);
|
||||||
|
|
||||||
s.set_child(Some(&layout));
|
s.set_child(Some(&layout));
|
||||||
s.set_css_classes(&["settings-view"]);
|
s.set_css_classes(&["settings-view"]);
|
||||||
|
|
Loading…
Reference in New Issue