mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-04-20 11:35:48 +00:00
feat!: remake of the profile editor
This commit is contained in:
parent
6a8dd10960
commit
cad6dbdf23
9 changed files with 429 additions and 613 deletions
|
@ -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 = >k::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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1 @@
|
|||
pub mod env_var_row_factory;
|
||||
pub mod switch_row_factory;
|
||||
pub mod path_row_factory;
|
||||
pub mod entry_row_factory;
|
||||
|
|
|
@ -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 = >k::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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = >k::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
6
src/ui/macros.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
#[macro_export]
|
||||
macro_rules! withclones {
|
||||
($($var:ident),+) => {
|
||||
$(let $var = $var.clone();)+
|
||||
};
|
||||
}
|
|
@ -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!();
|
||||
|
|
|
@ -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
167
src/ui/preference_rows.rs
Normal 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(>k::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 = >k::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(>k::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
|
||||
}
|
|
@ -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 = >k::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 = >k::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(>k::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 }
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue