mirror of
https://github.com/LizardByte/libvirtualhid.git
synced 2026-10-23 11:37:37 +09:00
368 lines
11 KiB
C++
368 lines
11 KiB
C++
/**
|
|
* @file tools/virtualhid_control_model.cpp
|
|
* @brief Testable model helper definitions for the native libvirtualhid control UI.
|
|
*/
|
|
|
|
// standard includes
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <iomanip>
|
|
#include <ranges>
|
|
#include <sstream>
|
|
|
|
// local includes
|
|
#include "virtualhid_control_model.hpp"
|
|
|
|
namespace lvh::tools::virtualhid_control {
|
|
namespace {
|
|
|
|
void append_summary_separator(std::wostringstream &stream, bool wrote) {
|
|
if (wrote) {
|
|
stream << L" | ";
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::wstring device_type_name(DeviceType type) {
|
|
switch (type) {
|
|
using enum DeviceType;
|
|
|
|
case gamepad:
|
|
return L"gamepad";
|
|
case keyboard:
|
|
return L"keyboard";
|
|
case mouse:
|
|
return L"mouse";
|
|
case touchscreen:
|
|
return L"touchscreen";
|
|
case trackpad:
|
|
return L"trackpad";
|
|
case pen_tablet:
|
|
return L"pen tablet";
|
|
}
|
|
return L"unknown";
|
|
}
|
|
|
|
std::wstring node_kind_name(DeviceNodeKind kind) {
|
|
switch (kind) {
|
|
using enum DeviceNodeKind;
|
|
|
|
case input_event:
|
|
return L"input";
|
|
case joystick:
|
|
return L"joystick";
|
|
case hidraw:
|
|
return L"hidraw";
|
|
case sysfs:
|
|
return L"sysfs";
|
|
case other:
|
|
return L"other";
|
|
}
|
|
return L"other";
|
|
}
|
|
|
|
std::wstring output_kind_name(GamepadOutputKind kind) {
|
|
switch (kind) {
|
|
using enum GamepadOutputKind;
|
|
|
|
case rumble:
|
|
return L"rumble";
|
|
case rgb_led:
|
|
return L"rgb led";
|
|
case adaptive_triggers:
|
|
return L"adaptive triggers";
|
|
case raw_report:
|
|
return L"raw report";
|
|
case trigger_rumble:
|
|
return L"trigger rumble";
|
|
case player_leds:
|
|
return L"player leds";
|
|
}
|
|
return L"raw report";
|
|
}
|
|
|
|
std::wstring battery_state_name(GamepadBatteryState state) {
|
|
switch (state) {
|
|
using enum GamepadBatteryState;
|
|
|
|
case unknown:
|
|
return L"unknown";
|
|
case discharging:
|
|
return L"discharging";
|
|
case charging:
|
|
return L"charging";
|
|
case full:
|
|
return L"full";
|
|
case voltage_or_temperature_error:
|
|
return L"voltage/temperature error";
|
|
case temperature_error:
|
|
return L"temperature error";
|
|
case charging_error:
|
|
return L"charging error";
|
|
}
|
|
return L"unknown";
|
|
}
|
|
|
|
int battery_choice_index(GamepadBatteryState state) {
|
|
for (std::size_t index = 0; index < battery_choices.size(); ++index) {
|
|
if (battery_choices[index].state == state) {
|
|
return static_cast<int>(index);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
std::wstring raw_hex(const std::vector<std::uint8_t> &bytes) {
|
|
std::wostringstream stream;
|
|
stream << std::hex << std::setfill(L'0');
|
|
for (const auto value : bytes) {
|
|
stream << std::setw(2) << static_cast<unsigned>(value);
|
|
}
|
|
return stream.str();
|
|
}
|
|
|
|
std::optional<DeviceProfile> profile_for_choice(const ProfileChoice &choice) {
|
|
switch (choice.kind) {
|
|
using enum GamepadProfileKind;
|
|
|
|
case generic:
|
|
return profiles::generic_gamepad();
|
|
case xbox_360:
|
|
return profiles::xbox_360();
|
|
case xbox_one:
|
|
return profiles::xbox_one();
|
|
case xbox_series:
|
|
return profiles::xbox_series();
|
|
case dualshock4:
|
|
return profiles::dualshock4();
|
|
case dualsense:
|
|
return profiles::dualsense();
|
|
case switch_pro:
|
|
return profiles::switch_pro();
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
MouseEvent mouse_event_for_action(
|
|
MouseControlAction action,
|
|
std::int32_t motion_step,
|
|
std::int32_t scroll_step
|
|
) {
|
|
using enum MouseControlAction;
|
|
using enum MouseEventKind;
|
|
|
|
switch (action) {
|
|
case move_left:
|
|
return MouseEvent {.kind = relative_motion, .x = -motion_step};
|
|
case move_right:
|
|
return MouseEvent {.kind = relative_motion, .x = motion_step};
|
|
case move_up:
|
|
return MouseEvent {.kind = relative_motion, .y = -motion_step};
|
|
case move_down:
|
|
return MouseEvent {.kind = relative_motion, .y = motion_step};
|
|
case wheel_up:
|
|
return MouseEvent {
|
|
.kind = vertical_scroll,
|
|
.high_resolution_scroll = scroll_step,
|
|
};
|
|
case wheel_down:
|
|
return MouseEvent {
|
|
.kind = vertical_scroll,
|
|
.high_resolution_scroll = -scroll_step,
|
|
};
|
|
case pan_left:
|
|
return MouseEvent {
|
|
.kind = horizontal_scroll,
|
|
.high_resolution_scroll = -scroll_step,
|
|
};
|
|
case pan_right:
|
|
return MouseEvent {
|
|
.kind = horizontal_scroll,
|
|
.high_resolution_scroll = scroll_step,
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
MouseEvent mouse_button_event(MouseButton button, bool pressed) {
|
|
return MouseEvent {
|
|
.kind = MouseEventKind::button,
|
|
.button = button,
|
|
.pressed = pressed,
|
|
};
|
|
}
|
|
|
|
int axis_to_slider(float value) {
|
|
return static_cast<int>(std::lround(std::clamp(value, -1.0F, 1.0F) * static_cast<float>(slider_scale)));
|
|
}
|
|
|
|
int trigger_to_slider(float value) {
|
|
return static_cast<int>(std::lround(std::clamp(value, 0.0F, 1.0F) * static_cast<float>(slider_scale)));
|
|
}
|
|
|
|
float slider_to_float(long value) {
|
|
return static_cast<float>(value) / static_cast<float>(slider_scale);
|
|
}
|
|
|
|
std::wstring yes_no(bool value) {
|
|
return value ? L"yes" : L"no";
|
|
}
|
|
|
|
std::string normalized_license_key(std::string_view license_key) {
|
|
const auto is_whitespace = [](char character) {
|
|
return std::isspace(static_cast<unsigned char>(character)) != 0;
|
|
};
|
|
const auto first = std::ranges::find_if_not(license_key, is_whitespace);
|
|
const auto last = std::ranges::find_if_not(std::views::reverse(license_key), is_whitespace).base();
|
|
return first == license_key.end() ? std::string {} : std::string {first, last};
|
|
}
|
|
|
|
bool supports_normalized_feedback(const DeviceProfile &profile) {
|
|
using enum GamepadOutputKind;
|
|
|
|
return supports_gamepad_output(profile, rumble) ||
|
|
supports_gamepad_output(profile, rgb_led) ||
|
|
supports_gamepad_output(profile, adaptive_triggers) ||
|
|
supports_gamepad_output(profile, trigger_rumble) ||
|
|
supports_gamepad_output(profile, player_leds);
|
|
}
|
|
|
|
std::wstring profile_feature_summary(const DeviceProfile &profile) {
|
|
using enum GamepadOutputKind;
|
|
|
|
const auto support = gamepad_profile_support(profile);
|
|
std::wostringstream stream;
|
|
stream << L"Features: battery " << yes_no(support.supports_battery);
|
|
stream << L" | rumble " << yes_no(supports_gamepad_output(profile, rumble));
|
|
stream << L" | trigger rumble " << yes_no(supports_gamepad_output(profile, trigger_rumble));
|
|
stream << L" | RGB LED " << yes_no(supports_gamepad_output(profile, rgb_led));
|
|
stream << L" | player LEDs " << yes_no(supports_gamepad_output(profile, player_leds));
|
|
stream << L" | adaptive triggers " << yes_no(supports_gamepad_output(profile, adaptive_triggers));
|
|
stream << L" | raw output " << yes_no(supports_gamepad_output(profile, raw_report));
|
|
return stream.str();
|
|
}
|
|
|
|
std::wstring device_feature_summary(const DeviceProfile &profile) {
|
|
if (profile.device_type == DeviceType::mouse) {
|
|
return L"Features: relative motion | five buttons | vertical wheel | horizontal wheel";
|
|
}
|
|
return profile_feature_summary(profile);
|
|
}
|
|
|
|
bool append_latest_output_summary(std::wostringstream &stream, const OutputState &state) {
|
|
auto wrote = false;
|
|
if (state.latest_rumble) {
|
|
stream << L"rumble low=" << state.latest_rumble->low_frequency_rumble
|
|
<< L" high=" << state.latest_rumble->high_frequency_rumble;
|
|
wrote = true;
|
|
}
|
|
if (state.latest_trigger_rumble) {
|
|
append_summary_separator(stream, wrote);
|
|
stream << L"trigger rumble L=" << state.latest_trigger_rumble->left_trigger_rumble
|
|
<< L" R=" << state.latest_trigger_rumble->right_trigger_rumble;
|
|
wrote = true;
|
|
}
|
|
if (state.latest_rgb_led) {
|
|
append_summary_separator(stream, wrote);
|
|
stream << L"RGB " << static_cast<unsigned>(state.latest_rgb_led->red) << L","
|
|
<< static_cast<unsigned>(state.latest_rgb_led->green) << L","
|
|
<< static_cast<unsigned>(state.latest_rgb_led->blue);
|
|
wrote = true;
|
|
}
|
|
if (state.latest_adaptive_triggers) {
|
|
append_summary_separator(stream, wrote);
|
|
stream << L"adaptive flags=" << static_cast<unsigned>(state.latest_adaptive_triggers->adaptive_trigger_flags);
|
|
wrote = true;
|
|
}
|
|
if (state.latest_player_leds) {
|
|
append_summary_separator(stream, wrote);
|
|
stream << L"player LEDs solid=";
|
|
for (const auto active : state.latest_player_leds->player_leds) {
|
|
stream << (active ? L'1' : L'0');
|
|
}
|
|
stream << L" flashing=";
|
|
for (const auto active : state.latest_player_leds->flashing_player_leds) {
|
|
stream << (active ? L'1' : L'0');
|
|
}
|
|
wrote = true;
|
|
}
|
|
if (!wrote && state.latest_raw_report) {
|
|
stream << L"raw report";
|
|
wrote = true;
|
|
}
|
|
return wrote;
|
|
}
|
|
|
|
std::wstring output_summary(const OutputState &state, const DeviceProfile &profile) {
|
|
std::wostringstream stream;
|
|
stream << L"Output: ";
|
|
if (state.outputs.empty()) {
|
|
stream << L"no reports received";
|
|
} else if (!append_latest_output_summary(stream, state)) {
|
|
stream << L"reports received";
|
|
}
|
|
|
|
if (!supports_normalized_feedback(profile)) {
|
|
stream << L" | profile has no normalized feedback categories";
|
|
}
|
|
return stream.str();
|
|
}
|
|
|
|
bool update_visible_controls_for_profile(
|
|
const DeviceProfile &profile,
|
|
std::array<bool, button_choices.size()> &visible_buttons,
|
|
bool &battery_controls_visible
|
|
) {
|
|
auto changed = false;
|
|
for (std::size_t index = 0; index < button_choices.size(); ++index) {
|
|
const auto visible = supports_gamepad_button(profile, button_choices[index].button);
|
|
changed = changed || visible_buttons[index] != visible;
|
|
visible_buttons[index] = visible;
|
|
}
|
|
changed = changed || battery_controls_visible != profile.capabilities.supports_battery;
|
|
battery_controls_visible = profile.capabilities.supports_battery;
|
|
return changed;
|
|
}
|
|
|
|
void record_output(
|
|
OutputState &state,
|
|
const GamepadOutput &output,
|
|
std::uint64_t &next_sequence,
|
|
std::size_t max_output_events
|
|
) {
|
|
state.outputs.push_back({.sequence = next_sequence++, .output = output});
|
|
if (state.outputs.size() > max_output_events) {
|
|
state.outputs.erase(
|
|
state.outputs.begin(),
|
|
state.outputs.begin() + static_cast<std::ptrdiff_t>(state.outputs.size() - max_output_events)
|
|
);
|
|
}
|
|
|
|
switch (output.kind) {
|
|
using enum GamepadOutputKind;
|
|
|
|
case rumble:
|
|
state.latest_rumble = output;
|
|
break;
|
|
case trigger_rumble:
|
|
state.latest_trigger_rumble = output;
|
|
break;
|
|
case rgb_led:
|
|
state.latest_rgb_led = output;
|
|
break;
|
|
case adaptive_triggers:
|
|
state.latest_adaptive_triggers = output;
|
|
break;
|
|
case player_leds:
|
|
state.latest_player_leds = output;
|
|
break;
|
|
case raw_report:
|
|
state.latest_raw_report = output;
|
|
break;
|
|
}
|
|
}
|
|
|
|
} // namespace lvh::tools::virtualhid_control
|