feat!: remake of the profile editor

This commit is contained in:
Gabriele Musco 2023-07-22 20:30:28 +02:00
parent 6a8dd10960
commit cad6dbdf23
No known key found for this signature in database
GPG key ID: 1068D795C80E51DE
9 changed files with 429 additions and 613 deletions

View file

@ -1,83 +0,0 @@
use crate::ui::profile_editor::ProfileEditorMsg;
use adw::prelude::*;
use gtk::prelude::*;
use relm4::prelude::*;
#[derive(Debug)]
pub struct EntryModel {
key: String,
name: String,
value: String,
}
pub struct EntryModelInit {
pub key: String,
pub name: String,
pub value: String,
}
#[derive(Debug)]
pub enum EntryModelMsg {
Changed(String),
}
#[derive(Debug)]
pub enum EntryModelOutMsg {
Changed(String, String),
}
#[relm4::factory(pub)]
impl FactoryComponent for EntryModel {
type Init = EntryModelInit;
type Input = EntryModelMsg;
type Output = EntryModelOutMsg;
type CommandOutput = ();
type Widgets = EntryModelWidgets;
type ParentInput = ProfileEditorMsg;
type ParentWidget = adw::PreferencesGroup;
view! {
root = adw::EntryRow {
set_title: &self.name,
set_text: &self.value,
add_suffix: clear_btn = &gtk::Button {
set_icon_name: "edit-clear-symbolic",
set_tooltip_text: Some("Clear"),
set_valign: gtk::Align::Center,
add_css_class: "flat",
add_css_class: "circular",
connect_clicked[root] => move |_| {
root.set_text("");
}
},
connect_changed[sender] => move |entry| {
sender.input_sender().emit(Self::Input::Changed(entry.text().to_string()));
},
}
}
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message {
Self::Input::Changed(val) => {
self.value = val.clone();
sender
.output_sender()
.emit(Self::Output::Changed(self.key.clone(), val));
}
}
}
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
Some(match output {
Self::Output::Changed(key, value) => ProfileEditorMsg::EntryChanged(key, value),
})
}
fn init_model(init: Self::Init, index: &Self::Index, sender: FactorySender<Self>) -> Self {
Self {
key: init.key,
name: init.name,
value: init.value,
}
}
}

View file

@ -1,4 +1 @@
pub mod env_var_row_factory;
pub mod switch_row_factory;
pub mod path_row_factory;
pub mod entry_row_factory;

View file

@ -1,124 +0,0 @@
use adw::prelude::*;
use gtk::prelude::*;
use relm4::prelude::*;
use crate::ui::profile_editor::ProfileEditorMsg;
#[derive(Debug)]
pub struct PathModel {
name: String,
key: String,
value: Option<String>,
path_label: gtk::Label,
filedialog: gtk::FileDialog,
}
pub struct PathModelInit {
pub name: String,
pub key: String,
pub value: Option<String>,
}
#[derive(Debug)]
pub enum PathModelMsg {
Changed(Option<String>),
OpenFileChooser,
}
#[derive(Debug)]
pub enum PathModelOutMsg {
/** key, value */
Changed(String, Option<String>),
}
#[relm4::factory(pub)]
impl FactoryComponent for PathModel {
type Init = PathModelInit;
type Input = PathModelMsg;
type Output = PathModelOutMsg;
type CommandOutput = ();
type Widgets = PathModelWidgets;
type ParentInput = ProfileEditorMsg;
type ParentWidget = adw::PreferencesGroup;
view! {
root = adw::ActionRow {
set_title: &self.name,
set_subtitle_lines: 0,
set_icon_name: Some("folder-open-symbolic"),
add_suffix: &self.path_label,
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-symbolic",
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;
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(), self.value.clone()))
}
Self::Input::OpenFileChooser => {
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> {
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.clone(),
key: init.key,
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(),
}
}
}

View file

