Merge pull request #13323 from jordan-woyak/iterative-mapping

DolphinQt: Add setting to enable iterative input mappings.
This commit is contained in:
JMC47 2025-02-15 16:19:02 -05:00 committed by GitHub
commit ce0f9139e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 112 additions and 49 deletions

View File

@ -11,8 +11,6 @@
#include "DolphinQt/Config/Mapping/IOWindow.h" #include "DolphinQt/Config/Mapping/IOWindow.h"
#include "DolphinQt/Config/Mapping/MappingWidget.h" #include "DolphinQt/Config/Mapping/MappingWidget.h"
#include "DolphinQt/Config/Mapping/MappingWindow.h" #include "DolphinQt/Config/Mapping/MappingWindow.h"
#include "DolphinQt/QtUtils/BlockUserInputFilter.h"
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
#include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h" #include "InputCommon/ControllerEmu/ControllerEmu.h"
@ -67,15 +65,11 @@ static QString RefToDisplayString(ControlReference* ref)
return expression; return expression;
} }
bool MappingButton::IsInput() const MappingButton::MappingButton(MappingWidget* parent, ControlReference* ref, ControlType control_type)
: ElidedButton{RefToDisplayString(ref)}, m_mapping_window{parent->GetParent()},
m_reference{ref}, m_control_type{control_type}
{ {
return m_reference->IsInput(); if (m_reference->IsInput())
}
MappingButton::MappingButton(MappingWidget* parent, ControlReference* ref)
: ElidedButton(RefToDisplayString(ref)), m_mapping_window(parent->GetParent()), m_reference(ref)
{
if (IsInput())
{ {
setToolTip( setToolTip(
tr("Left-click to detect input.\nMiddle-click to clear.\nRight-click for more options.")); tr("Left-click to detect input.\nMiddle-click to clear.\nRight-click for more options."));
@ -88,10 +82,8 @@ MappingButton::MappingButton(MappingWidget* parent, ControlReference* ref)
connect(this, &MappingButton::clicked, this, &MappingButton::Clicked); connect(this, &MappingButton::clicked, this, &MappingButton::Clicked);
connect(parent, &MappingWidget::ConfigChanged, this, &MappingButton::ConfigChanged); connect(parent, &MappingWidget::ConfigChanged, this, &MappingButton::ConfigChanged);
connect(this, &MappingButton::ConfigChanged, [this] { connect(this, &MappingButton::ConfigChanged,
setText(RefToDisplayString(m_reference)); [this] { setText(RefToDisplayString(m_reference)); });
m_is_mapping = false;
});
} }
void MappingButton::AdvancedPressed() void MappingButton::AdvancedPressed()
@ -114,7 +106,6 @@ void MappingButton::Clicked()
return; return;
} }
m_is_mapping = true;
m_mapping_window->QueueInputDetection(this); m_mapping_window->QueueInputDetection(this);
} }
@ -131,14 +122,6 @@ void MappingButton::Clear()
m_mapping_window->UnQueueInputDetection(this); m_mapping_window->UnQueueInputDetection(this);
} }
void MappingButton::StartMapping()
{
// 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) void MappingButton::mouseReleaseEvent(QMouseEvent* event)
{ {
switch (event->button()) switch (event->button())
@ -159,3 +142,8 @@ ControlReference* MappingButton::GetControlReference()
{ {
return m_reference; return m_reference;
} }
auto MappingButton::GetControlType() const -> ControlType
{
return m_control_type;
}

View File

@ -15,14 +15,21 @@ class MappingButton : public ElidedButton
{ {
Q_OBJECT Q_OBJECT
public: public:
MappingButton(MappingWidget* widget, ControlReference* ref); enum class ControlType
{
NormalInput,
ModifierInput,
Output,
};
MappingButton(MappingWidget* widget, ControlReference* ref, ControlType type);
bool IsInput() const;
ControlReference* GetControlReference(); ControlReference* GetControlReference();
void StartMapping(); ControlType GetControlType() const;
signals: signals:
void ConfigChanged(); void ConfigChanged();
void QueueNextButtonMapping();
private: private:
void Clear(); void Clear();
@ -33,5 +40,5 @@ private:
MappingWindow* const m_mapping_window; MappingWindow* const m_mapping_window;
ControlReference* const m_reference; ControlReference* const m_reference;
bool m_is_mapping = false; const ControlType m_control_type;
}; };

View File

@ -10,6 +10,7 @@
#include "DolphinQt/Config/Mapping/MappingButton.h" #include "DolphinQt/Config/Mapping/MappingButton.h"
#include "DolphinQt/Config/Mapping/MappingWindow.h" #include "DolphinQt/Config/Mapping/MappingWindow.h"
#include "DolphinQt/QtUtils/BlockUserInputFilter.h"
#include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h" #include "InputCommon/ControllerEmu/ControllerEmu.h"
@ -24,6 +25,11 @@ constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5);
// Ignore the mouse-click when queuing more buttons with "alternate mappings" enabled. // Ignore the mouse-click when queuing more buttons with "alternate mappings" enabled.
constexpr auto INPUT_DETECT_ENDING_IGNORE_TIME = std::chrono::milliseconds(50); constexpr auto INPUT_DETECT_ENDING_IGNORE_TIME = std::chrono::milliseconds(50);
bool ContainsAnalogInput(const ciface::Core::InputDetector::Results& results)
{
return std::ranges::any_of(results, [](auto& detection) { return detection.smoothness > 1; });
}
class MappingProcessor : public QWidget class MappingProcessor : public QWidget
{ {
public: public:
@ -49,7 +55,10 @@ public:
const auto& default_device = m_parent->GetController()->GetDefaultDevice(); const auto& default_device = m_parent->GetController()->GetDefaultDevice();
auto& button = m_clicked_mapping_buttons.front(); auto& button = m_clicked_mapping_buttons.front();
button->StartMapping(); // Focus just makes it more clear which button is currently being mapped.
button->setFocus();
button->setText(tr("[ Press Now ]"));
QtUtils::InstallKeyboardBlocker(button, button, &MappingButton::ConfigChanged);
std::vector device_strings{default_device.ToString()}; std::vector device_strings{default_device.ToString()};
if (m_parent->IsCreateOtherDeviceMappingsEnabled()) if (m_parent->IsCreateOtherDeviceMappingsEnabled())
@ -71,16 +80,40 @@ public:
m_input_detector->Update(INPUT_DETECT_INITIAL_TIME, confirmation_time, m_input_detector->Update(INPUT_DETECT_INITIAL_TIME, confirmation_time,
INPUT_DETECT_MAXIMUM_TIME); INPUT_DETECT_MAXIMUM_TIME);
if (m_input_detector->IsComplete()) if (!m_input_detector->IsComplete())
return;
auto* const button = m_clicked_mapping_buttons.front();
auto results = m_input_detector->TakeResults();
if (!FinalizeMapping(&results))
{ {
// No inputs detected. Cancel this and any other queued mappings. // No inputs detected. Cancel this and any other queued mappings.
if (!FinalizeMapping(m_input_detector->TakeResults())) CancelMapping();
CancelMapping(); }
else if (m_parent->IsIterativeMappingEnabled() && m_clicked_mapping_buttons.empty())
{
button->QueueNextButtonMapping();
if (m_clicked_mapping_buttons.empty())
return;
// Skip "Modifier" mappings when using analog inputs.
auto* next_button = m_clicked_mapping_buttons.front();
if (next_button->GetControlType() == MappingButton::ControlType::ModifierInput &&
ContainsAnalogInput(results))
{
// Clear "Modifier" mapping and queue the next button.
SetButtonExpression(next_button, "");
UnQueueInputDetection(next_button);
next_button->QueueNextButtonMapping();
}
} }
} }
bool FinalizeMapping(ciface::Core::InputDetector::Results detections) bool FinalizeMapping(ciface::Core::InputDetector::Results* detections_ptr)
{ {
auto& detections = *detections_ptr;
if (!ciface::MappingCommon::ContainsCompleteDetection(detections)) if (!ciface::MappingCommon::ContainsCompleteDetection(detections))
return false; return false;
@ -88,18 +121,22 @@ public:
const auto& default_device = m_parent->GetController()->GetDefaultDevice(); const auto& default_device = m_parent->GetController()->GetDefaultDevice();
auto& button = m_clicked_mapping_buttons.front(); auto& button = m_clicked_mapping_buttons.front();
auto* const control_reference = button->GetControlReference(); SetButtonExpression(
button, BuildExpression(detections, default_device, ciface::MappingCommon::Quote::On));
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); UnQueueInputDetection(button);
return true; return true;
} }
void SetButtonExpression(MappingButton* button, const std::string& expression)
{
auto* const control_reference = button->GetControlReference();
control_reference->SetExpression(expression);
m_parent->Save();
m_parent->GetController()->UpdateSingleControlReference(g_controller_interface,
control_reference);
}
void UpdateInputDetectionStartTimer() void UpdateInputDetectionStartTimer()
{ {
m_input_detector.reset(); m_input_detector.reset();
@ -110,22 +147,20 @@ public:
m_input_detection_start_timer->start(INPUT_DETECT_INITIAL_DELAY); m_input_detection_start_timer->start(INPUT_DETECT_INITIAL_DELAY);
} }
void UnQueueInputDetection(MappingButton* button) bool UnQueueInputDetection(MappingButton* button)
{ {
std::erase(m_clicked_mapping_buttons, button); if (!std::erase(m_clicked_mapping_buttons, button))
return false;
button->ConfigChanged(); button->ConfigChanged();
UpdateInputDetectionStartTimer(); UpdateInputDetectionStartTimer();
return true;
} }
void QueueInputDetection(MappingButton* button) void QueueInputDetection(MappingButton* button)
{ {
// UnQueue if already queued. // Just UnQueue if already queued.
if (std::erase(m_clicked_mapping_buttons, button)) if (UnQueueInputDetection(button))
{
button->ConfigChanged();
UpdateInputDetectionStartTimer();
return; return;
}
button->setText(QStringLiteral("[ ... ]")); button->setText(QStringLiteral("[ ... ]"));
m_clicked_mapping_buttons.push_back(button); m_clicked_mapping_buttons.push_back(button);
@ -136,7 +171,7 @@ public:
auto results = m_input_detector->TakeResults(); auto results = m_input_detector->TakeResults();
ciface::MappingCommon::RemoveDetectionsAfterTimePoint( ciface::MappingCommon::RemoveDetectionsAfterTimePoint(
&results, ciface::Core::DeviceContainer::Clock::now() - INPUT_DETECT_ENDING_IGNORE_TIME); &results, ciface::Core::DeviceContainer::Clock::now() - INPUT_DETECT_ENDING_IGNORE_TIME);
FinalizeMapping(std::move(results)); FinalizeMapping(&results);
} }
UpdateInputDetectionStartTimer(); UpdateInputDetectionStartTimer();
} }

