diff options
| author | JMC47 <JMC4789@gmail.com> | 2020-09-26 00:36:16 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-09-26 00:36:16 -0400 |
| commit | c64d41d3e783441e4f0b34d4b70cc754ca610b15 (patch) | |
| tree | 99fc4c788520c1ca60fa0e58412ae1ef7353dbb3 /Source/Core/InputCommon | |
| parent | 57f14b260bf043b6c261ca284b6c45fa3a7932de (diff) | |
| parent | 431eb4d60d8a413ad9a4a72b1305f8090410a741 (diff) | |
Merge pull request #8428 from jordan-woyak/better-hotkeys
InputCommon: Add hotkey support to input expressions.
Diffstat (limited to 'Source/Core/InputCommon')
7 files changed, 400 insertions, 23 deletions
diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index 1b1a24753c..d4f93450e3 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -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,8 +249,17 @@ 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 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, 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 ebed1ad421..2fd7fb02e7 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Device.cpp @@ -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)); - - if (detection_score > INPUT_DETECT_THRESHOLD) - return {device_state.device, &input_state.input}; + auto& input_state = device_state.input_states[i]; + input_state.Update(); + + 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 diff --git a/Source/Core/InputCommon/ControllerInterface/Device.h b/Source/Core/InputCommon/ControllerInterface/Device.h index d923d97076..2612f59627 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.h +++ b/Source/Core/InputCommon/ControllerInterface/Device.h @@ -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; 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<NSView*>(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)); |
