Merge pull request #8428 from jordan-woyak/better-hotkeys

InputCommon: Add hotkey support to input expressions.
This commit is contained in:
JMC47 2020-09-26 00:36:16 -04:00 committed by GitHub
commit c64d41d3e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 575 additions and 108 deletions

View File

@ -187,10 +187,10 @@ void GCPad::LoadDefaults(const ControllerInterface& ciface)
m_buttons->SetControlExpression(3, "S"); // Y m_buttons->SetControlExpression(3, "S"); // Y
m_buttons->SetControlExpression(4, "D"); // Z m_buttons->SetControlExpression(4, "D"); // Z
#ifdef _WIN32 #ifdef _WIN32
m_buttons->SetControlExpression(5, "!LMENU & RETURN"); // Start m_buttons->SetControlExpression(5, "RETURN"); // Start
#else #else
// OS X/Linux // OS X/Linux
m_buttons->SetControlExpression(5, "!`Alt_L` & Return"); // Start m_buttons->SetControlExpression(5, "Return"); // Start
#endif #endif
// stick modifiers to 50 % // stick modifiers to 50 %

View File

@ -603,9 +603,9 @@ void Wiimote::LoadDefaults(const ControllerInterface& ciface)
m_buttons->SetControlExpression(5, "E"); // + m_buttons->SetControlExpression(5, "E"); // +
#ifdef _WIN32 #ifdef _WIN32
m_buttons->SetControlExpression(6, "!LMENU & RETURN"); // Home m_buttons->SetControlExpression(6, "RETURN"); // Home
#else #else
m_buttons->SetControlExpression(6, "!`Alt_L` & Return"); // Home m_buttons->SetControlExpression(6, "Return"); // Home
#endif #endif
// Shake // Shake

View File