View File

@ -313,7 +313,27 @@ QGroupBox* MappingWidget::CreateControlsBox(const QString& name, ControllerEmu::
void MappingWidget::CreateControl(const ControllerEmu::Control* control, QFormLayout* layout, void MappingWidget::CreateControl(const ControllerEmu::Control* control, QFormLayout* layout,
bool indicator) bool indicator)
{ {
auto* const button = new MappingButton(this, control->control_ref.get()); // I know this check is terrible, but it's just UI code.
const bool is_modifier = control->name == "Modifier";
using ControlType = MappingButton::ControlType;
const auto control_type =
control->control_ref->IsInput() ?
(is_modifier ? ControlType::ModifierInput : ControlType::NormalInput) :
ControlType::Output;
auto* const button = new MappingButton(this, control->control_ref.get(), control_type);
if (control->control_ref->IsInput())
{
if (m_previous_mapping_button)
{
connect(m_previous_mapping_button, &MappingButton::QueueNextButtonMapping,
[this, button]() { m_parent->QueueInputDetection(button); });
}
m_previous_mapping_button = button;
}
button->setMinimumWidth(100); button->setMinimumWidth(100);
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);

View File

@ -7,6 +7,7 @@
#include <QWidget> #include <QWidget>
class InputConfig; class InputConfig;
class MappingButton;
class MappingNumeric; class MappingNumeric;
class MappingWindow; class MappingWindow;
class QFormLayout; class QFormLayout;
@ -55,4 +56,5 @@ protected:
private: private:
MappingWindow* m_parent; MappingWindow* m_parent;
MappingButton* m_previous_mapping_button = nullptr;
}; };

