diff --git a/README.md b/README.md index 839088a..7abeeb1 100644 --- a/README.md +++ b/README.md @@ -47,17 +47,19 @@ stays usable while a panel is open. | Input | Action | | --- | --- | +| **Trigger** | Point and click: open the entry under the laser, or press a control-bar button | +| **A** | Open the highlighted entry — folder navigates, video plays / streams. Outside the browser it is play / pause | +| **X** / **Y** | Download the highlighted entry (folders confirm first, showing their size) | +| **B** | Open or close the file browser; also closes a panel and cancels the download dialog | +| **Grip** | Toggle **that hand's** laser pointer on/off. Turn one off to navigate with the thumbstick or d-pad without the beam interfering, and to choose which controller points | | Thumbstick | Move the highlight in a panel; left/right seeks while playing | -| **A** | Open the highlighted browser entry — folder navigates, video plays / streams. Outside the browser it is play / pause | -| **B** | Open or close the file browser; also closes the settings panel and cancels the download dialog | -| **Trigger** | Laser pointer: click a panel row, or a control-bar button | -| **Grip** | Recenter the view in front of you | +| **Bumper** | Recenter the view in front of you | -The control bar has a dedicated **Download** button (arrow) that downloads the -entry highlighted in the browser — folders ask for confirmation and show their -size first. The **Downloads** button (list icon) opens the downloads manager, -where you can pause, resume, stop and clear jobs, then play a finished file or -open a finished folder. +The control bar has a **Download** button (arrow) that downloads the entry +highlighted in the browser, and a **Downloads** button (list icon) that opens the +downloads manager — where you can pause, resume, stop and clear jobs, then play a +finished file or open a finished folder. Opening the downloads manager replaces +the file browser rather than stacking on top of it. All button bindings can be changed in **Settings ▸ Controls** (press-to-bind). diff --git a/src/app/app.cpp b/src/app/app.cpp index 0041fdb..ff4be6e 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -435,6 +435,20 @@ int App::run() { !settings_.isListening() && !settings_.isOpen() && (in.buttonDown[XrInputState::BtnX] || in.buttonDown[XrInputState::BtnY]); + // Grip toggles that hand's laser pointer (SteamVR behaviour), so the + // stick / d-pad can drive a panel without the beam fighting it, and so + // the user picks which controller is the pointing hand. + if (!settings_.isListening()) { + for (int h = 0; h < 2; ++h) { + if (in.gripDown[h]) { + const bool on = !renderer_.laserOn(h); + renderer_.setLaserOn(h, on); + PIXELVR_LOG_INFO("laser [%s]: %s", + h == 0 ? "left" : "right", + on ? "on" : "off"); + } + } + } bool interacted = false; bool browserOpen = false; bool settingsOpen = false; @@ -679,7 +693,9 @@ int App::run() { renderer_.setDownloadsModal(false); } // Publish the browser panel first so the pointer can raycast it. - if (browser_.isOpen()) { + // The downloads window replaces the browser view instead of stacking + // on top of it, so nothing behind it can be selected by mistake. + if (browser_.isOpen() && !dlWin) { std::vector rows; rows.reserve(browser_.entries().size()); for (const auto& e : browser_.entries()) { @@ -827,8 +843,10 @@ int App::run() { #ifdef PIXELVR_HAVE_FREETYPE // Browsing stays interactive during downloads: they run in a separate // process now, so they no longer block directory access. (Only guard - // against an in-flight SMB directory scan.) - if (browser_.isOpen() && !browser_.isScanning()) { + // against an in-flight SMB directory scan.) Interaction is suspended + // while an overlay (downloads window / confirm dialog) owns the view. + if (browser_.isOpen() && !browser_.isScanning() && !downloadsWindow_ && + pendingDl_.empty()) { if (const int d = navStep(in.thumbstickY, nowT)) { browser_.move(d); renderer_.scrollBrowser(d); @@ -837,19 +855,15 @@ int App::run() { const int hovered = renderer_.browserHovered(); if (hovered >= 0) { renderer_.setBrowserSelected(hovered); - // The pointer drives the selection, so A acts on the entry - // actually being pointed at (not just the last trigger press). browser_.setSelected(hovered); } - // Trigger activates the selection whether it was made with the - // laser (hovered row) or moved with the thumbstick. Skipped when - // the pointer is on the control bar (that click is handled below). - // A opens / activates the highlighted entry: folders navigate, video - // files play. Downloading has its own button (X / Y), and when the - // laser is resting on a control-bar button that button takes the - // press instead (handled below). - if (in.togglePlay && renderer_.pointerButton() < 0 && - pendingDl_.empty()) { + // Open the highlighted entry. The trigger does it whenever the + // laser is not resting on a control-bar button (that click belongs + // to the bar, handled below); A does it too, which is the natural + // choice when moving the highlight with the thumbstick / d-pad. + // Downloading has its own buttons (X / Y). + if ((in.togglePlay || in.select) && + renderer_.pointerButton() < 0) { std::string playPath; if (browser_.activate(playPath) == Browser::Result::Play && !playPath.empty()) { diff --git a/src/gfx/vulkan_renderer.cpp b/src/gfx/vulkan_renderer.cpp index 45bbb83..72b55c8 100644 --- a/src/gfx/vulkan_renderer.cpp +++ b/src/gfx/vulkan_renderer.cpp @@ -1015,9 +1015,10 @@ void VulkanRenderer::updatePointer() { } const Mat4 inv = rigidInverse(panelBase); - // Prefer the right hand; fall back to the left. + // Prefer the right hand; fall back to the left. A hand whose laser is off + // takes no part in the raycast. for (int h : {1, 0}) { - if (!controllers_[h].active) { + if (!controllers_[h].active || !laserOn_[h]) { continue; } const Quat q{controllers_[h].orientation[0], controllers_[h].orientation[1], @@ -2151,7 +2152,7 @@ void VulkanRenderer::renderView(uint32_t viewIndex, uint32_t imageIndex, const float rayCol[4] = {0.25f, 0.60f, 1.0f, 0.85f}; const float cursorCol[4] = {1.0f, 1.0f, 1.0f, 0.95f}; for (int h = 0; h < 2; ++h) { - if (!controllers_[h].active) { + if (!controllers_[h].active || !laserOn_[h]) { continue; } const Quat q{controllers_[h].orientation[0], diff --git a/src/gfx/vulkan_renderer.hpp b/src/gfx/vulkan_renderer.hpp index ba381f1..0ea2618 100644 --- a/src/gfx/vulkan_renderer.hpp +++ b/src/gfx/vulkan_renderer.hpp @@ -202,6 +202,16 @@ public: }; void setControlBar(bool visible) { controlBarVisible_ = visible; } + // Per-hand laser pointer toggle (grip press). A hand with the laser off + // neither casts a beam nor takes part in the UI raycast, so the other hand + // can own the pointer while this one drives a panel with its stick/d-pad. + void setLaserOn(int hand, bool on) { + if (hand >= 0 && hand < 2) laserOn_[hand] = on; + } + bool laserOn(int hand) const { + return (hand >= 0 && hand < 2) ? laserOn_[hand] : false; + } + // The control-bar button currently under the pointer, or -1 if none. int pointerButton() const { return pointerButton_; } // True when the pointer is over the draggable timeline; pointerScrub() is @@ -365,6 +375,8 @@ private: bool pointerActive_ = false; int pointerRow_ = -1; int pointerHand_ = -1; + // Whether each hand's laser pointer is on (toggled with the grip button). + bool laserOn_[2] = {true, true}; float pointerHit_[3] = {0.0f, 0.0f, 0.0f}; bool controlBarVisible_ = false; int pointerButton_ = -1; diff --git a/src/ui/settings_menu.cpp b/src/ui/settings_menu.cpp index 6c52174..d59882d 100644 --- a/src/ui/settings_menu.cpp +++ b/src/ui/settings_menu.cpp @@ -343,7 +343,12 @@ void SettingsMenu::loadConfig() { else if (key == "distance") screenDistance_ = std::clamp(std::stof(val), 1.0f, 10.0f); else if (key == "bind_play_btn") bindPlayBtn_ = std::clamp(std::stoi(val), 0, kButtonCount - 1); else if (key == "bind_menu_btn") bindMenuBtn_ = std::clamp(std::stoi(val), 0, kButtonCount - 1); - else if (key == "bind_recenter_btn") bindRecenterBtn_ = std::clamp(std::stoi(val), 0, kButtonCount - 1); + else if (key == "bind_recenter_btn") { + const int b = std::clamp(std::stoi(val), 0, kButtonCount - 1); + // Grip used to default to recenter; it now toggles the laser pointer, + // so a saved grip binding is migrated to the new default (Bumper). + bindRecenterBtn_ = (b == 8) ? 6 : b; + } else if (key == "download_dir") downloadDirIdx_ = std::clamp(std::stoi(val), 0, kDownloadDirCount - 1); else if (key == "audio_lang") audioLangIdx_ = std::clamp(std::stoi(val), 0, kLangCount - 1); } diff --git a/src/ui/settings_menu.hpp b/src/ui/settings_menu.hpp index b6edcce..a6c43ce 100644 --- a/src/ui/settings_menu.hpp +++ b/src/ui/settings_menu.hpp @@ -109,7 +109,7 @@ private: // Function -> physical button (XrInputState::Button index); press-to-bind. int bindPlayBtn_ = 0; // default A int bindMenuBtn_ = 1; // default B - int bindRecenterBtn_ = 8; // default Grip + int bindRecenterBtn_ = 6; // default Bumper (grip toggles the laser) int listeningFunc_ = -1; // 0=play 1=menu 2=recenter while binding, else -1 int downloadDirIdx_ = 0; // index into the download-destination presets diff --git a/src/xr/xr_context.cpp b/src/xr/xr_context.cpp index d4693a8..6662250 100644 --- a/src/xr/xr_context.cpp +++ b/src/xr/xr_context.cpp @@ -543,6 +543,8 @@ void XrContext::syncActions() { input_.thumbstickRX = 0.0f; input_.thumbstickRY = 0.0f; for (int i = 0; i < XrInputState::BtnCount; ++i) input_.buttonDown[i] = false; + input_.gripDown[0] = false; + input_.gripDown[1] = false; if (!actionsReady_) { return; } @@ -628,6 +630,21 @@ void XrContext::syncActions() { st.isActive && st.currentState && st.changedSinceLastSync; } } + + // Per-hand grip press: `buttonDown` above aggregates both hands, but the laser + // pointer is toggled per hand, so query the squeeze action for each subaction. + if (buttonActions_[XrInputState::BtnGrip] != XR_NULL_HANDLE) { + for (int h = 0; h < 2; ++h) { + XrActionStateGetInfo gi{XR_TYPE_ACTION_STATE_GET_INFO}; + gi.action = buttonActions_[XrInputState::BtnGrip]; + gi.subactionPath = handPaths_[h]; + XrActionStateBoolean st{XR_TYPE_ACTION_STATE_BOOLEAN}; + if (XR_SUCCEEDED(xrGetActionStateBoolean(session_, &gi, &st))) { + input_.gripDown[h] = + st.isActive && st.currentState && st.changedSinceLastSync; + } + } + } } void XrContext::handleSessionStateChange(const XrEventDataSessionStateChanged& ev, diff --git a/src/xr/xr_context.hpp b/src/xr/xr_context.hpp index 0b69203..f4f87a0 100644 --- a/src/xr/xr_context.hpp +++ b/src/xr/xr_context.hpp @@ -56,6 +56,11 @@ struct XrInputState { BtnDpadUp, BtnDpadDown, BtnDpadLeft, BtnDpadRight, BtnCount }; bool buttonDown[BtnCount] = {}; + + // Per-hand grip (squeeze) rising edge: toggles that hand's laser pointer, so + // one controller can drive a panel with the stick while the other points. + // Index 0 = left, 1 = right. + bool gripDown[2] = {false, false}; }; // Owns the OpenXR instance, system, session, reference space, and swapchains,