mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 01:00:04 +02:00
Add opt-in visible-overlay laser mode setting
This commit is contained in:
1 parent
f80da228b3
commit
a9d6762ebd
12 files changed
+225
-28
No files matched your search
+5
-1
@@ -46,7 +46,7 @@ add_executable(frameyap src/main.cpp)
|
||||
target_link_libraries(frameyap PRIVATE frameyap_core)
|
||||
target_compile_definitions(frameyap PRIVATE FRAMEYAP_VERSION="${FRAMEYAP_VERSION}")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_library(frameyap_mount src/mount.cpp)
|
||||
add_library(frameyap_mount src/mount.cpp src/laser_setting.cpp)
|
||||
target_include_directories(frameyap_mount PUBLIC src)
|
||||
add_library(frameyap_worker src/worker.cpp)
|
||||
target_include_directories(frameyap_worker PUBLIC src)
|
||||
@@ -116,6 +116,10 @@ if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
|
||||
add_executable(frameyap_mount_test tests/mount_test.cpp)
|
||||
target_link_libraries(frameyap_mount_test PRIVATE frameyap_mount)
|
||||
add_test(NAME frameyap.mount COMMAND frameyap_mount_test)
|
||||
add_executable(frameyap_laser_setting_test tests/laser_setting_test.cpp)
|
||||
target_link_libraries(frameyap_laser_setting_test PRIVATE frameyap_mount)
|
||||
target_compile_options(frameyap_laser_setting_test PRIVATE -UNDEBUG)
|
||||
add_test(NAME frameyap.laser_setting COMMAND frameyap_laser_setting_test)
|
||||
add_executable(frameyap_lock_test tests/lock_test.cpp)
|
||||
target_include_directories(frameyap_lock_test PRIVATE src)
|
||||
add_test(NAME frameyap.instance_lock COMMAND frameyap_lock_test)
|
||||
|
||||
@@ -20,13 +20,16 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet
|
||||
## Controls
|
||||
|
||||
- **Right X (default Frame binding):** hold to record; release to
|
||||
transcribe. This PTT action is remappable through SteamVR bindings; delivery
|
||||
in the dashboard still requires an on-device test.
|
||||
transcribe. Repeated presses reached the controls-only diagnostic on Frame;
|
||||
live mic capture through this shortcut still needs guided acceptance. This
|
||||
PTT action is remappable through SteamVR bindings.
|
||||
- **Left grip (when active):** double-tap to explicitly send Enter. Never inferred from speech.
|
||||
- **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit.
|
||||
Review and settings share one Inconsolata/neon-framed surface. World-space by
|
||||
default; settings offer left wrist, right wrist and head mounting, plus recenter.
|
||||
Dashboard lasers provide clickable controls without forcing global laser mode.
|
||||
Dashboard lasers provide clickable controls. Settings → Lasers anytime is an
|
||||
opt-in, default-off system-wide laser mode while the panel is visible; it may
|
||||
affect games and is separate from experimental input overrides.
|
||||
- **Theme and controls:** optional `$XDG_CONFIG_HOME/frameyap/config.json` selects
|
||||
panel colors, a font path and Frame controller button mappings; missing fonts
|
||||
fall back to bundled Inconsolata. The installer creates/checks this file and
|
||||
|
||||
+24
-7
@@ -107,10 +107,25 @@ keeps the selection for the session and displays a warning. `--mount
|
||||
world|left-wrist|right-wrist|head` overrides the saved choice for one launch without
|
||||
writing it; `--head` remains an alias for `--mount head`.
|
||||
|
||||
Settings also has **Lasers anytime** (default off). When enabled, FrameYap sets
|
||||
OpenVR's `VROverlayFlags_MakeOverlaysInteractiveIfVisible` on its panel. OpenVR
|
||||
requests system-wide laser mouse mode while the panel is visible, including
|
||||
with Steam's dashboard closed; it may change interaction with games. Turning
|
||||
it off removes that request. This is **not** the experimental overlay action
|
||||
priority override and does not promise pass-through of a dashboard-owned
|
||||
button or PTT delivery during dashboard focus. The choice is saved as `on` or
|
||||
`off` in `$XDG_CONFIG_HOME/frameyap/lasers-anytime` (default
|
||||
`~/.config/frameyap/lasers-anytime`) using a private atomic file replacement;
|
||||
missing, symlinked or invalid files mean off. A failed save keeps the new
|
||||
choice only for the running session and shows a warning. To turn it back on
|
||||
after disabling it with the dashboard closed, open the dashboard to use its
|
||||
laser on the Settings button. Controls-only checks can toggle it temporarily
|
||||
but do not save the preference. This has not yet been accepted in a headset.
|
||||
|
||||
### Hardware-free UI checks
|
||||
|
||||
The default build tests mount parsing, persistence and pose geometry without any
|
||||
native dependencies. A FreeType-only opt-in build exercises the actual renderer,
|
||||
The default build tests mount parsing, laser preference persistence and pose
|
||||
geometry without any native dependencies. A FreeType-only opt-in build exercises the actual renderer,
|
||||
pointer gating, tab switches, pagination, recording state and redraw invalidation:
|
||||
|
||||
```sh
|
||||
@@ -136,11 +151,13 @@ updates are flicker-free.
|
||||
|
||||
`assets/actions.json` names six actions: left/right grip, PTT, cancel, insert,
|
||||
Enter. `bindings_frame_controller.json` maps right X click to hold-to-talk PTT;
|
||||
the grip bindings remain for optional remapping/diagnosis. Both grip actions
|
||||
were **inactive** during the dashboard controls check despite a loaded binding
|
||||
file; this does not prove whether right X will deliver until an on-device test.
|
||||
`bindings_knuckles.json` is an
|
||||
additional **Index/knuckles example only**. Collisions with scene actions require
|
||||
the grip bindings remain for optional remapping/diagnosis. In one dashboard
|
||||
probe grips were inactive; a later controls-only probe delivered repeated right
|
||||
X PTT BeginRecord/EndRecord callbacks. The wearer reports controller actions
|
||||
are usable with Steam's dashboard closed, not with the dashboard itself open.
|
||||
Neither probe used a microphone or established game-scene pass-through.
|
||||
`bindings_knuckles.json` is an additional **Index/knuckles example only**.
|
||||
Collisions with scene actions require
|
||||
separate on-device validation. Left grip double tap
|
||||
(releases <=250 ms, second press within 350 ms) requests explicit Enter only
|
||||
when enabled. Right grip: first short squeeze and release (<=250 ms), then
|
||||
|
||||
@@ -7,6 +7,8 @@ initialize OpenVR, open a microphone, run ASR, download files or inject input.
|
||||
|
||||
- Opt-in OpenVR RGBA overlay with world-space default and selectable wrist/head
|
||||
mounts, status, recording timer, paginated UTF-8 preview and explicit controls.
|
||||
A saved, default-off Lasers anytime setting requests system-wide laser mouse
|
||||
mode while FrameYap is visible; it may affect games, and is not an input override.
|
||||
- Remappable SteamVR actions. The default Steam Frame binding maps right X
|
||||
(hold to record, release to transcribe) to the existing PTT action using the
|
||||
observed `frame_controller` profile. Grip bindings remain, but both grip
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "laser_setting.hpp"
|
||||
#include "mount.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace frameyap {
|
||||
namespace {
|
||||
std::atomic<unsigned long> next_temp{0};
|
||||
}
|
||||
|
||||
std::filesystem::path default_laser_settings_path() {
|
||||
auto mount_path = default_mount_settings_path();
|
||||
return mount_path.empty() ? mount_path : mount_path.parent_path() / "lasers-anytime";
|
||||
}
|
||||
|
||||
bool load_lasers_anytime(const std::filesystem::path& path) {
|
||||
try {
|
||||
if (path.empty() || std::filesystem::is_symlink(path) || !std::filesystem::is_regular_file(path)) return false;
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input) return false;
|
||||
char data[9]{};
|
||||
input.read(data, sizeof(data));
|
||||
const auto size = input.gcount();
|
||||
return size == 3 && std::string_view(data, 3) == "on\n";
|
||||
} catch (...) { return false; }
|
||||
}
|
||||
|
||||
bool save_lasers_anytime(const std::filesystem::path& path, bool enabled) {
|
||||
std::filesystem::path temporary;
|
||||
int fd = -1;
|
||||
try {
|
||||
if (path.empty() || path.filename().empty() || std::filesystem::is_symlink(path)) return false;
|
||||
const auto dir = path.has_parent_path() ? path.parent_path() : std::filesystem::path(".");
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (ec) return false;
|
||||
for (int attempt = 0; attempt < 16; ++attempt) {
|
||||
temporary = dir / (path.filename().string() + ".tmp." + std::to_string(::getpid()) +
|
||||
"." + std::to_string(next_temp.fetch_add(1)));
|
||||
fd = ::open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
|
||||
if (fd >= 0) break;
|
||||
if (errno != EEXIST) return false;
|
||||
}
|
||||
if (fd < 0) return false;
|
||||
const std::string_view content = enabled ? "on\n" : "off\n";
|
||||
size_t written = 0;
|
||||
while (written < content.size()) {
|
||||
const auto n = ::write(fd, content.data() + written, content.size() - written);
|
||||
if (n < 0 && errno == EINTR) continue;
|
||||
if (n <= 0) { ::close(fd); fd = -1; std::filesystem::remove(temporary, ec); return false; }
|
||||
written += static_cast<size_t>(n);
|
||||
}
|
||||
const bool synced = ::fsync(fd) == 0;
|
||||
const bool closed = ::close(fd) == 0;
|
||||
fd = -1;
|
||||
if (!synced || !closed) { std::filesystem::remove(temporary, ec); return false; }
|
||||
std::filesystem::rename(temporary, path, ec);
|
||||
if (ec) { std::filesystem::remove(temporary, ec); return false; }
|
||||
return true;
|
||||
} catch (...) {
|
||||
if (fd >= 0) ::close(fd);
|
||||
if (!temporary.empty()) { std::error_code ec; std::filesystem::remove(temporary, ec); }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} // namespace frameyap
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
|
||||
namespace frameyap {
|
||||
// Opt-in, user-local preference. Missing or malformed files always mean off.
|
||||
std::filesystem::path default_laser_settings_path();
|
||||
bool load_lasers_anytime(const std::filesystem::path& path);
|
||||
bool save_lasers_anytime(const std::filesystem::path& path, bool enabled);
|
||||
} // namespace frameyap
|
||||
+2
-1
@@ -22,7 +22,8 @@ void help() {
|
||||
" --check-overlay --assets DIR [--font FILE] [--mount MODE] (5s, no mic/input)\n"
|
||||
" --check-controls --assets DIR [--font FILE] [--mount MODE] (30s, gestures only)\n\n"
|
||||
"Mount: world (first-launch default), left-wrist, right-wrist, head.\n"
|
||||
"Settings save the mount; --mount overrides it for this launch. --head is an alias.\n"
|
||||
"Settings save the mount and opt-in Lasers anytime mode (may affect games).\n"
|
||||
"--mount overrides placement for this launch. --head is an alias.\n"
|
||||
"Theme, font and Frame button mappings: $XDG_CONFIG_HOME/frameyap/config.json\n"
|
||||
"(or ~/.config/frameyap/config.json). CLI --font overrides config; missing fonts\n"
|
||||
"fall back to bundled Inconsolata, then a system DejaVu face.\n"
|
||||
|
||||
+34
-5
@@ -1,5 +1,6 @@
|
||||
#include "overlay.hpp"
|
||||
#include "gestures.hpp"
|
||||
#include "laser_setting.hpp"
|
||||
#include "panel_surface.hpp"
|
||||
|
||||
#include <openvr.h>
|
||||
@@ -50,7 +51,9 @@ struct Overlay::Impl {
|
||||
vr::VRActionSetHandle_t action_set = vr::k_ulInvalidActionSetHandle;
|
||||
std::array<vr::VRActionHandle_t, 6> actions{};
|
||||
std::filesystem::path settings_path;
|
||||
std::filesystem::path laser_settings_path;
|
||||
Mount mount;
|
||||
bool lasers_anytime = false, laser_change_failed = false;
|
||||
Config config;
|
||||
PanelSurface surface;
|
||||
Panel panel;
|
||||
@@ -75,7 +78,9 @@ struct Overlay::Impl {
|
||||
|
||||
Impl(const std::string& assets, const std::string& font, std::optional<Mount> requested, bool persist)
|
||||
: settings_path(default_mount_settings_path()),
|
||||
laser_settings_path(default_laser_settings_path()),
|
||||
mount(requested ? *requested : load_mount(settings_path)),
|
||||
lasers_anytime(load_lasers_anytime(laser_settings_path)),
|
||||
config(load_config(default_config_path())),
|
||||
surface(resolve_font(assets, font.empty() ? config.font : font), mount, config.theme),
|
||||
persist_mount(persist) {
|
||||
@@ -107,8 +112,14 @@ struct Overlay::Impl {
|
||||
overlay_check(overlay->CreateOverlay("local.frameyap.overlay.panel", "FrameYap", &handle), overlay, "CreateOverlay");
|
||||
overlay_check(overlay->SetOverlayWidthInMeters(handle, 0.85f), overlay, "SetOverlayWidthInMeters");
|
||||
overlay_check(overlay->SetOverlayInputMethod(handle, vr::VROverlayInputMethod_Mouse), overlay, "SetOverlayInputMethod");
|
||||
// Let dashboard lasers reach our controls without forcing global
|
||||
// laser-mouse mode over a running scene.
|
||||
// Normal priority actions stay unchanged. This separate, explicit
|
||||
// preference requests system-wide laser mouse mode only while the
|
||||
// panel is visible; it may affect interaction with a running game.
|
||||
if (lasers_anytime && overlay->SetOverlayFlag(handle, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true) != vr::VROverlayError_None) {
|
||||
lasers_anytime = false;
|
||||
laser_change_failed = true;
|
||||
}
|
||||
surface.set_lasers_anytime(lasers_anytime);
|
||||
overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true), overlay, "VisibleInDashboard");
|
||||
vr::HmdVector2_t mouse_scale{{float(W), float(H)}};
|
||||
overlay_check(overlay->SetOverlayMouseScale(handle, &mouse_scale), overlay, "SetOverlayMouseScale");
|
||||
@@ -144,7 +155,8 @@ struct Overlay::Impl {
|
||||
}
|
||||
if (effective == Mount::World && applied_mount && *applied_mount != Mount::World)
|
||||
world_ready = false; // a fresh world fallback near the wearer, not an old room location
|
||||
std::string note = save_failed ? "Preference could not be saved; using it for this session." : "";
|
||||
std::string note = laser_change_failed ? "SteamVR declined the laser mode change." :
|
||||
save_failed ? "Preference could not be saved; using it for this session." : "";
|
||||
if (effective != mount) note = save_failed ? "Wrist untracked; world fallback. Preference not saved." :
|
||||
"Wrist not tracked - using world space until it returns.";
|
||||
if (effective == Mount::World && !world_ready) {
|
||||
@@ -273,8 +285,20 @@ struct Overlay::Impl {
|
||||
last_pointer_event = "up button=" + std::to_string(event.data.mouse.button);
|
||||
if (event.data.mouse.button == vr::VRMouseButton_Left) {
|
||||
auto event_result = surface.pointer_up(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y);
|
||||
if (event_result.action || event_result.mount || event_result.recenter) ++pointer_actions;
|
||||
if (event_result.action || event_result.mount || event_result.recenter || event_result.lasers_anytime) ++pointer_actions;
|
||||
if (event_result.action) result.push_back(*event_result.action);
|
||||
if (event_result.lasers_anytime) {
|
||||
const bool enabled = *event_result.lasers_anytime;
|
||||
if (overlay->SetOverlayFlag(handle, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible,
|
||||
enabled) == vr::VROverlayError_None) {
|
||||
lasers_anytime = enabled;
|
||||
laser_change_failed = false;
|
||||
surface.set_lasers_anytime(enabled);
|
||||
save_failed = persist_mount && !save_lasers_anytime(laser_settings_path, enabled);
|
||||
} else {
|
||||
laser_change_failed = true;
|
||||
}
|
||||
}
|
||||
if (event_result.mount) {
|
||||
mount = *event_result.mount;
|
||||
save_failed = persist_mount && !save_mount(settings_path, mount);
|
||||
@@ -335,7 +359,12 @@ void Overlay::draw(const Panel& panel) { impl_->draw(panel); }
|
||||
std::string Overlay::controls_status() {
|
||||
// Diagnostic only: distinguish SteamVR binding/activity from our stricter
|
||||
// pose/role gate. Raw legacy state is read-only and may be unavailable.
|
||||
std::string result = "SteamVR update=" + std::to_string(int(impl_->action_update_error));
|
||||
bool laser_flag = false;
|
||||
const auto laser_error = impl_->overlay->GetOverlayFlag(impl_->handle,
|
||||
vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, &laser_flag);
|
||||
std::string result = "SteamVR update=" + std::to_string(int(impl_->action_update_error)) +
|
||||
" dashboard=" + (impl_->overlay->IsDashboardVisible() ? "Y" : "N") +
|
||||
" lasers-anytime=" + (laser_error == vr::VROverlayError_None ? (laser_flag ? "Y" : "N") : "n/a");
|
||||
for (size_t i = 0; i < 2; ++i) {
|
||||
const auto role = i == 0 ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand;
|
||||
const auto device = impl_->system->GetTrackedDeviceIndexForControllerRole(role);
|
||||
|
||||
+23
-10
@@ -26,9 +26,9 @@ struct Rect {
|
||||
}
|
||||
};
|
||||
enum class Control { Review, Settings, Prev, Next, Record, Cancel, Insert, Enter, Quit,
|
||||
World, Left, Right, Head, Recenter };
|
||||
World, Left, Right, Head, Recenter, LasersAnytime };
|
||||
struct Button { Rect r; Control id; const char* label; };
|
||||
constexpr std::array<Button, 14> buttons{{
|
||||
constexpr std::array<Button, 15> buttons{{
|
||||
{{32, 138, 180, 46}, Control::Review, "Review"},
|
||||
{{226, 138, 180, 46}, Control::Settings, "Settings"},
|
||||
{{32, 406, 154, 44}, Control::Prev, "Previous"},
|
||||
@@ -43,6 +43,7 @@ constexpr std::array<Button, 14> buttons{{
|
||||
{{32, 308, 454, 58}, Control::Left, "Left wrist"},
|
||||
{{514, 308, 454, 58}, Control::Right, "Right wrist"},
|
||||
{{32, 394, 300, 50}, Control::Recenter, "Recenter in front"},
|
||||
{{514, 394, 454, 50}, Control::LasersAnytime, "Lasers anytime"},
|
||||
}};
|
||||
std::optional<UiAction> action(Control c) {
|
||||
switch (c) {
|
||||
@@ -100,7 +101,7 @@ struct PanelSurface::Impl {
|
||||
Mount mount;
|
||||
Theme theme;
|
||||
Color background, card, ink, muted, cyan, pink;
|
||||
bool settings = false, dirty = true;
|
||||
bool settings = false, dirty = true, lasers_anytime = false;
|
||||
std::string placement_note;
|
||||
std::array<int, 2> pressed{{-1, -1}};
|
||||
std::vector<std::string> lines;
|
||||
@@ -238,7 +239,7 @@ struct PanelSurface::Impl {
|
||||
}
|
||||
bool visible(Control c) const {
|
||||
if (c == Control::Prev || c == Control::Next) return !settings;
|
||||
if (mounting(c) || c == Control::Recenter) return settings;
|
||||
if (mounting(c) || c == Control::Recenter || c == Control::LasersAnytime) return settings;
|
||||
return true;
|
||||
}
|
||||
bool enabled(Control c) const {
|
||||
@@ -294,11 +295,11 @@ struct PanelSurface::Impl {
|
||||
text(detail[i], 32, 486 + int(i) * 30, 24, muted, 936);
|
||||
if (detail.size() > 2) text("[detail truncated]", 730, 546, 22, pink, 968);
|
||||
} else {
|
||||
text("MOUNT THE MENU", 32, 224, 22, muted, 968);
|
||||
text("World stays put. Recenter places it in front of you.", 354, 426, 22, muted, 968);
|
||||
text(placement_note.empty() ? "Choose a mount. Preference is saved on this device." : placement_note,
|
||||
32, 486, 23, muted, 968);
|
||||
text("Tracking lost? Wrist placement falls back to world space.", 32, 518, 23, muted, 968);
|
||||
text("MOUNT AND INTERACTION", 32, 224, 22, muted, 968);
|
||||
text("Lasers anytime enables system-wide laser mode while this panel is visible.", 32, 472, 20, muted, 968);
|
||||
text(placement_note.empty() ? "May affect games. Default off; changes saved on this device." : placement_note,
|
||||
32, 509, 22, muted, 968);
|
||||
text("If off, open the dashboard to click this setting again.", 32, 538, 20, muted, 968);
|
||||
}
|
||||
rect({32, 550, 936, 1}, mix(card, cyan, .17f));
|
||||
for (size_t i = 0; i < buttons.size(); ++i) {
|
||||
@@ -306,7 +307,8 @@ struct PanelSurface::Impl {
|
||||
if (!visible(b.id)) continue;
|
||||
bool on = enabled(b.id);
|
||||
bool selected = (b.id == Control::Review && !settings) || (b.id == Control::Settings && settings) ||
|
||||
(mounting(b.id) && *mounting(b.id) == mount);
|
||||
(mounting(b.id) && *mounting(b.id) == mount) ||
|
||||
(b.id == Control::LasersAnytime && lasers_anytime);
|
||||
const Color fill = !on ? mix(background, card, .40f) :
|
||||
selected ? mix(card, cyan, .14f) : card;
|
||||
const Color accent = b.id == Control::Record && panel.recording ? pink : cyan;
|
||||
@@ -318,6 +320,9 @@ struct PanelSurface::Impl {
|
||||
const auto label = b.id == Control::Record && panel.recording ? "Stop" : b.label;
|
||||
text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9, 27, on ? ink : mix(background, muted, .48f), b.r.x + b.r.w - 8);
|
||||
if (mounting(b.id) && selected) text("ON", b.r.x + b.r.w - 56, b.r.y + 38, 23, cyan, b.r.x + b.r.w - 12);
|
||||
if (b.id == Control::LasersAnytime)
|
||||
text(lasers_anytime ? "ON" : "OFF", b.r.x + b.r.w - 66, b.r.y + 34, 22,
|
||||
lasers_anytime ? cyan : muted, b.r.x + b.r.w - 12);
|
||||
}
|
||||
dirty = false;
|
||||
return true;
|
||||
@@ -347,6 +352,7 @@ SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y) {
|
||||
else if (auto a = action(c)) result.action = a;
|
||||
else if (auto m = mounting(c)) { impl_->mount = *m; result.mount = *m; impl_->reset(); impl_->dirty = true; }
|
||||
else if (c == Control::Recenter) { result.recenter = true; impl_->reset(); }
|
||||
else if (c == Control::LasersAnytime) result.lasers_anytime = !impl_->lasers_anytime;
|
||||
else if (c == Control::Review || c == Control::Settings) { impl_->settings = c == Control::Settings; impl_->reset(); impl_->dirty = true; }
|
||||
else if (c == Control::Prev) { --impl_->page; impl_->dirty = true; }
|
||||
else if (c == Control::Next) { ++impl_->page; impl_->dirty = true; }
|
||||
@@ -356,4 +362,11 @@ void PanelSurface::reset_pointers() { impl_->reset(); }
|
||||
void PanelSurface::set_placement_note(std::string note) {
|
||||
if (impl_->placement_note != note) { impl_->placement_note = std::move(note); impl_->dirty = true; }
|
||||
}
|
||||
void PanelSurface::set_lasers_anytime(bool enabled) {
|
||||
if (impl_->lasers_anytime != enabled) {
|
||||
impl_->lasers_anytime = enabled;
|
||||
impl_->reset();
|
||||
impl_->dirty = true;
|
||||
}
|
||||
}
|
||||
} // namespace frameyap
|
||||
@@ -11,6 +11,7 @@ namespace frameyap {
|
||||
struct SurfaceEvent {
|
||||
std::optional<UiAction> action;
|
||||
std::optional<Mount> mount;
|
||||
std::optional<bool> lasers_anytime;
|
||||
bool recenter = false;
|
||||
};
|
||||
// One CPU RGBA canvas, independent of OpenVR. Settings replace the review area;
|
||||
@@ -29,6 +30,7 @@ public:
|
||||
void pointer_move(unsigned cursor, float x, float y);
|
||||
void reset_pointers();
|
||||
void set_placement_note(std::string note);
|
||||
void set_lasers_anytime(bool enabled);
|
||||
bool available(UiAction action) const;
|
||||
private:
|
||||
struct Impl;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "laser_setting.hpp"
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
int main() {
|
||||
using namespace frameyap;
|
||||
const auto pattern = (std::filesystem::temp_directory_path() / "frameyap-laser-test-XXXXXX").string();
|
||||
std::vector<char> buffer(pattern.begin(), pattern.end());
|
||||
buffer.push_back('\0');
|
||||
const char* created = ::mkdtemp(buffer.data());
|
||||
assert(created);
|
||||
const std::filesystem::path root(created);
|
||||
assert(::setenv("XDG_CONFIG_HOME", root.c_str(), 1) == 0);
|
||||
assert(default_laser_settings_path() == root / "frameyap/lasers-anytime");
|
||||
const auto path = default_laser_settings_path();
|
||||
assert(!load_lasers_anytime(path));
|
||||
assert(save_lasers_anytime(path, true));
|
||||
assert(load_lasers_anytime(path));
|
||||
assert((std::filesystem::status(path).permissions() &
|
||||
(std::filesystem::perms::group_all | std::filesystem::perms::others_all)) == std::filesystem::perms::none);
|
||||
assert(save_lasers_anytime(path, false));
|
||||
assert(!load_lasers_anytime(path));
|
||||
{ std::ofstream out(path, std::ios::trunc); out << "anything else\n"; }
|
||||
assert(!load_lasers_anytime(path));
|
||||
const auto link = root / "link";
|
||||
std::filesystem::create_symlink(path, link);
|
||||
assert(!load_lasers_anytime(link));
|
||||
assert(!save_lasers_anytime(link, true));
|
||||
assert(!load_lasers_anytime(path));
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
+10
-1
@@ -11,7 +11,9 @@ SurfaceEvent click(PanelSurface& surface, float x, float y, unsigned cursor = 0)
|
||||
surface.pointer_down(cursor, x, y);
|
||||
return surface.pointer_up(cursor, x, y);
|
||||
}
|
||||
void no_action(const SurfaceEvent& event) { assert(!event.action && !event.mount && !event.recenter); }
|
||||
void no_action(const SurfaceEvent& event) {
|
||||
assert(!event.action && !event.mount && !event.lasers_anytime && !event.recenter);
|
||||
}
|
||||
void snapshot(PanelSurface& surface, const std::string& path) {
|
||||
std::ofstream out(path, std::ios::binary);
|
||||
assert(out);
|
||||
@@ -104,6 +106,12 @@ int main(int argc, char** argv) {
|
||||
assert(click(surface, 680, 332).mount == Mount::RightWrist);
|
||||
assert(click(surface, 680, 260).mount == Mount::Head);
|
||||
assert(click(surface, 180, 260).mount == Mount::World);
|
||||
auto laser = click(surface, 680, 420);
|
||||
assert(laser.lasers_anytime == true && !laser.action && !laser.mount);
|
||||
surface.set_lasers_anytime(true); assert(surface.render(p));
|
||||
laser = click(surface, 680, 420);
|
||||
assert(laser.lasers_anytime == false && !laser.action && !laser.mount);
|
||||
surface.set_lasers_anytime(false); assert(surface.render(p));
|
||||
assert(click(surface, 280, 610).action == UiAction::Cancel);
|
||||
surface.set_placement_note("Wrist not tracked - using world space until it returns.");
|
||||
assert(surface.render(p)); assert(!surface.render(p));
|
||||
@@ -114,6 +122,7 @@ int main(int argc, char** argv) {
|
||||
no_action(click(surface, 100, 160));
|
||||
no_action(surface.pointer_up(1, 680, 260));
|
||||
no_action(click(surface, 680, 260)); // mount controls not active on Review
|
||||
no_action(click(surface, 680, 420)); // laser toggle only exists on Settings
|
||||
|
||||
// Long UTF-8, newlines and malformed bytes are bounded, paginated and navigable.
|
||||
p.transcript.clear();
|
||||
|
||||
Reference in new issue
Block a user