View File

@ -117,9 +117,13 @@ void MappingWindow::CreateDevicesLayout()
m_wait_for_alternate_mappings = new QAction(tr("Wait for Alternate Input Mappings"), options); m_wait_for_alternate_mappings = new QAction(tr("Wait for Alternate Input Mappings"), options);
m_wait_for_alternate_mappings->setCheckable(true); m_wait_for_alternate_mappings->setCheckable(true);
m_iterative_mapping = new QAction(tr("Enable Iterative Input Mapping"), options);
m_iterative_mapping->setCheckable(true);
options->addAction(refresh_action); options->addAction(refresh_action);
options->addAction(m_other_device_mappings); options->addAction(m_other_device_mappings);
options->addAction(m_wait_for_alternate_mappings); options->addAction(m_wait_for_alternate_mappings);
options->addAction(m_iterative_mapping);
options->setDefaultAction(refresh_action); options->setDefaultAction(refresh_action);
m_devices_combo->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); m_devices_combo->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
@ -368,6 +372,11 @@ bool MappingWindow::IsWaitForAlternateMappingsEnabled() const
return m_wait_for_alternate_mappings->isChecked(); return m_wait_for_alternate_mappings->isChecked();
} }
bool MappingWindow::IsIterativeMappingEnabled() const
{
return m_iterative_mapping->isChecked();
}
void MappingWindow::RefreshDevices() void MappingWindow::RefreshDevices()
{ {
g_controller_interface.RefreshDevices(); g_controller_interface.RefreshDevices();

View File

@ -53,6 +53,7 @@ public:
ControllerEmu::EmulatedController* GetController() const; ControllerEmu::EmulatedController* GetController() const;
bool IsCreateOtherDeviceMappingsEnabled() const; bool IsCreateOtherDeviceMappingsEnabled() const;
bool IsWaitForAlternateMappingsEnabled() const; bool IsWaitForAlternateMappingsEnabled() const;
bool IsIterativeMappingEnabled() const;
void ShowExtensionMotionTabs(bool show); void ShowExtensionMotionTabs(bool show);
void ActivateExtensionTab(); void ActivateExtensionTab();
@ -106,6 +107,7 @@ private:
QComboBox* m_devices_combo; QComboBox* m_devices_combo;
QAction* m_other_device_mappings; QAction* m_other_device_mappings;
QAction* m_wait_for_alternate_mappings; QAction* m_wait_for_alternate_mappings;
QAction* m_iterative_mapping;
// Profiles // Profiles
QGroupBox* m_profiles_box; QGroupBox* m_profiles_box;