@ -1,88 +0,0 @@
use adw::prelude::*;
use gtk::prelude::*;
use relm4::prelude::*;
use crate::ui::profile_editor::ProfileEditorMsg;
#[derive(Debug)]
pub struct SwitchModel {
name: String,
description: Option<String>,
key: String,
value: bool,
}
pub struct SwitchModelInit {
pub name: String,
pub description: Option<String>,
pub key: String,
pub value: bool,
}
#[derive(Debug)]
pub enum SwitchModelMsg {
Changed(bool),
}
#[derive(Debug)]
pub enum SwitchModelOutMsg {
/** key, value */
Changed(String, bool),
}
#[relm4::factory(pub)]
impl FactoryComponent for SwitchModel {
type Init = SwitchModelInit;
type Input = SwitchModelMsg;
type Output = SwitchModelOutMsg;
type CommandOutput = ();
type Widgets = SwitchModelWidgets;
type ParentInput = ProfileEditorMsg;
type ParentWidget = adw::PreferencesGroup;
view! {
root = adw::ActionRow {
set_title: &self.name,
set_subtitle_lines: 0,
set_subtitle: match &self.description {
Some(s) => s,
None => "".into(),
},
add_suffix: switch = &gtk::Switch {
set_valign: gtk::Align::Center,
set_active: self.value,
connect_state_set[sender] => move |_, state| {
sender.input(Self::Input::Changed(state));
gtk::Inhibit(false)
}
},
set_activatable_widget: Some(&switch),
}
}
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message {
Self::Input::Changed(val) => {
self.value = val.clone();
sender
.output_sender()
.emit(Self::Output::Changed(self.key.clone(), val))
}
}
}
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 {
Self {
name: init.name,
description: init.description,
key: init.key,
value: init.value,
}
}
}

6
src/ui/macros.rs Normal file
View file

@ -0,0 +1,6 @@
#[macro_export]
macro_rules! withclones {
($($var:ident),+) => {
$(let $var = $var.clone();)+
};
}

View file

