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(4, "D"); // Z
|
||||
#ifdef _WIN32
|
||||
m_buttons->SetControlExpression(5, "!LMENU & RETURN"); // Start
|
||||
m_buttons->SetControlExpression(5, "RETURN"); // Start
|
||||
#else
|
||||
// OS X/Linux
|
||||
m_buttons->SetControlExpression(5, "!`Alt_L` & Return"); // Start
|
||||
m_buttons->SetControlExpression(5, "Return"); // Start
|
||||
#endif
|
||||
|
||||
// stick modifiers to 50 %
|
||||
|
|
|
@ -603,9 +603,9 @@ void Wiimote::LoadDefaults(const ControllerInterface& ciface)
|
|||
m_buttons->SetControlExpression(5, "E"); // +
|
||||
|
||||
#ifdef _WIN32
|
||||
m_buttons->SetControlExpression(6, "!LMENU & RETURN"); // Home
|
||||
m_buttons->SetControlExpression(6, "RETURN"); // Home
|
||||
#else
|
||||
m_buttons->SetControlExpression(6, "!`Alt_L` & Return"); // Home
|
||||
m_buttons->SetControlExpression(6, "Return"); // Home
|
||||
#endif
|
||||
|
||||
// Shake
|
||||
|
@ -625,10 +625,10 @@ void Wiimote::LoadDefaults(const ControllerInterface& ciface)
|
|||
m_dpad->SetControlExpression(2, "LEFT"); // Left
|
||||
m_dpad->SetControlExpression(3, "RIGHT"); // Right
|
||||
#elif __APPLE__
|
||||
m_dpad->SetControlExpression(0, "Up Arrow"); // Up
|
||||
m_dpad->SetControlExpression(1, "Down Arrow"); // Down
|
||||
m_dpad->SetControlExpression(2, "Left Arrow"); // Left
|
||||
m_dpad->SetControlExpression(3, "Right Arrow"); // Right
|
||||
m_dpad->SetControlExpression(0, "Up Arrow"); // Up
|
||||
m_dpad->SetControlExpression(1, "Down Arrow"); // Down
|
||||
m_dpad->SetControlExpression(2, "Left Arrow"); // Left
|
||||
m_dpad->SetControlExpression(3, "Right Arrow"); // Right
|
||||
#else
|
||||
m_dpad->SetControlExpression(0, "Up"); // Up
|
||||
m_dpad->SetControlExpression(1, "Down"); // Down
|
||||
|
|
|
@ -409,83 +409,72 @@ void HotkeyManager::LoadDefaults(const ControllerInterface& 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) {
|
||||
m_keys[FindGroupByID(index)]
|
||||
->controls[GetIndexForGroup(FindGroupByID(index), index)]
|
||||
->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
|
||||
set_key_expression(HK_OPEN, CTRL + " & O");
|
||||
set_key_expression(HK_PLAY_PAUSE, NON + " & `F10`");
|
||||
set_key_expression(HK_OPEN, hotkey_string({"Ctrl", "O"}));
|
||||
set_key_expression(HK_PLAY_PAUSE, "F10");
|
||||
#ifdef _WIN32
|
||||
set_key_expression(HK_STOP, NON + " & ESCAPE");
|
||||
set_key_expression(HK_FULLSCREEN, ALT + " & RETURN");
|
||||
set_key_expression(HK_STOP, "ESCAPE");
|
||||
set_key_expression(HK_FULLSCREEN, hotkey_string({"Alt", "RETURN"}));
|
||||
#else
|
||||
set_key_expression(HK_STOP, NON + " & Escape");
|
||||
set_key_expression(HK_FULLSCREEN, ALT + " & Return");
|
||||
set_key_expression(HK_STOP, "Escape");
|
||||
set_key_expression(HK_FULLSCREEN, hotkey_string({"Alt", "Return"}));
|
||||
#endif
|
||||
set_key_expression(HK_STEP, NON + " & `F11`");
|
||||
set_key_expression(HK_STEP_OVER, SHIFT + " & `F10`");
|
||||
set_key_expression(HK_STEP_OUT, SHIFT + " & `F11`");
|
||||
set_key_expression(HK_BP_TOGGLE, SHIFT + " & `F9`");
|
||||
set_key_expression(HK_SCREENSHOT, NON + " & `F9`");
|
||||
set_key_expression(HK_WIIMOTE1_CONNECT, ALT + " & `F5`");
|
||||
set_key_expression(HK_WIIMOTE2_CONNECT, ALT + " & `F6`");
|
||||
set_key_expression(HK_WIIMOTE3_CONNECT, ALT + " & `F7`");
|
||||
set_key_expression(HK_WIIMOTE4_CONNECT, ALT + " & `F8`");
|
||||
set_key_expression(HK_BALANCEBOARD_CONNECT, ALT + " & `F9`");
|
||||
set_key_expression(HK_STEP, "F11");
|
||||
set_key_expression(HK_STEP_OVER, hotkey_string({"Shift", "F10"}));
|
||||
set_key_expression(HK_STEP_OUT, hotkey_string({"Shift", "F11"}));
|
||||
set_key_expression(HK_BP_TOGGLE, hotkey_string({"Shift", "F9"}));
|
||||
set_key_expression(HK_SCREENSHOT, "F9");
|
||||
set_key_expression(HK_WIIMOTE1_CONNECT, hotkey_string({"Alt", "F5"}));
|
||||
set_key_expression(HK_WIIMOTE2_CONNECT, hotkey_string({"Alt", "F6"}));
|
||||
set_key_expression(HK_WIIMOTE3_CONNECT, hotkey_string({"Alt", "F7"}));
|
||||
set_key_expression(HK_WIIMOTE4_CONNECT, hotkey_string({"Alt", "F8"}));
|
||||
set_key_expression(HK_BALANCEBOARD_CONNECT, hotkey_string({"Alt", "F9"}));
|
||||
#ifdef _WIN32
|
||||
set_key_expression(HK_TOGGLE_THROTTLE, NON + " & TAB");
|
||||
set_key_expression(HK_TOGGLE_THROTTLE, "TAB");
|
||||
#else
|
||||
set_key_expression(HK_TOGGLE_THROTTLE, NON + " & Tab");
|
||||
set_key_expression(HK_TOGGLE_THROTTLE, "Tab");
|
||||
#endif
|
||||
|
||||
// Freelook
|
||||
set_key_expression(HK_FREELOOK_DECREASE_SPEED, SHIFT + " & `1`");
|
||||
set_key_expression(HK_FREELOOK_INCREASE_SPEED, SHIFT + " & `2`");
|
||||
set_key_expression(HK_FREELOOK_RESET_SPEED, SHIFT + " & F");
|
||||
set_key_expression(HK_FREELOOK_UP, SHIFT + " & E");
|
||||
set_key_expression(HK_FREELOOK_DOWN, SHIFT + " & Q");
|
||||
set_key_expression(HK_FREELOOK_LEFT, SHIFT + " & A");
|
||||
set_key_expression(HK_FREELOOK_RIGHT, SHIFT + " & D");
|
||||
set_key_expression(HK_FREELOOK_ZOOM_IN, SHIFT + " & W");
|
||||
set_key_expression(HK_FREELOOK_ZOOM_OUT, SHIFT + " & S");
|
||||
set_key_expression(HK_FREELOOK_RESET, SHIFT + " & R");
|
||||
set_key_expression(HK_FREELOOK_INCREASE_FOV_X, SHIFT + " & `Axis Z+`");
|
||||
set_key_expression(HK_FREELOOK_DECREASE_FOV_X, SHIFT + " & `Axis Z-`");
|
||||
set_key_expression(HK_FREELOOK_INCREASE_FOV_Y, SHIFT + " & `Axis Z+`");
|
||||
set_key_expression(HK_FREELOOK_DECREASE_FOV_Y, SHIFT + " & `Axis Z-`");
|
||||
set_key_expression(HK_FREELOOK_DECREASE_SPEED, hotkey_string({"Shift", "1"}));
|
||||
set_key_expression(HK_FREELOOK_INCREASE_SPEED, hotkey_string({"Shift", "2"}));
|
||||
set_key_expression(HK_FREELOOK_RESET_SPEED, hotkey_string({"Shift", "F"}));
|
||||
set_key_expression(HK_FREELOOK_UP, hotkey_string({"Shift", "E"}));
|
||||
set_key_expression(HK_FREELOOK_DOWN, hotkey_string({"Shift", "Q"}));
|
||||
set_key_expression(HK_FREELOOK_LEFT, hotkey_string({"Shift", "A"}));
|
||||
set_key_expression(HK_FREELOOK_RIGHT, hotkey_string({"Shift", "D"}));
|
||||
set_key_expression(HK_FREELOOK_ZOOM_IN, hotkey_string({"Shift", "W"}));
|
||||
set_key_expression(HK_FREELOOK_ZOOM_OUT, hotkey_string({"Shift", "S"}));
|
||||
set_key_expression(HK_FREELOOK_RESET, hotkey_string({"Shift", "R"}));
|
||||
set_key_expression(HK_FREELOOK_INCREASE_FOV_X, hotkey_string({"Shift", "`Axis Z+`"}));
|
||||
set_key_expression(HK_FREELOOK_DECREASE_FOV_X, hotkey_string({"Shift", "`Axis Z-`"}));
|
||||
set_key_expression(HK_FREELOOK_INCREASE_FOV_Y, hotkey_string({"Shift", "`Axis Z+`"}));
|
||||
set_key_expression(HK_FREELOOK_DECREASE_FOV_Y, hotkey_string({"Shift", "`Axis Z-`"}));
|
||||
|
||||
// Savestates
|
||||
const std::string non_fmt = NON + " & `F{}`";
|
||||
const std::string shift_fmt = SHIFT + " & `F{}`";
|
||||
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_SAVE_STATE_SLOT_1 + i, fmt::format(shift_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,
|
||||
hotkey_string({"Shift", fmt::format("F%d", i + 1)}));
|
||||
}
|
||||
set_key_expression(HK_UNDO_LOAD_STATE, NON + " & `F12`");
|
||||
set_key_expression(HK_UNDO_SAVE_STATE, SHIFT + " & `F12`");
|
||||
set_key_expression(HK_UNDO_LOAD_STATE, "F12");
|
||||
set_key_expression(HK_UNDO_SAVE_STATE, hotkey_string({"Shift", "F12"}));
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "DolphinQt/Config/Mapping/MappingCommon.h"
|
||||
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QPushButton>
|
||||
|
@ -14,14 +15,23 @@
|
|||
|
||||
#include "DolphinQt/QtUtils/BlockUserInputFilter.h"
|
||||
#include "InputCommon/ControlReference/ControlReference.h"
|
||||
#include "InputCommon/ControllerInterface/Device.h"
|
||||
|
||||
#include "Common/Thread.h"
|
||||
|
||||
namespace MappingCommon
|
||||
{
|
||||
constexpr int INPUT_DETECT_TIME = 3000;
|
||||
constexpr int OUTPUT_TEST_TIME = 2000;
|
||||
constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3);
|
||||
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,
|
||||
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
|
||||
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);
|
||||
|
||||
|
@ -83,14 +97,7 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev
|
|||
|
||||
button->setText(old_text);
|
||||
|
||||
if (!input)
|
||||
return {};
|
||||
|
||||
ciface::Core::DeviceQualifier device_qualifier;
|
||||
device_qualifier.FromDevice(device.get());
|
||||
|
||||
return MappingCommon::GetExpressionForControl(QString::fromStdString(input->GetName()),
|
||||
device_qualifier, default_device, quote);
|
||||
return BuildExpression(detections, default_device, quote);
|
||||
}
|
||||
|
||||
void TestOutput(QPushButton* button, OutputReference* reference)
|
||||
|
@ -102,10 +109,103 @@ void TestOutput(QPushButton* button, OutputReference* reference)
|
|||
QApplication::processEvents();
|
||||
|
||||
reference->State(1.0);
|
||||
Common::SleepCurrentThread(OUTPUT_TEST_TIME);
|
||||
std::this_thread::sleep_for(OUTPUT_TEST_TIME);
|
||||
reference->State(0.0);
|
||||
|
||||
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
|
||||
|
|
|
@ -7,16 +7,12 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "InputCommon/ControllerInterface/Device.h"
|
||||
|
||||
class QString;
|
||||
class OutputReference;
|
||||
class QPushButton;
|
||||
|
||||
namespace ciface::Core
|
||||
{
|
||||
class DeviceContainer;
|
||||
class DeviceQualifier;
|
||||
} // namespace ciface::Core
|
||||
|
||||
namespace MappingCommon
|
||||
{
|
||||
enum class Quote
|
||||
|
@ -37,4 +33,9 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev
|
|||
|
||||
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
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
// Licensed under GPLv2+
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
@ -21,6 +23,55 @@ namespace ciface::ExpressionParser
|
|||
{
|
||||
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_)
|
||||
{
|
||||
}
|
||||
|
@ -112,6 +163,8 @@ Token Lexer::NextToken()
|
|||
return Token(TOK_LPAREN);
|
||||
case ')':
|
||||
return Token(TOK_RPAREN);
|
||||
case '@':
|
||||
return Token(TOK_HOTKEY);
|
||||
case '&':
|
||||
return Token(TOK_AND);
|
||||
case '|':
|
||||
|
@ -196,7 +249,16 @@ public:
|
|||
std::shared_ptr<Device> m_device;
|
||||
|
||||
explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {}
|
||||
|
||||
ControlState GetValue() const override
|
||||
{
|
||||
if (s_hotkey_suppressions.IsSuppressed(input))
|
||||
return 0;
|
||||
else
|
||||
return GetValueIgnoringSuppression();
|
||||
}
|
||||
|
||||
ControlState GetValueIgnoringSuppression() const
|
||||
{
|
||||
if (!input)
|
||||
return 0.0;
|
||||
|
@ -222,12 +284,46 @@ public:
|
|||
output = env.FindOutput(qualifier);
|
||||
}
|
||||
|
||||
Device::Input* GetInput() const { return input; };
|
||||
|
||||
private:
|
||||
ControlQualifier qualifier;
|
||||
Device::Input* input = 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
|
||||
{
|
||||
public:
|
||||
|
@ -374,6 +470,90 @@ protected:
|
|||
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
|
||||
// right-hand child. Its intended use is for supporting old-style barewords expressions.
|
||||
class CoalesceExpression : public Expression
|
||||
|
@ -600,6 +780,10 @@ private:
|
|||
{
|
||||
return ParseParens();
|
||||
}
|
||||
case TOK_HOTKEY:
|
||||
{
|
||||
return ParseHotkeys();
|
||||
}
|
||||
case TOK_SUB:
|
||||
{
|
||||
// An atom was expected but we got a subtraction symbol.
|
||||
|
@ -684,6 +868,39 @@ private:
|
|||
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(); }
|
||||
}; // namespace ExpressionParser
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ enum TokenType
|
|||
TOK_VARIABLE,
|
||||
TOK_BAREWORD,
|
||||
TOK_COMMENT,
|
||||
TOK_HOTKEY,
|
||||
// Binary Ops:
|
||||
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)
|
||||
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
|
||||
DIDEVCAPS mouse_caps = {};
|
||||
mouse_caps.dwSize = sizeof(mouse_caps);
|
||||
|
|
|
@ -13,13 +13,47 @@
|
|||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "Common/MathUtil.h"
|
||||
#include "Common/Thread.h"
|
||||
|
||||
namespace ciface::Core
|
||||
{
|
||||
// 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;
|
||||
|
||||
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()
|
||||
{
|
||||
// delete inputs
|
||||
|
@ -51,6 +85,20 @@ std::string Device::GetQualifiedName() const
|
|||
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
|
||||
{
|
||||
for (Input* input : m_inputs)
|
||||
|
@ -102,6 +150,11 @@ bool Device::FullAnalogSurface::IsMatchingName(std::string_view name) const
|
|||
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
|
||||
//
|
||||
|
@ -249,18 +302,54 @@ bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const
|
|||
return device != nullptr && device->IsValid();
|
||||
}
|
||||
|
||||
// Wait for input on a particular device.
|
||||
// Inputs are considered if they are first seen in a neutral state.
|
||||
// Wait for inputs on supplied devices.
|
||||
// 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
|
||||
// and also properly handles detection when using "FullAnalogSurface" inputs.
|
||||
// Upon input, return the detected Device and Input, else return nullptrs
|
||||
std::pair<std::shared_ptr<Device>, Device::Input*>
|
||||
DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device_strings) const
|
||||
// Multiple detections are returned until the various timeouts have been reached.
|
||||
auto DeviceContainer::DetectInput(const std::vector<std::string>& device_strings,
|
||||
std::chrono::milliseconds initial_wait,
|
||||
std::chrono::milliseconds confirmation_wait,
|
||||
std::chrono::milliseconds maximum_wait) const
|
||||
-> std::vector<InputDetection>
|
||||
{
|
||||
struct InputState
|
||||
{
|
||||
ciface::Core::Device::Input& input;
|
||||
ControlState initial_state;
|
||||
InputState(ciface::Core::Device::Input* input_) : input{input_} { stats.Push(0.0); }
|
||||
|
||||
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
|
||||
|
@ -285,13 +374,13 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device
|
|||
|
||||
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())
|
||||
continue;
|
||||
|
||||
// Undesirable axes will have negative values here when trying to map a
|
||||
// "FullAnalogSurface".
|
||||
input_states.push_back({*input, input->GetState()});
|
||||
input_states.push_back(InputState{input});
|
||||
}
|
||||
|
||||
if (!input_states.empty())
|
||||
|
@ -301,27 +390,59 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device
|
|||
if (device_states.empty())
|
||||
return {};
|
||||
|
||||
u32 time = 0;
|
||||
while (time < wait_ms)
|
||||
std::vector<InputDetection> detections;
|
||||
|
||||
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);
|
||||
time += 10;
|
||||
|
||||
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.
|
||||
const auto detection_score =
|
||||
(input_state.input.GetState() - std::abs(input_state.initial_state));
|
||||
auto& input_state = device_state.input_states[i];
|
||||
input_state.Update();
|
||||
|
||||
if (detection_score > INPUT_DETECT_THRESHOLD)
|
||||
return {device_state.device, &input_state.input};
|
||||
if (input_state.IsPressed())
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any releases of our detected inputs.
|
||||
for (auto& d : detections)
|
||||
{
|
||||
if (!d.release_time.has_value() && d.input->GetState() < (1 - INPUT_DETECT_THRESHOLD))
|
||||
d.release_time = Clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
// No input was detected. :'(
|
||||
return {};
|
||||
return detections;
|
||||
}
|
||||
} // namespace ciface::Core
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
@ -85,6 +86,11 @@ public:
|
|||
virtual ControlState GetState() const = 0;
|
||||
|
||||
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<Output*>& Outputs() const { return m_outputs; }
|
||||
|
||||
Input* GetParentMostInput(Input* input) const;
|
||||
|
||||
Input* FindInput(std::string_view name) const;
|
||||
Output* FindOutput(std::string_view name) const;
|
||||
|
||||
|
@ -147,6 +155,8 @@ protected:
|
|||
AddInput(new FullAnalogSurface(high, low));
|
||||
}
|
||||
|
||||
void AddCombinedInput(std::string name, const std::pair<std::string, std::string>& inputs);
|
||||
|
||||
private:
|
||||
int m_id;
|
||||
std::vector<Input*> m_inputs;
|
||||
|
@ -185,6 +195,17 @@ public:
|
|||
class DeviceContainer
|
||||
{
|
||||
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::Output* FindOutput(std::string_view name, const Device* def_dev) const;
|
||||
|
||||
|
@ -194,8 +215,10 @@ public:
|
|||
|
||||
bool HasConnectedDevice(const DeviceQualifier& qualifier) const;
|
||||
|
||||
std::pair<std::shared_ptr<Device>, Device::Input*>
|
||||
DetectInput(u32 wait_ms, const std::vector<std::string>& device_strings) const;
|
||||
std::vector<InputDetection> DetectInput(const std::vector<std::string>& device_strings,
|
||||
std::chrono::milliseconds initial_wait,
|
||||
std::chrono::milliseconds confirmation_wait,
|
||||
std::chrono::milliseconds maximum_wait) const;
|
||||
|
||||
protected:
|
||||
mutable std::recursive_mutex m_devices_mutex;
|
||||
|
|
|
@ -143,6 +143,11 @@ KeyboardAndMouse::KeyboardAndMouse(void* window)
|
|||
for (int keycode = 0; keycode < 0x80; ++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];
|
||||
|
||||
// cursor, with a hax for-loop
|
||||
|
|
|
@ -172,6 +172,11 @@ KeyboardMouse::KeyboardMouse(Window window, int opcode, int pointer, int keyboar
|
|||
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
|
||||
for (int i = 0; i < 32; i++)
|
||||
AddInput(new Button(i, &m_state.buttons));
|
||||
|
|
Loading…
Reference in New Issue