@ -409,83 +409,72 @@ void HotkeyManager::LoadDefaults(const ControllerInterface& ciface)
{ {
EmulatedController::LoadDefaults(ciface); EmulatedController::LoadDefaults(ciface);
#ifdef _WIN32
const std::string NON = "(!(LMENU | RMENU) & !(LSHIFT | RSHIFT) & !(LCONTROL | RCONTROL))";
const std::string ALT = "((LMENU | RMENU) & !(LSHIFT | RSHIFT) & !(LCONTROL | RCONTROL))";
const std::string SHIFT = "(!(LMENU | RMENU) & (LSHIFT | RSHIFT) & !(LCONTROL | RCONTROL))";
const std::string CTRL = "(!(LMENU | RMENU) & !(LSHIFT | RSHIFT) & (LCONTROL | RCONTROL))";
#elif __APPLE__
const std::string NON =
"(!`Left Alt` & !(`Left Shift`| `Right Shift`) & !(`Left Control` | `Right Control`))";
const std::string ALT =
"(`Left Alt` & !(`Left Shift`| `Right Shift`) & !(`Left Control` | `Right Control`))";
const std::string SHIFT =
"(!`Left Alt` & (`Left Shift`| `Right Shift`) & !(`Left Control` | `Right Control`))";
const std::string CTRL =
"(!`Left Alt` & !(`Left Shift`| `Right Shift`) & (`Left Control` | `Right Control`))";
#else
const std::string NON = "(!`Alt_L` & !(`Shift_L` | `Shift_R`) & !(`Control_L` | `Control_R` ))";
const std::string ALT = "(`Alt_L` & !(`Shift_L` | `Shift_R`) & !(`Control_L` | `Control_R` ))";
const std::string SHIFT = "(!`Alt_L` & (`Shift_L` | `Shift_R`) & !(`Control_L` | `Control_R` ))";
const std::string CTRL = "(!`Alt_L` & !(`Shift_L` | `Shift_R`) & (`Control_L` | `Control_R` ))";
#endif
auto set_key_expression = [this](int index, const std::string& expression) { auto set_key_expression = [this](int index, const std::string& expression) {
m_keys[FindGroupByID(index)] m_keys[FindGroupByID(index)]
->controls[GetIndexForGroup(FindGroupByID(index), index)] ->controls[GetIndexForGroup(FindGroupByID(index), index)]
->control_ref->SetExpression(expression); ->control_ref->SetExpression(expression);
}; };
auto hotkey_string = [](std::vector<std::string_view> inputs) {
std::string result;
for (auto& input : inputs)
{
if (!result.empty())
result += '+';
result += input;
}
return "@(" + result + ')';
};
// General hotkeys // General hotkeys
set_key_expression(HK_OPEN, CTRL + " & O"); set_key_expression(HK_OPEN, hotkey_string({"Ctrl", "O"}));
set_key_expression(HK_PLAY_PAUSE, NON + " & `F10`"); set_key_expression(HK_PLAY_PAUSE, "F10");
#ifdef _WIN32 #ifdef _WIN32
set_key_expression(HK_STOP, NON + " & ESCAPE"); set_key_expression(HK_STOP, "ESCAPE");
set_key_expression(HK_FULLSCREEN, ALT + " & RETURN"); set_key_expression(HK_FULLSCREEN, hotkey_string({"Alt", "RETURN"}));
#else #else
set_key_expression(HK_STOP, NON + " & Escape"); set_key_expression(HK_STOP, "Escape");
set_key_expression(HK_FULLSCREEN, ALT + " & Return"); set_key_expression(HK_FULLSCREEN, hotkey_string({"Alt", "Return"}));
#endif #endif
set_key_expression(HK_STEP, NON + " & `F11`"); set_key_expression(HK_STEP, "F11");
set_key_expression(HK_STEP_OVER, SHIFT + " & `F10`"); set_key_expression(HK_STEP_OVER, hotkey_string({"Shift", "F10"}));
set_key_expression(HK_STEP_OUT, SHIFT + " & `F11`"); set_key_expression(HK_STEP_OUT, hotkey_string({"Shift", "F11"}));
set_key_expression(HK_BP_TOGGLE, SHIFT + " & `F9`"); set_key_expression(HK_BP_TOGGLE, hotkey_string({"Shift", "F9"}));
set_key_expression(HK_SCREENSHOT, NON + " & `F9`"); set_key_expression(HK_SCREENSHOT, "F9");
set_key_expression(HK_WIIMOTE1_CONNECT, ALT + " & `F5`"); set_key_expression(HK_WIIMOTE1_CONNECT, hotkey_string({"Alt", "F5"}));
set_key_expression(HK_WIIMOTE2_CONNECT, ALT + " & `F6`"); set_key_expression(HK_WIIMOTE2_CONNECT, hotkey_string({"Alt", "F6"}));
set_key_expression(HK_WIIMOTE3_CONNECT, ALT + " & `F7`"); set_key_expression(HK_WIIMOTE3_CONNECT, hotkey_string({"Alt", "F7"}));
set_key_expression(HK_WIIMOTE4_CONNECT, ALT + " & `F8`"); set_key_expression(HK_WIIMOTE4_CONNECT, hotkey_string({"Alt", "F8"}));
set_key_expression(HK_BALANCEBOARD_CONNECT, ALT + " & `F9`"); set_key_expression(HK_BALANCEBOARD_CONNECT, hotkey_string({"Alt", "F9"}));
#ifdef _WIN32 #ifdef _WIN32
set_key_expression(HK_TOGGLE_THROTTLE, NON + " & TAB"); set_key_expression(HK_TOGGLE_THROTTLE, "TAB");
#else #else
set_key_expression(HK_TOGGLE_THROTTLE, NON + " & Tab"); set_key_expression(HK_TOGGLE_THROTTLE, "Tab");
#endif #endif
// Freelook // Freelook
set_key_expression(HK_FREELOOK_DECREASE_SPEED, SHIFT + " & `1`"); set_key_expression(HK_FREELOOK_DECREASE_SPEED, hotkey_string({"Shift", "1"}));
set_key_expression(HK_FREELOOK_INCREASE_SPEED, SHIFT + " & `2`"); set_key_expression(HK_FREELOOK_INCREASE_SPEED, hotkey_string({"Shift", "2"}));
set_key_expression(HK_FREELOOK_RESET_SPEED, SHIFT + " & F"); set_key_expression(HK_FREELOOK_RESET_SPEED, hotkey_string({"Shift", "F"}));
set_key_expression(HK_FREELOOK_UP, SHIFT + " & E"); set_key_expression(HK_FREELOOK_UP, hotkey_string({"Shift", "E"}));
set_key_expression(HK_FREELOOK_DOWN, SHIFT + " & Q"); set_key_expression(HK_FREELOOK_DOWN, hotkey_string({"Shift", "Q"}));
set_key_expression(HK_FREELOOK_LEFT, SHIFT + " & A"); set_key_expression(HK_FREELOOK_LEFT, hotkey_string({"Shift", "A"}));
set_key_expression(HK_FREELOOK_RIGHT, SHIFT + " & D"); set_key_expression(HK_FREELOOK_RIGHT, hotkey_string({"Shift", "D"}));
set_key_expression(HK_FREELOOK_ZOOM_IN, SHIFT + " & W"); set_key_expression(HK_FREELOOK_ZOOM_IN, hotkey_string({"Shift", "W"}));
set_key_expression(HK_FREELOOK_ZOOM_OUT, SHIFT + " & S"); set_key_expression(HK_FREELOOK_ZOOM_OUT, hotkey_string({"Shift", "S"}));
set_key_expression(HK_FREELOOK_RESET, SHIFT + " & R"); set_key_expression(HK_FREELOOK_RESET, hotkey_string({"Shift", "R"}));
set_key_expression(HK_FREELOOK_INCREASE_FOV_X, SHIFT + " & `Axis Z+`"); set_key_expression(HK_FREELOOK_INCREASE_FOV_X, hotkey_string({"Shift", "`Axis Z+`"}));
set_key_expression(HK_FREELOOK_DECREASE_FOV_X, SHIFT + " & `Axis Z-`"); set_key_expression(HK_FREELOOK_DECREASE_FOV_X, hotkey_string({"Shift", "`Axis Z-`"}));
set_key_expression(HK_FREELOOK_INCREASE_FOV_Y, SHIFT + " & `Axis Z+`"); set_key_expression(HK_FREELOOK_INCREASE_FOV_Y, hotkey_string({"Shift", "`Axis Z+`"}));
set_key_expression(HK_FREELOOK_DECREASE_FOV_Y, SHIFT + " & `Axis Z-`"); set_key_expression(HK_FREELOOK_DECREASE_FOV_Y, hotkey_string({"Shift", "`Axis Z-`"}));
// Savestates // Savestates
const std::string non_fmt = NON + " & `F{}`";
const std::string shift_fmt = SHIFT + " & `F{}`";
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
{ {
set_key_expression(HK_LOAD_STATE_SLOT_1 + i, fmt::format(non_fmt, i + 1)); set_key_expression(HK_LOAD_STATE_SLOT_1 + i, fmt::format("F%d", i + 1));
set_key_expression(HK_SAVE_STATE_SLOT_1 + i, fmt::format(shift_fmt, i + 1)); set_key_expression(HK_SAVE_STATE_SLOT_1 + i,
hotkey_string({"Shift", fmt::format("F%d", i + 1)}));
} }
set_key_expression(HK_UNDO_LOAD_STATE, NON + " & `F12`"); set_key_expression(HK_UNDO_LOAD_STATE, "F12");
set_key_expression(HK_UNDO_SAVE_STATE, SHIFT + " & `F12`"); set_key_expression(HK_UNDO_SAVE_STATE, hotkey_string({"Shift", "F12"}));
} }

View File

@ -5,6 +5,7 @@
#include "DolphinQt/Config/Mapping/MappingCommon.h" #include "DolphinQt/Config/Mapping/MappingCommon.h"
#include <tuple> #include <tuple>
#include <vector>
#include <QApplication> #include <QApplication>
#include <QPushButton> #include <QPushButton>
@ -14,14 +15,23 @@
#include "DolphinQt/QtUtils/BlockUserInputFilter.h" #include "DolphinQt/QtUtils/BlockUserInputFilter.h"
#include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerInterface/Device.h"
#include "Common/Thread.h" #include "Common/Thread.h"
namespace MappingCommon namespace MappingCommon
{ {
constexpr int INPUT_DETECT_TIME = 3000; constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3);
constexpr int OUTPUT_TEST_TIME = 2000; constexpr auto INPUT_DETECT_CONFIRMATION_TIME = std::chrono::milliseconds(500);
constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5);
constexpr auto OUTPUT_TEST_TIME = std::chrono::seconds(2);
// Pressing inputs at the same time will result in the & operator vs a hotkey expression.
constexpr auto HOTKEY_VS_CONJUNCION_THRESHOLD = std::chrono::milliseconds(50);
// Some devices (e.g. DS4) provide an analog and digital input for the trigger.
// We prefer just the analog input for simultaneous digital+analog input detections.
constexpr auto SPURIOUS_TRIGGER_COMBO_THRESHOLD = std::chrono::milliseconds(150);
QString GetExpressionForControl(const QString& control_name, QString GetExpressionForControl(const QString& control_name,
const ciface::Core::DeviceQualifier& control_device, const ciface::Core::DeviceQualifier& control_device,
@ -68,7 +78,11 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev
// Avoid that the button press itself is registered as an event // Avoid that the button press itself is registered as an event
Common::SleepCurrentThread(50); Common::SleepCurrentThread(50);
const auto [device, input] = device_container.DetectInput(INPUT_DETECT_TIME, device_strings); auto detections =
device_container.DetectInput(device_strings, INPUT_DETECT_INITIAL_TIME,
INPUT_DETECT_CONFIRMATION_TIME, INPUT_DETECT_MAXIMUM_TIME);
RemoveSpuriousTriggerCombinations(&detections);
const auto timer = new QTimer(button); const auto timer = new QTimer(button);
@ -83,14 +97,7 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev
button->setText(old_text); button->setText(old_text);
if (!input) return BuildExpression(detections, default_device, quote);
return {};
ciface::Core::DeviceQualifier device_qualifier;
device_qualifier.FromDevice(device.get());
return MappingCommon::GetExpressionForControl(QString::fromStdString(input->GetName()),
device_qualifier, default_device, quote);
} }
void TestOutput(QPushButton* button, OutputReference* reference) void TestOutput(QPushButton* button, OutputReference* reference)
@ -102,10 +109,103 @@ void TestOutput(QPushButton* button, OutputReference* reference)
QApplication::processEvents(); QApplication::processEvents();
reference->State(1.0); reference->State(1.0);
Common::SleepCurrentThread(OUTPUT_TEST_TIME); std::this_thread::sleep_for(OUTPUT_TEST_TIME);
reference->State(0.0); reference->State(0.0);
button->setText(old_text); button->setText(old_text);
} }
void RemoveSpuriousTriggerCombinations(
std::vector<ciface::Core::DeviceContainer::InputDetection>* detections)
{
const auto is_spurious = [&](auto& detection) {
return std::any_of(detections->begin(), detections->end(), [&](auto& d) {
// This is a suprious digital detection if a "smooth" (analog) detection is temporally near.
return &d != &detection && d.smoothness > 1 &&
abs(d.press_time - detection.press_time) < SPURIOUS_TRIGGER_COMBO_THRESHOLD;
});
};
detections->erase(std::remove_if(detections->begin(), detections->end(), is_spurious),
detections->end());
}
QString
BuildExpression(const std::vector<ciface::Core::DeviceContainer::InputDetection>& detections,
const ciface::Core::DeviceQualifier& default_device, Quote quote)
{
std::vector<const ciface::Core::DeviceContainer::InputDetection*> pressed_inputs;
QStringList alternations;
const auto get_control_expression = [&](auto& detection) {
// Return the parent-most name if there is one for better hotkey strings.
// Detection of L/R_Ctrl will be changed to just Ctrl.
// Users can manually map L_Ctrl if they so desire.
const auto input = (quote == Quote::On) ?
detection.device->GetParentMostInput(detection.input) :
detection.input;
ciface::Core::DeviceQualifier device_qualifier;
device_qualifier.FromDevice(detection.device.get());
return MappingCommon::GetExpressionForControl(QString::fromStdString(input->GetName()),
device_qualifier, default_device, quote);
};
bool new_alternation = false;
const auto handle_press = [&](auto& detection) {
pressed_inputs.emplace_back(&detection);
new_alternation = true;
};
const auto handle_release = [&]() {
if (!new_alternation)
return;
new_alternation = false;
QStringList alternation;
for (auto* input : pressed_inputs)
alternation.push_back(get_control_expression(*input));
const bool is_hotkey = pressed_inputs.size() >= 2 &&
(pressed_inputs[1]->press_time - pressed_inputs[0]->press_time) >
HOTKEY_VS_CONJUNCION_THRESHOLD;
if (is_hotkey)
{
alternations.push_back(QStringLiteral("@(%1)").arg(alternation.join(QLatin1Char('+'))));
}
else
{
alternation.sort();
alternations.push_back(alternation.join(QLatin1Char('&')));
}
};
for (auto& detection : detections)
{
// Remove since released inputs.
for (auto it = pressed_inputs.begin(); it != pressed_inputs.end();)
{
if (!((*it)->release_time > detection.press_time))
{
handle_release();
it = pressed_inputs.erase(it);
}
else
++it;
}
handle_press(detection);
}
handle_release();
alternations.removeDuplicates();
return alternations.join(QLatin1Char('|'));
}
} // namespace MappingCommon } // namespace MappingCommon