@ -6,7 +6,8 @@ use crate::config::Config;
use crate::constants::APP_NAME;
use crate::profile::{Profile, XRServiceType};
use crate::ui::app::{
AboutAction, BuildProfileAction, DebugViewToggleAction, LibsurviveSetupAction, BuildProfileCleanAction,
AboutAction, BuildProfileAction, BuildProfileCleanAction, DebugViewToggleAction,
LibsurviveSetupAction,
};
use crate::ui::profile_editor::ProfileEditorInit;
use crate::xr_devices::XRDevices;
@ -31,13 +32,15 @@ pub struct MainView {
#[tracker::do_not_track]
devices_box: Controller<DevicesBox>,
#[tracker::do_not_track]
profile_editor: Controller<ProfileEditor>,
#[tracker::do_not_track]
profile_not_editable_dialog: adw::MessageDialog,
#[tracker::do_not_track]
profile_delete_confirm_dialog: adw::MessageDialog,
#[tracker::do_not_track]
cannot_duplicate_profile_dialog: adw::MessageDialog,
#[tracker::do_not_track]
profile_editor: Option<Controller<ProfileEditor>>,
#[tracker::do_not_track]
root_win: gtk::Window,
}
#[derive(Debug)]
@ -75,6 +78,19 @@ pub struct MainViewInit {
pub root_win: gtk::Window,
}
impl MainView {
fn create_profile_editor(&mut self, sender: ComponentSender<MainView>, prof: Profile) {
self.profile_editor = Some(ProfileEditor::builder()
.launch(ProfileEditorInit {
root_win: self.root_win.clone(),
profile: prof,
})
.forward(sender.input_sender(), |message| match message {
ProfileEditorOutMsg::SaveProfile(p) => MainViewMsg::SaveProfile(p),
}));
}
}
#[relm4::component(pub)]
impl SimpleComponent for MainView {
type Init = MainViewInit;
@ -302,16 +318,15 @@ impl SimpleComponent for MainView {
}
Self::Input::EditProfile => {
if self.selected_profile.editable {
self.profile_editor
.emit(ProfileEditorMsg::Present(self.selected_profile.clone()));
self.create_profile_editor(sender, self.selected_profile.clone());
self.profile_editor.as_ref().unwrap().emit(ProfileEditorMsg::Present);
} else {
self.profile_not_editable_dialog.present();
}
}
Self::Input::CreateProfile => {
self.profile_editor
.sender()
.emit(ProfileEditorMsg::Present(Profile::default()));
self.create_profile_editor(sender, Profile::default());
self.profile_editor.as_ref().unwrap().sender().emit(ProfileEditorMsg::Present);
}
Self::Input::DeleteProfile => {
self.profile_delete_confirm_dialog.present();
@ -321,9 +336,8 @@ impl SimpleComponent for MainView {
}
Self::Input::DuplicateProfile => {
if self.selected_profile.can_be_built {
self.profile_editor.sender().emit(ProfileEditorMsg::Present(
self.selected_profile.create_duplicate(),
));
self.create_profile_editor(sender, self.selected_profile.create_duplicate());
self.profile_editor.as_ref().unwrap().sender().emit(ProfileEditorMsg::Present);
} else {
self.cannot_duplicate_profile_dialog.present();
}
@ -405,18 +419,13 @@ impl SimpleComponent for MainView {
root_win: init.root_win.clone(),
})
.detach(),
profile_editor: ProfileEditor::builder()
.launch(ProfileEditorInit {
root_win: init.root_win.clone(),
})
.forward(sender.input_sender(), |message| match message {
ProfileEditorOutMsg::SaveProfile(p) => Self::Input::SaveProfile(p),
}),
devices_box: DevicesBox::builder().launch(()).detach(),
selected_profile: init.selected_profile.clone(),
profile_not_editable_dialog,
profile_delete_confirm_dialog,
cannot_duplicate_profile_dialog,
root_win: init.root_win.clone(),
profile_editor: None,
tracker: 0,
};
let widgets = view_output!();

View file

@ -10,3 +10,5 @@ pub mod profile_editor;
pub mod factories;
pub mod wivrn_conf_editor;
pub mod devices_box;
pub mod preference_rows;
pub mod macros;

167
src/ui/preference_rows.rs Normal file
View file

@ -0,0 +1,167 @@
use adw::prelude::*;
use gtk::{
gio,
glib::{self, GString},
};
use relm4::prelude::*;
use crate::withclones;
pub fn switch_row<F: Fn(&gtk::Switch, bool) -> glib::signal::Inhibit + 'static>(
title: &str,
description: Option<&str>,
value: bool,
cb: F,
) -> adw::ActionRow {
let row = adw::ActionRow::builder()
.title(title)
.subtitle_lines(0)
.build();
if description.is_some() {
row.set_subtitle(description.unwrap());
}
let switch = gtk::Switch::builder()
.active(value)
.valign(gtk::Align::Center)
.build();
row.add_suffix(&switch);
row.set_activatable_widget(Some(&switch));
switch.connect_state_set(cb);
row
}
pub fn entry_row<F: Fn(&adw::EntryRow) + 'static>(
title: &str,
value: &str,
cb: F,
) -> adw::EntryRow {
let row = adw::EntryRow::builder().title(title).text(value).build();
let clear_btn = gtk::Button::builder()
.icon_name("edit-clear-symbolic")
.tooltip_text(GString::from("Clear"))
.css_classes(["circular", "flat"])
.valign(gtk::Align::Center)
.build();
row.add_suffix(&clear_btn);
{
let row_c = row.clone();
clear_btn.connect_clicked(move |_| {
row_c.set_text("");
});
}
row.connect_changed(cb);
row
}
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
title: &str,
description: Option<&str>,
value: Option<String>,
root_win: Option<gtk::Window>,
cb: F,
) -> adw::ActionRow {
let row = adw::ActionRow::builder()
.title(title)
.subtitle_lines(0)
.activatable(true)
.icon_name(GString::from("folder-open-symbolic"))
.build();
if description.is_some() {
row.set_subtitle(description.unwrap());
}
let path_label = &gtk::Label::builder()
.label(match value.as_ref() {
None => "(None)",
Some(p) => p.as_str(),
})
.build();
row.add_suffix(path_label);
let clear_btn = gtk::Button::builder()
.icon_name("edit-clear-symbolic")
.tooltip_text(GString::from("Clear Path"))
.css_classes(["circular", "flat"])
.valign(gtk::Align::Center)
.build();
row.add_suffix(&clear_btn);
{
withclones![path_label, cb];
clear_btn.connect_clicked(move |_| {
path_label.set_label("(None)");
cb(None)
});
}
let filedialog = gtk::FileDialog::builder()
.modal(true)
.title(format!("Select Path for {}", title))
.build();
{
withclones![path_label];
row.connect_activated(move |_| {
withclones![path_label, cb];
filedialog.select_folder(
root_win.as_ref(),
gio::Cancellable::NONE,
move |res| match res {
Ok(file) => {
let path = file.path();
if path.is_some() {
let path_s = path.unwrap().to_str().unwrap().to_string();
path_label.set_text(path_s.as_str());
cb(Some(path_s))
}
}
_ => {}
},
)
});
}
row
}
pub fn combo_row<F: Fn(&adw::ComboRow,) + 'static>(
title: &str,
description: Option<&str>,
value: &str,
values: Vec<String>,
cb: F,
) -> adw::ComboRow {
let row = adw::ComboRow::builder()
.title(title)
.subtitle_lines(0)
.model(&gtk::StringList::new(
values
.iter()
.map(String::as_str)
.collect::<Vec<&str>>()
.as_slice(),
))
.build();
if description.is_some() {
row.set_subtitle(description.unwrap());
}
let selected = values.iter().position(|v| *v == value.to_string());
row.set_selected(match selected {
Some(i) => i,
None => 0,
} as u32);
row.connect_selected_item_notify(cb);
row
}

