diff --git a/dash-frontend/assets/gui/tab/monado.xml b/dash-frontend/assets/gui/tab/monado.xml index 729f356..1534d01 100644 --- a/dash-frontend/assets/gui/tab/monado.xml +++ b/dash-frontend/assets/gui/tab/monado.xml @@ -1,7 +1,35 @@ + + + + + + + + +
+ +
\ No newline at end of file diff --git a/dash-frontend/assets/gui/view/app_launcher.xml b/dash-frontend/assets/gui/view/app_launcher.xml index 64955a6..63c4af0 100644 --- a/dash-frontend/assets/gui/view/app_launcher.xml +++ b/dash-frontend/assets/gui/view/app_launcher.xml @@ -14,11 +14,11 @@ -
+
-
+
- + \ No newline at end of file diff --git a/dash-frontend/assets/lang/de.json b/dash-frontend/assets/lang/de.json index 7cd575f..4188434 100644 --- a/dash-frontend/assets/lang/de.json +++ b/dash-frontend/assets/lang/de.json @@ -1,6 +1,6 @@ { "HOME_SCREEN": "Startbildschirm", - "MONADO_RUNTIME": "„Monado”-Laufzeitumgebung", + "MONADO_RUNTIME": "Monado-Laufzeitumgebung", "APPLICATIONS": "Anwendungen", "GAMES": "Spiele", "SETTINGS": "Einstellungen", diff --git a/dash-frontend/assets/lang/en.json b/dash-frontend/assets/lang/en.json index eea46fb..7342d2a 100644 --- a/dash-frontend/assets/lang/en.json +++ b/dash-frontend/assets/lang/en.json @@ -127,7 +127,7 @@ "HOME_SCREEN": "Home", "LIST_OF_PROCESSES": "Process list", "LIST_OF_WINDOWS": "Window list", - "MONADO_RUNTIME": "„Monado” runtime", + "MONADO_RUNTIME": "Monado runtime", "NO_WINDOWS_FOUND": "No windows found", "POPUP_ADD_DISPLAY": { "RESOLUTION": "Resolution" diff --git a/dash-frontend/assets/lang/es.json b/dash-frontend/assets/lang/es.json index c6f644c..06ee798 100644 --- a/dash-frontend/assets/lang/es.json +++ b/dash-frontend/assets/lang/es.json @@ -1,6 +1,6 @@ { "HOME_SCREEN": "Inicio", - "MONADO_RUNTIME": "„Monado” tiempo de ejecución", + "MONADO_RUNTIME": "Monado tiempo de ejecución", "APPLICATIONS": "Aplicaciones", "GAMES": "Juegos", "SETTINGS": "Ajustes", diff --git a/dash-frontend/assets/lang/pl.json b/dash-frontend/assets/lang/pl.json index b76f57d..7f48b75 100644 --- a/dash-frontend/assets/lang/pl.json +++ b/dash-frontend/assets/lang/pl.json @@ -50,7 +50,7 @@ "USE_SKYBOX_HELP": "Wyświetlaj niebo, jeśli nie ma aplikacji sceny lub passthrough", "USE_PASSTHROUGH_HELP": "Pozwól na passthrough, jeśli runtime XR to obsługuje", "SCREEN_RENDER_DOWN_HELP": "Pomaga redukować aliasing na ekranach o wysokiej rozdzielczości", - "SETS_ON_WATCH": "Lista zestawówna zegarku", + "SETS_ON_WATCH": "Lista zestawów na zegarku", "TROUBLESHOOTING": "Rozwiązywanie problemów", "CLEAR_SAVED_STATE": "Wyczyść zapisany stan", "CLEAR_PIPEWIRE_TOKENS": "Wyczyść tokeny PipeWire", diff --git a/dash-frontend/src/tab/monado.rs b/dash-frontend/src/tab/monado.rs index f6cdd93..8540ee6 100644 --- a/dash-frontend/src/tab/monado.rs +++ b/dash-frontend/src/tab/monado.rs @@ -1,43 +1,163 @@ -use std::marker::PhantomData; +use std::{collections::HashMap, marker::PhantomData, rc::Rc}; use wgui::{ assets::AssetPath, + components::checkbox::ComponentCheckbox, + globals::WguiGlobals, layout::WidgetID, - parser::{ParseDocumentParams, ParserState}, + parser::{self, Fetchable, ParseDocumentParams, ParserState}, + task::Tasks, }; +use wlx_common::dash_interface; use crate::{ frontend::Frontend, tab::{Tab, TabType}, }; +#[derive(Debug)] +enum Task { + Refresh, + FocusClient(String), +} + pub struct TabMonado { #[allow(dead_code)] - pub state: ParserState, + state: ParserState, + tasks: Tasks, + marker: PhantomData, + + globals: WguiGlobals, + id_list_parent: WidgetID, + + cells: Vec, + + ticks: u32, } impl Tab for TabMonado { fn get_type(&self) -> TabType { TabType::Games } + + fn update(&mut self, frontend: &mut Frontend, data: &mut T) -> anyhow::Result<()> { + for task in self.tasks.drain() { + match task { + Task::Refresh => self.refresh(frontend, data)?, + Task::FocusClient(name) => self.focus_client(frontend, data, name)?, + } + } + + // every few seconds + if self.ticks.is_multiple_of(500) { + self.tasks.push(Task::Refresh); + } + + self.ticks += 1; + + Ok(()) + } +} + +fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> { + ParseDocumentParams { + globals: globals.clone(), + path: AssetPath::BuiltIn("gui/tab/monado.xml"), + extra: Default::default(), + } +} + +fn yesno(n: bool) -> &'static str { + match n { + true => "yes", + false => "no", + } } impl TabMonado { pub fn new(frontend: &mut Frontend, parent_id: WidgetID) -> anyhow::Result { - let state = wgui::parser::parse_from_assets( - &ParseDocumentParams { - globals: frontend.layout.state.globals.clone(), - path: AssetPath::BuiltIn("gui/tab/monado.xml"), - extra: Default::default(), - }, - &mut frontend.layout, - parent_id, - )?; + let globals = frontend.layout.state.globals.clone(); + let state = wgui::parser::parse_from_assets(&doc_params(&globals), &mut frontend.layout, parent_id)?; + + let id_list_parent = state.get_widget_id("list_parent")?; + + let tasks = Tasks::::new(); + + tasks.push(Task::Refresh); Ok(Self { state, marker: PhantomData, + tasks, + globals, + id_list_parent, + ticks: 0, + cells: Vec::new(), }) } + + fn mount_client(&mut self, frontend: &mut Frontend, client: &dash_interface::MonadoClient) -> anyhow::Result<()> { + let mut par = HashMap::, Rc>::new(); + par.insert( + "checked".into(), + if client.is_primary { + Rc::from("1") + } else { + Rc::from("0") + }, + ); + par.insert("name".into(), client.name.clone().into()); + par.insert("flag_active".into(), yesno(client.is_active).into()); + par.insert("flag_focused".into(), yesno(client.is_focused).into()); + par.insert("flag_io_active".into(), yesno(client.is_io_active).into()); + par.insert("flag_overlay".into(), yesno(client.is_overlay).into()); + par.insert("flag_primary".into(), yesno(client.is_primary).into()); + par.insert("flag_visible".into(), yesno(client.is_visible).into()); + + let state_cell = self.state.parse_template( + &doc_params(&self.globals), + "Cell", + &mut frontend.layout, + self.id_list_parent, + par, + )?; + + let checkbox = state_cell.fetch_component_as::("checkbox")?; + checkbox.on_toggle({ + let tasks = self.tasks.clone(); + let client_name = client.name.clone(); + Box::new(move |_common, e| { + if e.checked { + tasks.push(Task::FocusClient(client_name.clone())); + } + Ok(()) + }) + }); + + self.cells.push(state_cell); + + Ok(()) + } + + fn refresh(&mut self, frontend: &mut Frontend, data: &mut T) -> anyhow::Result<()> { + log::debug!("refreshing monado client list"); + + let clients = frontend.interface.monado_client_list(data)?; + + frontend.layout.remove_children(self.id_list_parent); + self.cells.clear(); + + for client in clients { + self.mount_client(frontend, &client)?; + } + + Ok(()) + } + + fn focus_client(&mut self, frontend: &mut Frontend, data: &mut T, name: String) -> anyhow::Result<()> { + frontend.interface.monado_client_focus(data, &name)?; + self.tasks.push(Task::Refresh); + Ok(()) + } } diff --git a/dash-frontend/src/util/steam_utils.rs b/dash-frontend/src/util/steam_utils.rs index dad863e..0209dfd 100644 --- a/dash-frontend/src/util/steam_utils.rs +++ b/dash-frontend/src/util/steam_utils.rs @@ -129,7 +129,7 @@ pub fn stop(app_id: AppID, force_kill: bool) -> anyhow::Result<()> { log::info!("Killing process with PID {} and its children", game.pid); let _ = std::process::Command::new("pkill") - .arg(if force_kill { "-9" } else { "-11" }) + .arg(if force_kill { "-9" } else { "-15" }) .arg("-P") .arg(format!("{}", game.pid)) .spawn()?; diff --git a/scripts/translator/description.txt b/scripts/translator/description.txt index ccfb26b..7b489a4 100644 --- a/scripts/translator/description.txt +++ b/scripts/translator/description.txt @@ -5,6 +5,7 @@ Glossary: - wlx-overlay-s: The name of this software (also called WlxOverlay-S) - WayVR: A Wayland compositor intended to be used in VR - WayVR Dashboard: An application (and game) launcher which is displayed in front of the user +- Monado: A VR compositor - OpenVR: API made by Valve - OpenXR: API made by Khronos - OSC: OpenSoundControl diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs index e5c7f76..99cb366 100644 --- a/wgui/src/parser/mod.rs +++ b/wgui/src/parser/mod.rs @@ -10,9 +10,16 @@ mod widget_rectangle; mod widget_sprite; use crate::{ - assets::{normalize_path, AssetPath, AssetPathOwned}, components::{Component, ComponentWeak}, drawing::{self}, globals::WguiGlobals, i18n::Translation, layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair}, log::LogErr, parser::{ + assets::{AssetPath, AssetPathOwned, normalize_path}, + components::{Component, ComponentWeak}, + drawing::{self}, + globals::WguiGlobals, + i18n::Translation, + layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair}, + log::LogErr, + parser::{ component_button::parse_component_button, - component_checkbox::{parse_component_checkbox, CheckboxKind}, + component_checkbox::{CheckboxKind, parse_component_checkbox}, component_radio_group::parse_component_radio_group, component_slider::parse_component_slider, widget_div::parse_widget_div, @@ -20,7 +27,9 @@ use crate::{ widget_label::parse_widget_label, widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite, - }, widget::ConstructEssentials, windowing::context_menu + }, + widget::ConstructEssentials, + windowing::context_menu, }; use anyhow::Context; use ouroboros::self_referencing; @@ -215,7 +224,10 @@ impl ParserState { template_parameters: HashMap, Rc>, ) -> anyhow::Result { let Some(template) = self.data.templates.get(template_name) else { - anyhow::bail!("{:?}: no template named \"{template_name}\" found", self.path.get_path_buf()); + anyhow::bail!( + "{:?}: no template named \"{template_name}\" found", + self.path.get_path_buf().display() + ); }; let mut ctx = ParserContext { @@ -280,7 +292,7 @@ impl ParserState { for attrib in child.attributes() { let (key, value) = (attrib.name(), attrib.value()); - + match key { "text" => title = Some(Translation::from_raw_text(value)), "translation" => title = Some(Translation::from_translation_key(value)), @@ -291,7 +303,7 @@ impl ParserState { if !other.starts_with('_') { anyhow::bail!("unexpected \"{other}\" attribute"); } - attribs.push(AttribPair::new(key, replace_vars(value, &template_params))); + attribs.push(AttribPair::new(key, replace_vars(value, template_params))); } } } @@ -305,14 +317,12 @@ impl ParserState { }); } other => { - anyhow::bail!("{:?}: unexpected <{other}> tag", self.path.get_path_buf()); + anyhow::bail!("{:?}: unexpected <{other}> tag", self.path.get_path_buf().display()); } } } - Ok( - cells, - ) + Ok(cells) } } @@ -470,65 +480,71 @@ impl ParserContext<'_> { insert_color_vars!(self, "faded", def.faded_color, def.translucent_alpha); insert_color_vars!(self, "bg", def.bg_color, def.translucent_alpha); } -fn print_invalid_attrib(&self, tag_name: &str, key: &str, value: &str) { - log::warn!("{}: <{tag_name}> value for \"{key}\" is invalid: \"{value}\"", self.doc_params.path.get_str()); -} - -fn print_missing_attrib(&self, tag_name: &str, attr: &str) { - log::warn!("{}: <{tag_name}> is missing \"{attr}\".", self.doc_params.path.get_str()); -} - -fn parse_val(&self, tag_name: &str, key: &str, value: &str) -> Option { - let Ok(val) = value.parse::() else { - self.print_invalid_attrib(tag_name, key, value); - return None; - }; - Some(val) -} - -fn parse_percent(&self, tag_name: &str, key: &str, value: &str) -> Option { - let Some(val_str) = value.split('%').next() else { - self.print_invalid_attrib(tag_name, key, value); - return None; - }; - - let Ok(val) = val_str.parse::() else { - self.print_invalid_attrib(tag_name, key, value); - return None; - }; - Some(val / 100.0) -} - -fn parse_size_unit(&self, tag_name: &str, key: &str, value: &str) -> Option -where - T: taffy::prelude::FromPercent + taffy::prelude::FromLength, -{ - if is_percent(value) { - Some(taffy::prelude::percent(self.parse_percent(tag_name, key, value)?)) - } else { - Some(taffy::prelude::length(parse_f32(value)?)) + fn print_invalid_attrib(&self, tag_name: &str, key: &str, value: &str) { + log::warn!( + "{}: <{tag_name}> value for \"{key}\" is invalid: \"{value}\"", + self.doc_params.path.get_str() + ); } -} -fn parse_check_i32(&self, tag_name: &str, key: &str, value: &str, num: &mut i32) -> bool { - if let Some(value) = parse_i32(value) { - *num = value; - true - } else { - self.print_invalid_attrib(tag_name, key, value); - false + fn print_missing_attrib(&self, tag_name: &str, attr: &str) { + log::warn!( + "{}: <{tag_name}> is missing \"{attr}\".", + self.doc_params.path.get_str() + ); } -} -fn parse_check_f32(&self, tag_name: &str, key: &str, value: &str, num: &mut f32) -> bool { - if let Some(value) = parse_f32(value) { - *num = value; - true - } else { - self.print_invalid_attrib(tag_name, key, value); - false + fn parse_val(&self, tag_name: &str, key: &str, value: &str) -> Option { + let Ok(val) = value.parse::() else { + self.print_invalid_attrib(tag_name, key, value); + return None; + }; + Some(val) + } + + fn parse_percent(&self, tag_name: &str, key: &str, value: &str) -> Option { + let Some(val_str) = value.split('%').next() else { + self.print_invalid_attrib(tag_name, key, value); + return None; + }; + + let Ok(val) = val_str.parse::() else { + self.print_invalid_attrib(tag_name, key, value); + return None; + }; + Some(val / 100.0) + } + + fn parse_size_unit(&self, tag_name: &str, key: &str, value: &str) -> Option + where + T: taffy::prelude::FromPercent + taffy::prelude::FromLength, + { + if is_percent(value) { + Some(taffy::prelude::percent(self.parse_percent(tag_name, key, value)?)) + } else { + Some(taffy::prelude::length(parse_f32(value)?)) + } + } + + fn parse_check_i32(&self, tag_name: &str, key: &str, value: &str, num: &mut i32) -> bool { + if let Some(value) = parse_i32(value) { + *num = value; + true + } else { + self.print_invalid_attrib(tag_name, key, value); + false + } + } + + fn parse_check_f32(&self, tag_name: &str, key: &str, value: &str, num: &mut f32) -> bool { + if let Some(value) = parse_f32(value) { + *num = value; + true + } else { + self.print_invalid_attrib(tag_name, key, value); + false + } } -} } fn parse_i32(value: &str) -> Option { @@ -583,7 +599,6 @@ fn require_tag_by_name<'a>(node: &roxmltree::Node<'a, 'a>, name: &str) -> anyhow get_tag_by_name(node, name).ok_or_else(|| anyhow::anyhow!("Tag \"{name}\" not found")) } - fn parse_widget_other_internal( template: &Rc