Template stripped to the overlay platform (examples, demo UI and Frame test package removed), renamed to FrameAssistant, and working rules for the project recorded in CLAUDE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
5.2 KiB
C++
131 lines
5.2 KiB
C++
#include "ControllerNavigation.h"
|
|
|
|
#include <QCoreApplication>
|
|
#include <QKeyEvent>
|
|
#include <QQuickWindow>
|
|
#include <iterator>
|
|
|
|
namespace {
|
|
// OpenVR has no name function for EVRInputError.
|
|
QString inputErrorName(vr::EVRInputError err) {
|
|
static const char* const names[] = {"VRInputError_None",
|
|
"VRInputError_NameNotFound",
|
|
"VRInputError_WrongType",
|
|
"VRInputError_InvalidHandle",
|
|
"VRInputError_InvalidParam",
|
|
"VRInputError_NoSteam",
|
|
"VRInputError_MaxCapacityReached",
|
|
"VRInputError_IPCError",
|
|
"VRInputError_NoActiveActionSet",
|
|
"VRInputError_InvalidDevice",
|
|
"VRInputError_InvalidSkeleton",
|
|
"VRInputError_InvalidBoneCount",
|
|
"VRInputError_InvalidCompressedData",
|
|
"VRInputError_NoData",
|
|
"VRInputError_BufferTooSmall",
|
|
"VRInputError_MismatchedActionManifest",
|
|
"VRInputError_MissingSkeletonData",
|
|
"VRInputError_InvalidBoneIndex",
|
|
"VRInputError_InvalidPriority",
|
|
"VRInputError_PermissionDenied",
|
|
"VRInputError_InvalidRenderModel"};
|
|
const int index = int(err);
|
|
if (index >= 0 && index < int(std::size(names)))
|
|
return QString::fromLatin1(names[index]);
|
|
return QStringLiteral("VRInputError %1").arg(index);
|
|
}
|
|
|
|
constexpr qint64 kRepeatDelayMs = 400;
|
|
constexpr qint64 kRepeatIntervalMs = 120;
|
|
} // namespace
|
|
|
|
ControllerNavigation::ControllerNavigation(QObject* parent)
|
|
: QObject(parent),
|
|
m_actions{{
|
|
{"/actions/dashboard/in/focus_next", Qt::Key_Tab, Qt::NoModifier,
|
|
true},
|
|
{"/actions/dashboard/in/focus_previous", Qt::Key_Backtab,
|
|
Qt::ShiftModifier, true},
|
|
{"/actions/dashboard/in/increase", Qt::Key_Right, Qt::NoModifier,
|
|
true},
|
|
{"/actions/dashboard/in/decrease", Qt::Key_Left, Qt::NoModifier,
|
|
true},
|
|
{"/actions/dashboard/in/activate", Qt::Key_Space, Qt::NoModifier,
|
|
false},
|
|
{"/actions/dashboard/in/back", Qt::Key_Escape, Qt::NoModifier, false},
|
|
}} {
|
|
m_pollTimer.setInterval(11);
|
|
connect(&m_pollTimer, &QTimer::timeout, this, &ControllerNavigation::poll);
|
|
}
|
|
|
|
bool ControllerNavigation::initialize(const QString& manifestPath,
|
|
QString* error) {
|
|
vr::IVRInput* input = vr::VRInput();
|
|
if (!input) {
|
|
*error = QStringLiteral("IVRInput interface unavailable");
|
|
return false;
|
|
}
|
|
vr::EVRInputError err =
|
|
input->SetActionManifestPath(manifestPath.toUtf8().constData());
|
|
if (err != vr::VRInputError_None) {
|
|
*error = QStringLiteral("SetActionManifestPath(%1) failed: %2")
|
|
.arg(manifestPath, inputErrorName(err));
|
|
return false;
|
|
}
|
|
err = input->GetActionSetHandle("/actions/dashboard", &m_actionSet);
|
|
for (Action& action : m_actions) {
|
|
if (err == vr::VRInputError_None)
|
|
err = input->GetActionHandle(action.path, &action.handle);
|
|
}
|
|
if (err != vr::VRInputError_None) {
|
|
*error = QStringLiteral("Resolving input actions failed: %1")
|
|
.arg(inputErrorName(err));
|
|
return false;
|
|
}
|
|
m_clock.start();
|
|
m_pollTimer.start();
|
|
return true;
|
|
}
|
|
|
|
void ControllerNavigation::poll() {
|
|
vr::VRActiveActionSet_t activeSet{};
|
|
activeSet.ulActionSet = m_actionSet;
|
|
if (vr::VRInput()->UpdateActionState(&activeSet, sizeof(activeSet), 1) !=
|
|
vr::VRInputError_None)
|
|
return;
|
|
|
|
QQuickWindow* target = m_target ? m_target() : nullptr;
|
|
const qint64 now = m_clock.elapsed();
|
|
|
|
for (Action& action : m_actions) {
|
|
vr::InputDigitalActionData_t data{};
|
|
if (vr::VRInput()->GetDigitalActionData(action.handle, &data, sizeof(data),
|
|
vr::k_ulInvalidInputValueHandle) !=
|
|
vr::VRInputError_None ||
|
|
!data.bActive) {
|
|
action.held = false;
|
|
continue;
|
|
}
|
|
|
|
if (data.bState && !action.held) {
|
|
action.held = true;
|
|
action.nextRepeatMs = now + kRepeatDelayMs;
|
|
if (target) sendKey(target, action);
|
|
emit actionTriggered(QString::fromLatin1(action.path).section('/', -1),
|
|
target != nullptr);
|
|
} else if (data.bState && action.repeats && now >= action.nextRepeatMs) {
|
|
action.nextRepeatMs = now + kRepeatIntervalMs;
|
|
if (target) sendKey(target, action);
|
|
} else if (!data.bState) {
|
|
action.held = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ControllerNavigation::sendKey(QQuickWindow* window, const Action& action) {
|
|
QKeyEvent press(QEvent::KeyPress, action.key, action.modifiers);
|
|
QCoreApplication::sendEvent(window, &press);
|
|
QKeyEvent release(QEvent::KeyRelease, action.key, action.modifiers);
|
|
QCoreApplication::sendEvent(window, &release);
|
|
}
|