Merge pull request #8428 from jordan-woyak/better-hotkeys
InputCommon: Add hotkey support to input expressions.
This commit is contained in:
commit
c64d41d3e7
|
@ -187,10 +187,10 @@ void GCPad::LoadDefaults(const ControllerInterface& ciface)
|
||||||
m_buttons->SetControlExpression(3, "S"); // Y
|
m_buttons->SetControlExpression(3, "S"); // Y
|
||||||
m_buttons->SetControlExpression(4, "D"); // Z
|
m_buttons->SetControlExpression(4, "D"); // Z
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
m_buttons->SetControlExpression(5, "!LMENU & RETURN"); // Start
|
m_buttons->SetControlExpression(5, "RETURN"); // Start
|
||||||
#else
|
#else
|
||||||
// OS X/Linux
|
// OS X/Linux
|
||||||
m_buttons->SetControlExpression(5, "!`Alt_L` & Return"); // Start
|
m_buttons->SetControlExpression(5, "Return"); // Start
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// stick modifiers to 50 %
|
// stick modifiers to 50 %
|
||||||
|
|
|
@ -603,9 +603,9 @@ void Wiimote::LoadDefaults(const ControllerInterface& ciface)
|
||||||
m_buttons->SetControlExpression(5, "E"); // +
|
m_buttons->SetControlExpression(5, "E"); // +
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
m_buttons->SetControlExpression(6, "!LMENU & RETURN"); // Home
|
m_buttons->SetControlExpression(6, "RETURN"); // Home
|
||||||
#else
|
#else
|
||||||
m_buttons->SetControlExpression(6, "!`Alt_L` & Return"); // Home
|
m_buttons->SetControlExpression(6, "Return"); // Home
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Shake
|
// Shake
|
||||||
|
|
|
@ -409,83 +409,72 @@ void HotkeyManager::LoadDefaults(const ControllerInterface& ciface)
|
||||||
{
|
{
|
||||||
EmulatedController::LoadDefaults(ciface);
|
EmulatedController::LoadDefaults(ciface);
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
const std::string NON = "(!(LMENU | RMENU) & !(LSHIFT | RSHIFT) & !(LCONTROL | RCONTROL))";
|
|
||||||
const std::string ALT = "((LMENU | RMENU) & !(LSHIFT | RSHIFT) & !(LCONTROL | RCONTROL))";
|
|
||||||
const std::string SHIFT = "(!(LMENU | RMENU) & (LSHIFT | RSHIFT) & !(LCONTROL | RCONTROL))";
|
|
||||||
const std::string CTRL = "(!(LMENU | RMENU) & !(LSHIFT | RSHIFT) & (LCONTROL | RCONTROL))";
|
|
||||||
#elif __APPLE__
|
|
||||||
const std::string NON =
|
|
||||||
"(!`Left Alt` & !(`Left Shift`| `Right Shift`) & !(`Left Control` | `Right Control`))";
|
|
||||||
const std::string ALT =
|
|
||||||
"(`Left Alt` & !(`Left Shift`| `Right Shift`) & !(`Left Control` | `Right Control`))";
|
|
||||||
const std::string SHIFT =
|
|
||||||
"(!`Left Alt` & (`Left Shift`| `Right Shift`) & !(`Left Control` | `Right Control`))";
|
|
||||||
const std::string CTRL =
|
|
||||||
"(!`Left Alt` & !(`Left Shift`| `Right Shift`) & (`Left Control` | `Right Control`))";
|
|
||||||
#else
|
|
||||||
const std::string NON = "(!`Alt_L` & !(`Shift_L` | `Shift_R`) & !(`Control_L` | `Control_R` ))";
|
|
||||||
const std::string ALT = "(`Alt_L` & !(`Shift_L` | `Shift_R`) & !(`Control_L` | `Control_R` ))";
|
|
||||||
const std::string SHIFT = "(!`Alt_L` & (`Shift_L` | `Shift_R`) & !(`Control_L` | `Control_R` ))";
|
|
||||||
const std::string CTRL = "(!`Alt_L` & !(`Shift_L` | `Shift_R`) & (`Control_L` | `Control_R` ))";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
auto set_key_expression = [this](int index, const std::string& expression) {
|
auto set_key_expression = [this](int index, const std::string& expression) {
|
||||||
m_keys[FindGroupByID(index)]
|
m_keys[FindGroupByID(index)]
|
||||||
->controls[GetIndexForGroup(FindGroupByID(index), index)]
|
->controls[GetIndexForGroup(FindGroupByID(index), index)]
|
||||||
->control_ref->SetExpression(expression);
|
->control_ref->SetExpression(expression);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto hotkey_string = [](std::vector<std::string_view> inputs) {
|
||||||
|
std::string result;
|
||||||
|
for (auto& input : inputs)
|
||||||
|
{
|
||||||
|
if (!result.empty())
|
||||||
|
result += '+';
|
||||||
|
result += input;
|
||||||
|
}
|
||||||
|
return "@(" + result + ')';
|
||||||
|
};
|
||||||
|
|
||||||
// General hotkeys
|
// General hotkeys
|
||||||
set_key_expression(HK_OPEN, CTRL + " & O");
|
set_key_expression(HK_OPEN, hotkey_string({"Ctrl", "O"}));
|
||||||
set_key_expression(HK_PLAY_PAUSE, NON + " & `F10`");
|
set_key_expression(HK_PLAY_PAUSE, "F10");
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
set_key_expression(HK_STOP, NON + " & ESCAPE");
|
set_key_expression(HK_STOP, "ESCAPE");
|
||||||
set_key_expression(HK_FULLSCREEN, ALT + " & RETURN");
|
set_key_expression(HK_FULLSCREEN, hotkey_string({"Alt", "RETURN"}));
|
||||||
#else
|
#else
|
||||||
set_key_expression(HK_STOP, NON + " & Escape");
|
set_key_expression(HK_STOP, "Escape");
|
||||||
set_key_expression(HK_FULLSCREEN, ALT + " & Return");
|
set_key_expression(HK_FULLSCREEN, hotkey_string({"Alt", "Return"}));
|
||||||
#endif
|
#endif
|
||||||
set_key_expression(HK_STEP, NON + " & `F11`");
|
set_key_expression(HK_STEP, "F11");
|
||||||
set_key_expression(HK_STEP_OVER, SHIFT + " & `F10`");
|
set_key_expression(HK_STEP_OVER, hotkey_string({"Shift", "F10"}));
|
||||||
set_key_expression(HK_STEP_OUT, SHIFT + " & `F11`");
|
set_key_expression(HK_STEP_OUT, hotkey_string({"Shift", "F11"}));
|
||||||
set_key_expression(HK_BP_TOGGLE, SHIFT + " & `F9`");
|
set_key_expression(HK_BP_TOGGLE, hotkey_string({"Shift", "F9"}));
|
||||||
set_key_expression(HK_SCREENSHOT, NON + " & `F9`");
|
set_key_expression(HK_SCREENSHOT, "F9");
|
||||||
set_key_expression(HK_WIIMOTE1_CONNECT, ALT + " & `F5`");
|
set_key_expression(HK_WIIMOTE1_CONNECT, hotkey_string({"Alt", "F5"}));
|
||||||
set_key_expression(HK_WIIMOTE2_CONNECT, ALT + " & `F6`");
|
set_key_expression(HK_WIIMOTE2_CONNECT, hotkey_string({"Alt", "F6"}));
|
||||||
set_key_expression(HK_WIIMOTE3_CONNECT, ALT + " & `F7`");
|
set_key_expression(HK_WIIMOTE3_CONNECT, hotkey_string({"Alt", "F7"}));
|
||||||
set_key_expression(HK_WIIMOTE4_CONNECT, ALT + " & `F8`");
|
set_key_expression(HK_WIIMOTE4_CONNECT, hotkey_string({"Alt", "F8"}));
|
||||||
set_key_expression(HK_BALANCEBOARD_CONNECT, ALT + " & `F9`");
|
set_key_expression(HK_BALANCEBOARD_CONNECT, hotkey_string({"Alt", "F9"}));
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
set_key_expression(HK_TOGGLE_THROTTLE, NON + " & TAB");
|
set_key_expression(HK_TOGGLE_THROTTLE, "TAB");
|
||||||
#else
|
#else
|
||||||
set_key_expression(HK_TOGGLE_THROTTLE, NON + " & Tab");
|
set_key_expression(HK_TOGGLE_THROTTLE, "Tab");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Freelook
|
// Freelook
|
||||||
set_key_expression(HK_FREELOOK_DECREASE_SPEED, SHIFT + " & `1`");
|
set_key_expression(HK_FREELOOK_DECREASE_SPEED, hotkey_string({"Shift", "1"}));
|
||||||
set_key_expression(HK_FREELOOK_INCREASE_SPEED, SHIFT + " & `2`");
|
set_key_expression(HK_FREELOOK_INCREASE_SPEED, hotkey_string({"Shift", "2"}));
|
||||||
set_key_expression(HK_FREELOOK_RESET_SPEED, SHIFT + " & F");
|
set_key_expression(HK_FREELOOK_RESET_SPEED, hotkey_string({"Shift", "F"}));
|
||||||
set_key_expression(HK_FREELOOK_UP, SHIFT + " & E");
|
set_key_expression(HK_FREELOOK_UP, hotkey_string({"Shift", "E"}));
|
||||||
set_key_expression(HK_FREELOOK_DOWN, SHIFT + " & Q");
|
set_key_expression(HK_FREELOOK_DOWN, hotkey_string({"Shift", "Q"}));
|
||||||
set_key_expression(HK_FREELOOK_LEFT, SHIFT + " & A");
|
set_key_expression(HK_FREELOOK_LEFT, hotkey_string({"Shift", "A"}));
|
||||||
set_key_expression(HK_FREELOOK_RIGHT, SHIFT + " & D");
|
set_key_expression(HK_FREELOOK_RIGHT, hotkey_string({"Shift", "D"}));
|
||||||
set_key_expression(HK_FREELOOK_ZOOM_IN, SHIFT + " & W");
|
set_key_expression(HK_FREELOOK_ZOOM_IN, hotkey_string({"Shift", "W"}));
|
||||||
set_key_expression(HK_FREELOOK_ZOOM_OUT, SHIFT + " & S");
|
set_key_expression(HK_FREELOOK_ZOOM_OUT, hotkey_string({"Shift", "S"}));
|
||||||
set_key_expression(HK_FREELOOK_RESET, SHIFT + " & R");
|
set_key_expression(HK_FREELOOK_RESET, hotkey_string({"Shift", "R"}));
|
||||||
set_key_expression(HK_FREELOOK_INCREASE_FOV_X, SHIFT + " & `Axis Z+`");
|
set_key_expression(HK_FREELOOK_INCREASE_FOV_X, hotkey_string({"Shift", "`Axis Z+`"}));
|
||||||
set_key_expression(HK_FREELOOK_DECREASE_FOV_X, SHIFT + " & `Axis Z-`");
|
set_key_expression(HK_FREELOOK_DECREASE_FOV_X, hotkey_string({"Shift", "`Axis Z-`"}));
|
||||||
set_key_expression(HK_FREELOOK_INCREASE_FOV_Y, SHIFT + " & `Axis Z+`");
|
set_key_expression(HK_FREELOOK_INCREASE_FOV_Y, hotkey_string({"Shift", "`Axis Z+`"}));
|
||||||
set_key_expression(HK_FREELOOK_DECREASE_FOV_Y, SHIFT + " & `Axis Z-`");
|
set_key_expression(HK_FREELOOK_DECREASE_FOV_Y, hotkey_string({"Shift", "`Axis Z-`"}));
|
||||||
|
|
||||||
// Savestates
|
// Savestates
|
||||||
const std::string non_fmt = NON + " & `F{}`";
|
|
||||||
const std::string shift_fmt = SHIFT + " & `F{}`";
|
|
||||||
for (int i = 0; i < 8; i++)
|
for (int i = 0; i < 8; i++)
|
||||||
{
|
{
|
||||||
set_key_expression(HK_LOAD_STATE_SLOT_1 + i, fmt::format(non_fmt, i + 1));
|
set_key_expression(HK_LOAD_STATE_SLOT_1 + i, fmt::format("F%d", i + 1));
|
||||||
set_key_expression(HK_SAVE_STATE_SLOT_1 + i, fmt::format(shift_fmt, i + 1));
|
set_key_expression(HK_SAVE_STATE_SLOT_1 + i,
|
||||||
|
hotkey_string({"Shift", fmt::format("F%d", i + 1)}));
|
||||||
}
|
}
|
||||||
set_key_expression(HK_UNDO_LOAD_STATE, NON + " & `F12`");
|
set_key_expression(HK_UNDO_LOAD_STATE, "F12");
|
||||||
set_key_expression(HK_UNDO_SAVE_STATE, SHIFT + " & `F12`");
|
set_key_expression(HK_UNDO_SAVE_STATE, hotkey_string({"Shift", "F12"}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include "DolphinQt/Config/Mapping/MappingCommon.h"
|
#include "DolphinQt/Config/Mapping/MappingCommon.h"
|
||||||
|
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
@ -14,14 +15,23 @@
|
||||||
|
|
||||||
#include "DolphinQt/QtUtils/BlockUserInputFilter.h"
|
#include "DolphinQt/QtUtils/BlockUserInputFilter.h"
|
||||||
#include "InputCommon/ControlReference/ControlReference.h"
|
#include "InputCommon/ControlReference/ControlReference.h"
|
||||||
#include "InputCommon/ControllerInterface/Device.h"
|
|
||||||
|
|
||||||
#include "Common/Thread.h"
|
#include "Common/Thread.h"
|
||||||
|
|
||||||
namespace MappingCommon
|
namespace MappingCommon
|
||||||
{
|
{
|
||||||
constexpr int INPUT_DETECT_TIME = 3000;
|
constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3);
|
||||||
constexpr int OUTPUT_TEST_TIME = 2000;
|
constexpr auto INPUT_DETECT_CONFIRMATION_TIME = std::chrono::milliseconds(500);
|
||||||
|
constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5);
|
||||||
|
|
||||||
|
constexpr auto OUTPUT_TEST_TIME = std::chrono::seconds(2);
|
||||||
|
|
||||||
|
// Pressing inputs at the same time will result in the & operator vs a hotkey expression.
|
||||||
|
constexpr auto HOTKEY_VS_CONJUNCION_THRESHOLD = std::chrono::milliseconds(50);
|
||||||
|
|
||||||
|
// Some devices (e.g. DS4) provide an analog and digital input for the trigger.
|
||||||
|
// We prefer just the analog input for simultaneous digital+analog input detections.
|
||||||
|
constexpr auto SPURIOUS_TRIGGER_COMBO_THRESHOLD = std::chrono::milliseconds(150);
|
||||||
|
|
||||||
QString GetExpressionForControl(const QString& control_name,
|
QString GetExpressionForControl(const QString& control_name,
|
||||||
const ciface::Core::DeviceQualifier& control_device,
|
const ciface::Core::DeviceQualifier& control_device,
|
||||||
|
@ -68,7 +78,11 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev
|
||||||
// Avoid that the button press itself is registered as an event
|
// Avoid that the button press itself is registered as an event
|
||||||
Common::SleepCurrentThread(50);
|
Common::SleepCurrentThread(50);
|
||||||
|
|
||||||
const auto [device, input] = device_container.DetectInput(INPUT_DETECT_TIME, device_strings);
|
auto detections =
|
||||||
|
device_container.DetectInput(device_strings, INPUT_DETECT_INITIAL_TIME,
|
||||||
|
INPUT_DETECT_CONFIRMATION_TIME, INPUT_DETECT_MAXIMUM_TIME);
|
||||||
|
|
||||||
|
RemoveSpuriousTriggerCombinations(&detections);
|
||||||
|
|
||||||
const auto timer = new QTimer(button);
|
const auto timer = new QTimer(button);
|
||||||
|
|
||||||
|
@ -83,14 +97,7 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev
|
||||||
|
|
||||||
button->setText(old_text);
|
button->setText(old_text);
|
||||||
|
|
||||||
if (!input)
|
return BuildExpression(detections, default_device, quote);
|
||||||
return {};
|
|
||||||
|
|
||||||
ciface::Core::DeviceQualifier device_qualifier;
|
|
||||||
device_qualifier.FromDevice(device.get());
|
|
||||||
|
|
||||||
return MappingCommon::GetExpressionForControl(QString::fromStdString(input->GetName()),
|
|
||||||
device_qualifier, default_device, quote);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestOutput(QPushButton* button, OutputReference* reference)
|
void TestOutput(QPushButton* button, OutputReference* reference)
|
||||||
|
@ -102,10 +109,103 @@ void TestOutput(QPushButton* button, OutputReference* reference)
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
reference->State(1.0);
|
reference->State(1.0);
|
||||||
Common::SleepCurrentThread(OUTPUT_TEST_TIME);
|
std::this_thread::sleep_for(OUTPUT_TEST_TIME);
|
||||||
reference->State(0.0);
|
reference->State(0.0);
|
||||||
|
|
||||||
button->setText(old_text);
|
button->setText(old_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RemoveSpuriousTriggerCombinations(
|
||||||
|
std::vector<ciface::Core::DeviceContainer::InputDetection>* detections)
|
||||||
|
{
|
||||||
|
const auto is_spurious = [&](auto& detection) {
|
||||||
|
return std::any_of(detections->begin(), detections->end(), [&](auto& d) {
|
||||||
|
// This is a suprious digital detection if a "smooth" (analog) detection is temporally near.
|
||||||
|
return &d != &detection && d.smoothness > 1 &&
|
||||||
|
abs(d.press_time - detection.press_time) < SPURIOUS_TRIGGER_COMBO_THRESHOLD;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
detections->erase(std::remove_if(detections->begin(), detections->end(), is_spurious),
|
||||||
|
detections->end());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString
|
||||||
|
BuildExpression(const std::vector<ciface::Core::DeviceContainer::InputDetection>& detections,
|
||||||
|
const ciface::Core::DeviceQualifier& default_device, Quote quote)
|
||||||
|
{
|
||||||
|
std::vector<const ciface::Core::DeviceContainer::InputDetection*> pressed_inputs;
|
||||||
|
|
||||||
|
QStringList alternations;
|
||||||
|
|
||||||
|
const auto get_control_expression = [&](auto& detection) {
|
||||||
|
// Return the parent-most name if there is one for better hotkey strings.
|
||||||
|
// Detection of L/R_Ctrl will be changed to just Ctrl.
|
||||||
|
// Users can manually map L_Ctrl if they so desire.
|
||||||
|
const auto input = (quote == Quote::On) ?
|
||||||
|
detection.device->GetParentMostInput(detection.input) :
|
||||||
|
detection.input;
|
||||||
|
|
||||||
|
ciface::Core::DeviceQualifier device_qualifier;
|
||||||
|
device_qualifier.FromDevice(detection.device.get());
|
||||||
|
|
||||||
|
return MappingCommon::GetExpressionForControl(QString::fromStdString(input->GetName()),
|
||||||
|
device_qualifier, default_device, quote);
|
||||||
|
};
|
||||||
|
|
||||||
|
bool new_alternation = false;
|
||||||
|
|
||||||
|
const auto handle_press = [&](auto& detection) {
|
||||||
|
pressed_inputs.emplace_back(&detection);
|
||||||
|
new_alternation = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto handle_release = [&]() {
|
||||||
|
if (!new_alternation)
|
||||||
|
return;
|
||||||
|
|
||||||
|
new_alternation = false;
|
||||||
|
|
||||||
|
QStringList alternation;
|
||||||
|
for (auto* input : pressed_inputs)
|
||||||
|
alternation.push_back(get_control_expression(*input));
|
||||||
|
|
||||||
|
const bool is_hotkey = pressed_inputs.size() >= 2 &&
|
||||||
|
(pressed_inputs[1]->press_time - pressed_inputs[0]->press_time) >
|
||||||
|
HOTKEY_VS_CONJUNCION_THRESHOLD;
|
||||||
|
|
||||||
|
if (is_hotkey)
|
||||||
|
{
|
||||||
|
alternations.push_back(QStringLiteral("@(%1)").arg(alternation.join(QLatin1Char('+'))));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
alternation.sort();
|
||||||
|
alternations.push_back(alternation.join(QLatin1Char('&')));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto& detection : detections)
|
||||||
|
{
|
||||||
|
// Remove since released inputs.
|
||||||
|
for (auto it = pressed_inputs.begin(); it != pressed_inputs.end();)
|
||||||
|
{
|
||||||
|
if (!((*it)->release_time > detection.press_time))
|
||||||
|
{
|
||||||
|
handle_release();
|
||||||
|
it = pressed_inputs.erase(it);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_press(detection);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_release();
|
||||||
|
|
||||||
|
alternations.removeDuplicates();
|
||||||
|
return alternations.join(QLatin1Char('|'));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MappingCommon
|
} // namespace MappingCommon
|
||||||
|
|
|
@ -7,16 +7,12 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "InputCommon/ControllerInterface/Device.h"
|
||||||
|
|
||||||
class QString;
|
class QString;
|
||||||
class OutputReference;
|
class OutputReference;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
|
|
||||||
namespace ciface::Core
|
|
||||||
{
|
|
||||||
class DeviceContainer;
|
|
||||||
class DeviceQualifier;
|
|
||||||
} // namespace ciface::Core
|
|
||||||
|
|
||||||
namespace MappingCommon
|
namespace MappingCommon
|
||||||
{
|
{
|
||||||
enum class Quote
|
enum class Quote
|
||||||
|
@ -37,4 +33,9 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev
|
||||||
|
|
||||||
void TestOutput(QPushButton* button, OutputReference* reference);
|
void TestOutput(QPushButton* button, OutputReference* reference);
|
||||||
|
|
||||||
|
void RemoveSpuriousTriggerCombinations(std::vector<ciface::Core::DeviceContainer::InputDetection>*);
|
||||||
|
|
||||||
|
QString BuildExpression(const std::vector<ciface::Core::DeviceContainer::InputDetection>&,
|
||||||
|
const ciface::Core::DeviceQualifier& default_device, Quote quote);
|
||||||
|
|
||||||
} // namespace MappingCommon
|
} // namespace MappingCommon
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
// Licensed under GPLv2+
|
// Licensed under GPLv2+
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
@ -21,6 +23,55 @@ namespace ciface::ExpressionParser
|
||||||
{
|
{
|
||||||
using namespace ciface::Core;
|
using namespace ciface::Core;
|
||||||
|
|
||||||
|
class ControlExpression;
|
||||||
|
|
||||||
|
class HotkeySuppressions
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using Modifiers = std::vector<std::unique_ptr<ControlExpression>>;
|
||||||
|
|
||||||
|
struct InvokingDeleter
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
void operator()(T* func)
|
||||||
|
{
|
||||||
|
(*func)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using Suppressor = std::unique_ptr<std::function<void()>, InvokingDeleter>;
|
||||||
|
|
||||||
|
bool IsSuppressed(Device::Input* input) const
|
||||||
|
{
|
||||||
|
// Input is suppressed if it exists in the map at all.
|
||||||
|
return m_suppressions.lower_bound({input, nullptr}) !=
|
||||||
|
m_suppressions.lower_bound({input + 1, nullptr});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsSuppressedIgnoringModifiers(Device::Input* input, const Modifiers& ignore_modifiers) const;
|
||||||
|
|
||||||
|
// Suppresses each input + modifier pair.
|
||||||
|
// The returned object removes the suppression on destruction.
|
||||||
|
Suppressor MakeSuppressor(const Modifiers* modifiers,
|
||||||
|
const std::unique_ptr<ControlExpression>* final_input);
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Suppression = std::pair<Device::Input*, Device::Input*>;
|
||||||
|
using SuppressionLevel = u16;
|
||||||
|
|
||||||
|
void RemoveSuppression(Device::Input* modifier, Device::Input* final_input)
|
||||||
|
{
|
||||||
|
auto it = m_suppressions.find({final_input, modifier});
|
||||||
|
if ((--it->second) == 0)
|
||||||
|
m_suppressions.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holds counts of suppressions for each input/modifier pair.
|
||||||
|
std::map<Suppression, SuppressionLevel> m_suppressions;
|
||||||
|
};
|
||||||
|
|
||||||
|
static HotkeySuppressions s_hotkey_suppressions;
|
||||||
|
|
||||||
Token::Token(TokenType type_) : type(type_)
|
Token::Token(TokenType type_) : type(type_)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -112,6 +163,8 @@ Token Lexer::NextToken()
|
||||||
return Token(TOK_LPAREN);
|
return Token(TOK_LPAREN);
|
||||||
case ')':
|
case ')':
|
||||||
return Token(TOK_RPAREN);
|
return Token(TOK_RPAREN);
|
||||||
|
case '@':
|
||||||
|
return Token(TOK_HOTKEY);
|
||||||
case '&':
|
case '&':
|
||||||
return Token(TOK_AND);
|
return Token(TOK_AND);
|
||||||
case '|':
|
case '|':
|
||||||
|
@ -196,7 +249,16 @@ public:
|
||||||
std::shared_ptr<Device> m_device;
|
std::shared_ptr<Device> m_device;
|
||||||
|
|
||||||
explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {}
|
explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {}
|
||||||
|
|
||||||
ControlState GetValue() const override
|
ControlState GetValue() const override
|
||||||
|
{
|
||||||
|
if (s_hotkey_suppressions.IsSuppressed(input))
|
||||||
|
return 0;
|
||||||
|
else
|
||||||
|
return GetValueIgnoringSuppression();
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlState GetValueIgnoringSuppression() const
|
||||||
{
|
{
|
||||||
if (!input)
|
if (!input)
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
@ -222,12 +284,46 @@ public:
|
||||||
output = env.FindOutput(qualifier);
|
output = env.FindOutput(qualifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Device::Input* GetInput() const { return input; };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ControlQualifier qualifier;
|
ControlQualifier qualifier;
|
||||||
Device::Input* input = nullptr;
|
Device::Input* input = nullptr;
|
||||||
Device::Output* output = nullptr;
|
Device::Output* output = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool HotkeySuppressions::IsSuppressedIgnoringModifiers(Device::Input* input,
|
||||||
|
const Modifiers& ignore_modifiers) const
|
||||||
|
{
|
||||||
|
// Input is suppressed if it exists in the map with a modifier that we aren't ignoring.
|
||||||
|
auto it = m_suppressions.lower_bound({input, nullptr});
|
||||||
|
auto it_end = m_suppressions.lower_bound({input + 1, nullptr});
|
||||||
|
|
||||||
|
// We need to ignore L_Ctrl R_Ctrl when supplied Ctrl and vice-versa.
|
||||||
|
const auto is_same_modifier = [](Device::Input* i1, Device::Input* i2) {
|
||||||
|
return i1 == i2 || i1->IsChild(i2) || i2->IsChild(i1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return std::any_of(it, it_end, [&](auto& s) {
|
||||||
|
return std::none_of(begin(ignore_modifiers), end(ignore_modifiers),
|
||||||
|
[&](auto& m) { return is_same_modifier(m->GetInput(), s.first.second); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
HotkeySuppressions::Suppressor
|
||||||
|
HotkeySuppressions::MakeSuppressor(const Modifiers* modifiers,
|
||||||
|
const std::unique_ptr<ControlExpression>* final_input)
|
||||||
|
{
|
||||||
|
for (auto& modifier : *modifiers)
|
||||||
|
++m_suppressions[{(*final_input)->GetInput(), modifier->GetInput()}];
|
||||||
|
|
||||||
|
return Suppressor(std::make_unique<std::function<void()>>([this, modifiers, final_input]() {
|
||||||
|
for (auto& modifier : *modifiers)
|
||||||
|
RemoveSuppression(modifier->GetInput(), (*final_input)->GetInput());
|
||||||
|
}).release(),
|
||||||
|
InvokingDeleter{});
|
||||||
|
}
|
||||||
|
|
||||||
class BinaryExpression : public Expression
|
class BinaryExpression : public Expression
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -374,6 +470,90 @@ protected:
|
||||||
ControlState* m_value_ptr{};
|
ControlState* m_value_ptr{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class HotkeyExpression : public Expression
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HotkeyExpression(std::vector<std::unique_ptr<ControlExpression>> inputs)
|
||||||
|
: m_modifiers(std::move(inputs))
|
||||||
|
{
|
||||||
|
m_final_input = std::move(m_modifiers.back());
|
||||||
|
m_modifiers.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlState GetValue() const override
|
||||||
|
{
|
||||||
|
const bool modifiers_pressed = std::all_of(m_modifiers.begin(), m_modifiers.end(),
|
||||||
|
[](const std::unique_ptr<ControlExpression>& input) {
|
||||||
|
return input->GetValue() > CONDITION_THRESHOLD;
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto final_input_state = m_final_input->GetValueIgnoringSuppression();
|
||||||
|
|
||||||
|
if (modifiers_pressed)
|
||||||
|
{
|
||||||
|
// Ignore suppression of our own modifiers. This also allows superset modifiers to function.
|
||||||
|
const bool is_suppressed = s_hotkey_suppressions.IsSuppressedIgnoringModifiers(
|
||||||
|
m_final_input->GetInput(), m_modifiers);
|
||||||
|
|
||||||
|
if (final_input_state < CONDITION_THRESHOLD)
|
||||||
|
m_is_blocked = false;
|
||||||
|
|
||||||
|
// If some other hotkey suppressed us, require a release of final input to be ready again.
|
||||||
|
if (is_suppressed)
|
||||||
|
m_is_blocked = true;
|
||||||
|
|
||||||
|
if (m_is_blocked)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
EnableSuppression();
|
||||||
|
|
||||||
|
// Our modifiers are active. Pass through the final input.
|
||||||
|
return final_input_state;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_suppressor = {};
|
||||||
|
m_is_blocked = final_input_state > CONDITION_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetValue(ControlState) override {}
|
||||||
|
|
||||||
|
int CountNumControls() const override
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
for (auto& input : m_modifiers)
|
||||||
|
result += input->CountNumControls();
|
||||||
|
return result + m_final_input->CountNumControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateReferences(ControlEnvironment& env) override
|
||||||
|
{
|
||||||
|
for (auto& input : m_modifiers)
|
||||||
|
input->UpdateReferences(env);
|
||||||
|
|
||||||
|
m_final_input->UpdateReferences(env);
|
||||||
|
|
||||||
|
// We must update our suppression with valid pointers.
|
||||||
|
if (m_suppressor)
|
||||||
|
EnableSuppression();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void EnableSuppression() const
|
||||||
|
{
|
||||||
|
if (!m_suppressor)
|
||||||
|
m_suppressor = s_hotkey_suppressions.MakeSuppressor(&m_modifiers, &m_final_input);
|
||||||
|
}
|
||||||
|
|
||||||
|
HotkeySuppressions::Modifiers m_modifiers;
|
||||||
|
std::unique_ptr<ControlExpression> m_final_input;
|
||||||
|
mutable HotkeySuppressions::Suppressor m_suppressor;
|
||||||
|
mutable bool m_is_blocked = false;
|
||||||
|
};
|
||||||
|
|
||||||
// This class proxies all methods to its either left-hand child if it has bound controls, or its
|
// This class proxies all methods to its either left-hand child if it has bound controls, or its
|
||||||
// right-hand child. Its intended use is for supporting old-style barewords expressions.
|
// right-hand child. Its intended use is for supporting old-style barewords expressions.
|
||||||
class CoalesceExpression : public Expression
|
class CoalesceExpression : public Expression
|
||||||
|
@ -600,6 +780,10 @@ private:
|
||||||
{
|
{
|
||||||
return ParseParens();
|
return ParseParens();
|
||||||
}
|
}
|
||||||
|
case TOK_HOTKEY:
|
||||||
|
{
|
||||||
|
return ParseHotkeys();
|
||||||
|
}
|
||||||
case TOK_SUB:
|
case TOK_SUB:
|
||||||
{
|
{
|
||||||
// An atom was expected but we got a subtraction symbol.
|
// An atom was expected but we got a subtraction symbol.
|
||||||
|
@ -684,6 +868,39 @@ private:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ParseResult ParseHotkeys()
|
||||||
|
{
|
||||||
|
Token tok = Chew();
|
||||||
|
if (tok.type != TOK_LPAREN)
|
||||||
|
return ParseResult::MakeErrorResult(tok, _trans("Expected opening paren."));
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<ControlExpression>> inputs;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
tok = Chew();
|
||||||
|
|
||||||
|
if (tok.type != TOK_CONTROL && tok.type != TOK_BAREWORD)
|
||||||
|
return ParseResult::MakeErrorResult(tok, _trans("Expected name of input."));
|
||||||
|
|
||||||
|
ControlQualifier cq;
|
||||||
|
cq.FromString(tok.data);
|
||||||
|
inputs.emplace_back(std::make_unique<ControlExpression>(std::move(cq)));
|
||||||
|
|
||||||
|
tok = Chew();
|
||||||
|
|
||||||
|
if (tok.type == TOK_ADD)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (tok.type == TOK_RPAREN)
|
||||||
|
break;
|
||||||
|
|
||||||
|
return ParseResult::MakeErrorResult(tok, _trans("Expected + or closing paren."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseResult::MakeSuccessfulResult(std::make_unique<HotkeyExpression>(std::move(inputs)));
|
||||||
|
}
|
||||||
|
|
||||||
ParseResult ParseToplevel() { return ParseBinary(); }
|
ParseResult ParseToplevel() { return ParseBinary(); }
|
||||||
}; // namespace ExpressionParser
|
}; // namespace ExpressionParser
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ enum TokenType
|
||||||
TOK_VARIABLE,
|
TOK_VARIABLE,
|
||||||
TOK_BAREWORD,
|
TOK_BAREWORD,
|
||||||
TOK_COMMENT,
|
TOK_COMMENT,
|
||||||
|
TOK_HOTKEY,
|
||||||
// Binary Ops:
|
// Binary Ops:
|
||||||
TOK_BINARY_OPS_BEGIN,
|
TOK_BINARY_OPS_BEGIN,
|
||||||
TOK_AND = TOK_BINARY_OPS_BEGIN,
|
TOK_AND = TOK_BINARY_OPS_BEGIN,
|
||||||
|
|
|
@ -87,6 +87,11 @@ KeyboardMouse::KeyboardMouse(const LPDIRECTINPUTDEVICE8 kb_device,
|
||||||
for (u8 i = 0; i < sizeof(named_keys) / sizeof(*named_keys); ++i)
|
for (u8 i = 0; i < sizeof(named_keys) / sizeof(*named_keys); ++i)
|
||||||
AddInput(new Key(i, m_state_in.keyboard[named_keys[i].code]));
|
AddInput(new Key(i, m_state_in.keyboard[named_keys[i].code]));
|
||||||
|
|
||||||
|
// Add combined left/right modifiers with consistent naming across platforms.
|
||||||
|
AddCombinedInput("Alt", {"LMENU", "RMENU"});
|
||||||
|
AddCombinedInput("Shift", {"LSHIFT", "RSHIFT"});
|
||||||
|
AddCombinedInput("Ctrl", {"LCONTROL", "RCONTROL"});
|
||||||
|
|
||||||
// MOUSE
|
// MOUSE
|
||||||
DIDEVCAPS mouse_caps = {};
|
DIDEVCAPS mouse_caps = {};
|
||||||
mouse_caps.dwSize = sizeof(mouse_caps);
|
mouse_caps.dwSize = sizeof(mouse_caps);
|
||||||
|
|
|
@ -13,13 +13,47 @@
|
||||||
|
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
#include "Common/MathUtil.h"
|
||||||
#include "Common/Thread.h"
|
#include "Common/Thread.h"
|
||||||
|
|
||||||
namespace ciface::Core
|
namespace ciface::Core
|
||||||
{
|
{
|
||||||
// Compared to an input's current state (ideally 1.0) minus abs(initial_state) (ideally 0.0).
|
// Compared to an input's current state (ideally 1.0) minus abs(initial_state) (ideally 0.0).
|
||||||
|
// Note: Detect() logic assumes this is greater than 0.5.
|
||||||
constexpr ControlState INPUT_DETECT_THRESHOLD = 0.55;
|
constexpr ControlState INPUT_DETECT_THRESHOLD = 0.55;
|
||||||
|
|
||||||
|
class CombinedInput final : public Device::Input
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using Inputs = std::pair<Device::Input*, Device::Input*>;
|
||||||
|
|
||||||
|
CombinedInput(std::string name, const Inputs& inputs) : m_name(std::move(name)), m_inputs(inputs)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
ControlState GetState() const override
|
||||||
|
{
|
||||||
|
ControlState result = 0;
|
||||||
|
|
||||||
|
if (m_inputs.first)
|
||||||
|
result = m_inputs.first->GetState();
|
||||||
|
|
||||||
|
if (m_inputs.second)
|
||||||
|
result = std::max(result, m_inputs.second->GetState());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
std::string GetName() const override { return m_name; }
|
||||||
|
bool IsDetectable() const override { return false; }
|
||||||
|
bool IsChild(const Input* input) const override
|
||||||
|
{
|
||||||
|
return m_inputs.first == input || m_inputs.second == input;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::string m_name;
|
||||||
|
const std::pair<Device::Input*, Device::Input*> m_inputs;
|
||||||
|
};
|
||||||
|
|
||||||
Device::~Device()
|
Device::~Device()
|
||||||
{
|
{
|
||||||
// delete inputs
|
// delete inputs
|
||||||
|
@ -51,6 +85,20 @@ std::string Device::GetQualifiedName() const
|
||||||
return fmt::format("{}/{}/{}", GetSource(), GetId(), GetName());
|
return fmt::format("{}/{}/{}", GetSource(), GetId(), GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Device::GetParentMostInput(Input* child) const -> Input*
|
||||||
|
{
|
||||||
|
for (auto* input : m_inputs)
|
||||||
|
{
|
||||||
|
if (input->IsChild(child))
|
||||||
|
{
|
||||||
|
// Running recursively is currently unnecessary but it doesn't hurt.
|
||||||
|
return GetParentMostInput(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
Device::Input* Device::FindInput(std::string_view name) const
|
Device::Input* Device::FindInput(std::string_view name) const
|
||||||
{
|
{
|
||||||
for (Input* input : m_inputs)
|
for (Input* input : m_inputs)
|
||||||
|
@ -102,6 +150,11 @@ bool Device::FullAnalogSurface::IsMatchingName(std::string_view name) const
|
||||||
return old_name == name;
|
return old_name == name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Device::AddCombinedInput(std::string name, const std::pair<std::string, std::string>& inputs)
|
||||||
|
{
|
||||||
|
AddInput(new CombinedInput(std::move(name), {FindInput(inputs.first), FindInput(inputs.second)}));
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// DeviceQualifier :: ToString
|
// DeviceQualifier :: ToString
|
||||||
//
|
//
|
||||||
|
@ -249,18 +302,54 @@ bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const
|
||||||
return device != nullptr && device->IsValid();
|
return device != nullptr && device->IsValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for input on a particular device.
|
// Wait for inputs on supplied devices.
|
||||||
// Inputs are considered if they are first seen in a neutral state.
|
// Inputs are only considered if they are first seen in a neutral state.
|
||||||
// This is useful for crazy flightsticks that have certain buttons that are always held down
|
// This is useful for crazy flightsticks that have certain buttons that are always held down
|
||||||
// and also properly handles detection when using "FullAnalogSurface" inputs.
|
// and also properly handles detection when using "FullAnalogSurface" inputs.
|
||||||
// Upon input, return the detected Device and Input, else return nullptrs
|
// Multiple detections are returned until the various timeouts have been reached.
|
||||||
std::pair<std::shared_ptr<Device>, Device::Input*>
|
auto DeviceContainer::DetectInput(const std::vector<std::string>& device_strings,
|
||||||
DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device_strings) const
|
std::chrono::milliseconds initial_wait,
|
||||||
|
std::chrono::milliseconds confirmation_wait,
|
||||||
|
std::chrono::milliseconds maximum_wait) const
|
||||||
|
-> std::vector<InputDetection>
|
||||||
{
|
{
|
||||||
struct InputState
|
struct InputState
|
||||||
{
|
{
|
||||||
ciface::Core::Device::Input& input;
|
InputState(ciface::Core::Device::Input* input_) : input{input_} { stats.Push(0.0); }
|
||||||
ControlState initial_state;
|
|
||||||
|
ciface::Core::Device::Input* input;
|
||||||
|
ControlState initial_state = input->GetState();
|
||||||
|
ControlState last_state = initial_state;
|
||||||
|
MathUtil::RunningVariance<ControlState> stats;
|
||||||
|
|
||||||
|
// Prevent multiiple detections until after release.
|
||||||
|
bool is_ready = true;
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
const auto new_state = input->GetState();
|
||||||
|
|
||||||
|
if (!is_ready && new_state < (1 - INPUT_DETECT_THRESHOLD))
|
||||||
|
{
|
||||||
|
last_state = new_state;
|
||||||
|
is_ready = true;
|
||||||
|
stats.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto difference = new_state - last_state;
|
||||||
|
stats.Push(difference);
|
||||||
|
last_state = new_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPressed()
|
||||||
|
{
|
||||||
|
if (!is_ready)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// We want an input that was initially 0.0 and currently 1.0.
|
||||||
|
const auto detection_score = (last_state - std::abs(initial_state));
|
||||||
|
return detection_score > INPUT_DETECT_THRESHOLD;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DeviceState
|
struct DeviceState
|
||||||
|
@ -285,13 +374,13 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device
|
||||||
|
|
||||||
for (auto* input : device->Inputs())
|
for (auto* input : device->Inputs())
|
||||||
{
|
{
|
||||||
// Don't detect things like absolute cursor position.
|
// Don't detect things like absolute cursor positions, accelerometers, or gyroscopes.
|
||||||
if (!input->IsDetectable())
|
if (!input->IsDetectable())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Undesirable axes will have negative values here when trying to map a
|
// Undesirable axes will have negative values here when trying to map a
|
||||||
// "FullAnalogSurface".
|
// "FullAnalogSurface".
|
||||||
input_states.push_back({*input, input->GetState()});
|
input_states.push_back(InputState{input});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!input_states.empty())
|
if (!input_states.empty())
|
||||||
|
@ -301,27 +390,59 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device
|
||||||
if (device_states.empty())
|
if (device_states.empty())
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
u32 time = 0;
|
std::vector<InputDetection> detections;
|
||||||
while (time < wait_ms)
|
|
||||||
|
const auto start_time = Clock::now();
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
|
const auto now = Clock::now();
|
||||||
|
const auto elapsed_time = now - start_time;
|
||||||
|
|
||||||
|
if (elapsed_time >= maximum_wait || (detections.empty() && elapsed_time >= initial_wait) ||
|
||||||
|
(!detections.empty() && detections.back().release_time.has_value() &&
|
||||||
|
now >= *detections.back().release_time + confirmation_wait))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
Common::SleepCurrentThread(10);
|
Common::SleepCurrentThread(10);
|
||||||
time += 10;
|
|
||||||
|
|
||||||
for (auto& device_state : device_states)
|
for (auto& device_state : device_states)
|
||||||
{
|
{
|
||||||
for (auto& input_state : device_state.input_states)
|
for (std::size_t i = 0; i != device_state.input_states.size(); ++i)
|
||||||
{
|
{
|
||||||
// We want an input that was initially 0.0 and currently 1.0.
|
auto& input_state = device_state.input_states[i];
|
||||||
const auto detection_score =
|
input_state.Update();
|
||||||
(input_state.input.GetState() - std::abs(input_state.initial_state));
|
|
||||||
|
|
||||||
if (detection_score > INPUT_DETECT_THRESHOLD)
|
if (input_state.IsPressed())
|
||||||
return {device_state.device, &input_state.input};
|
{
|
||||||
|
input_state.is_ready = false;
|
||||||
|
|
||||||
|
// Digital presses will evaluate as 1 here.
|
||||||
|
// Analog presses will evaluate greater than 1.
|
||||||
|
const auto smoothness =
|
||||||
|
1 / std::sqrt(input_state.stats.Variance() / input_state.stats.Mean());
|
||||||
|
|
||||||
|
InputDetection new_detection;
|
||||||
|
new_detection.device = device_state.device;
|
||||||
|
new_detection.input = input_state.input;
|
||||||
|
new_detection.press_time = Clock::now();
|
||||||
|
new_detection.smoothness = smoothness;
|
||||||
|
|
||||||
|
// We found an input. Add it to our detections.
|
||||||
|
detections.emplace_back(std::move(new_detection));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No input was detected. :'(
|
// Check for any releases of our detected inputs.
|
||||||
return {};
|
for (auto& d : detections)
|
||||||
|
{
|
||||||
|
if (!d.release_time.has_value() && d.input->GetState() < (1 - INPUT_DETECT_THRESHOLD))
|
||||||
|
d.release_time = Clock::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return detections;
|
||||||
}
|
}
|
||||||
} // namespace ciface::Core
|
} // namespace ciface::Core
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
@ -85,6 +86,11 @@ public:
|
||||||
virtual ControlState GetState() const = 0;
|
virtual ControlState GetState() const = 0;
|
||||||
|
|
||||||
Input* ToInput() override { return this; }
|
Input* ToInput() override { return this; }
|
||||||
|
|
||||||
|
// Overridden by CombinedInput,
|
||||||
|
// so hotkey logic knows Ctrl, L_Ctrl, and R_Ctrl are the same,
|
||||||
|
// and so input detection can return the parent name.
|
||||||
|
virtual bool IsChild(const Input*) const { return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -119,6 +125,8 @@ public:
|
||||||
const std::vector<Input*>& Inputs() const { return m_inputs; }
|
const std::vector<Input*>& Inputs() const { return m_inputs; }
|
||||||
const std::vector<Output*>& Outputs() const { return m_outputs; }
|
const std::vector<Output*>& Outputs() const { return m_outputs; }
|
||||||
|
|
||||||
|
Input* GetParentMostInput(Input* input) const;
|
||||||
|
|
||||||
Input* FindInput(std::string_view name) const;
|
Input* FindInput(std::string_view name) const;
|
||||||
Output* FindOutput(std::string_view name) const;
|
Output* FindOutput(std::string_view name) const;
|
||||||
|
|
||||||
|
@ -147,6 +155,8 @@ protected:
|
||||||
AddInput(new FullAnalogSurface(high, low));
|
AddInput(new FullAnalogSurface(high, low));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AddCombinedInput(std::string name, const std::pair<std::string, std::string>& inputs);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int m_id;
|
int m_id;
|
||||||
std::vector<Input*> m_inputs;
|
std::vector<Input*> m_inputs;
|
||||||
|
@ -185,6 +195,17 @@ public:
|
||||||
class DeviceContainer
|
class DeviceContainer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
|
struct InputDetection
|
||||||
|
{
|
||||||
|
std::shared_ptr<Device> device;
|
||||||
|
Device::Input* input;
|
||||||
|
Clock::time_point press_time;
|
||||||
|
std::optional<Clock::time_point> release_time;
|
||||||
|
ControlState smoothness;
|
||||||
|
};
|
||||||
|
|
||||||
Device::Input* FindInput(std::string_view name, const Device* def_dev) const;
|
Device::Input* FindInput(std::string_view name, const Device* def_dev) const;
|
||||||
Device::Output* FindOutput(std::string_view name, const Device* def_dev) const;
|
Device::Output* FindOutput(std::string_view name, const Device* def_dev) const;
|
||||||
|
|
||||||
|
@ -194,8 +215,10 @@ public:
|
||||||
|
|
||||||
bool HasConnectedDevice(const DeviceQualifier& qualifier) const;
|
bool HasConnectedDevice(const DeviceQualifier& qualifier) const;
|
||||||
|
|
||||||
std::pair<std::shared_ptr<Device>, Device::Input*>
|
std::vector<InputDetection> DetectInput(const std::vector<std::string>& device_strings,
|
||||||
DetectInput(u32 wait_ms, const std::vector<std::string>& device_strings) const;
|
std::chrono::milliseconds initial_wait,
|
||||||
|
std::chrono::milliseconds confirmation_wait,
|
||||||
|
std::chrono::milliseconds maximum_wait) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
mutable std::recursive_mutex m_devices_mutex;
|
mutable std::recursive_mutex m_devices_mutex;
|
||||||
|
|
|
@ -143,6 +143,11 @@ KeyboardAndMouse::KeyboardAndMouse(void* window)
|
||||||
for (int keycode = 0; keycode < 0x80; ++keycode)
|
for (int keycode = 0; keycode < 0x80; ++keycode)
|
||||||
AddInput(new Key(keycode));
|
AddInput(new Key(keycode));
|
||||||
|
|
||||||
|
// Add combined left/right modifiers with consistent naming across platforms.
|
||||||
|
AddCombinedInput("Alt", {"Left Alt", "Right Alt"});
|
||||||
|
AddCombinedInput("Shift", {"Left Shift", "Right Shift"});
|
||||||
|
AddCombinedInput("Ctrl", {"Left Control", "Right Control"});
|
||||||
|
|
||||||
m_windowid = [[reinterpret_cast<NSView*>(window) window] windowNumber];
|
m_windowid = [[reinterpret_cast<NSView*>(window) window] windowNumber];
|
||||||
|
|
||||||
// cursor, with a hax for-loop
|
// cursor, with a hax for-loop
|
||||||
|
|
|
@ -172,6 +172,11 @@ KeyboardMouse::KeyboardMouse(Window window, int opcode, int pointer, int keyboar
|
||||||
delete temp_key;
|
delete temp_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add combined left/right modifiers with consistent naming across platforms.
|
||||||
|
AddCombinedInput("Alt", {"Alt_L", "Alt_R"});
|
||||||
|
AddCombinedInput("Shift", {"Shift_L", "Shift_R"});
|
||||||
|
AddCombinedInput("Ctrl", {"Control_L", "Control_R"});
|
||||||
|
|
||||||
// Mouse Buttons
|
// Mouse Buttons
|
||||||
for (int i = 0; i < 32; i++)
|
for (int i = 0; i < 32; i++)
|
||||||
AddInput(new Button(i, &m_state.buttons));
|
AddInput(new Button(i, &m_state.buttons));
|
||||||
|
|
Loading…
Reference in New Issue