feat: initial ui for importing steamvr calibration to libsurvive

This commit is contained in:
Gabriele Musco 2023-06-16 23:31:56 +02:00
parent f524cb68e4
commit 280988e1f7
No known key found for this signature in database
GPG key ID: 1068D795C80E51DE
6 changed files with 387 additions and 3 deletions

1
Cargo.lock generated
View file

@ -1119,6 +1119,7 @@ name = "rex2"
version = "0.1.0"
dependencies = [
"expect-dialog",
"gtk4",
"nix",
"relm4",
"relm4-components",

View file

@ -7,6 +7,9 @@ edition = "2021"
[dependencies]
expect-dialog = "1.0.0"
gtk4 = { version = "0.6.6", features = [
"v4_10"
] }
nix = "0.26.2"
relm4 = { version = "0.6.0", features = [
"libadwaita"

View file

@ -1,6 +1,7 @@
use super::about_dialog::AboutDialog;
use super::build_window::BuildWindow;
use super::debug_view::{DebugView, DebugViewMsg};
use super::libsurvive_setup_window::LibsurviveSetupWindow;
use super::main_view::MainViewMsg;
use crate::builders::build_libsurvive::get_build_libsurvive_runner;
use crate::builders::build_monado::get_build_monado_runner;
@ -22,12 +23,13 @@ use crate::runner::{Runner, RunnerStatus};
use crate::runner_pipeline::RunnerPipeline;
use crate::ui::build_window::BuildWindowMsg;
use crate::ui::debug_view::DebugViewInit;
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};
use relm4::adw::ResponseAppearance;
use relm4::adw::traits::MessageDialogExt;
use relm4::adw::ResponseAppearance;
use relm4::gtk::glib;
use relm4::{new_action_group, new_stateful_action, new_stateless_action, prelude::*};
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
@ -49,6 +51,8 @@ pub struct App {
dependencies_dialog: adw::MessageDialog,
#[tracker::do_not_track]
setcap_confirm_dialog: adw::MessageDialog,
#[tracker::do_not_track]
libsurvive_setup_window: Controller<LibsurviveSetupWindow>,
#[tracker::do_not_track]
config: Config,
@ -71,6 +75,7 @@ pub enum Msg {
ProfileSelected(String),
SetMonadoRuntime(bool),
RunSetCap,
OpenLibsurviveSetup,
}
impl App {
@ -264,7 +269,10 @@ impl SimpleComponent for App {
pipeline.start();
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateTitle(format!("Building Profile {}", profile.name)));
.emit(BuildWindowMsg::UpdateTitle(format!(
"Building Profile {}",
profile.name
)));
self.build_window
.sender()
.emit(BuildWindowMsg::UpdateCanClose(false));
@ -288,6 +296,13 @@ impl SimpleComponent for App {
set_current_openvrpaths_to_steam();
}
}
Msg::OpenLibsurviveSetup => {
self.libsurvive_setup_window
.sender()
.send(LibsurviveSetupMsg::Present(
self.get_selected_profile().clone(),
));
}
}
}
@ -357,6 +372,10 @@ impl SimpleComponent for App {
.transient_for(root)
.launch(())
.detach(),
libsurvive_setup_window: LibsurviveSetupWindow::builder()
.transient_for(root)
.launch(())
.detach(),
dependencies_dialog,
setcap_confirm_dialog,
enable_debug_view: config.debug_view_enabled,
@ -385,6 +404,13 @@ impl SimpleComponent for App {
})
};
let libsurvive_setup_action = {
let lss_sender = sender.clone();
RelmAction::<LibsurviveSetupAction>::new_stateless(move |_| {
lss_sender.input(Msg::OpenLibsurviveSetup);
})
};
let debug_view_toggle_action: RelmAction<DebugViewToggleAction> = {
let debugtoggle_sender = sender.clone();
RelmAction::<DebugViewToggleAction>::new_stateful(
@ -400,6 +426,7 @@ impl SimpleComponent for App {
actions.add_action(about_action);
actions.add_action(buildprofile_action);
actions.add_action(debug_view_toggle_action);
actions.add_action(libsurvive_setup_action);
root.insert_action_group(AppActionGroup::NAME, Some(&actions.into_action_group()));
@ -423,4 +450,5 @@ impl SimpleComponent for App {
new_action_group!(pub AppActionGroup, "win");
new_stateless_action!(pub AboutAction, AppActionGroup, "about");
new_stateless_action!(pub BuildProfileAction, AppActionGroup, "buildprofile");
new_stateless_action!(pub LibsurviveSetupAction, AppActionGroup, "libsurvivesetup");
new_stateful_action!(pub DebugViewToggleAction, AppActionGroup, "debugviewtoggle", (), bool);

View file

@ -0,0 +1,350 @@
use crate::{profile::Profile, runner::Runner};
use gtk::prelude::*;
use relm4::prelude::*;
use std::path::Path;
const NO_FILE_MSG: &str = "(No file selected)";
#[tracker::track]
pub struct LibsurviveSetupWindow {
steam_lighthouse_path: String,
#[tracker::do_not_track]
win: Option<adw::Window>,
#[tracker::do_not_track]
progressbar: Option<gtk::ProgressBar>,
#[tracker::do_not_track]
carousel: Option<adw::Carousel>,
#[tracker::do_not_track]
loading_page: Option<adw::StatusPage>,
#[tracker::do_not_track]
filefilter_listmodel: gtk::gio::ListStore,
#[tracker::do_not_track]
profile: Option<Profile>,
#[tracker::do_not_track]
calibration_runner: Option<Runner>,
}
#[derive(Debug)]
pub enum LibsurviveSetupMsg {
Present(Profile),
CalibrateLibsurvive,
SetSteamLighthousePath(Option<String>),
ChooseFileDialog,
}
#[relm4::component(pub)]
impl SimpleComponent for LibsurviveSetupWindow {
type Init = ();
type Input = LibsurviveSetupMsg;
type Output = ();
view! {
#[name(win)]
adw::Window {
set_modal: true,
set_default_size: (520, 400),
set_hide_on_close: true,
connect_close_request[sender, carousel, page1, progressbar] => move |_| {
carousel.scroll_to(&page1, true);
progressbar.set_fraction(0.0);
sender.input(LibsurviveSetupMsg::SetSteamLighthousePath(None));
gtk::Inhibit(false)
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: true,
set_spacing: 12,
set_margin_all: 12,
gtk::WindowHandle {
adw::HeaderBar {
add_css_class: "flat",
#[wrap(Some)]
set_title_widget: title_label = &gtk::Label {
set_label: "Setup Lighthouses",
add_css_class: "title",
},
}
},
#[name(carousel)]
adw::Carousel {
set_allow_long_swipes: false,
set_allow_scroll_wheel: false,
set_allow_mouse_drag: false,
set_hexpand: true,
set_vexpand: true,
#[name(page1)]
adw::StatusPage {
set_hexpand: true,
set_vexpand: true,
set_title: "Let's Get Started",
set_description: Some(concat!(
"This procedure will guide you through importing ",
"your SteamVR lighthouse calibration into Libsurvive.",
)),
gtk::Button {
set_hexpand: true,
set_halign: gtk::Align::Center,
add_css_class: "pill",
add_css_class: "suggested-action",
set_label: "Start Setup",
connect_clicked[carousel, page2] => move |_| {
carousel.scroll_to(
&page2, true
);
},
}
},
#[name(page2)]
adw::StatusPage {
set_hexpand: true,
set_vexpand: true,
set_title: "Train Your SteamVR Calibration",
set_description: Some(concat!(
"If you've already played extensively with SteamVR ",
"(either on Linux or on Windows), chances are you ",
"already have an adequate calibration profile; ",
"then you can skip this step.\n\n",
"If not, you can plug in your HMD, start SteamVR and ",
"slowly move your headset around your play space for ",
"a while. This will generate the necessary calibration ",
"data that will be imported by Libsurvive.",
)),
gtk::Button {
set_hexpand: true,
set_halign: gtk::Align::Center,
add_css_class: "pill",
add_css_class: "suggested-action",
set_label: "Next",
connect_clicked[carousel, page3] => move |_| {
carousel.scroll_to(
&page3, true
);
},
}
},
#[name(page3)]
adw::StatusPage {
set_hexpand: true,
set_vexpand: true,
set_title: "Import Calibration into Libsurvive",
set_description: Some(concat!(
"Plug in your HMD and place it on the floor, ",
"preferably in the middle of your play space, and ",
"in direct line of sight of all of your lighthouses.\n\n",
"Then select the SteamVR calibration file from your disk.",
"\n\nFinally, you can press the \"Import Calibration\" button.",
)),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 12,
adw::Bin {
set_hexpand: true,
add_css_class: "card",
set_halign: gtk::Align::Center,
gtk::Grid {
set_column_spacing: 6,
set_row_spacing: 6,
set_margin_all: 12,
set_hexpand: true,
set_halign: gtk::Align::Center,
attach: (
&gtk::Label::builder()
.use_markup(true)
.label("<b>Linux</b>")
.xalign(1.0)
.build(),
0, 0, 1, 1
),
attach: (
&gtk::Label::builder()
.use_markup(true)
.label("<tt>~/.steam/steam/config/lighthouse/lighthousedb.json</tt>")
.wrap(true)
.wrap_mode(gtk::pango::WrapMode::Char)
.selectable(true)
.xalign(0.0)
.build(),
1, 0, 1, 1
),
attach: (
&gtk::Label::builder()
.use_markup(true)
.label("<b>Windows</b>")
.xalign(1.0)
.build(),
0, 1, 1, 1
),
attach: (
&gtk::Label::builder()
.use_markup(true)
.label(
"<tt>\"C:\\Program Files (x86)\\steam\\config\\lighthouse\\lighthouse.json\"</tt>"
)
.wrap(true)
.wrap_mode(gtk::pango::WrapMode::Char)
.selectable(true)
.xalign(0.0)
.build(),
1, 1, 1, 1
),
},
},
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 12,
set_hexpand: true,
gtk::Button {
set_label: "Select File",
connect_clicked[sender] => move |_| {
sender.input(LibsurviveSetupMsg::ChooseFileDialog);
}
},
gtk::Label {
#[track = "model.changed(LibsurviveSetupWindow::steam_lighthouse_path())"]
set_label: model.steam_lighthouse_path.as_str(),
#[track = "model.changed(LibsurviveSetupWindow::steam_lighthouse_path())"]
set_tooltip_text: Some(model.steam_lighthouse_path.as_str()),
set_ellipsize: gtk::pango::EllipsizeMode::Start,
set_hexpand: true,
set_xalign: 1.0,
}
},
gtk::Button {
set_hexpand: true,
set_halign: gtk::Align::Center,
add_css_class: "pill",
add_css_class: "suggested-action",
set_label: "Import Calibration",
#[track = "model.changed(LibsurviveSetupWindow::steam_lighthouse_path())"]
set_sensitive: model.steam_lighthouse_path != NO_FILE_MSG,
connect_clicked[sender] => move |_| {
sender.input(
LibsurviveSetupMsg::CalibrateLibsurvive
);
},
}
}
},
#[name(loading_page)]
adw::StatusPage {
set_hexpand: true,
set_vexpand: true,
set_title: "Importing Calibration...\nDo Not Touch your Headset!",
set_description: Some("Please stand by"),
#[name(progressbar)]
gtk::ProgressBar {
set_margin_top: 12,
set_margin_bottom: 12,
set_margin_start: 24,
set_margin_end: 24,
set_fraction: 0.0,
set_hexpand: true,
}
}
},
adw::CarouselIndicatorDots {
set_carousel: Some(&carousel),
}
}
}
}
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
self.reset();
match message {
LibsurviveSetupMsg::Present(prof) => {
self.profile = Some(prof);
self.win.as_ref().unwrap().present();
}
LibsurviveSetupMsg::CalibrateLibsurvive => {
if self.steam_lighthouse_path == NO_FILE_MSG {
return;
}
let lh_path = Path::new(&self.steam_lighthouse_path);
if lh_path.is_file() {
self.calibration_runner = Some(Runner::new(
None,
format!(
"{pfx}/bin/survive-cli",
pfx = self.profile.as_ref().unwrap().prefix
),
vec![
"--steamvr-calibration".into(),
lh_path.to_str().unwrap().to_string(),
],
));
self.carousel
.as_ref()
.unwrap()
.scroll_to(self.loading_page.as_ref().unwrap(), true);
}
}
LibsurviveSetupMsg::SetSteamLighthousePath(n_path) => {
self.set_steam_lighthouse_path(match n_path {
None => NO_FILE_MSG.into(),
Some(p) => p,
});
}
LibsurviveSetupMsg::ChooseFileDialog => {
let chooser = gtk::FileDialog::builder()
.modal(true)
.title("Select SteamVR Calibration")
.filters(&self.filefilter_listmodel)
.build();
let fd_sender = sender.clone();
chooser.open(
Some(&self.win.as_ref().unwrap().clone()),
gtk::gio::Cancellable::NONE,
move |res| match res {
Ok(file) => {
let path = file.path();
if path.is_some() {
fd_sender.input(LibsurviveSetupMsg::SetSteamLighthousePath(Some(
path.unwrap().to_str().unwrap().to_string(),
)))
}
}
_ => {}
},
);
}
}
}
fn init(
init: Self::Init,
root: &Self::Root,
sender: relm4::ComponentSender<Self>,
) -> relm4::ComponentParts<Self> {
let json_filter = gtk::FileFilter::new();
json_filter.add_mime_type("application/json");
let mut model = LibsurviveSetupWindow {
win: None,
progressbar: None,
carousel: None,
loading_page: None,
steam_lighthouse_path: NO_FILE_MSG.into(),
filefilter_listmodel: gtk4::gio::ListStore::builder()
.item_type(gtk::FileFilter::static_type())
.build(),
profile: None,
calibration_runner: None,
tracker: 0,
};
model.filefilter_listmodel.append(&json_filter);
let widgets = view_output!();
model.win = Some(widgets.win.clone());
model.progressbar = Some(widgets.progressbar.clone());
model.carousel = Some(widgets.carousel.clone());
model.loading_page = Some(widgets.loading_page.clone());
ComponentParts { model, widgets }
}
}

View file

@ -1,7 +1,7 @@
use crate::config::Config;
use crate::constants::APP_NAME;
use crate::file_builders::active_runtime_json::{self, get_current_active_runtime};
use crate::ui::app::{AboutAction, BuildProfileAction, DebugViewToggleAction};
use crate::ui::app::{AboutAction, BuildProfileAction, DebugViewToggleAction, LibsurviveSetupAction};
use expect_dialog::ExpectDialog;
use gtk::prelude::*;
use relm4::prelude::*;
@ -51,6 +51,7 @@ impl SimpleComponent for MainView {
// value inside action is ignored
"_Debug View" => DebugViewToggleAction,
"_Build Profile" => BuildProfileAction,
"_Calibrate Lighthouses" => LibsurviveSetupAction,
},
section! {
"_About" => AboutAction,

View file

@ -3,3 +3,4 @@ pub mod main_view;
pub mod about_dialog;
pub mod debug_view;
pub mod build_window;
pub mod libsurvive_setup_window;