View file

@ -1,49 +1,34 @@
use super::factories::{
entry_row_factory::{EntryModel, EntryModelInit},
env_var_row_factory::{EnvVarModel, EnvVarModelInit},
path_row_factory::{PathModel, PathModelInit},
switch_row_factory::{SwitchModel, SwitchModelInit},
use super::{
factories::env_var_row_factory::{EnvVarModel, EnvVarModelInit},
preference_rows::combo_row,
};
use crate::{
env_var_descriptions::env_var_descriptions_as_paragraph,
profile::{Profile, XRServiceType},
ui::preference_rows::{entry_row, path_row, switch_row},
withclones,
};
use adw::prelude::*;
use gtk::prelude::*;
use relm4::{factory::FactoryVecDeque, prelude::*};
use std::{cell::RefCell, rc::Rc};
#[tracker::track]
pub struct ProfileEditor {
profile: Option<Profile>,
profile: Rc<RefCell<Profile>>,
#[tracker::do_not_track]
win: Option<adw::PreferencesWindow>,
#[tracker::do_not_track]
name_row: adw::EntryRow,
#[tracker::do_not_track]
type_row: adw::ComboRow,
#[tracker::do_not_track]
pull_on_build_switch: Option<gtk::Switch>,
#[tracker::do_not_track]
env_rows: FactoryVecDeque<EnvVarModel>,
#[tracker::do_not_track]
switch_rows: FactoryVecDeque<SwitchModel>,
#[tracker::do_not_track]
path_rows: FactoryVecDeque<PathModel>,
#[tracker::do_not_track]
repo_rows: FactoryVecDeque<EntryModel>,
}
#[derive(Debug)]
pub enum ProfileEditorMsg {
Present(Profile),
Present,
EnvVarChanged(String, String),
EnvVarDelete(String),
EntryChanged(String, String),
PathChanged(String, Option<String>),
SwitchChanged(String, bool),
ComboChanged(String, String),
AddEnvVar(String),
SaveProfile,
}
@ -55,6 +40,7 @@ pub enum ProfileEditorOutMsg {
pub struct ProfileEditorInit {
pub root_win: gtk::Window,
pub profile: Profile,
}
#[relm4::component(pub)]
@ -66,35 +52,204 @@ impl SimpleComponent for ProfileEditor {
view! {
#[name(win)]
adw::PreferencesWindow {
set_hide_on_close: true,
set_modal: true,
set_transient_for: Some(&init.root_win),
#[track = "model.changed(Self::profile())"]
set_title: match model.profile.as_ref() {
Some(p) => Some(p.name.as_str()),
None => None,
},
set_title: Some(model.profile.borrow().name.as_str()),
add: mainpage = &adw::PreferencesPage {
add: maingrp = &adw::PreferencesGroup {
set_title: "General",
model.name_row.clone(),
model.type_row.clone(),
adw::ActionRow {
set_title: "Update on Build",
add_suffix: pull_on_build_switch = &gtk::Switch {
set_valign: gtk::Align::Center,
connect_state_set[sender] => move |_, state| {
sender.input(Self::Input::SwitchChanged("pull_on_build".into(), state));
add: {
withclones![prof];
&entry_row("Profile Name", model.profile.borrow().name.as_str(), move |row| {
prof.borrow_mut().name = row.text().to_string();
})
},
add: {
withclones![prof];
&switch_row(
"Update on Build", None,
model.profile.borrow().pull_on_build,
move |_, state| {
prof.borrow_mut().pull_on_build = state;
gtk::Inhibit(false)
}
},
set_activatable_widget: Some(&pull_on_build_switch),
}
)
},
add: {
withclones![prof];
&path_row(
"Install Prefix",
None,
Some(model.profile.borrow().prefix.clone()),
Some(init.root_win.clone()),
move |n_path| {
prof.borrow_mut().prefix = n_path.unwrap_or_default()
},
)
},
},
add: xrservicegrp = &adw::PreferencesGroup {
set_title: "XR Service",
add: {
withclones![prof];
&combo_row(
"XR Service Type",
Some("Monado is for PCVR headsets, while WiVRn is for Andorid standalone headsets"),
model.profile.borrow().xrservice_type.to_string().as_str(),
XRServiceType::iter()
.map(XRServiceType::to_string)
.collect::<Vec<String>>(),
move |row| {
prof.borrow_mut().xrservice_type =
match row.selected() {
0 => XRServiceType::Monado,
1 => XRServiceType::Wivrn,
_ => panic!("XRServiceType combo row cannot have more than 2 choices"),
};
},
)
},
add: {
withclones![prof];
&path_row(
"XR Service Path",
None,
Some(model.profile.borrow().xrservice_path.clone()),
Some(init.root_win.clone()),
move |n_path| {
prof.borrow_mut().xrservice_path = n_path.unwrap_or_default()
},
)
},
add: {
withclones![prof];
&entry_row(
"XR Service Repo",
model.profile.borrow().xrservice_repo.clone().unwrap_or_default().as_str(),
move |row| {
let n_val = row.text().to_string();
prof.borrow_mut().xrservice_repo = (!n_val.is_empty()).then_some(n_val);
}
)
},
},
add: opencompgrp = &adw::PreferencesGroup {
set_title: "OpenComposite",
set_description: Some("OpenVR driver built on top of OpenXR"),
add: {
withclones![prof];
&path_row(
"OpenComposite Path", None,
Some(model.profile.borrow().opencomposite_path.clone()),
Some(init.root_win.clone()),
move |n_path| {
prof.borrow_mut().opencomposite_path = n_path.unwrap_or_default();
}
)
},
add: {
withclones![prof];
&entry_row(
"OpenComposite Repo",
model.profile.borrow().opencomposite_repo.clone().unwrap_or_default().as_str(),
move |row| {
let n_val = row.text().to_string();
prof.borrow_mut().opencomposite_repo = (!n_val.is_empty()).then_some(n_val);
}
)
},
},
add: libsurvivegrp = &adw::PreferencesGroup {
set_title: "Libsurvive",
set_description: Some("Lighthouse tracking driver"),
add: {
withclones![prof];
&switch_row(
"Enable Libsurvive", None,
model.profile.borrow().features.libsurvive.enabled,
move |_, state| {
prof.borrow_mut().features.libsurvive.enabled = state;
gtk::Inhibit(false)
}
)
},
add: {
withclones![prof];
&path_row(
"Libsurvive Path", None,
model.profile.borrow().features.libsurvive.path.clone(),
Some(init.root_win.clone()),
move |n_path| {
prof.borrow_mut().features.libsurvive.path = n_path;
}
)
},
add: {
withclones![prof];
&entry_row(
"Libsurvive Repo",
model.profile.borrow().features.libsurvive.repo.clone().unwrap_or_default().as_str(),
move |row| {
let n_val = row.text().to_string();
prof.borrow_mut().features.libsurvive.repo = (!n_val.is_empty()).then_some(n_val);
}
)
},
},
add: basaltgrp = &adw::PreferencesGroup {
set_title: "Basalt",
set_description: Some("Camera based SLAM tracking driver"),
add: {
withclones![prof];
&switch_row(
"Enable Basalt", None,
model.profile.borrow().features.basalt.enabled,
move |_, state| {
prof.borrow_mut().features.basalt.enabled = state;
gtk::Inhibit(false)
}
)
},
add: {
withclones![prof];
&path_row(
"Basalt Path", None,
model.profile.borrow().features.basalt.path.clone(),
Some(init.root_win.clone()),
move |n_path| {
prof.borrow_mut().features.basalt.path = n_path;
}
)
},
add: {
withclones![prof];
&entry_row(
"Basalt Repo",
model.profile.borrow().features.basalt.repo.clone().unwrap_or_default().as_str(),
move |row| {
let n_val = row.text().to_string();
prof.borrow_mut().features.basalt.repo = n_val.is_empty().then_some(n_val);
}
)
},
},
add: mercurygrp = &adw::PreferencesGroup {
set_title: "Mercury",
set_description: Some("Camera and OpenCV based hand tracking driver"),
add: {
withclones![prof];
&switch_row(
"Enable Mercury", None,
model.profile.borrow().features.mercury_enabled,
move |_, state| {
prof.borrow_mut().features.mercury_enabled = state;
gtk::Inhibit(false)
}
)
},
},
add: model.env_rows.widget(),
add: model.switch_rows.widget(),
add: model.path_rows.widget(),
add: model.repo_rows.widget(),
add: save_grp = &adw::PreferencesGroup {
add: save_box = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
@ -118,113 +273,11 @@ impl SimpleComponent for ProfileEditor {
self.reset();
match message {
Self::Input::Present(prof) => {
let p = prof.clone();
self.name_row.set_text(p.name.as_str());
self.type_row.set_selected(prof.xrservice_type.as_number());
self.pull_on_build_switch
.as_ref()
.unwrap()
.set_active(prof.pull_on_build);
{
let mut guard = self.env_rows.guard();
guard.clear();
for (k, v) in p.environment.iter() {
guard.push_back(EnvVarModelInit {
name: k.clone(),
value: v.clone(),
});
}
}
{
let mut guard = self.switch_rows.guard();
guard.clear();
guard.push_back(SwitchModelInit {
name: "Libsurvive".into(),
description: Some("Lighthouse based spacial tracking".into()),
key: "libsurvive_enabled".into(),
value: p.features.libsurvive.enabled,
});
guard.push_back(SwitchModelInit {
name: "Basalt".into(),
description: Some("Camera based SLAM tracking".into()),
key: "basalt_enabled".into(),
value: p.features.basalt.enabled,
});
guard.push_back(SwitchModelInit {
name: "Mercury".into(),
description: Some("Camera based hand tracking".into()),
key: "mercury_enabled".into(),
value: p.features.mercury_enabled,
});
}
{
let mut guard = self.path_rows.guard();
guard.clear();
guard.push_back(PathModelInit {
name: "XR Service Path".into(),
key: "xrservice_path".into(),
value: Some(p.xrservice_path),
});
guard.push_back(PathModelInit {
name: "OpenComposite Path".into(),
key: "opencomposite_path".into(),
value: Some(p.opencomposite_path),
});
guard.push_back(PathModelInit {
name: "Libsurvive Path".into(),
key: "libsurvive_path".into(),
value: p.features.libsurvive.path,
});
guard.push_back(PathModelInit {
name: "Basalt Path".into(),
key: "basalt_path".into(),
value: p.features.basalt.path,
});
guard.push_back(PathModelInit {
name: "Install Prefix".into(),
key: "prefix".into(),
value: Some(p.prefix),
});
}
{
let mut guard = self.repo_rows.guard();
guard.clear();
guard.push_back(EntryModelInit {
key: "xrservice_repo".into(),
name: "XR Service Repo".into(),
value: p.xrservice_repo.unwrap_or("".into()),
});
guard.push_back(EntryModelInit {
key: "opencomposite_repo".into(),
name: "OpenComposite Repo".into(),
value: p.opencomposite_repo.unwrap_or("".into()),
});
guard.push_back(EntryModelInit {
key: "libsurvive_repo".into(),
name: "Libsurvive Repo".into(),
value: p.features.libsurvive.repo.unwrap_or("".into()),
});
guard.push_back(EntryModelInit {
key: "basalt_repo".into(),
name: "Basalt Repo".into(),
value: p.features.basalt.repo.unwrap_or("".into()),
});
}
self.set_profile(Some(prof.clone()));
Self::Input::Present => {
self.win.as_ref().unwrap().present();
}
Self::Input::SaveProfile => {
let prof = self.profile.as_ref().unwrap();
let prof = self.profile.borrow();
if prof.validate() {
sender.output(ProfileEditorOutMsg::SaveProfile(prof.clone()));
self.win.as_ref().unwrap().close();
@ -237,14 +290,10 @@ impl SimpleComponent for ProfileEditor {
}
}
Self::Input::EnvVarChanged(name, value) => {
self.profile
.as_mut()
.unwrap()
.environment
.insert(name, value);
self.profile.borrow_mut().environment.insert(name, value);
}
Self::Input::EnvVarDelete(name) => {
self.profile.as_mut().unwrap().environment.remove(&name);
self.profile.borrow_mut().environment.remove(&name);
let pos = self
.env_rows
.guard()
@ -254,69 +303,8 @@ impl SimpleComponent for ProfileEditor {
self.env_rows.guard().remove(pos.unwrap());
}
}
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.features.libsurvive.path = value,
"basalt_path" => prof.features.basalt.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.features.libsurvive.enabled = value,
"basalt_enabled" => prof.features.basalt.enabled = value,
"mercury_enabled" => prof.features.mercury_enabled = value,
"pull_on_build" => prof.pull_on_build = value,
_ => panic!("Unknown profile switch key"),
}
}
Self::Input::EntryChanged(key, value) => {
let prof = self.profile.as_mut().unwrap();
match key.as_str() {
"name" => prof.name = value,
"xrservice_repo" => {
prof.xrservice_repo = match value.trim() {
"" => None,
s => Some(s.to_string()),
}
}
"opencomposite_repo" => {
prof.opencomposite_repo = match value.trim() {
"" => None,
s => Some(s.to_string()),
}
}
"libsurvive_repo" => {
prof.features.libsurvive.repo = match value.trim() {
"" => None,
s => Some(s.to_string()),
}
}
"basalt_repo" => {
prof.features.basalt.repo = match value.trim() {
"" => None,
s => Some(s.to_string()),
}
}
_ => 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(name) => {
let prof = self.profile.as_mut().unwrap();
let mut prof = self.profile.borrow_mut();
if !prof.environment.contains_key(&name) {
prof.environment.insert(name.clone(), "".to_string());
self.env_rows.guard().push_back(EnvVarModelInit {
@ -346,20 +334,6 @@ impl SimpleComponent for ProfileEditor {
.icon_name("list-add-symbolic")
.tooltip_text("Add Env Var")
.build();
{
let aeb_sender = sender.clone();
let entry = add_env_name_entry.clone();
let popover = add_env_popover.clone();
add_env_btn.connect_clicked(move |_| {
let name_gstr = entry.text();
let name = name_gstr.trim();
if !name.is_empty() {
popover.popdown();
entry.set_text("");
aeb_sender.input(Self::Input::AddEnvVar(name.to_string()));
}
});
}
add_env_popover_box.append(&add_env_name_entry);
add_env_popover_box.append(&add_env_btn);
add_env_popover.set_child(Some(&add_env_popover_box));
@ -373,26 +347,12 @@ impl SimpleComponent for ProfileEditor {
.halign(gtk::Align::End)
.build();
let profile = Rc::new(RefCell::new(init.profile));
let prof = profile.clone();
let mut model = Self {
profile: None,
profile,
win: None,
name_row: adw::EntryRow::builder().title("Name").build(),
type_row: adw::ComboRow::builder()
.title("XR Service Type")
.subtitle(
"Monado is for PCVR headsets, while WiVRn is for Andorid standalone headsets",
)
.model(&gtk::StringList::new(
XRServiceType::iter()
.map(XRServiceType::to_string)
.collect::<Vec<String>>()
.iter()
.map(String::as_str)
.collect::<Vec<&str>>()
.as_slice(),
))
.build(),
pull_on_build_switch: None,
env_rows: FactoryVecDeque::new(
adw::PreferencesGroup::builder()
.title("Environment Variables")
@ -401,64 +361,34 @@ impl SimpleComponent for ProfileEditor {
.build(),
sender.input_sender(),
),
switch_rows: FactoryVecDeque::new(
adw::PreferencesGroup::builder()
.title("Components")
.description("Enable or disable features")
.build(),
sender.input_sender(),
),
path_rows: FactoryVecDeque::new(
adw::PreferencesGroup::builder()
.title("Paths")
.description(concat!(
"Where the various components' repositories will be cloned to.\n\n",
"\"Install Prefix\" is the path where the components are ",
"installed after building."
))
.build(),
sender.input_sender(),
),
repo_rows: FactoryVecDeque::new(
adw::PreferencesGroup::builder()
.title("Repositories")
.description(concat!(
"Change the repositories from which various components are pulled.\n\n",
"Leave empty to use the default repository."
))
.build(),
sender.input_sender(),
),
tracker: 0,
};
{
let name_sender = sender.clone();
model.name_row.connect_changed(move |nr| {
name_sender.input(Self::Input::EntryChanged(
"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 mut guard = model.env_rows.guard();
guard.clear();
for (k, v) in prof.borrow().environment.iter() {
guard.push_back(EnvVarModelInit {
name: k.clone(),
value: v.clone(),
});
}
}
let widgets = view_output!();
model.win = Some(widgets.win.clone());
model.pull_on_build_switch = Some(widgets.pull_on_build_switch.clone());
{
withclones![sender, add_env_name_entry, add_env_popover];
add_env_btn.connect_clicked(move |_| {
let name_gstr = add_env_name_entry.text();
let name = name_gstr.trim();
if !name.is_empty() {
add_env_popover.popdown();
add_env_name_entry.set_text("");
sender.input(Self::Input::AddEnvVar(name.to_string()));
}
});
}
ComponentParts { model, widgets }
}