diff --git a/Source/Core/Common/StringUtil.h b/Source/Core/Common/StringUtil.h index f008dc55bb44..56fa756a46b3 100644 --- a/Source/Core/Common/StringUtil.h +++ b/Source/Core/Common/StringUtil.h @@ -235,6 +235,14 @@ inline bool IsPrintableCharacter(char c) return std::isprint(c, std::locale::classic()); } +/// Returns whether a character is a letter, i.e. whether 'a' <= c <= 'z' || 'A' <= c <= 'Z' +/// is true. Use this instead of calling std::isalpha directly to ensure +/// the C locale is being used and to avoid possibly undefined behaviour. +inline bool IsAlpha(char c) +{ + return std::isalpha(c, std::locale::classic()); +} + #ifdef _WIN32 std::vector CommandLineToUtf8Argv(const wchar_t* command_line); #endif diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 79557b58c73e..208ad54df779 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -482,6 +482,7 @@ + @@ -1069,6 +1070,7 @@ + diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp index 6e178a2ed8ea..50492e076747 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp @@ -34,6 +34,7 @@ #include "InputCommon/ControlReference/ExpressionParser.h" #include "InputCommon/ControllerEmu/ControllerEmu.h" #include "InputCommon/ControllerInterface/ControllerInterface.h" +#include "InputCommon/ControllerInterface/MappingCommon.h" constexpr int SLIDER_TICK_COUNT = 100; @@ -485,9 +486,10 @@ void IOWindow::AppendSelectedOption() if (m_option_list->currentRow() < 0) return; - m_expression_text->insertPlainText(MappingCommon::GetExpressionForControl( - m_option_list->item(m_option_list->currentRow(), 0)->text(), m_devq, - m_controller->GetDefaultDevice())); + m_expression_text->insertPlainText( + QString::fromStdString(ciface::MappingCommon::GetExpressionForControl( + m_option_list->item(m_option_list->currentRow(), 0)->text().toStdString(), m_devq, + m_controller->GetDefaultDevice()))); } void IOWindow::OnDeviceChanged() @@ -526,7 +528,7 @@ void IOWindow::OnDetectButtonPressed() { const auto expression = MappingCommon::DetectExpression(m_detect_button, g_controller_interface, {m_devq.ToString()}, - m_devq, MappingCommon::Quote::Off); + m_devq, ciface::MappingCommon::Quote::Off); if (expression.isEmpty()) return; diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp index fde917c2b168..0fdcb376af54 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp @@ -3,8 +3,7 @@ #include "DolphinQt/Config/Mapping/MappingCommon.h" -#include -#include +#include #include #include @@ -14,6 +13,7 @@ #include "DolphinQt/QtUtils/BlockUserInputFilter.h" #include "InputCommon/ControlReference/ControlReference.h" +#include "InputCommon/ControllerInterface/MappingCommon.h" #include "Common/Thread.h" @@ -25,44 +25,10 @@ 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, - const ciface::Core::DeviceQualifier& default_device, Quote quote) -{ - QString expr; - - // non-default device - if (control_device != default_device) - { - expr += QString::fromStdString(control_device.ToString()); - expr += QLatin1Char{':'}; - } - - // append the control name - expr += control_name; - - if (quote == Quote::On) - { - // If our expression contains any non-alpha characters - // we should quote it - const QRegularExpression reg(QStringLiteral("[^a-zA-Z]")); - if (reg.match(expr).hasMatch()) - expr = QStringLiteral("`%1`").arg(expr); - } - - return expr; -} - QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& device_container, const std::vector& device_strings, - const ciface::Core::DeviceQualifier& default_device, Quote quote) + const ciface::Core::DeviceQualifier& default_device, + ciface::MappingCommon::Quote quote) { const auto filter = new BlockUserInputFilter(button); @@ -83,7 +49,7 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev device_container.DetectInput(device_strings, INPUT_DETECT_INITIAL_TIME, INPUT_DETECT_CONFIRMATION_TIME, INPUT_DETECT_MAXIMUM_TIME); - RemoveSpuriousTriggerCombinations(&detections); + ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections); const auto timer = new QTimer(button); @@ -100,7 +66,7 @@ QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& dev button->setText(old_text); - return BuildExpression(detections, default_device, quote); + return QString::fromStdString(BuildExpression(detections, default_device, quote)); } void TestOutput(QPushButton* button, OutputReference* reference) @@ -118,97 +84,4 @@ void TestOutput(QPushButton* button, OutputReference* reference) 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 aa96ad76d7f7..f1385ebb6bee 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h @@ -7,6 +7,7 @@ #include #include "InputCommon/ControllerInterface/CoreDevice.h" +#include "InputCommon/ControllerInterface/MappingCommon.h" class QString; class OutputReference; @@ -14,27 +15,11 @@ class QPushButton; namespace MappingCommon { -enum class Quote -{ - On, - Off -}; - -QString GetExpressionForControl(const QString& control_name, - const ciface::Core::DeviceQualifier& control_device, - const ciface::Core::DeviceQualifier& default_device, - Quote quote = Quote::On); - QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& device_container, const std::vector& device_strings, const ciface::Core::DeviceQualifier& default_device, - Quote quote = Quote::On); + ciface::MappingCommon::Quote quote = ciface::MappingCommon::Quote::On); 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/CMakeLists.txt b/Source/Core/InputCommon/CMakeLists.txt index 576e87e71117..e600004cdacc 100644 --- a/Source/Core/InputCommon/CMakeLists.txt +++ b/Source/Core/InputCommon/CMakeLists.txt @@ -54,6 +54,8 @@ add_library(inputcommon ControllerInterface/ControllerInterface.h ControllerInterface/CoreDevice.cpp ControllerInterface/CoreDevice.h + ControllerInterface/MappingCommon.cpp + ControllerInterface/MappingCommon.h ControllerInterface/Wiimote/WiimoteController.cpp ControllerInterface/Wiimote/WiimoteController.h ControlReference/ControlReference.cpp diff --git a/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp b/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp new file mode 100644 index 000000000000..5adc8daf3503 --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/MappingCommon.cpp @@ -0,0 +1,149 @@ +// Copyright 2022 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "InputCommon/ControllerInterface/MappingCommon.h" + +#include +#include +#include +#include + +#include + +#include "Common/StringUtil.h" +#include "InputCommon/ControllerInterface/CoreDevice.h" + +namespace ciface::MappingCommon +{ +// 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); + +std::string GetExpressionForControl(const std::string& control_name, + const ciface::Core::DeviceQualifier& control_device, + const ciface::Core::DeviceQualifier& default_device, + Quote quote) +{ + std::string expr; + + // non-default device + if (control_device != default_device) + { + expr += control_device.ToString(); + expr += ':'; + } + + // append the control name + expr += control_name; + + if (quote == Quote::On) + { + // If our expression contains any non-alpha characters + // we should quote it + if (!std::all_of(expr.begin(), expr.end(), IsAlpha)) + expr = fmt::format("`{}`", expr); + } + + return expr; +} + +std::string +BuildExpression(const std::vector& detections, + const ciface::Core::DeviceQualifier& default_device, Quote quote) +{ + std::vector pressed_inputs; + + std::vector 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(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; + + std::vector 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(fmt::format("@({})", JoinStrings(alternation, "+"))); + } + else + { + std::sort(alternation.begin(), alternation.end()); + alternations.push_back(JoinStrings(alternation, "&")); + } + }; + + 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(); + + // Remove duplicates + std::sort(alternations.begin(), alternations.end()); + alternations.erase(std::unique(alternations.begin(), alternations.end()), alternations.end()); + + return JoinStrings(alternations, "|"); +} + +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()); +} + +} // namespace ciface::MappingCommon diff --git a/Source/Core/InputCommon/ControllerInterface/MappingCommon.h b/Source/Core/InputCommon/ControllerInterface/MappingCommon.h new file mode 100644 index 000000000000..182212555737 --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/MappingCommon.h @@ -0,0 +1,29 @@ +// Copyright 2022 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "InputCommon/ControllerInterface/CoreDevice.h" + +namespace ciface::MappingCommon +{ +enum class Quote +{ + On, + Off +}; + +std::string GetExpressionForControl(const std::string& control_name, + const ciface::Core::DeviceQualifier& control_device, + const ciface::Core::DeviceQualifier& default_device, + Quote quote = Quote::On); + +std::string BuildExpression(const std::vector&, + const ciface::Core::DeviceQualifier& default_device, Quote quote); + +void RemoveSpuriousTriggerCombinations(std::vector*); + +} // namespace ciface::MappingCommon