View File

@ -7,16 +7,12 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "InputCommon/ControllerInterface/Device.h"
class QString; class QString;
class OutputReference; class OutputReference;
class QPushButton; class QPushButton;
namespace ciface::Core
{
class DeviceContainer;
class DeviceQualifier;
} // namespace ciface::Core
namespace MappingCommon namespace MappingCommon
{ {
enum class Quote enum class Quote
@ -37,4 +33,9 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev
void TestOutput(QPushButton* button, OutputReference* reference); void TestOutput(QPushButton* button, OutputReference* reference);
void RemoveSpuriousTriggerCombinations(std::vector<ciface::Core::DeviceContainer::InputDetection>*);
QString BuildExpression(const std::vector<ciface::Core::DeviceContainer::InputDetection>&,
const ciface::Core::DeviceQualifier& default_device, Quote quote);
} // namespace MappingCommon } // namespace MappingCommon

View File

@ -2,9 +2,11 @@
// Licensed under GPLv2+ // Licensed under GPLv2+
// Refer to the license.txt file included. // Refer to the license.txt file included.
#include <algorithm>
#include <cassert> #include <cassert>
#include <cmath> #include <cmath>
#include <iostream> #include <iostream>
#include <map>
#include <memory> #include <memory>
#include <regex> #include <regex>
#include <string> #include <string>
@ -21,6 +23,55 @@ namespace ciface::ExpressionParser
{ {
using namespace ciface::Core; using namespace ciface::Core;
class ControlExpression;
class HotkeySuppressions
{
public:
using Modifiers = std::vector<std::unique_ptr<ControlExpression>>;
struct InvokingDeleter
{
template <typename T>
void operator()(T* func)
{
(*func)();
}
};
using Suppressor = std::unique_ptr<std::function<void()>, InvokingDeleter>;
bool IsSuppressed(Device::Input* input) const
{
// Input is suppressed if it exists in the map at all.
return m_suppressions.lower_bound({input, nullptr}) !=
m_suppressions.lower_bound({input + 1, nullptr});
}
bool IsSuppressedIgnoringModifiers(Device::Input* input, const Modifiers& ignore_modifiers) const;
// Suppresses each input + modifier pair.
// The returned object removes the suppression on destruction.
Suppressor MakeSuppressor(const Modifiers* modifiers,
const std::unique_ptr<ControlExpression>* final_input);
private:
using Suppression = std::pair<Device::Input*, Device::Input*>;
using SuppressionLevel = u16;
void RemoveSuppression(Device::Input* modifier, Device::Input* final_input)
{
auto it = m_suppressions.find({final_input, modifier});
if ((--it->second) == 0)
m_suppressions.erase(it);
}
// Holds counts of suppressions for each input/modifier pair.
std::map<Suppression, SuppressionLevel> m_suppressions;
};
static HotkeySuppressions s_hotkey_suppressions;
Token::Token(TokenType type_) : type(type_) Token::Token(TokenType type_) : type(type_)
{ {
} }
@ -112,6 +163,8 @@ Token Lexer::NextToken()
return Token(TOK_LPAREN); return Token(TOK_LPAREN);
case ')': case ')':
return Token(TOK_RPAREN); return Token(TOK_RPAREN);
case '@':
return Token(TOK_HOTKEY);
case '&': case '&':
return Token(TOK_AND); return Token(TOK_AND);
case '|': case '|':
@ -196,7 +249,16 @@ public:
std::shared_ptr<Device> m_device; std::shared_ptr<Device> m_device;
explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {} explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {}
ControlState GetValue() const override ControlState GetValue() const override
{
if (s_hotkey_suppressions.IsSuppressed(input))
return 0;
else
return GetValueIgnoringSuppression();
}
ControlState GetValueIgnoringSuppression() const
{ {
if (!input) if (!input)
return 0.0; return 0.0;
@ -222,12 +284,46 @@ public:
output = env.FindOutput(qualifier); output = env.FindOutput(qualifier);
} }
Device::Input* GetInput() const { return input; };
private: private:
ControlQualifier qualifier; ControlQualifier qualifier;
Device::Input* input = nullptr; Device::Input* input = nullptr;
Device::Output* output = nullptr; Device::Output* output = nullptr;
}; };
bool HotkeySuppressions::IsSuppressedIgnoringModifiers(Device::Input* input,
const Modifiers& ignore_modifiers) const
{
// Input is suppressed if it exists in the map with a modifier that we aren't ignoring.
auto it = m_suppressions.lower_bound({input, nullptr});
auto it_end = m_suppressions.lower_bound({input + 1, nullptr});
// We need to ignore L_Ctrl R_Ctrl when supplied Ctrl and vice-versa.
const auto is_same_modifier = [](Device::Input* i1, Device::Input* i2) {
return i1 == i2 || i1->IsChild(i2) || i2->IsChild(i1);
};
return std::any_of(it, it_end, [&](auto& s) {
return std::none_of(begin(ignore_modifiers), end(ignore_modifiers),
[&](auto& m) { return is_same_modifier(m->GetInput(), s.first.second); });
});
}
HotkeySuppressions::Suppressor
HotkeySuppressions::MakeSuppressor(const Modifiers* modifiers,
const std::unique_ptr<ControlExpression>* final_input)
{
for (auto& modifier : *modifiers)
++m_suppressions[{(*final_input)->GetInput(), modifier->GetInput()}];
return Suppressor(std::make_unique<std::function<void()>>([this, modifiers, final_input]() {
for (auto& modifier : *modifiers)
RemoveSuppression(modifier->GetInput(), (*final_input)->GetInput());
}).release(),
InvokingDeleter{});
}
class BinaryExpression : public Expression class BinaryExpression : public Expression
{ {
public: public:
@ -374,6 +470,90 @@ protected:
ControlState* m_value_ptr{}; ControlState* m_value_ptr{};
}; };
class HotkeyExpression : public Expression
{
public:
HotkeyExpression(std::vector<std::unique_ptr<ControlExpression>> inputs)
: m_modifiers(std::move(inputs))
{
m_final_input = std::move(m_modifiers.back());
m_modifiers.pop_back();
}
ControlState GetValue() const override
{
const bool modifiers_pressed = std::all_of(m_modifiers.begin(), m_modifiers.end(),
[](const std::unique_ptr<ControlExpression>& input) {
return input->GetValue() > CONDITION_THRESHOLD;
});
const auto final_input_state = m_final_input->GetValueIgnoringSuppression();
if (modifiers_pressed)
{
// Ignore suppression of our own modifiers. This also allows superset modifiers to function.
const bool is_suppressed = s_hotkey_suppressions.IsSuppressedIgnoringModifiers(
m_final_input->GetInput(), m_modifiers);
if (final_input_state < CONDITION_THRESHOLD)
m_is_blocked = false;
// If some other hotkey suppressed us, require a release of final input to be ready again.
if (is_suppressed)
m_is_blocked = true;
if (m_is_blocked)
return 0;
EnableSuppression();
// Our modifiers are active. Pass through the final input.
return final_input_state;
}
else
{
m_suppressor = {};
m_is_blocked = final_input_state > CONDITION_THRESHOLD;
}
return 0;
}
void SetValue(ControlState) override {}
int CountNumControls() const override
{
int result = 0;
for (auto& input : m_modifiers)
result += input->CountNumControls();
return result + m_final_input->CountNumControls();
}
void UpdateReferences(ControlEnvironment& env) override
{
for (auto& input : m_modifiers)
input->UpdateReferences(env);
m_final_input->UpdateReferences(env);
// We must update our suppression with valid pointers.
if (m_suppressor)
EnableSuppression();
}
private:
void EnableSuppression() const
{
if (!m_suppressor)
m_suppressor = s_hotkey_suppressions.MakeSuppressor(&m_modifiers, &m_final_input);
}
HotkeySuppressions::Modifiers m_modifiers;
std::unique_ptr<ControlExpression> m_final_input;
mutable HotkeySuppressions::Suppressor m_suppressor;
mutable bool m_is_blocked = false;
};
// This class proxies all methods to its either left-hand child if it has bound controls, or its // This class proxies all methods to its either left-hand child if it has bound controls, or its
// right-hand child. Its intended use is for supporting old-style barewords expressions. // right-hand child. Its intended use is for supporting old-style barewords expressions.
class CoalesceExpression : public Expression class CoalesceExpression : public Expression
@ -600,6 +780,10 @@ private:
{ {
return ParseParens(); return ParseParens();
} }
case TOK_HOTKEY:
{
return ParseHotkeys();
}
case TOK_SUB: case TOK_SUB:
{ {
// An atom was expected but we got a subtraction symbol. // An atom was expected but we got a subtraction symbol.
@ -684,6 +868,39 @@ private:
return result; return result;
} }
ParseResult ParseHotkeys()
{
Token tok = Chew();
if (tok.type != TOK_LPAREN)
return ParseResult::MakeErrorResult(tok, _trans("Expected opening paren."));
std::vector<std::unique_ptr<ControlExpression>> inputs;
while (true)
{
tok = Chew();
if (tok.type != TOK_CONTROL && tok.type != TOK_BAREWORD)
return ParseResult::MakeErrorResult(tok, _trans("Expected name of input."));
ControlQualifier cq;
cq.FromString(tok.data);
inputs.emplace_back(std::make_unique<ControlExpression>(std::move(cq)));
tok = Chew();
if (tok.type == TOK_ADD)
continue;
if (tok.type == TOK_RPAREN)
break;
return ParseResult::MakeErrorResult(tok, _trans("Expected + or closing paren."));
}
return ParseResult::MakeSuccessfulResult(std::make_unique<HotkeyExpression>(std::move(inputs)));
}
ParseResult ParseToplevel() { return ParseBinary(); } ParseResult ParseToplevel() { return ParseBinary(); }
}; // namespace ExpressionParser }; // namespace ExpressionParser

View File

@ -26,6 +26,7 @@ enum TokenType
TOK_VARIABLE, TOK_VARIABLE,
TOK_BAREWORD, TOK_BAREWORD,
TOK_COMMENT, TOK_COMMENT,
TOK_HOTKEY,
// Binary Ops: // Binary Ops:
TOK_BINARY_OPS_BEGIN, TOK_BINARY_OPS_BEGIN,
TOK_AND = TOK_BINARY_OPS_BEGIN, TOK_AND = TOK_BINARY_OPS_BEGIN,

View File

@ -87,6 +87,11 @@ KeyboardMouse::KeyboardMouse(const LPDIRECTINPUTDEVICE8 kb_device,
for (u8 i = 0; i < sizeof(named_keys) / sizeof(*named_keys); ++i) for (u8 i = 0; i < sizeof(named_keys) / sizeof(*named_keys); ++i)
AddInput(new Key(i, m_state_in.keyboard[named_keys[i].code])); AddInput(new Key(i, m_state_in.keyboard[named_keys[i].code]));
// Add combined left/right modifiers with consistent naming across platforms.
AddCombinedInput("Alt", {"LMENU", "RMENU"});
AddCombinedInput("Shift", {"LSHIFT", "RSHIFT"});
AddCombinedInput("Ctrl", {"LCONTROL", "RCONTROL"});
// MOUSE // MOUSE
DIDEVCAPS mouse_caps = {}; DIDEVCAPS mouse_caps = {};
mouse_caps.dwSize = sizeof(mouse_caps); mouse_caps.dwSize = sizeof(mouse_caps);

View File

@ -13,13 +13,47 @@
#include <fmt/format.h> #include <fmt/format.h>
#include "Common/MathUtil.h"
#include "Common/Thread.h" #include "Common/Thread.h"
namespace ciface::Core namespace ciface::Core
{ {
// Compared to an input's current state (ideally 1.0) minus abs(initial_state) (ideally 0.0). // Compared to an input's current state (ideally 1.0) minus abs(initial_state) (ideally 0.0).
// Note: Detect() logic assumes this is greater than 0.5.
constexpr ControlState INPUT_DETECT_THRESHOLD = 0.55; constexpr ControlState INPUT_DETECT_THRESHOLD = 0.55;
class CombinedInput final : public Device::Input
{
public:
using Inputs = std::pair<Device::Input*, Device::Input*>;
CombinedInput(std::string name, const Inputs& inputs) : m_name(std::move(name)), m_inputs(inputs)
{
}
ControlState GetState() const override
{
ControlState result = 0;
if (m_inputs.first)
result = m_inputs.first->GetState();
if (m_inputs.second)
result = std::max(result, m_inputs.second->GetState());
return result;
}
std::string GetName() const override { return m_name; }
bool IsDetectable() const override { return false; }
bool IsChild(const Input* input) const override
{
return m_inputs.first == input || m_inputs.second == input;
}
private:
const std::string m_name;
const std::pair<Device::Input*, Device::Input*> m_inputs;
};
Device::~Device() Device::~Device()
{ {
// delete inputs // delete inputs
@ -51,6 +85,20 @@ std::string Device::GetQualifiedName() const
return fmt::format("{}/{}/{}", GetSource(), GetId(), GetName()); return fmt::format("{}/{}/{}", GetSource(), GetId(), GetName());
} }
auto Device::GetParentMostInput(Input* child) const -> Input*
{
for (auto* input : m_inputs)
{
if (input->IsChild(child))
{
// Running recursively is currently unnecessary but it doesn't hurt.
return GetParentMostInput(input);
}
}
return child;
}
Device::Input* Device::FindInput(std::string_view name) const Device::Input* Device::FindInput(std::string_view name) const
{ {
for (Input* input : m_inputs) for (Input* input : m_inputs)
@ -102,6 +150,11 @@ bool Device::FullAnalogSurface::IsMatchingName(std::string_view name) const
return old_name == name; return old_name == name;
} }
void Device::AddCombinedInput(std::string name, const std::pair<std::string, std::string>& inputs)
{
AddInput(new CombinedInput(std::move(name), {FindInput(inputs.first), FindInput(inputs.second)}));
}
// //
// DeviceQualifier :: ToString // DeviceQualifier :: ToString
// //
@ -249,18 +302,54 @@ bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const
return device != nullptr && device->IsValid(); return device != nullptr && device->IsValid();
} }
// Wait for input on a particular device. // Wait for inputs on supplied devices.
// Inputs are considered if they are first seen in a neutral state. // Inputs are only considered if they are first seen in a neutral state.
// This is useful for crazy flightsticks that have certain buttons that are always held down // This is useful for crazy flightsticks that have certain buttons that are always held down
// and also properly handles detection when using "FullAnalogSurface" inputs. // and also properly handles detection when using "FullAnalogSurface" inputs.
// Upon input, return the detected Device and Input, else return nullptrs // Multiple detections are returned until the various timeouts have been reached.
std::pair<std::shared_ptr<Device>, Device::Input*> auto DeviceContainer::DetectInput(const std::vector<std::string>& device_strings,
DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device_strings) const std::chrono::milliseconds initial_wait,
std::chrono::milliseconds confirmation_wait,
std::chrono::milliseconds maximum_wait) const
-> std::vector<InputDetection>
{ {
struct InputState struct InputState
{ {
ciface::Core::Device::Input& input; InputState(ciface::Core::Device::Input* input_) : input{input_} { stats.Push(0.0); }
ControlState initial_state;
ciface::Core::Device::Input* input;
ControlState initial_state = input->GetState();
ControlState last_state = initial_state;
MathUtil::RunningVariance<ControlState> stats;
// Prevent multiiple detections until after release.
bool is_ready = true;
void Update()
{
const auto new_state = input->GetState();
if (!is_ready && new_state < (1 - INPUT_DETECT_THRESHOLD))
{
last_state = new_state;
is_ready = true;
stats.Clear();
}
const auto difference = new_state - last_state;
stats.Push(difference);
last_state = new_state;
}
bool IsPressed()
{
if (!is_ready)
return false;
// We want an input that was initially 0.0 and currently 1.0.
const auto detection_score = (last_state - std::abs(initial_state));
return detection_score > INPUT_DETECT_THRESHOLD;
}
}; };
struct DeviceState struct DeviceState
@ -285,13 +374,13 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device
for (auto* input : device->Inputs()) for (auto* input : device->Inputs())
{ {
// Don't detect things like absolute cursor position. // Don't detect things like absolute cursor positions, accelerometers, or gyroscopes.
if (!input->IsDetectable()) if (!input->IsDetectable())
continue; continue;
// Undesirable axes will have negative values here when trying to map a // Undesirable axes will have negative values here when trying to map a
// "FullAnalogSurface". // "FullAnalogSurface".
input_states.push_back({*input, input->GetState()}); input_states.push_back(InputState{input});
} }
if (!input_states.empty()) if (!input_states.empty())
@ -301,27 +390,59 @@ DeviceContainer::DetectInput(u32 wait_ms, const std::vector<std::string>& device
if (device_states.empty()) if (device_states.empty())
return {}; return {};
u32 time = 0; std::vector<InputDetection> detections;
while (time < wait_ms)
const auto start_time = Clock::now();
while (true)
{ {
const auto now = Clock::now();
const auto elapsed_time = now - start_time;
if (elapsed_time >= maximum_wait || (detections.empty() && elapsed_time >= initial_wait) ||
(!detections.empty() && detections.back().release_time.has_value() &&
now >= *detections.back().release_time + confirmation_wait))
{
break;
}
Common::SleepCurrentThread(10); Common::SleepCurrentThread(10);
time += 10;
for (auto& device_state : device_states) for (auto& device_state : device_states)
{ {
for (auto& input_state : device_state.input_states) for (std::size_t i = 0; i != device_state.input_states.size(); ++i)
{ {
// We want an input that was initially 0.0 and currently 1.0. auto& input_state = device_state.input_states[i];
const auto detection_score = input_state.Update();
(input_state.input.GetState() - std::abs(input_state.initial_state));
if (detection_score > INPUT_DETECT_THRESHOLD) if (input_state.IsPressed())
return {device_state.device, &input_state.input}; {
input_state.is_ready = false;
// Digital presses will evaluate as 1 here.
// Analog presses will evaluate greater than 1.
const auto smoothness =
1 / std::sqrt(input_state.stats.Variance() / input_state.stats.Mean());
InputDetection new_detection;
new_detection.device = device_state.device;
new_detection.input = input_state.input;
new_detection.press_time = Clock::now();
new_detection.smoothness = smoothness;
// We found an input. Add it to our detections.
detections.emplace_back(std::move(new_detection));
} }
} }
} }
// No input was detected. :'( // Check for any releases of our detected inputs.
return {}; for (auto& d : detections)
{
if (!d.release_time.has_value() && d.input->GetState() < (1 - INPUT_DETECT_THRESHOLD))
d.release_time = Clock::now();
}
}
return detections;
} }
} // namespace ciface::Core } // namespace ciface::Core

View File

@ -4,6 +4,7 @@
#pragma once #pragma once
#include <chrono>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
@ -85,6 +86,11 @@ public:
virtual ControlState GetState() const = 0; virtual ControlState GetState() const = 0;
Input* ToInput() override { return this; } Input* ToInput() override { return this; }
// Overridden by CombinedInput,
// so hotkey logic knows Ctrl, L_Ctrl, and R_Ctrl are the same,
// and so input detection can return the parent name.
virtual bool IsChild(const Input*) const { return false; }
}; };
// //
@ -119,6 +125,8 @@ public:
const std::vector<Input*>& Inputs() const { return m_inputs; } const std::vector<Input*>& Inputs() const { return m_inputs; }
const std::vector<Output*>& Outputs() const { return m_outputs; } const std::vector<Output*>& Outputs() const { return m_outputs; }
Input* GetParentMostInput(Input* input) const;
Input* FindInput(std::string_view name) const; Input* FindInput(std::string_view name) const;
Output* FindOutput(std::string_view name) const; Output* FindOutput(std::string_view name) const;
@ -147,6 +155,8 @@ protected:
AddInput(new FullAnalogSurface(high, low)); AddInput(new FullAnalogSurface(high, low));
} }
void AddCombinedInput(std::string name, const std::pair<std::string, std::string>& inputs);
private: private:
int m_id; int m_id;
std::vector<Input*> m_inputs; std::vector<Input*> m_inputs;
@ -185,6 +195,17 @@ public:
class DeviceContainer class DeviceContainer
{ {
public: public:
using Clock = std::chrono::steady_clock;
struct InputDetection
{
std::shared_ptr<Device> device;
Device::Input* input;
Clock::time_point press_time;
std::optional<Clock::time_point> release_time;
ControlState smoothness;
};
Device::Input* FindInput(std::string_view name, const Device* def_dev) const; Device::Input* FindInput(std::string_view name, const Device* def_dev) const;
Device::Output* FindOutput(std::string_view name, const Device* def_dev) const; Device::Output* FindOutput(std::string_view name, const Device* def_dev) const;
@ -194,8 +215,10 @@ public:
bool HasConnectedDevice(const DeviceQualifier& qualifier) const; bool HasConnectedDevice(const DeviceQualifier& qualifier) const;
std::pair<std::shared_ptr<Device>, Device::Input*> std::vector<InputDetection> DetectInput(const std::vector<std::string>& device_strings,
DetectInput(u32 wait_ms, const std::vector<std::string>& device_strings) const; std::chrono::milliseconds initial_wait,
std::chrono::milliseconds confirmation_wait,
std::chrono::milliseconds maximum_wait) const;
protected: protected:
mutable std::recursive_mutex m_devices_mutex; mutable std::recursive_mutex m_devices_mutex;

View File

@ -143,6 +143,11 @@ KeyboardAndMouse::KeyboardAndMouse(void* window)
for (int keycode = 0; keycode < 0x80; ++keycode) for (int keycode = 0; keycode < 0x80; ++keycode)
AddInput(new Key(keycode)); AddInput(new Key(keycode));
// Add combined left/right modifiers with consistent naming across platforms.
AddCombinedInput("Alt", {"Left Alt", "Right Alt"});
AddCombinedInput("Shift", {"Left Shift", "Right Shift"});
AddCombinedInput("Ctrl", {"Left Control", "Right Control"});
m_windowid = [[reinterpret_cast<NSView*>(window) window] windowNumber]; m_windowid = [[reinterpret_cast<NSView*>(window) window] windowNumber];
// cursor, with a hax for-loop // cursor, with a hax for-loop

View File

@ -172,6 +172,11 @@ KeyboardMouse::KeyboardMouse(Window window, int opcode, int pointer, int keyboar
delete temp_key; delete temp_key;
} }
// Add combined left/right modifiers with consistent naming across platforms.
AddCombinedInput("Alt", {"Alt_L", "Alt_R"});
AddCombinedInput("Shift", {"Shift_L", "Shift_R"});
AddCombinedInput("Ctrl", {"Control_L", "Control_R"});
// Mouse Buttons // Mouse Buttons
for (int i = 0; i < 32; i++) for (int i = 0; i < 32; i++)
AddInput(new Button(i, &m_state.buttons)); AddInput(new Button(i, &m_state.buttons));