feat: initial implementation of profile editing

This commit is contained in:
Gabriele Musco 2023-06-25 12:13:20 +02:00
commit 13a1a951a9
No known key found for this signature in database
GPG key ID: 1068D795C80E51DE
12 changed files with 368 additions and 63 deletions

32
Cargo.lock generated
View file

@ -1351,6 +1351,12 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "1.3.1" version = "1.3.1"
@ -1419,6 +1425,18 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core", "rand_core",
] ]
@ -1427,6 +1445,9 @@ name = "rand_core"
version = "0.6.4" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "raw-window-handle" name = "raw-window-handle"
@ -1588,6 +1609,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tracker", "tracker",
"uuid",
] ]
[[package]] [[package]]
@ -2042,6 +2064,16 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "uuid"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81"
dependencies = [
"getrandom",
"rand",
]
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View file

@ -33,3 +33,4 @@ serde = { version = "1.0.163", features = [
] } ] }
serde_json = "1.0.96" serde_json = "1.0.96"
tracker = "0.2.1" tracker = "0.2.1"
uuid = { version = "1.3.4", features = ["v4", "fast-rng"] }

View file

@ -8,7 +8,7 @@ use std::{fs::File, io::BufReader};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub selected_profile_name: String, pub selected_profile_uuid: String,
pub debug_view_enabled: bool, pub debug_view_enabled: bool,
pub user_profiles: Vec<Profile>, pub user_profiles: Vec<Profile>,
} }
@ -17,7 +17,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Config { Config {
// TODO: handle first start with no profile selected // TODO: handle first start with no profile selected
selected_profile_name: "".to_string(), selected_profile_uuid: "".to_string(),
debug_view_enabled: false, debug_view_enabled: false,
user_profiles: vec![], user_profiles: vec![],
} }
@ -26,7 +26,7 @@ impl Default for Config {
impl Config { impl Config {
pub fn get_selected_profile(&self, profiles: &Vec<Profile>) -> Profile { pub fn get_selected_profile(&self, profiles: &Vec<Profile>) -> Profile {
match profiles.iter().find(|p| {p.name == self.selected_profile_name}) { match profiles.iter().find(|p| {p.uuid == self.selected_profile_uuid}) {
Some(p) => p.clone(), Some(p) => p.clone(),
None => profiles.get(0).expect_dialog("No profiles found").clone(), None => profiles.get(0).expect_dialog("No profiles found").clone(),
} }
@ -65,6 +65,10 @@ impl Config {
pub fn get_config() -> Self { pub fn get_config() -> Self {
Self::from_path(Self::config_file_path()) Self::from_path(Self::config_file_path())
} }
pub fn set_profiles(&mut self, profiles: &Vec<Profile>) {
self.user_profiles = profiles.iter().filter(|p| p.editable).map(Profile::clone).collect();
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -5,6 +5,7 @@ use crate::{
use expect_dialog::ExpectDialog; use expect_dialog::ExpectDialog;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display, fs::File, io::BufReader, slice::Iter}; use std::{collections::HashMap, fmt::Display, fs::File, io::BufReader, slice::Iter};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum XRServiceType { pub enum XRServiceType {
@ -27,7 +28,8 @@ impl XRServiceType {
pub fn as_number(&self) -> u32 { pub fn as_number(&self) -> u32 {
match self { match self {
Self::Monado => 0, Self::Wivrn => 1, Self::Monado => 0,
Self::Wivrn => 1,
} }
} }
} }
@ -43,6 +45,7 @@ impl Display for XRServiceType {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Profile { pub struct Profile {
pub uuid: String,
pub name: String, pub name: String,
pub xrservice_path: String, pub xrservice_path: String,
pub xrservice_type: XRServiceType, pub xrservice_type: XRServiceType,
@ -69,6 +72,7 @@ impl Display for Profile {
impl Default for Profile { impl Default for Profile {
fn default() -> Self { fn default() -> Self {
Self { Self {
uuid: Uuid::new_v4().to_string(),
name: "Default profile name".into(), name: "Default profile name".into(),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,
@ -125,6 +129,26 @@ impl Profile {
let writer = get_writer(path_s); let writer = get_writer(path_s);
serde_json::to_writer_pretty(writer, self).expect_dialog("Could not write profile") serde_json::to_writer_pretty(writer, self).expect_dialog("Could not write profile")
} }
pub fn create_duplicate(&self) -> Self {
let mut dup = self.clone();
dup.uuid = Uuid::new_v4().to_string();
dup.editable = true;
dup.name = format!("Duplicate of {}", dup.name);
dup
}
pub fn validate(&self) -> bool {
!self.name.is_empty()
&& self.editable
&& !self.uuid.is_empty()
&& !self.xrservice_path.is_empty()
&& !self.prefix.is_empty()
&& (!self.libsurvive_enabled
|| self.libsurvive_path.as_ref().is_some_and(|p| !p.is_empty()))
&& (!self.basalt_enabled || self.basalt_path.as_ref().is_some_and(|p| !p.is_empty()))
&& (!self.mercury_enabled || self.mercury_path.as_ref().is_some_and(|p| !p.is_empty()))
}
} }
#[cfg(test)] #[cfg(test)]
@ -166,6 +190,7 @@ mod tests {
env.insert("XRT_COMPOSITOR_SCALE_PERCENTAGE".into(), "140".into()); env.insert("XRT_COMPOSITOR_SCALE_PERCENTAGE".into(), "140".into());
env.insert("XRT_COMPOSITOR_COMPUTE".into(), "1".into()); env.insert("XRT_COMPOSITOR_COMPUTE".into(), "1".into());
let p = Profile { let p = Profile {
uuid: "demo".into(),
name: "Demo profile".into(), name: "Demo profile".into(),
xrservice_path: String::from("/home/user/monado"), xrservice_path: String::from("/home/user/monado"),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,

View file

@ -8,6 +8,7 @@ pub fn system_valve_index_profile() -> Profile {
environment.insert("SURVIVE_GLOBALSCENESOLVER".into(), "0".into()); environment.insert("SURVIVE_GLOBALSCENESOLVER".into(), "0".into());
environment.insert("SURVIVE_TIMECODE_OFFSET_MS".into(), "-6.94".into()); environment.insert("SURVIVE_TIMECODE_OFFSET_MS".into(), "-6.94".into());
Profile { Profile {
uuid: "system-valve-index-default".into(),
name: format!("Valve Index (System) - {name} Default", name = APP_NAME), name: format!("Valve Index (System) - {name} Default", name = APP_NAME),
opencomposite_path: data_opencomposite_path(), opencomposite_path: data_opencomposite_path(),
xrservice_path: "".into(), xrservice_path: "".into(),

View file

@ -11,6 +11,7 @@ pub fn valve_index_profile() -> Profile {
environment.insert("SURVIVE_TIMECODE_OFFSET_MS".into(), "-6.94".into()); environment.insert("SURVIVE_TIMECODE_OFFSET_MS".into(), "-6.94".into());
environment.insert("LD_LIBRARY_PATH".into(), format!("{pfx}/lib", pfx = prefix)); environment.insert("LD_LIBRARY_PATH".into(), format!("{pfx}/lib", pfx = prefix));
Profile { Profile {
uuid: "valve-index-default".into(),
name: format!("Valve Index - {name} Default", name = APP_NAME), name: format!("Valve Index - {name} Default", name = APP_NAME),
xrservice_path: data_monado_path(), xrservice_path: data_monado_path(),
xrservice_type: XRServiceType::Monado, xrservice_type: XRServiceType::Monado,

View file

@ -8,6 +8,7 @@ pub fn wivrn_profile() -> Profile {
let mut environment: HashMap<String, String> = HashMap::new(); let mut environment: HashMap<String, String> = HashMap::new();
environment.insert("LD_LIBRARY_PATH".into(), format!("{pfx}/lib", pfx = prefix)); environment.insert("LD_LIBRARY_PATH".into(), format!("{pfx}/lib", pfx = prefix));
Profile { Profile {
uuid: "wivrn-default".into(),
name: format!("WiVRn - {name} Default", name = APP_NAME), name: format!("WiVRn - {name} Default", name = APP_NAME),
xrservice_path: data_wivrn_path(), xrservice_path: data_wivrn_path(),
xrservice_type: XRServiceType::Wivrn, xrservice_type: XRServiceType::Wivrn,

View file

@ -29,7 +29,7 @@ use crate::ui::libsurvive_setup_window::LibsurviveSetupMsg;
use crate::ui::main_view::{MainView, MainViewInit, MainViewOutMsg}; use crate::ui::main_view::{MainView, MainViewInit, MainViewOutMsg};
use expect_dialog::ExpectDialog; use expect_dialog::ExpectDialog;
use gtk::prelude::*; use gtk::prelude::*;
use relm4::actions::{ActionGroupName, RelmAction, RelmActionGroup, AccelsPlus}; use relm4::actions::{AccelsPlus, ActionGroupName, RelmAction, RelmActionGroup};
use relm4::adw::traits::MessageDialogExt; use relm4::adw::traits::MessageDialogExt;
use relm4::adw::ResponseAppearance; use relm4::adw::ResponseAppearance;
use relm4::gtk::glib; use relm4::gtk::glib;
@ -77,7 +77,9 @@ pub enum Msg {
BuildProfile, BuildProfile,
EnableDebugViewChanged(bool), EnableDebugViewChanged(bool),
DoStartStopXRService, DoStartStopXRService,
ProfileSelected(String), ProfileSelected(Profile),
DeleteProfile,
SaveProfile(Profile),
RunSetCap, RunSetCap,
OpenLibsurviveSetup, OpenLibsurviveSetup,
Quit, Quit,
@ -103,6 +105,17 @@ impl App {
runner.start(); runner.start();
self.xrservice_runner = Some(runner); self.xrservice_runner = Some(runner);
} }
pub fn profiles_list(config: &Config) -> Vec<Profile> {
let mut profiles = vec![
valve_index_profile(),
system_valve_index_profile(),
wivrn_profile(),
];
profiles.extend(config.user_profiles.clone());
profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
profiles
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -196,7 +209,9 @@ impl SimpleComponent for App {
); );
// apparently setcap on wivrn causes issues, so in case // apparently setcap on wivrn causes issues, so in case
// it's not monado, we're just skipping this // it's not monado, we're just skipping this
if self.get_selected_profile().xrservice_type == XRServiceType::Monado { if self.get_selected_profile().xrservice_type
== XRServiceType::Monado
{
self.setcap_confirm_dialog.present(); self.setcap_confirm_dialog.present();
} }
self.build_window self.build_window
@ -320,6 +335,47 @@ impl SimpleComponent for App {
.emit(BuildWindowMsg::UpdateCanClose(false)); .emit(BuildWindowMsg::UpdateCanClose(false));
self.build_pipeline = Some(pipeline); self.build_pipeline = Some(pipeline);
} }
Msg::DeleteProfile => {
let todel = self.get_selected_profile();
if todel.editable {
self.config.user_profiles = self
.config
.user_profiles
.iter()
.filter(|p| p.uuid != todel.uuid)
.map(|p| p.clone())
.collect();
self.config.save();
self.profiles = Self::profiles_list(&self.config);
self.main_view.sender().emit(MainViewMsg::UpdateSelectedProfile(
self.get_selected_profile()
));
self.main_view
.sender()
.emit(MainViewMsg::UpdateProfiles(
self.profiles.clone(),
self.config.clone(),
))
}
}
Msg::SaveProfile(prof) => {
match self.profiles.iter().position(|p| p.uuid == prof.uuid) {
None => {},
Some(index) => {
self.profiles.remove(index);
}
}
self.profiles.push(prof);
self.profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
self.config.set_profiles(&self.profiles);
self.config.save();
self.main_view
.sender()
.emit(MainViewMsg::UpdateProfiles(
self.profiles.clone(),
self.config.clone(),
))
}
Msg::RunSetCap => { Msg::RunSetCap => {
if !check_dependency(pkexec_dep()) { if !check_dependency(pkexec_dep()) {
println!("pkexec not found, skipping setcap"); println!("pkexec not found, skipping setcap");
@ -331,11 +387,11 @@ impl SimpleComponent for App {
)); ));
} }
} }
Msg::ProfileSelected(prof_name) => { Msg::ProfileSelected(prof) => {
if prof_name == self.config.selected_profile_name { if prof.uuid == self.config.selected_profile_uuid {
return; return;
} }
self.config.selected_profile_name = prof_name; self.config.selected_profile_uuid = prof.uuid;
self.config.save(); self.config.save();
let profile = self.get_selected_profile(); let profile = self.get_selected_profile();
self.main_view self.main_view
@ -349,7 +405,7 @@ impl SimpleComponent for App {
self.get_selected_profile().clone(), self.get_selected_profile().clone(),
)) ))
.expect_dialog("Failed to present Libsurvive Setup Window"); .expect_dialog("Failed to present Libsurvive Setup Window");
}, }
Msg::Quit => { Msg::Quit => {
self.application.quit(); self.application.quit();
} }
@ -362,12 +418,7 @@ impl SimpleComponent for App {
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let config = Config::get_config(); let config = Config::get_config();
let mut profiles = vec![ let profiles = Self::profiles_list(&config);
valve_index_profile(),
system_valve_index_profile(),
wivrn_profile(),
];
profiles.extend(config.user_profiles.clone());
let dependencies_dialog = adw::MessageDialog::builder() let dependencies_dialog = adw::MessageDialog::builder()
.modal(true) .modal(true)
.transient_for(root) .transient_for(root)
@ -410,7 +461,9 @@ impl SimpleComponent for App {
.forward(sender.input_sender(), |message| match message { .forward(sender.input_sender(), |message| match message {
MainViewOutMsg::EnableDebugViewChanged(val) => Msg::EnableDebugViewChanged(val), MainViewOutMsg::EnableDebugViewChanged(val) => Msg::EnableDebugViewChanged(val),
MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService, MainViewOutMsg::DoStartStopXRService => Msg::DoStartStopXRService,
MainViewOutMsg::ProfileSelected(name) => Msg::ProfileSelected(name), MainViewOutMsg::ProfileSelected(uuid) => Msg::ProfileSelected(uuid),
MainViewOutMsg::DeleteProfile => Msg::DeleteProfile,
MainViewOutMsg::SaveProfile(p) => Msg::SaveProfile(p),
}), }),
debug_view: DebugView::builder() debug_view: DebugView::builder()
.launch(DebugViewInit { .launch(DebugViewInit {
@ -492,13 +545,15 @@ impl SimpleComponent for App {
actions.add_action(libsurvive_setup_action); actions.add_action(libsurvive_setup_action);
root.insert_action_group(AppActionGroup::NAME, Some(&actions.into_action_group())); root.insert_action_group(AppActionGroup::NAME, Some(&actions.into_action_group()));
model.application.set_accelerators_for_action::<QuitAction>(&["<Control>q"]); model
.application
.set_accelerators_for_action::<QuitAction>(&["<Control>q"]);
model model
.main_view .main_view
.sender() .sender()
.emit(MainViewMsg::UpdateProfileNames( .emit(MainViewMsg::UpdateProfiles(
model.profiles.iter().map(|p| p.clone().name).collect(), model.profiles.clone(),
model.config.clone(), model.config.clone(),
)); ));

View file

@ -8,8 +8,9 @@ use crate::ui::profile_editor::ProfileEditorMsg;
pub struct PathModel { pub struct PathModel {
name: String, name: String,
key: String, key: String,
value: String, value: Option<String>,
path_label: gtk::Label, path_label: gtk::Label,
filedialog: gtk::FileDialog,
} }
pub struct PathModelInit { pub struct PathModelInit {
@ -20,14 +21,14 @@ pub struct PathModelInit {
#[derive(Debug)] #[derive(Debug)]
pub enum PathModelMsg { pub enum PathModelMsg {
Changed(String), Changed(Option<String>),
OpenFileChooser, OpenFileChooser,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum PathModelOutMsg { pub enum PathModelOutMsg {
/** key, value */ /** key, value */
Changed(String, String), Changed(String, Option<String>),
} }
#[relm4::factory(pub)] #[relm4::factory(pub)]
@ -47,41 +48,77 @@ impl FactoryComponent for PathModel {
set_icon_name: Some("folder-open-symbolic"), set_icon_name: Some("folder-open-symbolic"),
add_suffix: &self.path_label, add_suffix: &self.path_label,
set_activatable: true, set_activatable: true,
add_suffix: unset_btn = &gtk::Button {
set_valign: gtk::Align::Center,
add_css_class: "flat",
add_css_class: "circular",
set_icon_name: "edit-clear",
set_tooltip_text: Some("Clear Path"),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::Changed(None));
}
},
connect_activated[sender] => move |_| { connect_activated[sender] => move |_| {
sender.input(Self::Input::OpenFileChooser) sender.input(Self::Input::OpenFileChooser)
} },
} }
} }
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) { fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message { match message {
Self::Input::Changed(val) => { Self::Input::Changed(val) => {
self.value = val.clone(); self.value = val;
self.path_label.set_label(val.as_str()); self.path_label.set_label(match self.value.as_ref() {
Some(val) => val.as_str(),
None => "(None)".into(),
});
sender sender
.output_sender() .output_sender()
.emit(Self::Output::Changed(self.key.clone(), val)) .emit(Self::Output::Changed(self.key.clone(), self.value.clone()))
} }
Self::Input::OpenFileChooser => { Self::Input::OpenFileChooser => {
println!("file chooser"); let fd_sender = sender.clone();
self.filedialog.select_folder(
self.init_root().root().and_downcast_ref::<gtk::Window>(),
gtk::gio::Cancellable::NONE,
move |res| match res {
Ok(file) => {
let path = file.path();
if path.is_some() {
fd_sender.input(Self::Input::Changed(
Some(path.unwrap().to_str().unwrap().to_string())
));
}
}
_ => {}
},
);
} }
} }
} }
fn forward_to_parent(_output: Self::Output) -> Option<Self::ParentInput> { fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
None Some(match output {
Self::Output::Changed(key, value) => ProfileEditorMsg::PathChanged(key, value)
})
} }
fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> Self { fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> Self {
Self { Self {
name: init.name, name: init.name.clone(),
key: init.key, key: init.key,
value: match init.value.as_ref() { value: init.value.clone(),
Some(val) => val.clone(), None => "".into() path_label: gtk::Label::builder()
}, .label(match init.value {
path_label: gtk::Label::builder().label(match init.value { Some(val) => val,
Some(val) => val, None => "(None)".into() None => "(None)".into(),
}).ellipsize(gtk::pango::EllipsizeMode::Start).build(), })
.ellipsize(gtk::pango::EllipsizeMode::Start)
.build(),
filedialog: gtk::FileDialog::builder()
.modal(true)
.title(format!("Select Path for {}", init.name.clone()))
.build(),
} }
} }
} }

View file

@ -71,8 +71,10 @@ impl FactoryComponent for SwitchModel {
} }
} }
fn forward_to_parent(_output: Self::Output) -> Option<Self::ParentInput> { fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
None Some(match output {
Self::Output::Changed(key, value) => ProfileEditorMsg::SwitchChanged(key, value)
})
} }
fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> Self { fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> Self {

View file

@ -1,5 +1,5 @@
use super::install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg}; use super::install_wivrn_box::{InstallWivrnBox, InstallWivrnBoxInit, InstallWivrnBoxMsg};
use super::profile_editor::{ProfileEditor, ProfileEditorMsg}; use super::profile_editor::{ProfileEditor, ProfileEditorMsg, ProfileEditorOutMsg};
use super::runtime_switcher_box::{ use super::runtime_switcher_box::{
RuntimeSwitcherBox, RuntimeSwitcherBoxInit, RuntimeSwitcherBoxMsg, RuntimeSwitcherBox, RuntimeSwitcherBoxInit, RuntimeSwitcherBoxMsg,
}; };
@ -22,7 +22,7 @@ use relm4_icons::icon_name;
pub struct MainView { pub struct MainView {
xrservice_active: bool, xrservice_active: bool,
enable_debug_view: bool, enable_debug_view: bool,
profile_names: Vec<String>, profiles: Vec<Profile>,
selected_profile: Profile, selected_profile: Profile,
#[tracker::do_not_track] #[tracker::do_not_track]
profiles_dropdown: Option<gtk::DropDown>, profiles_dropdown: Option<gtk::DropDown>,
@ -36,6 +36,8 @@ pub struct MainView {
profile_editor: Controller<ProfileEditor>, profile_editor: Controller<ProfileEditor>,
#[tracker::do_not_track] #[tracker::do_not_track]
profile_not_editable_dialog: adw::MessageDialog, profile_not_editable_dialog: adw::MessageDialog,
#[tracker::do_not_track]
profile_delete_confirm_dialog: adw::MessageDialog,
} }
#[derive(Debug)] #[derive(Debug)]
@ -44,20 +46,24 @@ pub enum MainViewMsg {
StartStopClicked, StartStopClicked,
XRServiceActiveChanged(bool, Option<Profile>), XRServiceActiveChanged(bool, Option<Profile>),
EnableDebugViewChanged(bool), EnableDebugViewChanged(bool),
UpdateProfileNames(Vec<String>, Config), UpdateProfiles(Vec<Profile>, Config),
SetSelectedProfile(u32), SetSelectedProfile(u32),
ProfileSelected(u32), ProfileSelected(u32),
UpdateSelectedProfile(Profile), UpdateSelectedProfile(Profile),
EditProfile, EditProfile,
CreateProfile, CreateProfile,
DeleteProfile,
DuplicateProfile, DuplicateProfile,
SaveProfile(Profile),
} }
#[derive(Debug)] #[derive(Debug)]
pub enum MainViewOutMsg { pub enum MainViewOutMsg {
EnableDebugViewChanged(bool), EnableDebugViewChanged(bool),
DoStartStopXRService, DoStartStopXRService,
ProfileSelected(String), ProfileSelected(Profile),
DeleteProfile,
SaveProfile(Profile),
} }
pub struct MainViewInit { pub struct MainViewInit {
@ -156,9 +162,9 @@ impl SimpleComponent for MainView {
gtk::DropDown { gtk::DropDown {
set_hexpand: true, set_hexpand: true,
set_tooltip_text: Some("Profiles"), set_tooltip_text: Some("Profiles"),
#[track = "model.changed(Self::profile_names())"] #[track = "model.changed(Self::profiles())"]
set_model: Some(&{ set_model: Some(&{
let names: Vec<_> = model.profile_names.iter().map(String::as_str).collect(); let names: Vec<_> = model.profiles.iter().map(|p| p.name.as_str()).collect();
gtk::StringList::new(&names) gtk::StringList::new(&names)
}), }),
connect_selected_item_notify[sender] => move |this| { connect_selected_item_notify[sender] => move |this| {
@ -178,8 +184,18 @@ impl SimpleComponent for MainView {
connect_clicked[sender] => move |_| { connect_clicked[sender] => move |_| {
sender.input(Self::Input::CreateProfile); sender.input(Self::Input::CreateProfile);
} }
},
gtk::Button {
set_icon_name: "edit-delete-symbolic",
add_css_class: "destructive-action",
set_tooltip_text: Some("Delete Profile"),
#[track = "model.changed(Self::selected_profile())"]
set_sensitive: model.selected_profile.editable,
connect_clicked[sender] => move |_| {
sender.input(Self::Input::DeleteProfile);
} }
}, },
},
} }
} }
} }
@ -222,17 +238,17 @@ impl SimpleComponent for MainView {
.sender() .sender()
.emit(RuntimeSwitcherBoxMsg::UpdateSelectedProfile(prof.clone())); .emit(RuntimeSwitcherBoxMsg::UpdateSelectedProfile(prof.clone()));
} }
Self::Input::UpdateProfileNames(names, config) => { Self::Input::UpdateProfiles(profiles, config) => {
self.set_profile_names(names); self.set_profiles(profiles);
// why send another message to set the dropdown selection? // why send another message to set the dropdown selection?
// set_* from tracker likely updates the view obj in the next // set_* from tracker likely updates the view obj in the next
// draw, so selecting here will result in nothing cause the // draw, so selecting here will result in nothing cause the
// dropdown is effectively empty // dropdown is effectively empty
sender.input(MainViewMsg::SetSelectedProfile({ sender.input(MainViewMsg::SetSelectedProfile({
let pos = self let pos = self
.profile_names .profiles
.iter() .iter()
.position(|p| p.clone() == config.selected_profile_name); .position(|p| p.uuid == config.selected_profile_uuid);
match pos { match pos {
Some(idx) => idx as u32, Some(idx) => idx as u32,
None => 0, None => 0,
@ -247,10 +263,8 @@ impl SimpleComponent for MainView {
.set_selected(index); .set_selected(index);
} }
Self::Input::ProfileSelected(position) => { Self::Input::ProfileSelected(position) => {
// self.install_wivrn_box.sender.emit(InstallWivrnBoxMsg::Upda);
// TODO: send profile to install_wivrn_box
sender.output(MainViewOutMsg::ProfileSelected( sender.output(MainViewOutMsg::ProfileSelected(
self.profile_names.get(position as usize).unwrap().clone(), self.profiles.get(position as usize).unwrap().clone(),
)); ));
} }
Self::Input::EditProfile => { Self::Input::EditProfile => {
@ -262,10 +276,20 @@ impl SimpleComponent for MainView {
} }
} }
Self::Input::CreateProfile => { Self::Input::CreateProfile => {
println!("create"); self.profile_editor
.sender()
.emit(ProfileEditorMsg::Present(Profile::default()));
}
Self::Input::DeleteProfile => {
self.profile_delete_confirm_dialog.present();
}
Self::Input::SaveProfile(prof) => {
sender.output(Self::Output::SaveProfile(prof));
} }
Self::Input::DuplicateProfile => { Self::Input::DuplicateProfile => {
println!("dup"); self.profile_editor.sender().emit(ProfileEditorMsg::Present(
self.selected_profile.create_duplicate(),
));
} }
} }
} }
@ -277,12 +301,12 @@ impl SimpleComponent for MainView {
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let profile_not_editable_dialog = adw::MessageDialog::builder() let profile_not_editable_dialog = adw::MessageDialog::builder()
.modal(true) .modal(true)
.hide_on_close(true)
.heading("This profile is not editable") .heading("This profile is not editable")
.body(concat!( .body(concat!(
"You can duplicate it and edit the new copy. ", "You can duplicate it and edit the new copy. ",
"Do you want to duplicate the current profile?" "Do you want to duplicate the current profile?"
)) ))
.hide_on_close(true)
.build(); .build();
profile_not_editable_dialog.add_response("no", "_No"); profile_not_editable_dialog.add_response("no", "_No");
profile_not_editable_dialog.add_response("yes", "_Yes"); profile_not_editable_dialog.add_response("yes", "_Yes");
@ -298,11 +322,31 @@ impl SimpleComponent for MainView {
}); });
} }
let profile_delete_confirm_dialog = adw::MessageDialog::builder()
.modal(true)
.hide_on_close(true)
.heading("Are you sure you want to delete this profile?")
.build();
profile_delete_confirm_dialog.add_response("no", "_No");
profile_delete_confirm_dialog.add_response("yes", "_Yes");
profile_delete_confirm_dialog
.set_response_appearance("no", ResponseAppearance::Destructive);
profile_delete_confirm_dialog.set_response_appearance("yes", ResponseAppearance::Suggested);
{
let pdc_sender = sender.clone();
profile_delete_confirm_dialog.connect_response(None, move |_, res| {
if res == "yes" {
pdc_sender.output(Self::Output::DeleteProfile);
}
});
}
let mut model = Self { let mut model = Self {
xrservice_active: false, xrservice_active: false,
enable_debug_view: init.config.debug_view_enabled, enable_debug_view: init.config.debug_view_enabled,
profiles_dropdown: None, profiles_dropdown: None,
profile_names: vec![], profiles: vec![],
steam_launch_options_box: SteamLaunchOptionsBox::builder().launch(()).detach(), steam_launch_options_box: SteamLaunchOptionsBox::builder().launch(()).detach(),
install_wivrn_box: InstallWivrnBox::builder() install_wivrn_box: InstallWivrnBox::builder()
.launch(InstallWivrnBoxInit { .launch(InstallWivrnBoxInit {
@ -316,9 +360,12 @@ impl SimpleComponent for MainView {
.detach(), .detach(),
profile_editor: ProfileEditor::builder() profile_editor: ProfileEditor::builder()
.launch(ProfileEditorInit {}) .launch(ProfileEditorInit {})
.detach(), .forward(sender.input_sender(), |message| match message {
ProfileEditorOutMsg::SaveProfile(p) => Self::Input::SaveProfile(p),
}),
selected_profile: init.selected_profile.clone(), selected_profile: init.selected_profile.clone(),
profile_not_editable_dialog, profile_not_editable_dialog,
profile_delete_confirm_dialog,
tracker: 0, tracker: 0,
}; };
let widgets = view_output!(); let widgets = view_output!();

View file

@ -35,8 +35,17 @@ pub struct ProfileEditor {
pub enum ProfileEditorMsg { pub enum ProfileEditorMsg {
Present(Profile), Present(Profile),
EntryChanged(String, String), EntryChanged(String, String),
TextChanged(String, String),
PathChanged(String, Option<String>),
SwitchChanged(String, bool),
ComboChanged(String, String),
AddEnvVar, AddEnvVar,
SaveProfile, // ? SaveProfile,
}
#[derive(Debug)]
pub enum ProfileEditorOutMsg {
SaveProfile(Profile),
} }
pub struct ProfileEditorInit {} pub struct ProfileEditorInit {}
@ -45,7 +54,7 @@ pub struct ProfileEditorInit {}
impl SimpleComponent for ProfileEditor { impl SimpleComponent for ProfileEditor {
type Init = ProfileEditorInit; type Init = ProfileEditorInit;
type Input = ProfileEditorMsg; type Input = ProfileEditorMsg;
type Output = (); type Output = ProfileEditorOutMsg;
view! { view! {
#[name(win)] #[name(win)]
@ -66,6 +75,21 @@ impl SimpleComponent for ProfileEditor {
add: model.env_rows.widget(), add: model.env_rows.widget(),
add: model.switch_rows.widget(), add: model.switch_rows.widget(),
add: model.path_rows.widget(), add: model.path_rows.widget(),
add: save_grp = &adw::PreferencesGroup {
add: save_box = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
gtk::Button {
set_halign: gtk::Align::Center,
set_label: "Save",
add_css_class: "pill",
add_css_class: "suggested-action",
connect_clicked[sender] => move |_| {
sender.input(Self::Input::SaveProfile);
},
},
}
},
} }
} }
} }
@ -135,20 +159,71 @@ impl SimpleComponent for ProfileEditor {
key: "mercury_path".into(), key: "mercury_path".into(),
value: p.mercury_path, value: p.mercury_path,
}); });
self.path_rows.guard().push_back(PathModelInit {
name: "Install Prefix".into(),
key: "prefix".into(),
value: Some(p.prefix),
});
self.set_profile(Some(prof.clone())); self.set_profile(Some(prof.clone()));
self.win.as_ref().unwrap().present(); self.win.as_ref().unwrap().present();
} }
Self::Input::SaveProfile => {} Self::Input::SaveProfile => {
let prof = self.profile.as_ref().unwrap();
if prof.validate() {
sender.output(ProfileEditorOutMsg::SaveProfile(prof.clone()));
self.win.as_ref().unwrap().close();
} else {
self.win.as_ref().unwrap().add_toast(adw::Toast::builder().title("Profile failed validation").build());
}
}
// TODO: rename to reflect this is only for env
// + make entryfactory take a signal to send to parent
Self::Input::EntryChanged(name, value) => { Self::Input::EntryChanged(name, value) => {
println!("{}: {}", name, value);
self.profile self.profile
.as_mut() .as_mut()
.unwrap() .unwrap()
.environment .environment
.insert(name, value); .insert(name, value);
} }
Self::Input::PathChanged(key, value) => {
let prof = self.profile.as_mut().unwrap();
match key.as_str() {
"xrservice_path" => prof.xrservice_path = value.unwrap_or("".to_string()),
"opencomposite_path" => {
prof.opencomposite_path = value.unwrap_or("".to_string())
}
"libsurvive_path" => prof.libsurvive_path = value,
"basalt_path" => prof.basalt_path = value,
"mercury_path" => prof.mercury_path = value,
"prefix" => prof.prefix = value.unwrap_or("".to_string()),
_ => panic!("Unknown profile path key"),
}
}
Self::Input::SwitchChanged(key, value) => {
let prof = self.profile.as_mut().unwrap();
match key.as_str() {
"libsurvive_enabled" => prof.libsurvive_enabled = value,
"basalt_enabled" => prof.basalt_enabled = value,
"mercury_enabled" => prof.mercury_enabled = value,
_ => panic!("Unknown profile switch key"),
}
}
Self::Input::TextChanged(key, value) => {
let prof = self.profile.as_mut().unwrap();
match key.as_str() {
"name" => prof.name = value,
_ => panic!("Unknown profile text key"),
}
}
Self::Input::ComboChanged(key, value) => {
let prof = self.profile.as_mut().unwrap();
match key.as_str() {
"xrservice_type" => prof.xrservice_type = XRServiceType::from_string(value),
_ => panic!("Unknown profile text key"),
}
}
Self::Input::AddEnvVar => { Self::Input::AddEnvVar => {
println!("Add env var"); println!("Add env var");
} }
@ -218,6 +293,30 @@ impl SimpleComponent for ProfileEditor {
tracker: 0, tracker: 0,
}; };
{
let name_sender = sender.clone();
model.name_row.connect_changed(move |nr| {
name_sender.input(Self::Input::TextChanged(
"name".to_string(),
nr.text().to_string(),
));
});
}
{
let type_sender = sender.clone();
model.type_row.connect_selected_item_notify(move |tr| {
type_sender.input(Self::Input::ComboChanged(
"xrservice_type".to_string(),
match tr.selected() {
0 => "monado".to_string(),
1 => "wivrn".to_string(),
_ => panic!("XRServiceType combo row cannot have more than 2 choices"),
},
));
});
}
let widgets = view_output!(); let widgets = view_output!();
model.win = Some(widgets.win.clone()); model.win = Some(widgets.win.clone());