From 52547379c99f290118641f29168cfae5cc94db51 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Tue, 22 Oct 2019 19:12:55 -0500 Subject: [PATCH 01/11] ExpressionParser: Add Hotkey syntax. --- .../ControlReference/ExpressionParser.cpp | 97 +++++++++++++++++++ .../ControlReference/ExpressionParser.h | 1 + 2 files changed, 98 insertions(+) diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index 1b1a24753c..0533b88a16 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. +#include #include #include #include @@ -112,6 +113,8 @@ Token Lexer::NextToken() return Token(TOK_LPAREN); case ')': return Token(TOK_RPAREN); + case '@': + return Token(TOK_HOTKEY); case '&': return Token(TOK_AND); case '|': @@ -374,6 +377,63 @@ protected: ControlState* m_value_ptr{}; }; +class HotkeyExpression : public Expression +{ +public: + HotkeyExpression(std::vector> inputs) + : m_inputs(std::move(inputs)) + { + } + + ControlState GetValue() const override + { + if (m_inputs.empty()) + return 0; + + const bool modifiers_pressed = std::all_of(m_inputs.begin(), std::prev(m_inputs.end()), + [](const std::unique_ptr& input) { + // TODO: kill magic number. + return input->GetValue() > 0.5; + }); + + if (modifiers_pressed) + { + // TODO: kill magic number. + const bool final_input_pressed = (**m_inputs.rbegin()).GetValue() > 0.5; + + if (m_is_ready) + return final_input_pressed; + + if (!final_input_pressed) + m_is_ready = true; + } + else + m_is_ready = false; + + return 0; + } + + void SetValue(ControlState) override {} + + int CountNumControls() const override + { + int result = 0; + for (auto& input : m_inputs) + result += input->CountNumControls(); + return result; + } + + void UpdateReferences(ControlEnvironment& env) override + { + for (auto& input : m_inputs) + input->UpdateReferences(env); + } + +private: + std::vector> m_inputs; + mutable bool m_is_ready = 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 +660,10 @@ private: { return ParseParens(); } + case TOK_HOTKEY: + { + return ParseHotkeys(); + } case TOK_SUB: { // An atom was expected but we got a subtraction symbol. @@ -684,6 +748,39 @@ private: return result; } + ParseResult ParseHotkeys() + { + Token tok = Chew(); + if (tok.type != TOK_LPAREN) + return ParseResult::MakeErrorResult(tok, _trans("Expected opening paren.")); + + std::vector> 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(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(std::move(inputs))); + } + ParseResult ParseToplevel() { return ParseBinary(); } }; // namespace ExpressionParser diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.h b/Source/Core/InputCommon/ControlReference/ExpressionParser.h index c0d807bf17..a879528d4b 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.h +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.h @@ -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, From e6ba495486b2c7ac337fa4ecd62597a0f9a56e51 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Wed, 23 Oct 2019 16:45:21 -0500 Subject: [PATCH 02/11] ExpressionParser: Suppress inputs when hotkey modifiers are pressed. --- .../ControlReference/ExpressionParser.cpp | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index 0533b88a16..04ddef976e 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include "Common/Common.h" +#include "Common/ScopeGuard.h" #include "Common/StringUtil.h" #include "InputCommon/ControlReference/ExpressionParser.h" @@ -22,6 +24,32 @@ namespace ciface::ExpressionParser { using namespace ciface::Core; +class HotkeySuppressions +{ +public: + bool IsSuppressed(Device::Input* input) const { return m_suppressions.count(input) != 0; } + + using Suppressor = std::unique_ptr; + + Suppressor MakeSuppressor(Device::Input* input) + { + ++m_suppressions[input]; + return std::make_unique([this, input]() { RemoveSuppression(input); }); + } + +private: + void RemoveSuppression(Device::Input* input) + { + auto it = m_suppressions.find(input); + if (--(it->second) == 0) + m_suppressions.erase(it); + } + + std::map m_suppressions; +}; + +static HotkeySuppressions s_hotkey_suppressions; + Token::Token(TokenType type_) : type(type_) { } @@ -201,7 +229,7 @@ public: explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {} ControlState GetValue() const override { - if (!input) + if (!input || s_hotkey_suppressions.IsSuppressed(input)) return 0.0; // Note: Inputs may return negative values in situations where opposing directions are @@ -225,6 +253,8 @@ public: output = env.FindOutput(qualifier); } + Device::Input* GetInput() const { return input; }; + private: ControlQualifier qualifier; Device::Input* input = nullptr; @@ -398,17 +428,31 @@ public: if (modifiers_pressed) { + auto& final_input = **m_inputs.rbegin(); + + // Remove supression before getting value. + m_suppressor = {}; + + const ControlState final_input_state = final_input.GetValue(); + // TODO: kill magic number. - const bool final_input_pressed = (**m_inputs.rbegin()).GetValue() > 0.5; + if (final_input_state < 0.5) + m_is_ready = true; if (m_is_ready) - return final_input_pressed; + { + // Only suppress input when we have at least one modifier. + if (m_inputs.size() > 1) + m_suppressor = s_hotkey_suppressions.MakeSuppressor(final_input.GetInput()); - if (!final_input_pressed) - m_is_ready = true; + return final_input_state; + } } else + { + m_suppressor = {}; m_is_ready = false; + } return 0; } @@ -425,12 +469,15 @@ public: void UpdateReferences(ControlEnvironment& env) override { + m_suppressor = {}; + for (auto& input : m_inputs) input->UpdateReferences(env); } private: std::vector> m_inputs; + mutable HotkeySuppressions::Suppressor m_suppressor; mutable bool m_is_ready = false; }; From b3acc7403f33033bfce6db92ea4b8ec6fc968665 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Wed, 23 Oct 2019 17:20:42 -0500 Subject: [PATCH 03/11] InputCommon: Support detecting combinations of inputs. (Hotkeys) --- .../Config/Mapping/MappingCommon.cpp | 24 ++++++++++---- .../ControllerInterface/Device.cpp | 33 +++++++++++++++---- .../InputCommon/ControllerInterface/Device.h | 2 +- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp index bbd15ad831..7963fc22d1 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp @@ -68,7 +68,7 @@ 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); + const auto detections = device_container.DetectInput(INPUT_DETECT_TIME, device_strings); const auto timer = new QTimer(button); @@ -83,14 +83,24 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev button->setText(old_text); - if (!input) - return {}; + QString full_expression; - ciface::Core::DeviceQualifier device_qualifier; - device_qualifier.FromDevice(device.get()); + for (auto [device, input] : detections) + { + ciface::Core::DeviceQualifier device_qualifier; + device_qualifier.FromDevice(device.get()); - return MappingCommon::GetExpressionForControl(QString::fromStdString(input->GetName()), - device_qualifier, default_device, quote); + if (!full_expression.isEmpty()) + full_expression += QChar::fromLatin1('+'); + + full_expression += MappingCommon::GetExpressionForControl( + QString::fromStdString(input->GetName()), device_qualifier, default_device, quote); + } + + if (detections.size() > 1) + return QStringLiteral("@(%1)").arg(std::move(full_expression)); + + return full_expression; } void TestOutput(QPushButton* button, OutputReference* reference) diff --git a/Source/Core/InputCommon/ControllerInterface/Device.cpp b/Source/Core/InputCommon/ControllerInterface/Device.cpp index ebed1ad421..cd056af0d8 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Device.cpp @@ -18,6 +18,7 @@ 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; Device::~Device() @@ -253,13 +254,14 @@ bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const // Inputs are 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, Device::Input*> +// Detects multiple inputs if they are pressed before others are released. +// Upon input, return the detected Device and Input pairs, else return an empty container +std::vector, Device::Input*>> DeviceContainer::DetectInput(u32 wait_ms, const std::vector& device_strings) const { struct InputState { - ciface::Core::Device::Input& input; + ciface::Core::Device::Input* input; ControlState initial_state; }; @@ -291,7 +293,7 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector& device // Undesirable axes will have negative values here when trying to map a // "FullAnalogSurface". - input_states.push_back({*input, input->GetState()}); + input_states.push_back({input, input->GetState()}); } if (!input_states.empty()) @@ -301,6 +303,8 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector& device if (device_states.empty()) return {}; + std::vector, Device::Input*>> detections; + u32 time = 0; while (time < wait_ms) { @@ -309,16 +313,31 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector& device 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) { + auto& input_state = device_state.input_states[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)); + (input_state.input->GetState() - std::abs(input_state.initial_state)); if (detection_score > INPUT_DETECT_THRESHOLD) - return {device_state.device, &input_state.input}; + { + // We found an input. Add it to our detections. + detections.emplace_back(device_state.device, input_state.input); + + // And remove from input_states to prevent more detections. + device_state.input_states.erase(device_state.input_states.begin() + i--); + } } } + + for (auto& detection : detections) + { + // If one of our detected inputs is released we are done. + if (detection.second->GetState() < (1 - INPUT_DETECT_THRESHOLD)) + return detections; + } } // No input was detected. :'( diff --git a/Source/Core/InputCommon/ControllerInterface/Device.h b/Source/Core/InputCommon/ControllerInterface/Device.h index d923d97076..43e61e53fd 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.h +++ b/Source/Core/InputCommon/ControllerInterface/Device.h @@ -194,7 +194,7 @@ public: bool HasConnectedDevice(const DeviceQualifier& qualifier) const; - std::pair, Device::Input*> + std::vector, Device::Input*>> DetectInput(u32 wait_ms, const std::vector& device_strings) const; protected: From d2729df281485651cbc3c27e4c9419f43fe61408 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Fri, 25 Oct 2019 18:49:06 -0500 Subject: [PATCH 04/11] ExpressionParser: Allow duplicate and superset modifier hotkeys to function. --- .../ControlReference/ExpressionParser.cpp | 138 +++++++++++++----- 1 file changed, 102 insertions(+), 36 deletions(-) diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index 04ddef976e..7747ae956d 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -24,28 +24,43 @@ namespace ciface::ExpressionParser { using namespace ciface::Core; +class ControlExpression; + class HotkeySuppressions { public: - bool IsSuppressed(Device::Input* input) const { return m_suppressions.count(input) != 0; } - using Suppressor = std::unique_ptr; - Suppressor MakeSuppressor(Device::Input* input) + bool IsSuppressed(Device::Input* input) const { - ++m_suppressions[input]; - return std::make_unique([this, input]() { RemoveSuppression(input); }); + // An input is suppressed if it exists in the map (with any modifier). + auto it = m_suppressions.lower_bound({input, nullptr}); + return it != m_suppressions.end() && (it->first.first == input); } + // Suppresses each input + modifier pair. + // The returned object removes the suppression on destruction. + Suppressor MakeSuppressor(const std::vector>& modifiers, + const std::unique_ptr& final_input); + + // Removes suppression for each input + modifier pair. + // The returned object restores the original suppression on destruction. + Suppressor MakeAntiSuppressor(const std::vector>& modifiers, + const std::unique_ptr& final_input); + private: - void RemoveSuppression(Device::Input* input) + using Suppression = std::pair; + using SuppressionLevel = u16; + + void RemoveSuppression(Device::Input* modifier, Device::Input* final_input) { - auto it = m_suppressions.find(input); - if (--(it->second) == 0) + auto it = m_suppressions.find({final_input, modifier}); + if ((--it->second) == 0) m_suppressions.erase(it); } - std::map m_suppressions; + // Holds counts of suppressions for each input/modifier pair. + std::map m_suppressions; }; static HotkeySuppressions s_hotkey_suppressions; @@ -227,9 +242,18 @@ public: std::shared_ptr m_device; explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {} + ControlState GetValue() const override { - if (!input || s_hotkey_suppressions.IsSuppressed(input)) + if (s_hotkey_suppressions.IsSuppressed(input)) + return 0; + else + return GetValueIgnoringSuppression(); + } + + ControlState GetValueIgnoringSuppression() const + { + if (!input) return 0.0; // Note: Inputs may return negative values in situations where opposing directions are @@ -261,6 +285,35 @@ private: Device::Output* output = nullptr; }; +HotkeySuppressions::Suppressor +HotkeySuppressions::MakeSuppressor(const std::vector>& modifiers, + const std::unique_ptr& final_input) +{ + for (auto& modifier : modifiers) + ++m_suppressions[{final_input->GetInput(), modifier->GetInput()}]; + + return std::make_unique([this, &modifiers, &final_input]() { + for (auto& modifier : modifiers) + RemoveSuppression(modifier->GetInput(), final_input->GetInput()); + }); +} + +HotkeySuppressions::Suppressor HotkeySuppressions::MakeAntiSuppressor( + const std::vector>& modifiers, + const std::unique_ptr& final_input) +{ + decltype(m_suppressions) unsuppressed_modifiers; + + for (auto& modifier : modifiers) + unsuppressed_modifiers.insert( + m_suppressions.extract({final_input->GetInput(), modifier->GetInput()})); + + return std::make_unique( + [this, unsuppressed_modifiers{std::move(unsuppressed_modifiers)}]() mutable { + m_suppressions.merge(unsuppressed_modifiers); + }); +} + class BinaryExpression : public Expression { public: @@ -411,42 +464,45 @@ class HotkeyExpression : public Expression { public: HotkeyExpression(std::vector> inputs) - : m_inputs(std::move(inputs)) + : m_modifiers(std::move(inputs)) { + m_final_input = std::move(m_modifiers.back()); + m_modifiers.pop_back(); } ControlState GetValue() const override { - if (m_inputs.empty()) - return 0; - - const bool modifiers_pressed = std::all_of(m_inputs.begin(), std::prev(m_inputs.end()), + const bool modifiers_pressed = std::all_of(m_modifiers.begin(), m_modifiers.end(), [](const std::unique_ptr& input) { // TODO: kill magic number. return input->GetValue() > 0.5; }); + const auto final_input_state = m_final_input->GetValueIgnoringSuppression(); + if (modifiers_pressed) { - auto& final_input = **m_inputs.rbegin(); - - // Remove supression before getting value. - m_suppressor = {}; - - const ControlState final_input_state = final_input.GetValue(); - - // TODO: kill magic number. if (final_input_state < 0.5) - m_is_ready = true; - - if (m_is_ready) { - // Only suppress input when we have at least one modifier. - if (m_inputs.size() > 1) - m_suppressor = s_hotkey_suppressions.MakeSuppressor(final_input.GetInput()); + if (!m_suppressor) + EnableSuppression(); - return final_input_state; + m_is_ready = true; } + + // Ignore suppression of our own modifiers. This also allows superset modifiers to function. + const auto anti_suppression = + s_hotkey_suppressions.MakeAntiSuppressor(m_modifiers, m_final_input); + + const bool is_suppressed = s_hotkey_suppressions.IsSuppressed(m_final_input->GetInput()); + + // If some other hotkey suppressed us, require a release of final input to be ready again. + if (is_suppressed) + m_is_ready = false; + + // Our modifiers are active. Pass through the final input. + if (m_is_ready) + return final_input_state; } else { @@ -462,21 +518,31 @@ public: int CountNumControls() const override { int result = 0; - for (auto& input : m_inputs) + for (auto& input : m_modifiers) result += input->CountNumControls(); - return result; + return result + m_final_input->CountNumControls(); } void UpdateReferences(ControlEnvironment& env) override { - m_suppressor = {}; - - for (auto& input : m_inputs) + 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: - std::vector> m_inputs; + void EnableSuppression() const + { + m_suppressor = s_hotkey_suppressions.MakeSuppressor(m_modifiers, m_final_input); + } + + std::vector> m_modifiers; + std::unique_ptr m_final_input; mutable HotkeySuppressions::Suppressor m_suppressor; mutable bool m_is_ready = false; }; From f015c99a5193400a9798616c0a265e5c66916c55 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Fri, 25 Oct 2019 19:33:59 -0500 Subject: [PATCH 05/11] ControllerInterface: Add platform consistent names for modifier keys. --- .../DInput/DInputKeyboardMouse.cpp | 5 +++ .../ControllerInterface/Device.cpp | 33 +++++++++++++++++++ .../InputCommon/ControllerInterface/Device.h | 17 ++++++++++ .../Quartz/QuartzKeyboardAndMouse.mm | 5 +++ .../ControllerInterface/Xlib/XInput2.cpp | 5 +++ 5 files changed, 65 insertions(+) diff --git a/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.cpp b/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.cpp index 1e617d6c24..5aca9f5280 100644 --- a/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.cpp +++ b/Source/Core/InputCommon/ControllerInterface/DInput/DInputKeyboardMouse.cpp @@ -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); diff --git a/Source/Core/InputCommon/ControllerInterface/Device.cpp b/Source/Core/InputCommon/ControllerInterface/Device.cpp index cd056af0d8..e57e1c7682 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Device.cpp @@ -103,6 +103,39 @@ bool Device::FullAnalogSurface::IsMatchingName(std::string_view name) const return old_name == name; } +Device::CombinedInput::CombinedInput(std::string name, const Inputs& inputs) + : m_name(std::move(name)), m_inputs(inputs) +{ +} + +ControlState Device::CombinedInput::GetState() const +{ + 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 Device::CombinedInput::GetName() const +{ + return m_name; +} + +bool Device::CombinedInput::IsDetectable() +{ + return false; +} + +void Device::AddCombinedInput(std::string name, const std::pair& inputs) +{ + AddInput(new CombinedInput(std::move(name), {FindInput(inputs.first), FindInput(inputs.second)})); +} + // // DeviceQualifier :: ToString // diff --git a/Source/Core/InputCommon/ControllerInterface/Device.h b/Source/Core/InputCommon/ControllerInterface/Device.h index 43e61e53fd..495c64cbde 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.h +++ b/Source/Core/InputCommon/ControllerInterface/Device.h @@ -147,6 +147,23 @@ protected: AddInput(new FullAnalogSurface(high, low)); } + class CombinedInput final : public Input + { + public: + using Inputs = std::pair; + + CombinedInput(std::string name, const Inputs& inputs); + ControlState GetState() const override; + std::string GetName() const override; + bool IsDetectable() override; + + private: + const std::string m_name; + const std::pair m_inputs; + }; + + void AddCombinedInput(std::string name, const std::pair& inputs); + private: int m_id; std::vector m_inputs; diff --git a/Source/Core/InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.mm b/Source/Core/InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.mm index 7c4673acd7..d594ac9e5f 100644 --- a/Source/Core/InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.mm +++ b/Source/Core/InputCommon/ControllerInterface/Quartz/QuartzKeyboardAndMouse.mm @@ -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(window) window] windowNumber]; // cursor, with a hax for-loop diff --git a/Source/Core/InputCommon/ControllerInterface/Xlib/XInput2.cpp b/Source/Core/InputCommon/ControllerInterface/Xlib/XInput2.cpp index d049f22c8c..9ec1c2be35 100644 --- a/Source/Core/InputCommon/ControllerInterface/Xlib/XInput2.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Xlib/XInput2.cpp @@ -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)); From f7bf26cd60f269a83a0d1f646e8291de2b890ef2 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Fri, 25 Oct 2019 19:35:21 -0500 Subject: [PATCH 06/11] Core: Clean up default hotkey expressions. --- Source/Core/Core/HW/GCPadEmu.cpp | 4 +- Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp | 12 +- Source/Core/Core/HotkeyManager.cpp | 107 ++++++++---------- 3 files changed, 56 insertions(+), 67 deletions(-) diff --git a/Source/Core/Core/HW/GCPadEmu.cpp b/Source/Core/Core/HW/GCPadEmu.cpp index 8b16173312..02e4efe4d3 100644 --- a/Source/Core/Core/HW/GCPadEmu.cpp +++ b/Source/Core/Core/HW/GCPadEmu.cpp @@ -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 % diff --git a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp index 2702ae5b6b..549251aed1 100644 --- a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp @@ -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 diff --git a/Source/Core/Core/HotkeyManager.cpp b/Source/Core/Core/HotkeyManager.cpp index 235b6f739d..656542032d 100644 --- a/Source/Core/Core/HotkeyManager.cpp +++ b/Source/Core/Core/HotkeyManager.cpp @@ -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 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"})); } From aae913fbc6da714b3777e3a3c20fd06d1e423a0e Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 2 Nov 2019 09:55:45 -0500 Subject: [PATCH 07/11] InputCommon: Clean up modifier ignoring logic. --- .../ControlReference/ExpressionParser.cpp | 71 +++++++++---------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index 7747ae956d..9e36c49a05 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -14,6 +14,7 @@ #include #include "Common/Common.h" +#include "Common/Logging/Log.h" #include "Common/ScopeGuard.h" #include "Common/StringUtil.h" @@ -29,24 +30,22 @@ class ControlExpression; class HotkeySuppressions { public: + using Modifiers = std::vector>; using Suppressor = std::unique_ptr; bool IsSuppressed(Device::Input* input) const { - // An input is suppressed if it exists in the map (with any modifier). - auto it = m_suppressions.lower_bound({input, nullptr}); - return it != m_suppressions.end() && (it->first.first == input); + // 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 std::vector>& modifiers, - const std::unique_ptr& final_input); - - // Removes suppression for each input + modifier pair. - // The returned object restores the original suppression on destruction. - Suppressor MakeAntiSuppressor(const std::vector>& modifiers, - const std::unique_ptr& final_input); + Suppressor MakeSuppressor(const Modifiers* modifiers, + const std::unique_ptr* final_input); private: using Suppression = std::pair; @@ -285,33 +284,30 @@ private: Device::Output* output = nullptr; }; -HotkeySuppressions::Suppressor -HotkeySuppressions::MakeSuppressor(const std::vector>& modifiers, - const std::unique_ptr& final_input) +bool HotkeySuppressions::IsSuppressedIgnoringModifiers(Device::Input* input, + const Modifiers& ignore_modifiers) const { - for (auto& modifier : modifiers) - ++m_suppressions[{final_input->GetInput(), modifier->GetInput()}]; + // 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}); - return std::make_unique([this, &modifiers, &final_input]() { - for (auto& modifier : modifiers) - RemoveSuppression(modifier->GetInput(), final_input->GetInput()); + return std::any_of(it, it_end, [&](auto& s) { + return std::none_of(begin(ignore_modifiers), end(ignore_modifiers), + [&](auto& m) { return m->GetInput() == s.first.second; }); }); } -HotkeySuppressions::Suppressor HotkeySuppressions::MakeAntiSuppressor( - const std::vector>& modifiers, - const std::unique_ptr& final_input) +HotkeySuppressions::Suppressor +HotkeySuppressions::MakeSuppressor(const Modifiers* modifiers, + const std::unique_ptr* final_input) { - decltype(m_suppressions) unsuppressed_modifiers; + for (auto& modifier : *modifiers) + ++m_suppressions[{(*final_input)->GetInput(), modifier->GetInput()}]; - for (auto& modifier : modifiers) - unsuppressed_modifiers.insert( - m_suppressions.extract({final_input->GetInput(), modifier->GetInput()})); - - return std::make_unique( - [this, unsuppressed_modifiers{std::move(unsuppressed_modifiers)}]() mutable { - m_suppressions.merge(unsuppressed_modifiers); - }); + return std::make_unique([this, modifiers, final_input]() { + for (auto& modifier : *modifiers) + RemoveSuppression(modifier->GetInput(), (*final_input)->GetInput()); + }); } class BinaryExpression : public Expression @@ -474,15 +470,14 @@ public: { const bool modifiers_pressed = std::all_of(m_modifiers.begin(), m_modifiers.end(), [](const std::unique_ptr& input) { - // TODO: kill magic number. - return input->GetValue() > 0.5; + return input->GetValue() > CONDITION_THRESHOLD; }); const auto final_input_state = m_final_input->GetValueIgnoringSuppression(); if (modifiers_pressed) { - if (final_input_state < 0.5) + if (final_input_state < CONDITION_THRESHOLD) { if (!m_suppressor) EnableSuppression(); @@ -491,10 +486,8 @@ public: } // Ignore suppression of our own modifiers. This also allows superset modifiers to function. - const auto anti_suppression = - s_hotkey_suppressions.MakeAntiSuppressor(m_modifiers, m_final_input); - - const bool is_suppressed = s_hotkey_suppressions.IsSuppressed(m_final_input->GetInput()); + const bool is_suppressed = s_hotkey_suppressions.IsSuppressedIgnoringModifiers( + m_final_input->GetInput(), m_modifiers); // If some other hotkey suppressed us, require a release of final input to be ready again. if (is_suppressed) @@ -538,10 +531,10 @@ public: private: void EnableSuppression() const { - m_suppressor = s_hotkey_suppressions.MakeSuppressor(m_modifiers, m_final_input); + m_suppressor = s_hotkey_suppressions.MakeSuppressor(&m_modifiers, &m_final_input); } - std::vector> m_modifiers; + HotkeySuppressions::Modifiers m_modifiers; std::unique_ptr m_final_input; mutable HotkeySuppressions::Suppressor m_suppressor; mutable bool m_is_ready = false; From d8ad8c386121e9c937360b102289231d86693dd1 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sun, 26 Jan 2020 13:58:20 -0600 Subject: [PATCH 08/11] InputCommon: Make hotkeys and input detection aware of Ctrl -> L_Ctrl / R_Ctrl hierarchy. --- .../Config/Mapping/MappingCommon.cpp | 6 ++ .../ControlReference/ExpressionParser.cpp | 7 +- .../ControllerInterface/Device.cpp | 74 ++++++++++++------- .../InputCommon/ControllerInterface/Device.h | 22 ++---- 4 files changed, 65 insertions(+), 44 deletions(-) diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp index 7963fc22d1..4b887c10fa 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp @@ -93,6 +93,12 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev if (!full_expression.isEmpty()) full_expression += QChar::fromLatin1('+'); + // 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. + if (quote == Quote::On) + input = device->GetParentMostInput(input); + full_expression += MappingCommon::GetExpressionForControl( QString::fromStdString(input->GetName()), device_qualifier, default_device, quote); } diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index 9e36c49a05..cbc4a1462b 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -291,9 +291,14 @@ bool HotkeySuppressions::IsSuppressedIgnoringModifiers(Device::Input* input, 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 m->GetInput() == s.first.second; }); + [&](auto& m) { return is_same_modifier(m->GetInput(), s.first.second); }); }); } diff --git a/Source/Core/InputCommon/ControllerInterface/Device.cpp b/Source/Core/InputCommon/ControllerInterface/Device.cpp index e57e1c7682..271a42c9ab 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Device.cpp @@ -21,6 +21,38 @@ namespace ciface::Core // 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; + + 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 m_inputs; +}; + Device::~Device() { // delete inputs @@ -52,6 +84,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) @@ -103,34 +149,6 @@ bool Device::FullAnalogSurface::IsMatchingName(std::string_view name) const return old_name == name; } -Device::CombinedInput::CombinedInput(std::string name, const Inputs& inputs) - : m_name(std::move(name)), m_inputs(inputs) -{ -} - -ControlState Device::CombinedInput::GetState() const -{ - 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 Device::CombinedInput::GetName() const -{ - return m_name; -} - -bool Device::CombinedInput::IsDetectable() -{ - return false; -} - void Device::AddCombinedInput(std::string name, const std::pair& inputs) { AddInput(new CombinedInput(std::move(name), {FindInput(inputs.first), FindInput(inputs.second)})); diff --git a/Source/Core/InputCommon/ControllerInterface/Device.h b/Source/Core/InputCommon/ControllerInterface/Device.h index 495c64cbde..fa97a79a59 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.h +++ b/Source/Core/InputCommon/ControllerInterface/Device.h @@ -85,6 +85,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 +124,8 @@ public: const std::vector& Inputs() const { return m_inputs; } const std::vector& 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,21 +154,6 @@ protected: AddInput(new FullAnalogSurface(high, low)); } - class CombinedInput final : public Input - { - public: - using Inputs = std::pair; - - CombinedInput(std::string name, const Inputs& inputs); - ControlState GetState() const override; - std::string GetName() const override; - bool IsDetectable() override; - - private: - const std::string m_name; - const std::pair m_inputs; - }; - void AddCombinedInput(std::string name, const std::pair& inputs); private: From 48b76ff90f62183d5ad25a33ce9855b22e33dd97 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Wed, 12 Feb 2020 16:49:17 -0600 Subject: [PATCH 09/11] InputCommon: Improve input detection to produce buton combinations. --- .../Config/Mapping/MappingCommon.cpp | 142 ++++++++++++++---- .../DolphinQt/Config/Mapping/MappingCommon.h | 13 +- .../ControllerInterface/Device.cpp | 107 +++++++++---- .../InputCommon/ControllerInterface/Device.h | 18 ++- 4 files changed, 215 insertions(+), 65 deletions(-) diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp index 4b887c10fa..eb61963300 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp @@ -5,6 +5,7 @@ #include "DolphinQt/Config/Mapping/MappingCommon.h" #include +#include #include #include @@ -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 detections = 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,30 +97,7 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev button->setText(old_text); - QString full_expression; - - for (auto [device, input] : detections) - { - ciface::Core::DeviceQualifier device_qualifier; - device_qualifier.FromDevice(device.get()); - - if (!full_expression.isEmpty()) - full_expression += QChar::fromLatin1('+'); - - // 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. - if (quote == Quote::On) - input = device->GetParentMostInput(input); - - full_expression += MappingCommon::GetExpressionForControl( - QString::fromStdString(input->GetName()), device_qualifier, default_device, quote); - } - - if (detections.size() > 1) - return QStringLiteral("@(%1)").arg(std::move(full_expression)); - - return full_expression; + return BuildExpression(detections, default_device, quote); } void TestOutput(QPushButton* button, OutputReference* reference) @@ -118,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* 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& detections, + const ciface::Core::DeviceQualifier& default_device, Quote quote) +{ + std::vector 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 diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h index f3372bdb27..27a3cf7041 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h @@ -7,16 +7,12 @@ #include #include +#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*); + +QString BuildExpression(const std::vector&, + const ciface::Core::DeviceQualifier& default_device, Quote quote); + } // namespace MappingCommon diff --git a/Source/Core/InputCommon/ControllerInterface/Device.cpp b/Source/Core/InputCommon/ControllerInterface/Device.cpp index 271a42c9ab..2fd7fb02e7 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Device.cpp @@ -13,6 +13,7 @@ #include +#include "Common/MathUtil.h" #include "Common/Thread.h" namespace ciface::Core @@ -301,19 +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. -// Detects multiple inputs if they are pressed before others are released. -// Upon input, return the detected Device and Input pairs, else return an empty container -std::vector, Device::Input*>> -DeviceContainer::DetectInput(u32 wait_ms, const std::vector& device_strings) const +// Multiple detections are returned until the various timeouts have been reached. +auto DeviceContainer::DetectInput(const std::vector& device_strings, + std::chrono::milliseconds initial_wait, + std::chrono::milliseconds confirmation_wait, + std::chrono::milliseconds maximum_wait) const + -> std::vector { struct InputState { + InputState(ciface::Core::Device::Input* input_) : input{input_} { stats.Push(0.0); } + ciface::Core::Device::Input* input; - ControlState initial_state; + ControlState initial_state = input->GetState(); + ControlState last_state = initial_state; + MathUtil::RunningVariance 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 @@ -338,13 +374,13 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector& 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()) @@ -354,44 +390,59 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector& device if (device_states.empty()) return {}; - std::vector, Device::Input*>> detections; + std::vector detections; - u32 time = 0; - 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); - time += 10; for (auto& device_state : device_states) { for (std::size_t i = 0; i != device_state.input_states.size(); ++i) { auto& input_state = device_state.input_states[i]; + input_state.Update(); - // 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)); - - if (detection_score > INPUT_DETECT_THRESHOLD) + if (input_state.IsPressed()) { - // We found an input. Add it to our detections. - detections.emplace_back(device_state.device, input_state.input); + input_state.is_ready = false; - // And remove from input_states to prevent more detections. - device_state.input_states.erase(device_state.input_states.begin() + i--); + // 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)); } } } - for (auto& detection : detections) + // Check for any releases of our detected inputs. + for (auto& d : detections) { - // If one of our detected inputs is released we are done. - if (detection.second->GetState() < (1 - INPUT_DETECT_THRESHOLD)) - return 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 diff --git a/Source/Core/InputCommon/ControllerInterface/Device.h b/Source/Core/InputCommon/ControllerInterface/Device.h index fa97a79a59..2612f59627 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.h +++ b/Source/Core/InputCommon/ControllerInterface/Device.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include #include @@ -194,6 +195,17 @@ public: class DeviceContainer { public: + using Clock = std::chrono::steady_clock; + + struct InputDetection + { + std::shared_ptr device; + Device::Input* input; + Clock::time_point press_time; + std::optional 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; @@ -203,8 +215,10 @@ public: bool HasConnectedDevice(const DeviceQualifier& qualifier) const; - std::vector, Device::Input*>> - DetectInput(u32 wait_ms, const std::vector& device_strings) const; + std::vector DetectInput(const std::vector& 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; From 44927da420e8715cf041a50a38de08bc649e6685 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sun, 16 Feb 2020 13:09:12 -0600 Subject: [PATCH 10/11] ExpressionParser: Replace ScopeGuard with custom deleter unique_ptr. --- .../ControlReference/ExpressionParser.cpp | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index cbc4a1462b..7636f6f1aa 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -15,7 +15,6 @@ #include "Common/Common.h" #include "Common/Logging/Log.h" -#include "Common/ScopeGuard.h" #include "Common/StringUtil.h" #include "InputCommon/ControlReference/ExpressionParser.h" @@ -31,7 +30,17 @@ class HotkeySuppressions { public: using Modifiers = std::vector>; - using Suppressor = std::unique_ptr; + + struct InvokingDeleter + { + template + void operator()(T* func) + { + (*func)(); + } + }; + + using Suppressor = std::unique_ptr, InvokingDeleter>; bool IsSuppressed(Device::Input* input) const { @@ -309,10 +318,11 @@ HotkeySuppressions::MakeSuppressor(const Modifiers* modifiers, for (auto& modifier : *modifiers) ++m_suppressions[{(*final_input)->GetInput(), modifier->GetInput()}]; - return std::make_unique([this, modifiers, final_input]() { - for (auto& modifier : *modifiers) - RemoveSuppression(modifier->GetInput(), (*final_input)->GetInput()); - }); + return Suppressor(std::make_unique>([this, modifiers, final_input]() { + for (auto& modifier : *modifiers) + RemoveSuppression(modifier->GetInput(), (*final_input)->GetInput()); + }).release(), + InvokingDeleter{}); } class BinaryExpression : public Expression From 431eb4d60d8a413ad9a4a72b1305f8090410a741 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sun, 30 Aug 2020 19:25:35 -0500 Subject: [PATCH 11/11] ExpressionParser: Improve hotkey suppression logic. Allow activation with simultaneous press of modifier and final input. --- .../ControlReference/ExpressionParser.cpp | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index 7636f6f1aa..d4f93450e3 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -14,7 +14,6 @@ #include #include "Common/Common.h" -#include "Common/Logging/Log.h" #include "Common/StringUtil.h" #include "InputCommon/ControlReference/ExpressionParser.h" @@ -492,30 +491,29 @@ public: if (modifiers_pressed) { - if (final_input_state < CONDITION_THRESHOLD) - { - if (!m_suppressor) - EnableSuppression(); - - m_is_ready = true; - } - // 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_ready = false; + m_is_blocked = true; + + if (m_is_blocked) + return 0; + + EnableSuppression(); // Our modifiers are active. Pass through the final input. - if (m_is_ready) - return final_input_state; + return final_input_state; } else { m_suppressor = {}; - m_is_ready = false; + m_is_blocked = final_input_state > CONDITION_THRESHOLD; } return 0; @@ -546,13 +544,14 @@ public: private: void EnableSuppression() const { - m_suppressor = s_hotkey_suppressions.MakeSuppressor(&m_modifiers, &m_final_input); + if (!m_suppressor) + m_suppressor = s_hotkey_suppressions.MakeSuppressor(&m_modifiers, &m_final_input); } HotkeySuppressions::Modifiers m_modifiers; std::unique_ptr m_final_input; mutable HotkeySuppressions::Suppressor m_suppressor; - mutable bool m_is_ready = false; + mutable bool m_is_blocked = false; }; // This class proxies all methods to its either left-hand child if it has bound controls, or its