Set up the settings user interface #225
|
@ -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<T> {
|
||||
|
@ -62,6 +66,7 @@ impl From<HotseatPlayerRequest> 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<RwLock<Config>>,
|
||||
config: Arc<RwLock<Config>>,
|
||||
// state: Arc<RwLock<AppState>>,
|
||||
database: Arc<RwLock<Option<Database>>>,
|
||||
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,
|
||||
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<Database>> {
|
||||
self.library.read().unwrap()
|
||||
}
|
||||
|
||||
/*
|
||||
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
||||
match request {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String> for DatabasePath {
|
||||
impl From<String> 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(),
|
||||
|
|
|
@ -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<DatabasePath> = 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: (),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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 = "*" }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<schemalist>
|
||||
<schema id="com.luminescent-dreams.kifu-gtk.dev" path="/com/luminescent-dreams/kifu-gtk/dev/">
|
||||
<key name="database-path" type="s">
|
||||
<key name="library-path" type="s">
|
||||
<default>""</default>
|
||||
<summary>Path to the directory of games</summary>
|
||||
</key>
|
||||
|
@ -10,4 +10,4 @@
|
|||
<summary>Language override, use system settings if empty</summary>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
</schemalist>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<schemalist>
|
||||
<schema id="com.luminescent-dreams.kifu-gtk" path="/com/luminescent-dreams/kifu-gtk/">
|
||||
<key name="database-path" type="s">
|
||||
<key name="library-path" type="s">
|
||||
<default>""</default>
|
||||
<summary>Path to the directory of games</summary>
|
||||
</key>
|
||||
|
@ -10,4 +10,4 @@
|
|||
<summary>Language override, use system settings if empty</summary>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
||||
</schemalist>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<AppView>,
|
||||
|
||||
// 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<RwLock<Option<SettingsViewModel>>>,
|
||||
}
|
||||
|
||||
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<AppView>) {
|
||||
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<gtk::Widget>) -> adw::ViewStack {
|
||||
// self.content.set_child(Some(content));
|
||||
// }
|
||||
}
|
|
@ -1,10 +1,29 @@
|
|||
/*
|
||||
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<F, A>(trace_name: &str, f: F) -> A
|
||||
|
|
|
@ -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<CoreNotification>, 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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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<gtk::Widget>) {
|
||||
self.content.set_child(Some(content));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CoreNotification>,
|
||||
notification_observer: Arc<LocalObserver<CoreNotification>>,
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<CoreNotification>,
|
||||
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<LocalObserver<CoreNotification>>,
|
||||
pub widget: views::SettingsView,
|
||||
}
|
||||
|
||||
impl SettingsViewModel {
|
||||
fn new(core: Core) -> Self {
|
||||
pub fn new(parent: &impl IsA<gtk::Window>, 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
mod settings;
|
||||
pub use settings::SettingsView;
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<gtk::Window>,
|
||||
library_path: Option<LibraryPath>,
|
||||
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")))
|
||||
.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<SettingsPrivate>) @extends gtk::Frame, gtk::Widget, @implements gtk::Accessible, gtk::Orientable;
|
||||
}
|
||||
|
||||
impl SettingsView {
|
||||
pub fn new(
|
||||
parent: &impl IsA<gtk::Window>,
|
||||
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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue