diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp index 9c9dc3e3b6..574a16fdb9 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp @@ -4,7 +4,6 @@ #include "DolphinQt/Config/Mapping/IOWindow.h" #include -#include #include #include @@ -20,16 +19,15 @@ #include #include #include +#include #include -#include "Core/Core.h" - #include "DolphinQt/Config/Mapping/MappingCommon.h" #include "DolphinQt/Config/Mapping/MappingIndicator.h" -#include "DolphinQt/Config/Mapping/MappingWidget.h" #include "DolphinQt/Config/Mapping/MappingWindow.h" #include "DolphinQt/QtUtils/BlockUserInputFilter.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" +#include "DolphinQt/QtUtils/SetWindowDecorations.h" #include "DolphinQt/Settings.h" #include "InputCommon/ControlReference/ControlReference.h" @@ -40,6 +38,9 @@ namespace { +constexpr auto INPUT_DETECT_TIME = std::chrono::seconds(2); +constexpr auto OUTPUT_TEST_TIME = std::chrono::seconds(2); + QTextCharFormat GetSpecialCharFormat() { QTextCharFormat format; @@ -228,15 +229,17 @@ private: bool m_should_paint_state_indicator = false; }; -IOWindow::IOWindow(MappingWidget* parent, ControllerEmu::EmulatedController* controller, +IOWindow::IOWindow(MappingWindow* window, ControllerEmu::EmulatedController* controller, ControlReference* ref, IOWindow::Type type) - : QDialog(parent), m_reference(ref), m_original_expression(ref->GetExpression()), + : QDialog(window), m_reference(ref), m_original_expression(ref->GetExpression()), m_controller(controller), m_type(type) { + SetQWidgetWindowDecorations(this); + CreateMainLayout(); - connect(parent, &MappingWidget::Update, this, &IOWindow::Update); - connect(parent->GetParent(), &MappingWindow::ConfigChanged, this, &IOWindow::ConfigChanged); + connect(window, &MappingWindow::Update, this, &IOWindow::Update); + connect(window, &MappingWindow::ConfigChanged, this, &IOWindow::ConfigChanged); connect(&Settings::Instance(), &Settings::ConfigChanged, this, &IOWindow::ConfigChanged); setWindowTitle(type == IOWindow::Type::Input ? tr("Configure Input") : tr("Configure Output")); @@ -258,18 +261,24 @@ void IOWindow::CreateMainLayout() m_devices_combo = new QComboBox(); m_option_list = new QTableWidget(); - m_select_button = new QPushButton(tr("Select")); - m_detect_button = new QPushButton(tr("Detect"), this); - m_test_button = new QPushButton(tr("Test"), this); + + m_select_button = + new QPushButton(m_type == IOWindow::Type::Input ? tr("Insert Input") : tr("Insert Output")); + m_detect_button = new QPushButton(tr("Detect Input"), this); + m_test_button = new QPushButton(tr("Test Output"), this); m_button_box = new QDialogButtonBox(); m_clear_button = new QPushButton(tr("Clear")); m_scalar_spinbox = new QSpinBox(); - m_parse_text = new InputStateLineEdit([this] { - const auto lock = m_controller->GetStateLock(); - return m_reference->GetState(); - }); - m_parse_text->setReadOnly(true); + if (m_type == Type::Input) + { + m_parse_text = new InputStateLineEdit([this] { return m_reference->GetState(); }); + } + else + { + m_parse_text = new InputStateLineEdit( + [this] { return m_output_test_timer->isActive() * m_reference->range; }); + } m_expression_text = new QPlainTextEdit(); m_expression_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); @@ -419,11 +428,17 @@ void IOWindow::CreateMainLayout() m_button_box->addButton(m_clear_button, QDialogButtonBox::ActionRole); m_button_box->addButton(QDialogButtonBox::Ok); + m_output_test_timer = new QTimer(this); + m_output_test_timer->setSingleShot(true); + setLayout(m_main_layout); } void IOWindow::ConfigChanged() { + emit DetectInputComplete(); + emit TestOutputComplete(); + const QSignalBlocker blocker(this); const auto lock = ControllerEmu::EmulatedController::GetStateLock(); @@ -444,6 +459,31 @@ void IOWindow::Update() { m_option_list->viewport()->update(); m_parse_text->update(); + + if (!m_input_detector) + return; + + if (m_input_detector->IsComplete()) + { + const auto results = m_input_detector->TakeResults(); + + emit DetectInputComplete(); + + if (results.empty()) + return; + + // Select the first detected input. + auto list = m_option_list->findItems(QString::fromStdString(results.front().input->GetName()), + Qt::MatchFixedString); + if (list.empty()) + return; + + m_option_list->setCurrentItem(list.front()); + } + else + { + m_input_detector->Update(INPUT_DETECT_TIME, {}, INPUT_DETECT_TIME); + } } void IOWindow::ConnectWidgets() @@ -453,8 +493,50 @@ void IOWindow::ConnectWidgets() connect(&Settings::Instance(), &Settings::ReleaseDevices, this, &IOWindow::ReleaseDevices); connect(&Settings::Instance(), &Settings::DevicesChanged, this, &IOWindow::UpdateDeviceList); - connect(m_detect_button, &QPushButton::clicked, this, &IOWindow::OnDetectButtonPressed); - connect(m_test_button, &QPushButton::clicked, this, &IOWindow::OnTestButtonPressed); + // Input detection: + // Clicking "Detect" button starts a timer before the actual detection. + auto* const input_detect_start_timer = new QTimer(this); + input_detect_start_timer->setSingleShot(true); + connect(m_detect_button, &QPushButton::clicked, [this, input_detect_start_timer] { + m_detect_button->setText(tr("[ ... ]")); + input_detect_start_timer->start(MappingCommon::INPUT_DETECT_INITIAL_DELAY); + }); + connect(input_detect_start_timer, &QTimer::timeout, [this] { + m_detect_button->setText(tr("[ Press Now ]")); + m_input_detector = std::make_unique(); + const auto lock = m_controller->GetStateLock(); + m_input_detector->Start(g_controller_interface, {m_devq.ToString()}); + QtUtils::InstallKeyboardBlocker(m_detect_button, this, &IOWindow::DetectInputComplete); + }); + connect(this, &IOWindow::DetectInputComplete, + [this, initial_text = m_detect_button->text(), input_detect_start_timer] { + input_detect_start_timer->stop(); + m_input_detector.reset(); + m_detect_button->setText(initial_text); + }); + + // Rumble testing: + connect(m_test_button, &QPushButton::clicked, [this] { + // Stop if already started. + if (m_output_test_timer->isActive()) + { + emit IOWindow::TestOutputComplete(); + return; + } + m_test_button->setText(QStringLiteral("[ ... ]")); + m_output_test_timer->start(OUTPUT_TEST_TIME); + const auto lock = m_controller->GetStateLock(); + m_reference->State(1.0); + }); + connect(m_output_test_timer, &QTimer::timeout, + [this, initial_text = m_test_button->text()] { emit TestOutputComplete(); }); + connect(this, &IOWindow::TestOutputComplete, [this, initial_text = m_test_button->text()] { + m_output_test_timer->stop(); + m_test_button->setText(initial_text); + const auto lock = m_controller->GetStateLock(); + m_reference->State(0.0); + }); + connect(this, &QWidget::destroyed, this, &IOWindow::TestOutputComplete); connect(m_button_box, &QDialogButtonBox::clicked, this, &IOWindow::OnDialogButtonPressed); connect(m_devices_combo, &QComboBox::currentTextChanged, this, &IOWindow::OnDeviceChanged); @@ -546,30 +628,10 @@ void IOWindow::OnDialogButtonPressed(QAbstractButton* button) } } -void IOWindow::OnDetectButtonPressed() -{ - const auto expression = - MappingCommon::DetectExpression(m_detect_button, g_controller_interface, {m_devq.ToString()}, - m_devq, ciface::MappingCommon::Quote::Off); - - if (expression.isEmpty()) - return; - - const auto list = m_option_list->findItems(expression, Qt::MatchFixedString); - - // Try to select the first. If this fails, the last selected item would still appear as such - if (!list.empty()) - m_option_list->setCurrentItem(list[0]); -} - -void IOWindow::OnTestButtonPressed() -{ - MappingCommon::TestOutput(m_test_button, static_cast(m_reference)); -} - void IOWindow::OnRangeChanged(int value) { m_reference->range = value / 100.0; + emit TestOutputComplete(); } void IOWindow::ReleaseDevices() @@ -670,6 +732,8 @@ void IOWindow::UpdateDeviceList() void IOWindow::UpdateExpression(std::string new_expression, UpdateMode mode) { + emit TestOutputComplete(); + const auto lock = m_controller->GetStateLock(); if (mode != UpdateMode::Force && new_expression == m_reference->GetExpression()) return; @@ -719,6 +783,7 @@ InputStateDelegate::InputStateDelegate(IOWindow* parent, int column, InputStateLineEdit::InputStateLineEdit(std::function state_evaluator) : m_state_evaluator(std::move(state_evaluator)) { + setReadOnly(true); } static void PaintStateIndicator(QPainter& painter, const QRect& region, ControlState state) diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.h b/Source/Core/DolphinQt/Config/Mapping/IOWindow.h index 1a0c38f96e..0ca2c8436a 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.h +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.h @@ -12,11 +12,10 @@ #include #include -#include "Common/Flag.h" #include "InputCommon/ControllerInterface/CoreDevice.h" class ControlReference; -class MappingWidget; +class MappingWindow; class QAbstractButton; class QDialogButtonBox; class QLineEdit; @@ -66,9 +65,13 @@ public: Output }; - explicit IOWindow(MappingWidget* parent, ControllerEmu::EmulatedController* m_controller, + explicit IOWindow(MappingWindow* window, ControllerEmu::EmulatedController* m_controller, ControlReference* ref, Type type); +signals: + void DetectInputComplete(); + void TestOutputComplete(); + private: std::shared_ptr GetSelectedDevice() const; @@ -79,8 +82,6 @@ private: void OnDialogButtonPressed(QAbstractButton* button); void OnDeviceChanged(); - void OnDetectButtonPressed(); - void OnTestButtonPressed(); void OnRangeChanged(int range); void AppendSelectedOption(); @@ -115,10 +116,12 @@ private: // Input actions QPushButton* m_detect_button; + std::unique_ptr m_input_detector; QComboBox* m_functions_combo; // Output actions QPushButton* m_test_button; + QTimer* m_output_test_timer; // Textarea QPlainTextEdit* m_expression_text; diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp index bfc3d751e4..794ed02997 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp @@ -9,13 +9,12 @@ #include #include "DolphinQt/Config/Mapping/IOWindow.h" -#include "DolphinQt/Config/Mapping/MappingCommon.h" #include "DolphinQt/Config/Mapping/MappingWidget.h" #include "DolphinQt/Config/Mapping/MappingWindow.h" +#include "DolphinQt/QtUtils/BlockUserInputFilter.h" #include "DolphinQt/QtUtils/SetWindowDecorations.h" #include "InputCommon/ControlReference/ControlReference.h" -#include "InputCommon/ControllerEmu/ControlGroup/Buttons.h" #include "InputCommon/ControllerEmu/ControllerEmu.h" #include "InputCommon/ControllerInterface/ControllerInterface.h" @@ -74,7 +73,7 @@ bool MappingButton::IsInput() const } MappingButton::MappingButton(MappingWidget* parent, ControlReference* ref, bool indicator) - : ElidedButton(RefToDisplayString(ref)), m_parent(parent), m_reference(ref) + : ElidedButton(RefToDisplayString(ref)), m_mapping_window(parent->GetParent()), m_reference(ref) { if (IsInput()) { @@ -92,17 +91,22 @@ MappingButton::MappingButton(MappingWidget* parent, ControlReference* ref, bool connect(parent, &MappingWidget::Update, this, &MappingButton::UpdateIndicator); connect(parent, &MappingWidget::ConfigChanged, this, &MappingButton::ConfigChanged); + connect(this, &MappingButton::ConfigChanged, [this] { + setText(RefToDisplayString(m_reference)); + m_is_mapping = false; + }); } void MappingButton::AdvancedPressed() { - IOWindow io(m_parent, m_parent->GetController(), m_reference, + m_mapping_window->CancelMapping(); + + IOWindow io(m_mapping_window, m_mapping_window->GetController(), m_reference, m_reference->IsInput() ? IOWindow::Type::Input : IOWindow::Type::Output); - SetQWidgetWindowDecorations(&io); io.exec(); ConfigChanged(); - m_parent->SaveSettings(); + m_mapping_window->Save(); } void MappingButton::Clicked() @@ -113,31 +117,8 @@ void MappingButton::Clicked() return; } - const auto default_device_qualifier = m_parent->GetController()->GetDefaultDevice(); - - QString expression; - - if (m_parent->GetParent()->IsMappingAllDevices()) - { - expression = MappingCommon::DetectExpression(this, g_controller_interface, - g_controller_interface.GetAllDeviceStrings(), - default_device_qualifier); - } - else - { - expression = MappingCommon::DetectExpression(this, g_controller_interface, - {default_device_qualifier.ToString()}, - default_device_qualifier); - } - - if (expression.isEmpty()) - return; - - m_reference->SetExpression(expression.toStdString()); - m_parent->GetController()->UpdateSingleControlReference(g_controller_interface, m_reference); - - ConfigChanged(); - m_parent->SaveSettings(); + m_is_mapping = true; + m_mapping_window->QueueInputDetection(this); } void MappingButton::Clear() @@ -145,22 +126,21 @@ void MappingButton::Clear() m_reference->range = 100.0 / SLIDER_TICK_COUNT; m_reference->SetExpression(""); - m_parent->GetController()->UpdateSingleControlReference(g_controller_interface, m_reference); + m_mapping_window->GetController()->UpdateSingleControlReference(g_controller_interface, + m_reference); - m_parent->SaveSettings(); - ConfigChanged(); + m_mapping_window->Save(); + + m_mapping_window->UnQueueInputDetection(this); } void MappingButton::UpdateIndicator() { - if (!isActiveWindow()) - return; + QFont f = m_mapping_window->font(); - QFont f = m_parent->font(); - - // If the input state is "true" (we can't know the state of outputs), show it in bold. - if (m_reference->IsInput() && m_reference->GetState()) + if (isActiveWindow() && m_reference->IsInput() && m_reference->GetState() && !m_is_mapping) f.setBold(true); + // If the expression has failed to parse, show it in italic. // Some expressions still work even the failed to parse so don't prevent the GetState() above. if (m_reference->GetParseStatus() == ciface::ExpressionParser::ParseStatus::SyntaxError) @@ -169,9 +149,12 @@ void MappingButton::UpdateIndicator() setFont(f); } -void MappingButton::ConfigChanged() +void MappingButton::StartMapping() { - setText(RefToDisplayString(m_reference)); + // Focus just makes it more clear which button is currently being mapped. + setFocus(); + setText(tr("[ Press Now ]")); + QtUtils::InstallKeyboardBlocker(this, this, &MappingButton::ConfigChanged); } void MappingButton::mouseReleaseEvent(QMouseEvent* event) @@ -189,3 +172,8 @@ void MappingButton::mouseReleaseEvent(QMouseEvent* event) return; } } + +ControlReference* MappingButton::GetControlReference() +{ + return m_reference; +} diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingButton.h b/Source/Core/DolphinQt/Config/Mapping/MappingButton.h index 7606604ec9..11da51da6c 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingButton.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingButton.h @@ -3,11 +3,11 @@ #pragma once -#include "Common/Flag.h" #include "DolphinQt/QtUtils/ElidedButton.h" class ControlReference; class MappingWidget; +class MappingWindow; class QEvent; class QMouseEvent; @@ -18,16 +18,21 @@ public: MappingButton(MappingWidget* widget, ControlReference* ref, bool indicator); bool IsInput() const; + ControlReference* GetControlReference(); + void StartMapping(); + +signals: + void ConfigChanged(); private: void Clear(); void UpdateIndicator(); - void ConfigChanged(); void AdvancedPressed(); void Clicked(); void mouseReleaseEvent(QMouseEvent* event) override; - MappingWidget* m_parent; - ControlReference* m_reference; + MappingWindow* const m_mapping_window; + ControlReference* const m_reference; + bool m_is_mapping = false; }; diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp index 0fdcb376af..edce169c28 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp @@ -3,19 +3,18 @@ #include "DolphinQt/Config/Mapping/MappingCommon.h" -#include +#include +#include -#include -#include -#include -#include #include -#include "DolphinQt/QtUtils/BlockUserInputFilter.h" -#include "InputCommon/ControlReference/ControlReference.h" -#include "InputCommon/ControllerInterface/MappingCommon.h" +#include "DolphinQt/Config/Mapping/MappingButton.h" +#include "DolphinQt/Config/Mapping/MappingWindow.h" -#include "Common/Thread.h" +#include "InputCommon/ControlReference/ControlReference.h" +#include "InputCommon/ControllerEmu/ControllerEmu.h" +#include "InputCommon/ControllerInterface/ControllerInterface.h" +#include "InputCommon/ControllerInterface/MappingCommon.h" namespace MappingCommon { @@ -23,65 +22,128 @@ constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3); constexpr auto INPUT_DETECT_CONFIRMATION_TIME = std::chrono::milliseconds(0); constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5); -constexpr auto OUTPUT_TEST_TIME = std::chrono::seconds(2); - -QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& device_container, - const std::vector& device_strings, - const ciface::Core::DeviceQualifier& default_device, - ciface::MappingCommon::Quote quote) +class MappingProcessor : public QWidget { - const auto filter = new BlockUserInputFilter(button); +public: + MappingProcessor(MappingWindow* parent) : QWidget{parent}, m_parent{parent} + { + using MW = MappingWindow; + using MP = MappingProcessor; - button->installEventFilter(filter); - button->grabKeyboard(); - button->grabMouse(); + connect(parent, &MW::Update, this, &MP::ProcessMappingButtons); + connect(parent, &MW::ConfigChanged, this, &MP::CancelMapping); - const auto old_text = button->text(); - button->setText(QStringLiteral("...")); + connect(parent, &MW::UnQueueInputDetection, this, &MP::UnQueueInputDetection); + connect(parent, &MW::QueueInputDetection, this, &MP::QueueInputDetection); + connect(parent, &MW::CancelMapping, this, &MP::CancelMapping); - // The button text won't be updated if we don't process events here - QApplication::processEvents(); + m_input_detection_start_timer = new QTimer(this); + m_input_detection_start_timer->setSingleShot(true); + connect(m_input_detection_start_timer, &QTimer::timeout, this, &MP::StartInputDetection); + } - // Avoid that the button press itself is registered as an event - Common::SleepCurrentThread(50); + void StartInputDetection() + { + const auto& default_device = m_parent->GetController()->GetDefaultDevice(); + auto& button = m_clicked_mapping_buttons.front(); - auto detections = - device_container.DetectInput(device_strings, INPUT_DETECT_INITIAL_TIME, - INPUT_DETECT_CONFIRMATION_TIME, INPUT_DETECT_MAXIMUM_TIME); + button->StartMapping(); - ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections); + std::vector device_strings{default_device.ToString()}; + if (m_parent->IsMappingAllDevices()) + device_strings = g_controller_interface.GetAllDeviceStrings(); - const auto timer = new QTimer(button); + m_input_detector = std::make_unique(); + const auto lock = m_parent->GetController()->GetStateLock(); + m_input_detector->Start(g_controller_interface, device_strings); + } - timer->setSingleShot(true); + void ProcessMappingButtons() + { + if (!m_input_detector) + return; - button->connect(timer, &QTimer::timeout, [button, filter] { - button->releaseMouse(); - button->releaseKeyboard(); - button->removeEventFilter(filter); - }); + m_input_detector->Update(INPUT_DETECT_INITIAL_TIME, INPUT_DETECT_CONFIRMATION_TIME, + INPUT_DETECT_MAXIMUM_TIME); - // Prevent mappings of "space", "return", or mouse clicks from re-activating detection. - timer->start(500); + if (m_input_detector->IsComplete()) + { + auto detections = m_input_detector->TakeResults(); + ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections); - button->setText(old_text); + // No inputs detected. Cancel this and any other queued mappings. + if (detections.empty()) + { + CancelMapping(); + return; + } - return QString::fromStdString(BuildExpression(detections, default_device, quote)); -} + const auto& default_device = m_parent->GetController()->GetDefaultDevice(); + auto& button = m_clicked_mapping_buttons.front(); + auto* const control_reference = button->GetControlReference(); -void TestOutput(QPushButton* button, OutputReference* reference) + control_reference->SetExpression( + BuildExpression(detections, default_device, ciface::MappingCommon::Quote::On)); + m_parent->Save(); + + m_parent->GetController()->UpdateSingleControlReference(g_controller_interface, + control_reference); + UnQueueInputDetection(button); + } + } + + void UpdateInputDetectionStartTimer() + { + m_input_detector.reset(); + + if (m_clicked_mapping_buttons.empty()) + m_input_detection_start_timer->stop(); + else + m_input_detection_start_timer->start(INPUT_DETECT_INITIAL_DELAY); + } + + void UnQueueInputDetection(MappingButton* button) + { + std::erase(m_clicked_mapping_buttons, button); + button->ConfigChanged(); + UpdateInputDetectionStartTimer(); + } + + void QueueInputDetection(MappingButton* button) + { + // UnQueue if already queued. + if (std::erase(m_clicked_mapping_buttons, button)) + { + button->ConfigChanged(); + UpdateInputDetectionStartTimer(); + return; + } + + button->setText(QStringLiteral("[ ... ]")); + m_clicked_mapping_buttons.push_back(button); + UpdateInputDetectionStartTimer(); + } + + void CancelMapping() + { + // Signal buttons to take on their proper input expression text. + for (auto* button : m_clicked_mapping_buttons) + button->ConfigChanged(); + + m_clicked_mapping_buttons = {}; + UpdateInputDetectionStartTimer(); + } + +private: + std::deque m_clicked_mapping_buttons; + std::unique_ptr m_input_detector; + QTimer* m_input_detection_start_timer; + MappingWindow* const m_parent; +}; + +void CreateMappingProcessor(MappingWindow* window) { - const auto old_text = button->text(); - button->setText(QStringLiteral("...")); - - // The button text won't be updated if we don't process events here - QApplication::processEvents(); - - reference->State(1.0); - std::this_thread::sleep_for(OUTPUT_TEST_TIME); - reference->State(0.0); - - button->setText(old_text); + new MappingProcessor{window}; } } // namespace MappingCommon diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h index f1385ebb6b..fe894d7b42 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingCommon.h @@ -3,23 +3,14 @@ #pragma once -#include -#include +#include -#include "InputCommon/ControllerInterface/CoreDevice.h" -#include "InputCommon/ControllerInterface/MappingCommon.h" - -class QString; -class OutputReference; -class QPushButton; +class MappingWindow; namespace MappingCommon { -QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& device_container, - const std::vector& device_strings, - const ciface::Core::DeviceQualifier& default_device, - ciface::MappingCommon::Quote quote = ciface::MappingCommon::Quote::On); - -void TestOutput(QPushButton* button, OutputReference* reference); +// A slight delay improves behavior when "clicking" the detect button via key-press. +static constexpr auto INPUT_DETECT_INITIAL_DELAY = std::chrono::milliseconds{100}; +void CreateMappingProcessor(MappingWindow*); } // namespace MappingCommon diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp index 96a5040704..082c97314b 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp @@ -20,7 +20,6 @@ #include "DolphinQt/Config/Mapping/MappingWindow.h" #include "DolphinQt/QtUtils/SetWindowDecorations.h" -#include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControllerEmu/Control/Control.h" #include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" #include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h" @@ -338,10 +337,10 @@ MappingWidget::CreateSettingAdvancedMappingButton(ControllerEmu::NumericSettingB setting.SetExpressionFromValue(); // Ensure the UI has the game-controller indicator while editing the expression. + // And cancel in-progress mappings. ConfigChanged(); - IOWindow io(this, GetController(), &setting.GetInputReference(), IOWindow::Type::Input); - SetQWidgetWindowDecorations(&io); + IOWindow io(GetParent(), GetController(), &setting.GetInputReference(), IOWindow::Type::Input); io.exec(); setting.SimplifyIfPossible(); diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.h b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.h index 0ec5227f34..48b7a46190 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.h @@ -3,15 +3,10 @@ #pragma once -#include -#include - #include #include -class ControlGroupBox; class InputConfig; -class MappingButton; class MappingNumeric; class MappingWindow; class QFormLayout; diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp index a41920e2c1..c1b1d1d880 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp @@ -15,7 +15,6 @@ #include #include -#include "Core/Core.h" #include "Core/HotkeyManager.h" #include "Common/CommonPaths.h" @@ -41,6 +40,7 @@ #include "DolphinQt/Config/Mapping/HotkeyTAS.h" #include "DolphinQt/Config/Mapping/HotkeyUSBEmu.h" #include "DolphinQt/Config/Mapping/HotkeyWii.h" +#include "DolphinQt/Config/Mapping/MappingCommon.h" #include "DolphinQt/Config/Mapping/WiimoteEmuExtension.h" #include "DolphinQt/Config/Mapping/WiimoteEmuExtensionMotionInput.h" #include "DolphinQt/Config/Mapping/WiimoteEmuExtensionMotionSimulation.h" @@ -90,6 +90,8 @@ MappingWindow::MappingWindow(QWidget* parent, Type type, int port_num) [] { HotkeyManagerEmu::Enable(true); }); filter->connect(filter, &WindowActivationEventFilter::windowActivated, [] { HotkeyManagerEmu::Enable(false); }); + + MappingCommon::CreateMappingProcessor(this); } void MappingWindow::CreateDevicesLayout() @@ -182,9 +184,8 @@ void MappingWindow::CreateMainLayout() void MappingWindow::ConnectWidgets() { - connect(&Settings::Instance(), &Settings::DevicesChanged, this, - &MappingWindow::OnGlobalDevicesChanged); - connect(this, &MappingWindow::ConfigChanged, this, &MappingWindow::OnGlobalDevicesChanged); + connect(&Settings::Instance(), &Settings::DevicesChanged, this, &MappingWindow::ConfigChanged); + connect(this, &MappingWindow::ConfigChanged, this, &MappingWindow::UpdateDeviceList); connect(m_devices_combo, &QComboBox::currentIndexChanged, this, &MappingWindow::OnSelectDevice); connect(m_reset_clear, &QPushButton::clicked, this, &MappingWindow::OnClearFieldsPressed); @@ -200,6 +201,8 @@ void MappingWindow::ConnectWidgets() // We currently use the "Close" button as an "Accept" button so we must save on reject. connect(this, &QDialog::rejected, [this] { emit Save(); }); connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(m_tab_widget, &QTabWidget::currentChanged, this, &MappingWindow::CancelMapping); } void MappingWindow::UpdateProfileIndex() @@ -342,6 +345,8 @@ void MappingWindow::OnSelectDevice(int) const auto device = m_devices_combo->currentData().toString().toStdString(); m_controller->SetDefaultDevice(device); + + emit ConfigChanged(); m_controller->UpdateReferences(g_controller_interface); } @@ -355,7 +360,7 @@ void MappingWindow::RefreshDevices() g_controller_interface.RefreshDevices(); } -void MappingWindow::OnGlobalDevicesChanged() +void MappingWindow::UpdateDeviceList() { const QSignalBlocker blocker(m_devices_combo); diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.h b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.h index 8a0c4198aa..f2c8717427 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.h @@ -5,9 +5,6 @@ #include #include -#include - -#include "InputCommon/ControllerInterface/CoreDevice.h" namespace ControllerEmu { @@ -15,6 +12,8 @@ class EmulatedController; } class InputConfig; +class MappingButton; + class QComboBox; class QDialogButtonBox; class QEvent; @@ -58,10 +57,14 @@ public: signals: // Emitted when config has changed so widgets can update to reflect the change. void ConfigChanged(); - // Emitted at 30hz for real-time indicators to be updated. + // Emitted at INDICATOR_UPDATE_FREQ Hz for real-time indicators to be updated. void Update(); void Save(); + void UnQueueInputDetection(MappingButton*); + void QueueInputDetection(MappingButton*); + void CancelMapping(); + private: void SetMappingType(Type type); void CreateDevicesLayout(); @@ -82,11 +85,11 @@ private: void UpdateProfileIndex(); void UpdateProfileButtonState(); void PopulateProfileSelection(); + void UpdateDeviceList(); void OnDefaultFieldsPressed(); void OnClearFieldsPressed(); void OnSelectDevice(int index); - void OnGlobalDevicesChanged(); ControllerEmu::EmulatedController* m_controller = nullptr; diff --git a/Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.cpp b/Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.cpp index c72b80189d..14163860af 100644 --- a/Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.cpp +++ b/Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.cpp @@ -3,12 +3,34 @@ #include "DolphinQt/QtUtils/BlockUserInputFilter.h" -#include +#include -bool BlockUserInputFilter::eventFilter(QObject* object, QEvent* event) +#include +#include + +namespace QtUtils { - const QEvent::Type event_type = event->type(); - return event_type == QEvent::KeyPress || event_type == QEvent::KeyRelease || - event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonRelease || - event_type == QEvent::MouseButtonDblClick; + +// Leave filter active for a bit to prevent Return/Space detection from reactivating the button. +constexpr auto REMOVAL_DELAY = std::chrono::milliseconds{100}; + +BlockKeyboardInputFilter::BlockKeyboardInputFilter(QObject* parent) : QObject{parent} +{ + parent->installEventFilter(this); } + +void BlockKeyboardInputFilter::ScheduleRemoval() +{ + auto* const timer = new QTimer(this); + timer->setSingleShot(true); + connect(timer, &QTimer::timeout, [this] { delete this; }); + timer->start(REMOVAL_DELAY); +} + +bool BlockKeyboardInputFilter::eventFilter(QObject* object, QEvent* event) +{ + const auto event_type = event->type(); + return event_type == QEvent::KeyPress || event_type == QEvent::KeyRelease; +} + +} // namespace QtUtils diff --git a/Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.h b/Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.h index 44420f3c6e..7199c085d7 100644 --- a/Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.h +++ b/Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.h @@ -5,14 +5,26 @@ #include -class QEvent; +namespace QtUtils +{ -class BlockUserInputFilter : public QObject +class BlockKeyboardInputFilter : public QObject { Q_OBJECT public: - using QObject::QObject; + BlockKeyboardInputFilter(QObject* parent); + void ScheduleRemoval(); private: bool eventFilter(QObject* object, QEvent* event) override; }; + +template +void InstallKeyboardBlocker(QObject* obj, T* removal_signal_object, void (T::*removal_signal)()) +{ + removal_signal_object->connect(removal_signal_object, removal_signal, + new QtUtils::BlockKeyboardInputFilter{obj}, + &QtUtils::BlockKeyboardInputFilter::ScheduleRemoval); +} + +} // namespace QtUtils diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp index 377ea4444f..2008fc71e8 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp @@ -345,6 +345,20 @@ auto DeviceContainer::DetectInput(const std::vector& device_strings std::chrono::milliseconds confirmation_wait, std::chrono::milliseconds maximum_wait) const -> std::vector +{ + InputDetector input_detector; + input_detector.Start(*this, device_strings); + + while (!input_detector.IsComplete()) + { + Common::SleepCurrentThread(10); + input_detector.Update(initial_wait, confirmation_wait, maximum_wait); + } + + return input_detector.TakeResults(); +} + +struct InputDetector::Impl { struct InputState { @@ -355,7 +369,7 @@ auto DeviceContainer::DetectInput(const std::vector& device_strings ControlState last_state = initial_state; MathUtil::RunningVariance stats; - // Prevent multiiple detections until after release. + // Prevent multiple detections until after release. bool is_ready = true; void Update() @@ -392,18 +406,32 @@ auto DeviceContainer::DetectInput(const std::vector& device_strings std::vector input_states; }; - // Acquire devices and initial input states. std::vector device_states; +}; + +InputDetector::InputDetector() : m_start_time{}, m_state{} +{ +} + +void InputDetector::Start(const DeviceContainer& container, + const std::vector& device_strings) + +{ + m_start_time = Clock::now(); + m_detections = {}; + m_state = std::make_unique(); + + // Acquire devices and initial input states. for (const auto& device_string : device_strings) { DeviceQualifier dq; dq.FromString(device_string); - auto device = FindDevice(dq); + auto device = container.FindDevice(dq); if (!device) continue; - std::vector input_states; + std::vector input_states; for (auto* input : device->Inputs()) { @@ -413,38 +441,40 @@ auto DeviceContainer::DetectInput(const std::vector& device_strings // Undesirable axes will have negative values here when trying to map a // "FullAnalogSurface". - input_states.push_back(InputState{input}); + input_states.push_back(Impl::InputState{input}); } if (!input_states.empty()) - device_states.emplace_back(DeviceState{std::move(device), std::move(input_states)}); + m_state->device_states.emplace_back( + Impl::DeviceState{std::move(device), std::move(input_states)}); } - if (device_states.empty()) - return {}; + // If no inputs were found via the supplied device strings, immediately complete. + if (m_state->device_states.empty()) + m_state.reset(); +} - std::vector detections; - - const auto start_time = Clock::now(); - while (true) +void InputDetector::Update(std::chrono::milliseconds initial_wait, + std::chrono::milliseconds confirmation_wait, + std::chrono::milliseconds maximum_wait) +{ + if (m_state) { const auto now = Clock::now(); - const auto elapsed_time = now - start_time; + const auto elapsed_time = now - m_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)) + if (elapsed_time >= maximum_wait || (m_detections.empty() && elapsed_time >= initial_wait) || + (!m_detections.empty() && m_detections.back().release_time.has_value() && + now >= *m_detections.back().release_time + confirmation_wait)) { - break; + m_state.reset(); + return; } - Common::SleepCurrentThread(10); - - for (auto& device_state : device_states) + for (auto& device_state : m_state->device_states) { - for (std::size_t i = 0; i != device_state.input_states.size(); ++i) + for (auto& input_state : device_state.input_states) { - auto& input_state = device_state.input_states[i]; input_state.Update(); if (input_state.IsPressed()) @@ -456,26 +486,42 @@ auto DeviceContainer::DetectInput(const std::vector& device_strings const auto smoothness = 1 / std::sqrt(input_state.stats.Variance() / input_state.stats.Mean()); - InputDetection new_detection; + Detection 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)); + m_detections.emplace_back(std::move(new_detection)); } } } // Check for any releases of our detected inputs. - for (auto& d : detections) + for (auto& d : m_detections) { if (!d.release_time.has_value() && d.input->GetState() < (1 - INPUT_DETECT_THRESHOLD)) d.release_time = Clock::now(); } } - - return detections; } + +InputDetector::~InputDetector() = default; + +bool InputDetector::IsComplete() const +{ + return !m_state; +} + +auto InputDetector::GetResults() const -> const std::vector& +{ + return m_detections; +} + +auto InputDetector::TakeResults() -> std::vector +{ + return std::move(m_detections); +} + } // namespace ciface::Core diff --git a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h index cd8256396e..0370fce894 100644 --- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.h +++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.h @@ -245,5 +245,32 @@ protected: mutable std::recursive_mutex m_devices_mutex; std::vector> m_devices; }; + +class InputDetector +{ +public: + using Detection = DeviceContainer::InputDetection; + + InputDetector(); + ~InputDetector(); + + void Start(const DeviceContainer& container, const std::vector& device_strings); + void Update(std::chrono::milliseconds initial_wait, std::chrono::milliseconds confirmation_wait, + std::chrono::milliseconds maximum_wait); + bool IsComplete() const; + + const std::vector& GetResults() const; + + // move-return'd to prevent copying. + std::vector TakeResults(); + +private: + struct Impl; + + Clock::time_point m_start_time; + std::vector m_detections; + std::unique_ptr m_state; +}; + } // namespace Core } // namespace ciface