mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-04-20 11:35:48 +00:00
feat: initial implementation of profile editing
This commit is contained in:
parent
6440a4f8b4
commit
13a1a951a9
12 changed files with 368 additions and 63 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
@ -1351,6 +1351,12 @@ version = "0.3.27"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
|
@ -1419,6 +1425,18 @@ version = "0.8.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
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",
|
||||
]
|
||||
|
||||
|
@ -1427,6 +1445,9 @@ name = "rand_core"
|
|||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
|
@ -1588,6 +1609,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tracker",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2042,6 +2064,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
|
|
@ -33,3 +33,4 @@ serde = { version = "1.0.163", features = [
|
|||
] }
|
||||
serde_json = "1.0.96"
|
||||
tracker = "0.2.1"
|
||||
uuid = { version = "1.3.4", features = ["v4", "fast-rng"] }
|
||||
|
|
|
@ -8,7 +8,7 @@ use std::{fs::File, io::BufReader};
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub selected_profile_name: String,
|
||||
pub selected_profile_uuid: String,
|
||||
pub debug_view_enabled: bool,
|
||||
pub user_profiles: Vec<Profile>,
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ impl Default for Config {
|
|||
fn default() -> Self {
|
||||
Config {
|
||||
// TODO: handle first start with no profile selected
|
||||
selected_profile_name: "".to_string(),
|
||||
selected_profile_uuid: "".to_string(),
|
||||
debug_view_enabled: false,
|
||||
user_profiles: vec![],
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ impl Default for Config {
|
|||
|
||||
impl Config {
|
||||
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(),
|
||||
None => profiles.get(0).expect_dialog("No profiles found").clone(),
|
||||
}
|
||||
|
@ -65,6 +65,10 @@ impl Config {
|
|||
pub fn get_config() -> Self {
|
||||
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)]
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::{
|
|||
use expect_dialog::ExpectDialog;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fmt::Display, fs::File, io::BufReader, slice::Iter};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum XRServiceType {
|
||||
|
@ -27,7 +28,8 @@ impl XRServiceType {
|
|||
|
||||
pub fn as_number(&self) -> u32 {
|
||||
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)]
|
||||
pub struct Profile {
|
||||
pub uuid: String,
|
||||
pub name: String,
|
||||
pub xrservice_path: String,
|
||||
pub xrservice_type: XRServiceType,
|
||||
|
@ -69,6 +72,7 @@ impl Display for Profile {
|
|||
impl Default for Profile {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
uuid: Uuid::new_v4().to_string(),
|
||||
name: "Default profile name".into(),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
|
@ -125,6 +129,26 @@ impl Profile {
|
|||
let writer = get_writer(path_s);
|
||||
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)]
|
||||
|
@ -166,6 +190,7 @@ mod tests {
|
|||
env.insert("XRT_COMPOSITOR_SCALE_PERCENTAGE".into(), "140".into());
|
||||
env.insert("XRT_COMPOSITOR_COMPUTE".into(), "1".into());
|
||||
let p = Profile {
|
||||
uuid: "demo".into(),
|
||||
name: "Demo profile".into(),
|
||||
xrservice_path: String::from("/home/user/monado"),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
|
|
|
@ -8,6 +8,7 @@ pub fn system_valve_index_profile() -> Profile {
|
|||
environment.insert("SURVIVE_GLOBALSCENESOLVER".into(), "0".into());
|
||||
environment.insert("SURVIVE_TIMECODE_OFFSET_MS".into(), "-6.94".into());
|
||||
Profile {
|
||||
uuid: "system-valve-index-default".into(),
|
||||
name: format!("Valve Index (System) - {name} Default", name = APP_NAME),
|
||||
opencomposite_path: data_opencomposite_path(),
|
||||
xrservice_path: "".into(),
|
||||
|
|
|
@ -11,6 +11,7 @@ pub fn valve_index_profile() -> Profile {
|
|||
environment.insert("SURVIVE_TIMECODE_OFFSET_MS".into(), "-6.94".into());
|
||||
environment.insert("LD_LIBRARY_PATH".into(), format!("{pfx}/lib", pfx = prefix));
|
||||
Profile {
|
||||
uuid: "valve-index-default".into(),
|
||||
name: format!("Valve Index - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_monado_path(),
|
||||
xrservice_type: XRServiceType::Monado,
|
||||
|
|
|
@ -8,6 +8,7 @@ pub fn wivrn_profile() -> Profile {
|
|||
let mut environment: HashMap<String, String> = HashMap::new();
|
||||
environment.insert("LD_LIBRARY_PATH".into(), format!("{pfx}/lib", pfx = prefix));
|
||||
Profile {
|
||||
uuid: "wivrn-default".into(),
|
||||
name: format!("WiVRn - {name} Default", name = APP_NAME),
|
||||
xrservice_path: data_wivrn_path(),
|
||||
xrservice_type: XRServiceType::Wivrn,
|
||||
|
|
|
@ -29,7 +29,7 @@ use crate::ui::libsurvive_setup_window::LibsurviveSetupMsg;
|
|||
use crate::ui::main_view::{MainView, MainViewInit, MainViewOutMsg};
|
||||
use expect_dialog::ExpectDialog;
|
||||
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::ResponseAppearance;
|
||||
use relm4::gtk::glib;
|
||||
|
@ -77,7 +77,9 @@ pub enum Msg {
|
|||
BuildProfile,
|
||||
EnableDebugViewChanged(bool),
|
||||
DoStartStopXRService,
|
||||
ProfileSelected(String),
|
||||
ProfileSelected(Profile),
|
||||
DeleteProfile,
|
||||
SaveProfile(Profile),
|
||||
RunSetCap,
|
||||
OpenLibsurviveSetup,
|
||||
Quit,
|
||||
|
@ -103,6 +105,17 @@ impl App {
|
|||
runner.start();
|
||||
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)]
|
||||
|
@ -196,7 +209,9 @@ impl SimpleComponent for App {
|
|||
);
|
||||
// apparently setcap on wivrn causes issues, so in case
|
||||
// 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.build_window
|
||||
|
@ -320,6 +335,47 @@ impl SimpleComponent for App {
|
|||
.emit(BuildWindowMsg::UpdateCanClose(false));
|
||||
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 => {
|
||||
if !check_dependency(pkexec_dep()) {
|
||||
println!("pkexec not found, skipping setcap");
|
||||
|
@ -331,11 +387,11 @@ impl SimpleComponent for App {
|
|||
));
|
||||
}
|
||||
}
|
||||
Msg::ProfileSelected(prof_name) => {
|
||||
if prof_name == self.config.selected_profile_name {
|
||||
Msg::ProfileSelected(prof) => {
|
||||
if prof.uuid == self.config.selected_profile_uuid {
|
||||
return;
|
||||
}
|
||||
self.config.selected_profile_name = prof_name;
|
||||
self.config.selected_profile_uuid = prof.uuid;
|
||||
self.config.save();
|
||||
let profile = self.get_selected_profile();
|
||||
self.main_view
|
||||
|
@ -349,7 +405,7 @@ impl SimpleComponent for App {
|
|||
self.get_selected_profile().clone(),
|
||||
))
|
||||
.expect_dialog("Failed to present Libsurvive Setup Window");
|
||||
},
|
||||
}
|
||||
Msg::Quit => {
|
||||
self.application.quit();
|
||||
}
|
||||
|
@ -362,12 +418,7 @@ impl SimpleComponent for App {
|
|||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let config = Config::get_config();
|
||||
let mut profiles = vec![
|
||||
valve_index_profile(),
|
||||
system_valve_index_profile(),
|
||||
wivrn_profile(),
|
||||
];
|
||||
profiles.extend(config.user_profiles.clone());
|
||||
let profiles = Self::profiles_list(&config);
|
||||
let dependencies_dialog = adw::MessageDialog::builder()
|
||||
.modal(true)
|
||||
.transient_for(root)
|
||||
|
@ -410,7 +461,9 @@ impl SimpleComponent for App {
|
|||
.forward(sender.input_sender(), |message| match message {
|
||||
MainViewOutMsg::EnableDebugViewChanged(val) => Msg::EnableDebugViewChanged(val),
|
||||
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()
|
||||
.launch(DebugViewInit {
|
||||
|
@ -492,13 +545,15 @@ impl SimpleComponent for App {
|
|||
actions.add_action(libsurvive_setup_action);
|
||||
|
||||
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
|
||||
.main_view
|
||||
.sender()
|
||||
.emit(MainViewMsg::UpdateProfileNames(
|
||||
model.profiles.iter().map(|p| p.clone().name).collect(),
|
||||
.emit(MainViewMsg::UpdateProfiles(
|
||||
model.profiles.clone(),
|
||||
model.config.clone(),
|
||||
));
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@ use crate::ui::profile_editor::ProfileEditorMsg;
|
|||
pub struct PathModel {
|
||||
name: String,
|
||||
key: String,
|
||||
value: String,
|
||||
value: Option<String>,
|
||||
path_label: gtk::Label,
|
||||
filedialog: gtk::FileDialog,
|
||||
}
|
||||
|
||||
pub struct PathModelInit {
|
||||
|
@ -20,14 +21,14 @@ pub struct PathModelInit {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub enum PathModelMsg {
|
||||
Changed(String),
|
||||
Changed(Option<String>),
|
||||
OpenFileChooser,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PathModelOutMsg {
|
||||
/** key, value */
|
||||
Changed(String, String),
|
||||
Changed(String, Option<String>),
|
||||
}
|
||||
|
||||
#[relm4::factory(pub)]
|
||||
|
@ -47,41 +48,77 @@ impl FactoryComponent for PathModel {
|
|||
set_icon_name: Some("folder-open-symbolic"),
|
||||
add_suffix: &self.path_label,
|
||||
set_activatable: true,
|
||||
add_suffix: unset_btn = >k::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 |_| {
|
||||
sender.input(Self::Input::OpenFileChooser)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
|
||||
match message {
|
||||
Self::Input::Changed(val) => {
|
||||
self.value = val.clone();
|
||||
self.path_label.set_label(val.as_str());
|
||||
self.value = val;
|
||||
self.path_label.set_label(match self.value.as_ref() {
|
||||
Some(val) => val.as_str(),
|
||||
None => "(None)".into(),
|
||||
});
|
||||
sender
|
||||
.output_sender()
|
||||
.emit(Self::Output::Changed(self.key.clone(), val))
|
||||
.emit(Self::Output::Changed(self.key.clone(), self.value.clone()))
|
||||
}
|
||||
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> {
|
||||
None
|
||||
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
|
||||
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 {
|
||||
Self {
|
||||
name: init.name,
|
||||
name: init.name.clone(),
|
||||
key: init.key,
|
||||
value: match init.value.as_ref() {
|
||||
Some(val) => val.clone(), None => "".into()
|
||||
},
|
||||
path_label: gtk::Label::builder().label(match init.value {
|
||||
Some(val) => val, None => "(None)".into()
|
||||
}).ellipsize(gtk::pango::EllipsizeMode::Start).build(),
|
||||
value: init.value.clone(),
|
||||
path_label: gtk::Label::builder()
|
||||
.label(match init.value {
|
||||
Some(val) => val,
|
||||
None => "(None)".into(),
|
||||
})
|
||||
.ellipsize(gtk::pango::EllipsizeMode::Start)
|
||||
.build(),
|
||||
filedialog: gtk::FileDialog::builder()
|
||||
.modal(true)
|
||||
.title(format!("Select Path for {}", init.name.clone()))
|
||||
.build(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,8 +71,10 @@ impl FactoryComponent for SwitchModel {
|
|||
}
|
||||
}
|
||||
|
||||
fn forward_to_parent(_output: Self::Output) -> Option<Self::ParentInput> {
|
||||
None
|
||||
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
|
||||
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 {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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::{
|
||||
RuntimeSwitcherBox, RuntimeSwitcherBoxInit, RuntimeSwitcherBoxMsg,
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ use relm4_icons::icon_name;
|
|||
pub struct MainView {
|
||||
xrservice_active: bool,
|
||||
enable_debug_view: bool,
|
||||
profile_names: Vec<String>,
|
||||
profiles: Vec<Profile>,
|
||||
selected_profile: Profile,
|
||||
#[tracker::do_not_track]
|
||||
profiles_dropdown: Option<gtk::DropDown>,
|
||||
|
@ -36,6 +36,8 @@ pub struct MainView {
|
|||
profile_editor: Controller<ProfileEditor>,
|
||||
#[tracker::do_not_track]
|
||||
profile_not_editable_dialog: adw::MessageDialog,
|
||||
#[tracker::do_not_track]
|
||||
profile_delete_confirm_dialog: adw::MessageDialog,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -44,20 +46,24 @@ pub enum MainViewMsg {
|
|||
StartStopClicked,
|
||||
XRServiceActiveChanged(bool, Option<Profile>),
|
||||
EnableDebugViewChanged(bool),
|
||||
UpdateProfileNames(Vec<String>, Config),
|
||||
UpdateProfiles(Vec<Profile>, Config),
|
||||
SetSelectedProfile(u32),
|
||||
ProfileSelected(u32),
|
||||
UpdateSelectedProfile(Profile),
|
||||
EditProfile,
|
||||
CreateProfile,
|
||||
DeleteProfile,
|
||||
DuplicateProfile,
|
||||
SaveProfile(Profile),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MainViewOutMsg {
|
||||
EnableDebugViewChanged(bool),
|
||||
DoStartStopXRService,
|
||||
ProfileSelected(String),
|
||||
ProfileSelected(Profile),
|
||||
DeleteProfile,
|
||||
SaveProfile(Profile),
|
||||
}
|
||||
|
||||
pub struct MainViewInit {
|
||||
|
@ -156,9 +162,9 @@ impl SimpleComponent for MainView {
|
|||
gtk::DropDown {
|
||||
set_hexpand: true,
|
||||
set_tooltip_text: Some("Profiles"),
|
||||
#[track = "model.changed(Self::profile_names())"]
|
||||
#[track = "model.changed(Self::profiles())"]
|
||||
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)
|
||||
}),
|
||||
connect_selected_item_notify[sender] => move |this| {
|
||||
|
@ -178,7 +184,17 @@ impl SimpleComponent for MainView {
|
|||
connect_clicked[sender] => move |_| {
|
||||
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()
|
||||
.emit(RuntimeSwitcherBoxMsg::UpdateSelectedProfile(prof.clone()));
|
||||
}
|
||||
Self::Input::UpdateProfileNames(names, config) => {
|
||||
self.set_profile_names(names);
|
||||
Self::Input::UpdateProfiles(profiles, config) => {
|
||||
self.set_profiles(profiles);
|
||||
// why send another message to set the dropdown selection?
|
||||
// set_* from tracker likely updates the view obj in the next
|
||||
// draw, so selecting here will result in nothing cause the
|
||||
// dropdown is effectively empty
|
||||
sender.input(MainViewMsg::SetSelectedProfile({
|
||||
let pos = self
|
||||
.profile_names
|
||||
.profiles
|
||||
.iter()
|
||||
.position(|p| p.clone() == config.selected_profile_name);
|
||||
.position(|p| p.uuid == config.selected_profile_uuid);
|
||||
match pos {
|
||||
Some(idx) => idx as u32,
|
||||
None => 0,
|
||||
|
@ -247,10 +263,8 @@ impl SimpleComponent for MainView {
|
|||
.set_selected(index);
|
||||
}
|
||||
Self::Input::ProfileSelected(position) => {
|
||||
// self.install_wivrn_box.sender.emit(InstallWivrnBoxMsg::Upda);
|
||||
// TODO: send profile to install_wivrn_box
|
||||
sender.output(MainViewOutMsg::ProfileSelected(
|
||||
self.profile_names.get(position as usize).unwrap().clone(),
|
||||
self.profiles.get(position as usize).unwrap().clone(),
|
||||
));
|
||||
}
|
||||
Self::Input::EditProfile => {
|
||||
|
@ -262,10 +276,20 @@ impl SimpleComponent for MainView {
|
|||
}
|
||||
}
|
||||
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 => {
|
||||
println!("dup");
|
||||
self.profile_editor.sender().emit(ProfileEditorMsg::Present(
|
||||
self.selected_profile.create_duplicate(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -277,12 +301,12 @@ impl SimpleComponent for MainView {
|
|||
) -> ComponentParts<Self> {
|
||||
let profile_not_editable_dialog = adw::MessageDialog::builder()
|
||||
.modal(true)
|
||||
.hide_on_close(true)
|
||||
.heading("This profile is not editable")
|
||||
.body(concat!(
|
||||
"You can duplicate it and edit the new copy. ",
|
||||
"Do you want to duplicate the current profile?"
|
||||
))
|
||||
.hide_on_close(true)
|
||||
.build();
|
||||
profile_not_editable_dialog.add_response("no", "_No");
|
||||
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 {
|
||||
xrservice_active: false,
|
||||
enable_debug_view: init.config.debug_view_enabled,
|
||||
profiles_dropdown: None,
|
||||
profile_names: vec![],
|
||||
profiles: vec![],
|
||||
steam_launch_options_box: SteamLaunchOptionsBox::builder().launch(()).detach(),
|
||||
install_wivrn_box: InstallWivrnBox::builder()
|
||||
.launch(InstallWivrnBoxInit {
|
||||
|
@ -316,9 +360,12 @@ impl SimpleComponent for MainView {
|
|||
.detach(),
|
||||
profile_editor: ProfileEditor::builder()
|
||||
.launch(ProfileEditorInit {})
|
||||
.detach(),
|
||||
.forward(sender.input_sender(), |message| match message {
|
||||
ProfileEditorOutMsg::SaveProfile(p) => Self::Input::SaveProfile(p),
|
||||
}),
|
||||
selected_profile: init.selected_profile.clone(),
|
||||
profile_not_editable_dialog,
|
||||
profile_delete_confirm_dialog,
|
||||
tracker: 0,
|
||||
};
|
||||
let widgets = view_output!();
|
||||
|
|
|
@ -35,8 +35,17 @@ pub struct ProfileEditor {
|
|||
pub enum ProfileEditorMsg {
|
||||
Present(Profile),
|
||||
EntryChanged(String, String),
|
||||
TextChanged(String, String),
|
||||
PathChanged(String, Option<String>),
|
||||
SwitchChanged(String, bool),
|
||||
ComboChanged(String, String),
|
||||
AddEnvVar,
|
||||
SaveProfile, // ?
|
||||
SaveProfile,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProfileEditorOutMsg {
|
||||
SaveProfile(Profile),
|
||||
}
|
||||
|
||||
pub struct ProfileEditorInit {}
|
||||
|
@ -45,7 +54,7 @@ pub struct ProfileEditorInit {}
|
|||
impl SimpleComponent for ProfileEditor {
|
||||
type Init = ProfileEditorInit;
|
||||
type Input = ProfileEditorMsg;
|
||||
type Output = ();
|
||||
type Output = ProfileEditorOutMsg;
|
||||
|
||||
view! {
|
||||
#[name(win)]
|
||||
|
@ -66,6 +75,21 @@ impl SimpleComponent for ProfileEditor {
|
|||
add: model.env_rows.widget(),
|
||||
add: model.switch_rows.widget(),
|
||||
add: model.path_rows.widget(),
|
||||
add: save_grp = &adw::PreferencesGroup {
|
||||
add: save_box = >k::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(),
|
||||
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.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) => {
|
||||
println!("{}: {}", name, value);
|
||||
self.profile
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.environment
|
||||
.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 => {
|
||||
println!("Add env var");
|
||||
}
|
||||
|
@ -218,6 +293,30 @@ impl SimpleComponent for ProfileEditor {
|
|||
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!();
|
||||
model.win = Some(widgets.win.clone());
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue