summaryrefslogtreecommitdiff
path: root/Source/Core
diff options
context:
space:
mode:
authorOatmealDome <OatmealDome@users.noreply.github.com>2025-01-07 16:55:55 -0500
committerGitHub <noreply@github.com>2025-01-07 16:55:55 -0500
commit696b363f478961bb18420b79ef9f09eabd600e55 (patch)
tree18f9f1c178a08ca694f42867288e78b01732c5c5 /Source/Core
parentb35f7af355dad22b1b2d408c5d17d781e506686f (diff)
parentf12846a0e93ea9ad69f884fb850cce379bc5bed0 (diff)
Merge pull request #13162 from jordan-woyak/non-blocking-input-detection
DolphinQt/InputCommon: Make input mapping and output testing non-blocking.
Diffstat (limited to 'Source/Core')
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp148
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/IOWindow.h13
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp72
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingButton.h13
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingCommon.cpp192
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingCommon.h19
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingWidget.cpp5
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingWidget.h5
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp14
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingWindow.h13
-rw-r--r--Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.cpp32
-rw-r--r--Source/Core/DolphinQt/QtUtils/BlockUserInputFilter.h18
-rw-r--r--Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp100
-rw-r--r--Source/Core/InputCommon/ControllerInterface/CoreDevice.h27
14 files changed, 451 insertions, 220 deletions
diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp
index 9c9dc3e3b6..f2b4805f54 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 <optional>
-#include <thread>
#include <QBrush>
#include <QColor>
@@ -20,16 +19,15 @@
#include <QSlider>
#include <QSpinBox>
#include <QTableWidget>
+#include <QTimer>
#include <QVBoxLayout>
-#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,29 @@ 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<ControlState>();
- });
- m_parse_text->setReadOnly(true);
+ if (m_type == Type::Input)
+ {
+ m_parse_text = new InputStateLineEdit([this] {
+ const auto lock = m_controller->GetStateLock();
+ return m_reference->GetState<ControlState>();
+ });
+ }
+ else
+ {
+ m_parse_text = new InputStateLineEdit([this] {
+ const auto lock = m_controller->GetStateLock();
+ return m_output_test_timer->isActive() * m_reference->range;
+ });
+ }
m_expression_text = new QPlainTextEdit();
m_expression_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
@@ -419,11 +433,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 +464,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 +498,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<ciface::Core::InputDetector>();
+ 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 +633,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<OutputReference*>(m_reference));
-}
-
void IOWindow::OnRangeChanged(int value)
{
m_reference->range = value / 100.0;
+ emit TestOutputComplete();
}
void IOWindow::ReleaseDevices()
@@ -670,6 +737,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 +788,7 @@ InputStateDelegate::InputStateDelegate(IOWindow* parent, int column,
InputStateLineEdit::InputStateLineEdit(std::function<ControlState()> 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 <QString>
#include <QSyntaxHighlighter>
-#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<ciface::Core::Device> 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<ciface::Core::InputDetector> 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 <QString>
#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<bool>())
+ if (isActiveWindow() && m_reference->IsInput() && m_reference->GetState<bool>() && !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,85 +3,147 @@
#include "DolphinQt/Config/Mapping/MappingCommon.h"
-#include <chrono>
+#include <deque>
+#include <memory>
-#include <QApplication>
-#include <QPushButton>
-#include <QRegularExpression>
-#include <QString>
#include <QTimer>
-#include "DolphinQt/QtUtils/BlockUserInputFilter.h"
+#include "DolphinQt/Config/Mapping/MappingButton.h"
+#include "DolphinQt/Config/Mapping/MappingWindow.h"
+
#include "InputCommon/ControlReference/ControlReference.h"
+#include "InputCommon/ControllerEmu/ControllerEmu.h"
+#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/ControllerInterface/MappingCommon.h"
-#include "Common/Thread.h"
-
namespace MappingCommon
{
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<std::string>& device_strings,
- const ciface::Core::DeviceQualifier& default_device,
- ciface::MappingCommon::Quote quote)
+class MappingProcessor : public QWidget
{
- const auto filter = new BlockUserInputFilter(button);
-
- button->installEventFilter(filter);
- button->grabKeyboard();
- button->grabMouse();
-
- 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();
-
- // Avoid that the button press itself is registered as an event
- Common::SleepCurrentThread(50);
-
- auto detections =
- device_container.DetectInput(device_strings, INPUT_DETECT_INITIAL_TIME,
- INPUT_DETECT_CONFIRMATION_TIME, INPUT_DETECT_MAXIMUM_TIME);
-
- ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections);
-
- const auto timer = new QTimer(button);
-
- timer->setSingleShot(true);
-
- button->connect(timer, &QTimer::timeout, [button, filter] {
- button->releaseMouse();
- button->releaseKeyboard();
- button->removeEventFilter(filter);
- });
-
- // Prevent mappings of "space", "return", or mouse clicks from re-activating detection.
- timer->start(500);
-
- button->setText(old_text);
-
- return QString::fromStdString(BuildExpression(detections, default_device, quote));
-}
-
-void TestOutput(QPushButton* button, OutputReference* reference)
+public:
+ MappingProcessor(MappingWindow* parent) : QWidget{parent}, m_parent{parent}
+ {
+ using MW = MappingWindow;
+ using MP = MappingProcessor;
+
+ connect(parent, &MW::Update, this, &MP::ProcessMappingButtons);
+ connect(parent, &MW::ConfigChanged, this, &MP::CancelMapping);
+
+ connect(parent, &MW::UnQueueInputDetection, this, &MP::UnQueueInputDetection);
+ connect(parent, &MW::QueueInputDetection, this, &MP::QueueInputDetection);
+ connect(parent, &MW::CancelMapping, this, &MP::CancelMapping);
+
+ 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);
+ }
+
+ void StartInputDetection()
+ {
+ const auto& default_device = m_parent->GetController()->GetDefaultDevice();
+ auto& button = m_clicked_mapping_buttons.front();
+
+ button->StartMapping();
+
+ std::vector device_strings{default_device.ToString()};
+ if (m_parent->IsMappingAllDevices())
+ device_strings = g_controller_interface.GetAllDeviceStrings();
+
+ m_input_detector = std::make_unique<ciface::Core::InputDetector>();
+ const auto lock = m_parent->GetController()->GetStateLock();
+ m_input_detector->Start(g_controller_interface, device_strings);
+ }
+
+ void ProcessMappingButtons()
+ {
+ if (!m_input_detector)
+ return;
+
+ m_input_detector->Update(INPUT_DETECT_INITIAL_TIME, INPUT_DETECT_CONFIRMATION_TIME,
+ INPUT_DETECT_MAXIMUM_TIME);
+
+ if (m_input_detector->IsComplete())
+ {
+ auto detections = m_input_detector->TakeResults();
+ ciface::MappingCommon::RemoveSpuriousTriggerCombinations(&detections);
+
+ // No inputs detected. Cancel this and any other queued mappings.
+ if (detections.empty())
+ {
+ CancelMapping();
+ return;
+ }
+
+ const auto& default_device = m_parent->GetController()->GetDefaultDevice();
+ auto& button = m_clicked_mapping_buttons.front();
+ auto* const control_reference = button->GetControlReference();
+
+ 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<MappingButton*> m_clicked_mapping_buttons;
+ std::unique_ptr<ciface::Core::InputDetector> 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 <string>
-#include <vector>
+#include <chrono>
-#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<std::string>& 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 ae4095a82c..30f0e41e86 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"
@@ -340,10 +339,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 ee05834736..31cf964fc3 100644
--- a/Source/Core/DolphinQt/Config/Mapping/MappingWidget.h
+++ b/Source/Core/DolphinQt/Config/Mapping/MappingWidget.h
@@ -3,15 +3,10 @@
#pragma once
-#include <memory>
-#include <vector>
-
#include <QString>
#include <QWidget>
-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 d5af344c30..df3f10bd34 100644
--- a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp
+++ b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp
@@ -41,6 +41,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"
@@ -93,6 +94,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()
@@ -185,9 +188,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);
@@ -203,6 +205,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()
@@ -345,6 +349,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);
}
@@ -358,7 +364,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 <QDialog>
#include <QString>
-#include <memory>
-
-#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 <chrono>
+
#include <QEvent>
+#include <QTimer>
+
+namespace QtUtils
+{
-bool BlockUserInputFilter::eventFilter(QObject* object, QEvent* event)
+// 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}
{
- 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;
+ 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 <QObject>
-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 <typename T>
+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..b9141658e6 100644
--- a/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp
+++ b/Source/Core/InputCommon/ControllerInterface/CoreDevice.cpp
@@ -346,6 +346,20 @@ auto DeviceContainer::DetectInput(const std::vector<std::string>& device_strings
std::chrono::milliseconds maximum_wait) const
-> std::vector<InputDetection>
{
+ 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
{
InputState(ciface::Core::Device::Input* input_) : input{input_} { stats.Push(0.0); }
@@ -355,7 +369,7 @@ auto DeviceContainer::DetectInput(const std::vector<std::string>& device_strings
ControlState last_state = initial_state;
MathUtil::RunningVariance<ControlState> 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<std::string>& device_strings
std::vector<InputState> input_states;
};
- // Acquire devices and initial input states.
std::vector<DeviceState> device_states;
+};
+
+InputDetector::InputDetector() : m_start_time{}, m_state{}
+{
+}
+
+void InputDetector::Start(const DeviceContainer& container,
+ const std::vector<std::string>& device_strings)
+
+{
+ m_start_time = Clock::now();
+ m_detections = {};
+ m_state = std::make_unique<Impl>();
+
+ // 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<InputState> input_states;
+ std::vector<Impl::InputState> input_states;
for (auto* input : device->Inputs())
{
@@ -413,38 +441,42 @@ auto DeviceContainer::DetectInput(const std::vector<std::string>& 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 {};
-
- std::vector<InputDetection> detections;
+ // If no inputs were found via the supplied device strings, immediately complete.
+ if (m_state->device_states.empty())
+ m_state.reset();
+}
- 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 +488,42 @@ auto DeviceContainer::DetectInput(const std::vector<std::string>& 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<Detection>&
+{
+ return m_detections;
}
+
+auto InputDetector::TakeResults() -> std::vector<Detection>
+{
+ 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<std::shared_ptr<Device>> m_devices;
};
+
+class InputDetector
+{
+public:
+ using Detection = DeviceContainer::InputDetection;
+
+ InputDetector();
+ ~InputDetector();
+
+ void Start(const DeviceContainer& container, const std::vector<std::string>& 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<Detection>& GetResults() const;
+
+ // move-return'd to prevent copying.
+ std::vector<Detection> TakeResults();
+
+private:
+ struct Impl;
+
+ Clock::time_point m_start_time;
+ std::vector<Detection> m_detections;
+ std::unique_ptr<Impl> m_state;
+};
+
} // namespace Core
} // namespace ciface