diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt
index 7b1c1c6b56..50b6d5a5a1 100644
--- a/pcsx2-qt/CMakeLists.txt
+++ b/pcsx2-qt/CMakeLists.txt
@@ -56,6 +56,8 @@ target_sources(pcsx2-qt PRIVATE
Settings/ControllerGlobalSettingsWidget.cpp
Settings/ControllerGlobalSettingsWidget.h
Settings/ControllerGlobalSettingsWidget.ui
+ Settings/ControllerMacroEditWidget.ui
+ Settings/ControllerMacroWidget.ui
Settings/ControllerSettingsDialog.cpp
Settings/ControllerSettingsDialog.h
Settings/ControllerSettingsDialog.ui
diff --git a/pcsx2-qt/Settings/ControllerBindingWidget.ui b/pcsx2-qt/Settings/ControllerBindingWidget.ui
index 327d289d3c..60f76d6f7d 100644
--- a/pcsx2-qt/Settings/ControllerBindingWidget.ui
+++ b/pcsx2-qt/Settings/ControllerBindingWidget.ui
@@ -7,13 +7,13 @@
0
0
833
- 560
+ 617
Form
-
+
0
@@ -31,9 +31,73 @@
Controller Type
-
+
-
-
+
+
-
+
+
+ -
+
+
+ Bindings
+
+
+ true
+
+
+
+ ..
+
+
+ Qt::ToolButtonTextBesideIcon
+
+
+ true
+
+
+
+ -
+
+
+ Settings
+
+
+
+ ..
+
+
+ true
+
+
+ Qt::ToolButtonTextBesideIcon
+
+
+ true
+
+
+
+ -
+
+
+ Macros
+
+
+
+ ..
+
+
+ true
+
+
+ Qt::ToolButtonTextBesideIcon
+
+
+ true
+
+
+
+
-
@@ -51,25 +115,31 @@
-
-
-
+
- Automatic binding
+ Automatic Mapping
..
+
+ Qt::ToolButtonTextBesideIcon
+
-
-
+
- Clear Bindings
+ Clear Mapping
..
+
+ Qt::ToolButtonTextBesideIcon
+
@@ -77,6 +147,9 @@
+ -
+
+
diff --git a/pcsx2-qt/Settings/ControllerBindingWidget_DualShock2.ui b/pcsx2-qt/Settings/ControllerBindingWidget_DualShock2.ui
index 030e17d480..e748245d28 100644
--- a/pcsx2-qt/Settings/ControllerBindingWidget_DualShock2.ui
+++ b/pcsx2-qt/Settings/ControllerBindingWidget_DualShock2.ui
@@ -415,25 +415,6 @@
- -
-
-
-
- 80
- 0
-
-
-
- x
-
-
- 3.000000000000000
-
-
- 0.100000000000000
-
-
-
@@ -488,74 +469,6 @@
- -
-
-
- Select
-
-
-
- 6
-
-
- 6
-
-
- 6
-
-
- 6
-
-
-
-
-
-
- 100
- 16777215
-
-
-
- PushButton
-
-
-
-
-
-
- -
-
-
- Start
-
-
-
- 6
-
-
- 6
-
-
- 6
-
-
- 6
-
-
-
-
-
-
- 100
- 16777215
-
-
-
- PushButton
-
-
-
-
-
-
-
@@ -624,62 +537,6 @@
- -
-
-
- Pressure Modifier
-
-
-
-
-
-
-
- 80
- 0
-
-
-
- x
-
-
- 1.000000000000000
-
-
- 0.010000000000000
-
-
- 0.500000000000000
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 100
- 0
-
-
-
-
- 16777215
- 16777215
-
-
-
- PushButton
-
-
-
-
-
-
-
@@ -714,6 +571,74 @@
+ -
+
+
+ Start
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Select
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 100
+ 16777215
+
+
+
+ PushButton
+
+
+
+
+
+
-
@@ -1093,19 +1018,6 @@
- -
-
-
- x
-
-
- 3.000000000000000
-
-
- 0.100000000000000
-
-
-
@@ -1181,6 +1093,19 @@
-
+
-
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
-
@@ -1221,19 +1146,6 @@
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
@@ -1274,59 +1186,63 @@
- -
-
+
-
+
- Analog Sensitivity
+ Pressure Modifier
-
-
-
-
-
- 200
+
+
-
+
+
+
+ 80
+ 0
+
-
- Qt::Horizontal
+
+ x
+
+
+ 1.000000000000000
+
+
+ 0.010000000000000
+
+
+ 0.500000000000000
- -
-
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 100
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
- 100%
+ PushButton
- -
-
-
- Analog Deadzone
-
-
-
-
-
-
- 100
-
-
- Qt::Horizontal
-
-
-
- -
-
-
- 0%
-
-
-
-
-
-
- -
+
-
Analog
@@ -1366,7 +1282,6 @@
-
diff --git a/pcsx2-qt/Settings/ControllerBindingWidgets.cpp b/pcsx2-qt/Settings/ControllerBindingWidgets.cpp
index 2df94d01db..7792a02477 100644
--- a/pcsx2-qt/Settings/ControllerBindingWidgets.cpp
+++ b/pcsx2-qt/Settings/ControllerBindingWidgets.cpp
@@ -15,6 +15,7 @@
#include "PrecompiledHeader.h"
+#include
#include
#include
#include
@@ -45,15 +46,18 @@ ControllerBindingWidget::ControllerBindingWidget(QWidget* parent, ControllerSett
m_ui.controllerType, m_config_section, "Type", PAD::GetDefaultPadType(port));
connect(m_ui.controllerType, QOverload::of(&QComboBox::currentIndexChanged), this, &ControllerBindingWidget::onTypeChanged);
- connect(m_ui.automaticBinding, &QPushButton::clicked, this, &ControllerBindingWidget::doAutomaticBinding);
- connect(m_ui.clearBindings, &QPushButton::clicked, this, &ControllerBindingWidget::doClearBindings);
+ connect(m_ui.bindings, &QPushButton::clicked, this, &ControllerBindingWidget::onBindingsClicked);
+ connect(m_ui.settings, &QPushButton::clicked, this, &ControllerBindingWidget::onSettingsClicked);
+ connect(m_ui.macros, &QPushButton::clicked, this, &ControllerBindingWidget::onMacrosClicked);
+ connect(m_ui.automaticBinding, &QPushButton::clicked, this, &ControllerBindingWidget::onAutomaticBindingClicked);
+ connect(m_ui.clearBindings, &QPushButton::clicked, this, &ControllerBindingWidget::onClearBindingsClicked);
}
ControllerBindingWidget::~ControllerBindingWidget() = default;
QIcon ControllerBindingWidget::getIcon() const
{
- return m_current_widget->getIcon();
+ return m_bindings_widget->getIcon();
}
void ControllerBindingWidget::populateControllerTypes()
@@ -64,36 +68,101 @@ void ControllerBindingWidget::populateControllerTypes()
void ControllerBindingWidget::onTypeChanged()
{
- const bool is_initializing = (m_current_widget == nullptr);
+ const bool is_initializing = (m_ui.stackedWidget->count() == 0);
m_controller_type = m_dialog->getStringValue(m_config_section.c_str(), "Type", PAD::GetDefaultPadType(m_port_number));
- if (!is_initializing)
+ if (m_bindings_widget)
{
- m_ui.verticalLayout->removeWidget(m_current_widget);
- delete m_current_widget;
- m_current_widget = nullptr;
+ m_ui.stackedWidget->removeWidget(m_bindings_widget);
+ delete m_bindings_widget;
+ m_bindings_widget = nullptr;
+ }
+ if (m_settings_widget)
+ {
+ m_ui.stackedWidget->removeWidget(m_settings_widget);
+ delete m_settings_widget;
+ m_settings_widget = nullptr;
+ }
+ if (m_macros_widget)
+ {
+ m_ui.stackedWidget->removeWidget(m_macros_widget);
+ delete m_macros_widget;
+ m_macros_widget = nullptr;
}
- const int index = m_ui.controllerType->findData(QString::fromStdString(m_controller_type));
- if (index >= 0 && index != m_ui.controllerType->currentIndex())
- {
- QSignalBlocker sb(m_ui.controllerType);
- m_ui.controllerType->setCurrentIndex(index);
- }
+ const PAD::ControllerInfo* cinfo = PAD::GetControllerInfo(m_controller_type);
+ const bool has_settings = (cinfo && cinfo->num_settings > 0);
+ const bool has_macros = (cinfo && cinfo->num_bindings > 0);
+ m_ui.settings->setEnabled(has_settings);
+ m_ui.macros->setEnabled(has_macros);
if (m_controller_type == "DualShock2")
- m_current_widget = ControllerBindingWidget_DualShock2::createInstance(this);
+ m_bindings_widget = ControllerBindingWidget_DualShock2::createInstance(this);
else
- m_current_widget = new ControllerBindingWidget_Base(this);
+ m_bindings_widget = new ControllerBindingWidget_Base(this);
- m_ui.verticalLayout->addWidget(m_current_widget, 1);
+ m_ui.stackedWidget->addWidget(m_bindings_widget);
+ m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);
+
+ if (has_settings)
+ {
+ m_settings_widget = new ControllerCustomSettingsWidget(this, m_ui.stackedWidget);
+ m_ui.stackedWidget->addWidget(m_settings_widget);
+ }
+
+ if (has_macros)
+ {
+ m_macros_widget = new ControllerMacroWidget(this);
+ m_ui.stackedWidget->addWidget(m_macros_widget);
+ }
+
+ updateHeaderToolButtons();
// no need to do this on first init, only changes
if (!is_initializing)
m_dialog->updateListDescription(m_port_number, this);
}
-void ControllerBindingWidget::doAutomaticBinding()
+void ControllerBindingWidget::updateHeaderToolButtons()
+{
+ const QWidget* current_widget = m_ui.stackedWidget->currentWidget();
+ const QSignalBlocker bindings_sb(m_ui.bindings);
+ const QSignalBlocker settings_sb(m_ui.settings);
+ const QSignalBlocker macros_sb(m_ui.macros);
+
+ const bool is_bindings = (current_widget == m_bindings_widget);
+ m_ui.bindings->setChecked(is_bindings);
+ m_ui.automaticBinding->setEnabled(is_bindings);
+ m_ui.clearBindings->setEnabled(is_bindings);
+ m_ui.macros->setChecked(current_widget == m_macros_widget);
+ m_ui.settings->setChecked((current_widget == m_settings_widget));
+}
+
+void ControllerBindingWidget::onBindingsClicked()
+{
+ m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);
+ updateHeaderToolButtons();
+}
+
+void ControllerBindingWidget::onSettingsClicked()
+{
+ if (!m_settings_widget)
+ return;
+
+ m_ui.stackedWidget->setCurrentWidget(m_settings_widget);
+ updateHeaderToolButtons();
+}
+
+void ControllerBindingWidget::onMacrosClicked()
+{
+ if (!m_macros_widget)
+ return;
+
+ m_ui.stackedWidget->setCurrentWidget(m_macros_widget);
+ updateHeaderToolButtons();
+}
+
+void ControllerBindingWidget::onAutomaticBindingClicked()
{
QMenu menu(this);
bool added = false;
@@ -118,7 +187,7 @@ void ControllerBindingWidget::doAutomaticBinding()
menu.exec(QCursor::pos());
}
-void ControllerBindingWidget::doClearBindings()
+void ControllerBindingWidget::onClearBindingsClicked()
{
if (QMessageBox::question(QtUtils::GetRootWidget(this), tr("Clear Bindings"),
tr("Are you sure you want to clear all bindings for this controller? This action cannot be undone.")) != QMessageBox::Yes)
@@ -174,6 +243,388 @@ void ControllerBindingWidget::saveAndRefresh()
g_emu_thread->applySettings();
}
+
+//////////////////////////////////////////////////////////////////////////
+
+ControllerMacroWidget::ControllerMacroWidget(ControllerBindingWidget* parent)
+ : QWidget(parent)
+{
+ m_ui.setupUi(this);
+ setWindowTitle(tr("Controller Port %1 Macros").arg(parent->getPortNumber() + 1u));
+ createWidgets(parent);
+}
+
+ControllerMacroWidget::~ControllerMacroWidget() = default;
+
+void ControllerMacroWidget::updateListItem(u32 index)
+{
+ m_ui.portList->item(static_cast(index))
+ ->setText(tr("Macro %1\n%2").arg(index + 1).arg(m_macros[index]->getSummary()));
+}
+
+void ControllerMacroWidget::createWidgets(ControllerBindingWidget* parent)
+{
+ for (u32 i = 0; i < NUM_MACROS; i++)
+ {
+ m_macros[i] = new ControllerMacroEditWidget(this, parent, i);
+ m_ui.container->addWidget(m_macros[i]);
+
+ QListWidgetItem* item = new QListWidgetItem();
+ item->setIcon(QIcon::fromTheme(QStringLiteral("flashlight-line")));
+ m_ui.portList->addItem(item);
+ updateListItem(i);
+ }
+
+ m_ui.portList->setCurrentRow(0);
+ m_ui.container->setCurrentIndex(0);
+
+ connect(m_ui.portList, &QListWidget::currentRowChanged, m_ui.container, &QStackedWidget::setCurrentIndex);
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget,
+ u32 index)
+ : QWidget(parent)
+ , m_parent(parent)
+ , m_bwidget(bwidget)
+ , m_index(index)
+{
+ m_ui.setupUi(this);
+
+ ControllerSettingsDialog* dialog = m_bwidget->getDialog();
+ const std::string& section = m_bwidget->getConfigSection();
+ const PAD::ControllerInfo* cinfo = PAD::GetControllerInfo(m_bwidget->getControllerType());
+ if (!cinfo)
+ {
+ // Shouldn't ever happen.
+ return;
+ }
+
+ // load binds (single string joined by &)
+ const std::string binds_string(
+ dialog->getStringValue(section.c_str(), fmt::format("Macro{}Binds", index + 1u).c_str(), ""));
+ const std::vector buttons_split(StringUtil::SplitString(binds_string, '&', true));
+
+ for (const std::string_view& button : buttons_split)
+ {
+ for (u32 i = 0; i < cinfo->num_bindings; i++)
+ {
+ if (button == cinfo->bindings[i].name)
+ {
+ m_binds.push_back(&cinfo->bindings[i]);
+ break;
+ }
+ }
+ }
+
+ // populate list view
+ for (u32 i = 0; i < cinfo->num_bindings; i++)
+ {
+ const PAD::ControllerBindingInfo& bi = cinfo->bindings[i];
+ if (bi.type == PAD::ControllerBindingType::Motor)
+ continue;
+
+ QListWidgetItem* item = new QListWidgetItem();
+ item->setText(QString::fromUtf8(bi.display_name));
+ item->setCheckState((std::find(m_binds.begin(), m_binds.end(), &bi) != m_binds.end()) ? Qt::Checked :
+ Qt::Unchecked);
+ m_ui.bindList->addItem(item);
+ }
+
+ m_frequency = dialog->getIntValue(section.c_str(), fmt::format("Macro{}Frequency", index + 1u).c_str(), 0);
+ updateFrequencyText();
+
+ m_ui.trigger->initialize(dialog->getProfileSettingsInterface(), section, fmt::format("Macro{}", index + 1u));
+
+ connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); });
+ connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); });
+ connect(m_ui.setFrequency, &QAbstractButton::clicked, this, &ControllerMacroEditWidget::onSetFrequencyClicked);
+ connect(m_ui.bindList, &QListWidget::itemChanged, this, &ControllerMacroEditWidget::updateBinds);
+}
+
+ControllerMacroEditWidget::~ControllerMacroEditWidget() = default;
+
+QString ControllerMacroEditWidget::getSummary() const
+{
+ QString str;
+ for (const PAD::ControllerBindingInfo* bi : m_binds)
+ {
+ if (!str.isEmpty())
+ str += static_cast('/');
+ str += QString::fromUtf8(bi->name);
+ }
+ return str.isEmpty() ? tr("Not Configured") : str;
+}
+
+void ControllerMacroEditWidget::onSetFrequencyClicked()
+{
+ bool okay;
+ int new_freq = QInputDialog::getInt(this, tr("Set Frequency"), tr("Frequency: "), static_cast(m_frequency), 0,
+ std::numeric_limits::max(), 1, &okay);
+ if (!okay)
+ return;
+
+ m_frequency = static_cast(new_freq);
+ updateFrequency();
+}
+
+void ControllerMacroEditWidget::modFrequency(s32 delta)
+{
+ if (delta < 0 && m_frequency == 0)
+ return;
+
+ m_frequency = static_cast(static_cast(m_frequency) + delta);
+ updateFrequency();
+}
+
+void ControllerMacroEditWidget::updateFrequency()
+{
+ m_bwidget->getDialog()->setIntValue(m_bwidget->getConfigSection().c_str(),
+ fmt::format("Macro{}Frequency", m_index + 1u).c_str(),
+ static_cast(m_frequency));
+ updateFrequencyText();
+}
+
+void ControllerMacroEditWidget::updateFrequencyText()
+{
+ if (m_frequency == 0)
+ m_ui.frequencyText->setText(tr("Macro will not repeat."));
+ else
+ m_ui.frequencyText->setText(tr("Macro will toggle buttons every %1 frames.").arg(m_frequency));
+}
+
+void ControllerMacroEditWidget::updateBinds()
+{
+ ControllerSettingsDialog* dialog = m_bwidget->getDialog();
+ const PAD::ControllerInfo* cinfo = PAD::GetControllerInfo(m_bwidget->getControllerType());
+ if (!cinfo)
+ return;
+
+ std::vector new_binds;
+ for (u32 i = 0, bind_index = 0; i < cinfo->num_bindings; i++)
+ {
+ const PAD::ControllerBindingInfo& bi = cinfo->bindings[i];
+ if (bi.type == PAD::ControllerBindingType::Motor)
+ continue;
+
+ const QListWidgetItem* item = m_ui.bindList->item(static_cast(bind_index));
+ bind_index++;
+
+ if (!item)
+ {
+ // shouldn't happen
+ continue;
+ }
+
+ if (item->checkState() == Qt::Checked)
+ new_binds.push_back(&bi);
+ }
+ if (m_binds == new_binds)
+ return;
+
+ m_binds = std::move(new_binds);
+
+ std::string binds_string;
+ for (const PAD::ControllerBindingInfo* bi : m_binds)
+ {
+ if (!binds_string.empty())
+ binds_string.append(" & ");
+ binds_string.append(bi->name);
+ }
+
+ const std::string& section = m_bwidget->getConfigSection();
+ const std::string key(fmt::format("Macro{}Binds", m_index + 1u));
+ if (binds_string.empty())
+ dialog->clearSettingValue(section.c_str(), key.c_str());
+ else
+ dialog->setStringValue(section.c_str(), key.c_str(), binds_string.c_str());
+
+ m_parent->updateListItem(m_index);
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+ControllerCustomSettingsWidget::ControllerCustomSettingsWidget(ControllerBindingWidget* parent, QWidget* parent_widget)
+ : QWidget(parent_widget)
+ , m_parent(parent)
+{
+ const PAD::ControllerInfo* cinfo = PAD::GetControllerInfo(parent->getControllerType());
+ if (!cinfo || cinfo->num_settings == 0)
+ return;
+
+ QGroupBox* gbox = new QGroupBox(tr("%1 Settings").arg(qApp->translate("PAD", cinfo->display_name)), this);
+ QGridLayout* gbox_layout = new QGridLayout(gbox);
+ createSettingWidgets(parent, gbox, gbox_layout, cinfo);
+
+ QVBoxLayout* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->addWidget(gbox);
+
+ QHBoxLayout* bottom_hlayout = new QHBoxLayout();
+ QPushButton* restore_defaults = new QPushButton(tr("Restore Default Settings"), this);
+ restore_defaults->setIcon(QIcon::fromTheme(QStringLiteral("restart-line")));
+ connect(restore_defaults, &QPushButton::clicked, this, &ControllerCustomSettingsWidget::restoreDefaults);
+ bottom_hlayout->addStretch(1);
+ bottom_hlayout->addWidget(restore_defaults);
+ layout->addLayout(bottom_hlayout);
+ layout->addStretch(1);
+}
+
+ControllerCustomSettingsWidget::~ControllerCustomSettingsWidget() = default;
+
+void ControllerCustomSettingsWidget::createSettingWidgets(ControllerBindingWidget* parent, QWidget* widget_parent,
+ QGridLayout* layout, const PAD::ControllerInfo* cinfo)
+{
+ const std::string& section = parent->getConfigSection();
+ SettingsInterface* sif = parent->getDialog()->getProfileSettingsInterface();
+ int current_row = 0;
+
+ for (u32 i = 0; i < cinfo->num_settings; i++)
+ {
+ const PAD::ControllerSettingInfo& si = cinfo->settings[i];
+ std::string key_name = si.key;
+
+ switch (si.type)
+ {
+ case PAD::ControllerSettingInfo::Type::Boolean:
+ {
+ QCheckBox* cb = new QCheckBox(qApp->translate(cinfo->name, si.visible_name), widget_parent);
+ cb->setObjectName(QString::fromUtf8(si.key));
+ ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, cb, section, std::move(key_name),
+ si.BooleanDefaultValue());
+ layout->addWidget(cb, current_row, 0, 1, 4);
+ current_row++;
+ }
+ break;
+
+ case PAD::ControllerSettingInfo::Type::Integer:
+ {
+ QSpinBox* sb = new QSpinBox(widget_parent);
+ sb->setObjectName(QString::fromUtf8(si.key));
+ sb->setMinimum(si.IntegerMinValue());
+ sb->setMaximum(si.IntegerMaxValue());
+ sb->setSingleStep(si.IntegerStepValue());
+ SettingWidgetBinder::BindWidgetToIntSetting(sif, sb, section, std::move(key_name), si.IntegerDefaultValue());
+ layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.visible_name), widget_parent), current_row, 0);
+ layout->addWidget(sb, current_row, 1, 1, 3);
+ current_row++;
+ }
+ break;
+
+ case PAD::ControllerSettingInfo::Type::Float:
+ {
+ QDoubleSpinBox* sb = new QDoubleSpinBox(widget_parent);
+ sb->setObjectName(QString::fromUtf8(si.key));
+ sb->setMinimum(si.FloatMinValue());
+ sb->setMaximum(si.FloatMaxValue());
+ sb->setSingleStep(si.FloatStepValue());
+ SettingWidgetBinder::BindWidgetToFloatSetting(sif, sb, section, std::move(key_name), si.FloatDefaultValue());
+ layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.visible_name), widget_parent), current_row, 0);
+ layout->addWidget(sb, current_row, 1, 1, 3);
+ current_row++;
+ }
+ break;
+
+ case PAD::ControllerSettingInfo::Type::String:
+ {
+ QLineEdit* le = new QLineEdit(widget_parent);
+ le->setObjectName(QString::fromUtf8(si.key));
+ SettingWidgetBinder::BindWidgetToStringSetting(sif, le, section, std::move(key_name), si.StringDefaultValue());
+ layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.visible_name), widget_parent), current_row, 0);
+ layout->addWidget(le, current_row, 1, 1, 3);
+ current_row++;
+ }
+ break;
+
+ case PAD::ControllerSettingInfo::Type::Path:
+ {
+ QLineEdit* le = new QLineEdit(widget_parent);
+ le->setObjectName(QString::fromUtf8(si.key));
+ QPushButton* browse_button = new QPushButton(tr("Browse..."), widget_parent);
+ SettingWidgetBinder::BindWidgetToStringSetting(sif, le, section, std::move(key_name), si.StringDefaultValue());
+ connect(browse_button, &QPushButton::clicked, [this, le]() {
+ QString path = QFileDialog::getOpenFileName(this, tr("Select File"));
+ if (!path.isEmpty())
+ le->setText(path);
+ });
+
+ QHBoxLayout* hbox = new QHBoxLayout();
+ hbox->addWidget(le, 1);
+ hbox->addWidget(browse_button);
+
+ layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.visible_name), widget_parent), current_row, 0);
+ layout->addLayout(hbox, current_row, 1, 1, 3);
+ current_row++;
+ }
+ break;
+ }
+
+ QLabel* label = new QLabel(si.description ? qApp->translate(cinfo->name, si.description) : QString(), widget_parent);
+ label->setWordWrap(true);
+ layout->addWidget(label, current_row++, 0, 1, 4);
+
+ layout->addItem(new QSpacerItem(1, 10, QSizePolicy::Minimum, QSizePolicy::Fixed), current_row++, 0, 1, 4);
+ }
+}
+
+void ControllerCustomSettingsWidget::restoreDefaults()
+{
+ const PAD::ControllerInfo* cinfo = PAD::GetControllerInfo(m_parent->getControllerType());
+ if (!cinfo || cinfo->num_settings == 0)
+ return;
+
+ for (u32 i = 0; i < cinfo->num_settings; i++)
+ {
+ const PAD::ControllerSettingInfo& si = cinfo->settings[i];
+ const QString key(QString::fromStdString(si.key));
+
+ switch (si.type)
+ {
+ case PAD::ControllerSettingInfo::Type::Boolean:
+ {
+ QCheckBox* widget = findChild(QString::fromStdString(si.key));
+ if (widget)
+ widget->setChecked(si.BooleanDefaultValue());
+ }
+ break;
+
+ case PAD::ControllerSettingInfo::Type::Integer:
+ {
+ QSpinBox* widget = findChild(QString::fromStdString(si.key));
+ if (widget)
+ widget->setValue(si.IntegerDefaultValue());
+ }
+ break;
+
+ case PAD::ControllerSettingInfo::Type::Float:
+ {
+ QDoubleSpinBox* widget = findChild(QString::fromStdString(si.key));
+ if (widget)
+ widget->setValue(si.FloatDefaultValue());
+ }
+ break;
+
+ case PAD::ControllerSettingInfo::Type::String:
+ {
+ QLineEdit* widget = findChild(QString::fromStdString(si.key));
+ if (widget)
+ widget->setText(QString::fromUtf8(si.StringDefaultValue()));
+ }
+ break;
+
+ case PAD::ControllerSettingInfo::Type::Path:
+ {
+ QLineEdit* widget = findChild(QString::fromStdString(si.key));
+ if (widget)
+ widget->setText(QString::fromUtf8(si.StringDefaultValue()));
+ }
+ break;
+ }
+ }
+}
+
+
//////////////////////////////////////////////////////////////////////////
ControllerBindingWidget_Base::ControllerBindingWidget_Base(ControllerBindingWidget* parent)
diff --git a/pcsx2-qt/Settings/ControllerBindingWidgets.h b/pcsx2-qt/Settings/ControllerBindingWidgets.h
index c91794039a..a591c659a0 100644
--- a/pcsx2-qt/Settings/ControllerBindingWidgets.h
+++ b/pcsx2-qt/Settings/ControllerBindingWidgets.h
@@ -15,13 +15,20 @@
#pragma once
+#include "PAD/Host/PAD.h"
+
#include
#include "ui_ControllerBindingWidget.h"
#include "ui_ControllerBindingWidget_DualShock2.h"
+#include "ui_ControllerMacroWidget.h"
+#include "ui_ControllerMacroEditWidget.h"
class InputBindingWidget;
class ControllerSettingsDialog;
+class ControllerCustomSettingsWidget;
+class ControllerMacroWidget;
+class ControllerMacroEditWidget;
class ControllerBindingWidget_Base;
class ControllerBindingWidget final : public QWidget
@@ -41,11 +48,15 @@ public:
private Q_SLOTS:
void onTypeChanged();
- void doAutomaticBinding();
- void doClearBindings();
+ void onAutomaticBindingClicked();
+ void onClearBindingsClicked();
+ void onBindingsClicked();
+ void onSettingsClicked();
+ void onMacrosClicked();
private:
void populateControllerTypes();
+ void updateHeaderToolButtons();
void doDeviceAutomaticBinding(const QString& device);
void saveAndRefresh();
@@ -57,9 +68,87 @@ private:
std::string m_controller_type;
u32 m_port_number;
- ControllerBindingWidget_Base* m_current_widget = nullptr;
+ ControllerBindingWidget_Base* m_bindings_widget = nullptr;
+ ControllerCustomSettingsWidget* m_settings_widget = nullptr;
+ ControllerMacroWidget* m_macros_widget = nullptr;
};
+
+//////////////////////////////////////////////////////////////////////////
+
+class ControllerMacroWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ ControllerMacroWidget(ControllerBindingWidget* parent);
+ ~ControllerMacroWidget();
+
+ void updateListItem(u32 index);
+
+private:
+ static constexpr u32 NUM_MACROS = PAD::NUM_MACRO_BUTTONS_PER_CONTROLLER;
+
+ void createWidgets(ControllerBindingWidget* parent);
+
+ Ui::ControllerMacroWidget m_ui;
+ ControllerSettingsDialog* m_dialog;
+ std::array m_macros;
+};
+
+//////////////////////////////////////////////////////////////////////////
+
+class ControllerMacroEditWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget, u32 index);
+ ~ControllerMacroEditWidget();
+
+ QString getSummary() const;
+
+private Q_SLOTS:
+ void onSetFrequencyClicked();
+ void updateBinds();
+
+private:
+ void modFrequency(s32 delta);
+ void updateFrequency();
+ void updateFrequencyText();
+
+ Ui::ControllerMacroEditWidget m_ui;
+
+ ControllerMacroWidget* m_parent;
+ ControllerBindingWidget* m_bwidget;
+ u32 m_index;
+
+ std::vector m_binds;
+ u32 m_frequency = 0;
+};
+
+//////////////////////////////////////////////////////////////////////////
+
+class ControllerCustomSettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ ControllerCustomSettingsWidget(ControllerBindingWidget* parent, QWidget* parent_widget);
+ ~ControllerCustomSettingsWidget();
+
+ void createSettingWidgets(ControllerBindingWidget* parent, QWidget* widget_parent, QGridLayout* layout, const PAD::ControllerInfo* cinfo);
+
+private Q_SLOTS:
+ void restoreDefaults();
+
+private:
+ ControllerBindingWidget* m_parent;
+};
+
+//////////////////////////////////////////////////////////////////////////
+
+
class ControllerBindingWidget_Base : public QWidget
{
Q_OBJECT
diff --git a/pcsx2-qt/Settings/ControllerMacroEditWidget.ui b/pcsx2-qt/Settings/ControllerMacroEditWidget.ui
new file mode 100644
index 0000000000..788a59b355
--- /dev/null
+++ b/pcsx2-qt/Settings/ControllerMacroEditWidget.ui
@@ -0,0 +1,157 @@
+
+
+ ControllerMacroEditWidget
+
+
+
+ 0
+ 0
+ 595
+ 473
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Binds/Buttons
+
+
+
-
+
+
+ Select the buttons which you want to trigger with this macro. All buttons are activated concurrently.
+
+
+ true
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Trigger
+
+
+
-
+
+
+ Select the trigger to activate this macro. This can be a single button, or combination of buttons (chord). Shift-click for multiple triggers.
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+ -
+
+
+ PushButton
+
+
+
+
+
+
+ -
+
+
+ Frequency
+
+
+
-
+
+
-
+
+
+ Macro will toggle every N frames.
+
+
+
+ -
+
+
+ Set...
+
+
+
+ -
+
+
+
+ 0
+ 20
+
+
+
+ Qt::UpArrow
+
+
+
+ -
+
+
+
+ 0
+ 20
+
+
+
+ Qt::DownArrow
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ InputBindingWidget
+ QPushButton
+ Settings/InputBindingWidget.h
+
+
+
+
+
diff --git a/pcsx2-qt/Settings/ControllerMacroWidget.ui b/pcsx2-qt/Settings/ControllerMacroWidget.ui
new file mode 100644
index 0000000000..773ffc4adc
--- /dev/null
+++ b/pcsx2-qt/Settings/ControllerMacroWidget.ui
@@ -0,0 +1,69 @@
+
+
+ ControllerMacroWidget
+
+
+
+ 0
+ 0
+ 799
+ 493
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 0
+
+
+
+
+ 150
+ 16777215
+
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/pcsx2-qt/Settings/ControllerSettingsDialog.cpp b/pcsx2-qt/Settings/ControllerSettingsDialog.cpp
index 242a509d37..d2dd07f697 100644
--- a/pcsx2-qt/Settings/ControllerSettingsDialog.cpp
+++ b/pcsx2-qt/Settings/ControllerSettingsDialog.cpp
@@ -266,6 +266,14 @@ bool ControllerSettingsDialog::getBoolValue(const char* section, const char* key
return Host::GetBaseBoolSettingValue(section, key, default_value);
}
+s32 ControllerSettingsDialog::getIntValue(const char* section, const char* key, s32 default_value) const
+{
+ if (m_profile_interface)
+ return m_profile_interface->GetIntValue(section, key, default_value);
+ else
+ return Host::GetBaseIntSettingValue(section, key, default_value);
+}
+
std::string ControllerSettingsDialog::getStringValue(const char* section, const char* key, const char* default_value) const
{
std::string value;
@@ -291,6 +299,21 @@ void ControllerSettingsDialog::setBoolValue(const char* section, const char* key
}
}
+void ControllerSettingsDialog::setIntValue(const char* section, const char* key, s32 value)
+{
+ if (m_profile_interface)
+ {
+ m_profile_interface->SetIntValue(section, key, value);
+ m_profile_interface->Save();
+ g_emu_thread->reloadGameSettings();
+ }
+ else
+ {
+ QtHost::SetBaseIntSettingValue(section, key, value);
+ g_emu_thread->applySettings();
+ }
+}
+
void ControllerSettingsDialog::setStringValue(const char* section, const char* key, const char* value)
{
if (m_profile_interface)
@@ -301,7 +324,7 @@ void ControllerSettingsDialog::setStringValue(const char* section, const char* k
}
else
{
- QtHost::SetBaseStringSettingValue(key, section, value);
+ QtHost::SetBaseStringSettingValue(section, key, value);
g_emu_thread->applySettings();
}
}
diff --git a/pcsx2-qt/Settings/ControllerSettingsDialog.h b/pcsx2-qt/Settings/ControllerSettingsDialog.h
index 149623cb2d..1569ebb099 100644
--- a/pcsx2-qt/Settings/ControllerSettingsDialog.h
+++ b/pcsx2-qt/Settings/ControllerSettingsDialog.h
@@ -64,8 +64,10 @@ public:
// Helper functions for updating setting values globally or in the profile.
bool getBoolValue(const char* section, const char* key, bool default_value) const;
+ s32 getIntValue(const char* section, const char* key, s32 default_value) const;
std::string getStringValue(const char* section, const char* key, const char* default_value) const;
void setBoolValue(const char* section, const char* key, bool value);
+ void setIntValue(const char* section, const char* key, s32 value);
void setStringValue(const char* section, const char* key, const char* value);
void clearSettingValue(const char* section, const char* key);
diff --git a/pcsx2-qt/Settings/ControllerSettingsDialog.ui b/pcsx2-qt/Settings/ControllerSettingsDialog.ui
index f1e4807824..4b4fdedede 100644
--- a/pcsx2-qt/Settings/ControllerSettingsDialog.ui
+++ b/pcsx2-qt/Settings/ControllerSettingsDialog.ui
@@ -9,8 +9,8 @@
0
0
- 1276
- 672
+ 1300
+ 680
diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj
index 8f29f2eb6a..ab6ca66a78 100644
--- a/pcsx2-qt/pcsx2-qt.vcxproj
+++ b/pcsx2-qt/pcsx2-qt.vcxproj
@@ -335,6 +335,12 @@
+
+ Document
+
+
+ Document
+
diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters
index dcf392a431..5d3fb9c1bb 100644
--- a/pcsx2-qt/pcsx2-qt.vcxproj.filters
+++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters
@@ -405,6 +405,12 @@
Tools\Input Recording
+
+ Settings
+
+
+ Settings
+
diff --git a/pcsx2-qt/resources/icons/black/svg/checkbox-multiple-blank-line.svg b/pcsx2-qt/resources/icons/black/svg/checkbox-multiple-blank-line.svg
new file mode 100644
index 0000000000..0ca7a11c59
--- /dev/null
+++ b/pcsx2-qt/resources/icons/black/svg/checkbox-multiple-blank-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pcsx2-qt/resources/icons/black/svg/flashlight-line.svg b/pcsx2-qt/resources/icons/black/svg/flashlight-line.svg
new file mode 100644
index 0000000000..80452ba28c
--- /dev/null
+++ b/pcsx2-qt/resources/icons/black/svg/flashlight-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pcsx2-qt/resources/icons/white/svg/checkbox-multiple-blank-line.svg b/pcsx2-qt/resources/icons/white/svg/checkbox-multiple-blank-line.svg
new file mode 100644
index 0000000000..672c3d0fa0
--- /dev/null
+++ b/pcsx2-qt/resources/icons/white/svg/checkbox-multiple-blank-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pcsx2-qt/resources/icons/white/svg/flashlight-line.svg b/pcsx2-qt/resources/icons/white/svg/flashlight-line.svg
new file mode 100644
index 0000000000..45924d102e
--- /dev/null
+++ b/pcsx2-qt/resources/icons/white/svg/flashlight-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pcsx2-qt/resources/resources.qrc b/pcsx2-qt/resources/resources.qrc
index 9561d481ff..f2db7c78c5 100644
--- a/pcsx2-qt/resources/resources.qrc
+++ b/pcsx2-qt/resources/resources.qrc
@@ -7,6 +7,7 @@
icons/black/svg/artboard-2-line.svg
icons/black/svg/book-open-line.svg
icons/black/svg/brush-line.svg
+ icons/black/svg/checkbox-multiple-blank-line.svg
icons/black/svg/close-line.svg
icons/black/svg/dashboard-line.svg
icons/black/svg/disc-line.svg
@@ -21,6 +22,7 @@
icons/black/svg/file-search-line.svg
icons/black/svg/file-settings-line.svg
icons/black/svg/filter-line.svg
+ icons/black/svg/flashlight-line.svg
icons/black/svg/flask-line.svg
icons/black/svg/folder-add-line.svg
icons/black/svg/folder-open-line.svg
@@ -59,6 +61,7 @@
icons/white/svg/artboard-2-line.svg
icons/white/svg/book-open-line.svg
icons/white/svg/brush-line.svg
+ icons/white/svg/checkbox-multiple-blank-line.svg
icons/white/svg/close-line.svg
icons/white/svg/dashboard-line.svg
icons/white/svg/disc-line.svg
@@ -73,6 +76,7 @@
icons/white/svg/file-search-line.svg
icons/white/svg/file-settings-line.svg
icons/white/svg/filter-line.svg
+ icons/white/svg/flashlight-line.svg
icons/white/svg/flask-line.svg
icons/white/svg/folder-add-line.svg
icons/white/svg/folder-open-line.svg
diff --git a/pcsx2/PAD/Host/PAD.cpp b/pcsx2/PAD/Host/PAD.cpp
index 19d748bc03..988721a75c 100644
--- a/pcsx2/PAD/Host/PAD.cpp
+++ b/pcsx2/PAD/Host/PAD.cpp
@@ -183,6 +183,62 @@ u8 PADpoll(u8 value)
return pad_poll(value);
}
+const char* PAD::ControllerSettingInfo::StringDefaultValue() const
+{
+ return default_value ? default_value : "";
+}
+
+bool PAD::ControllerSettingInfo::BooleanDefaultValue() const
+{
+ return default_value ? StringUtil::FromChars(default_value).value_or(false) : false;
+}
+
+s32 PAD::ControllerSettingInfo::IntegerDefaultValue() const
+{
+ return default_value ? StringUtil::FromChars(default_value).value_or(0) : 0;
+}
+
+s32 PAD::ControllerSettingInfo::IntegerMinValue() const
+{
+ static constexpr s32 fallback_value = std::numeric_limits::min();
+ return min_value ? StringUtil::FromChars(min_value).value_or(fallback_value) : fallback_value;
+}
+
+s32 PAD::ControllerSettingInfo::IntegerMaxValue() const
+{
+ static constexpr s32 fallback_value = std::numeric_limits::max();
+ return max_value ? StringUtil::FromChars(max_value).value_or(fallback_value) : fallback_value;
+}
+
+s32 PAD::ControllerSettingInfo::IntegerStepValue() const
+{
+ static constexpr s32 fallback_value = 1;
+ return step_value ? StringUtil::FromChars(step_value).value_or(fallback_value) : fallback_value;
+}
+
+float PAD::ControllerSettingInfo::FloatDefaultValue() const
+{
+ return default_value ? StringUtil::FromChars(default_value).value_or(0.0f) : 0.0f;
+}
+
+float PAD::ControllerSettingInfo::FloatMinValue() const
+{
+ static constexpr float fallback_value = std::numeric_limits::min();
+ return min_value ? StringUtil::FromChars(min_value).value_or(fallback_value) : fallback_value;
+}
+
+float PAD::ControllerSettingInfo::FloatMaxValue() const
+{
+ static constexpr float fallback_value = std::numeric_limits::max();
+ return max_value ? StringUtil::FromChars(max_value).value_or(fallback_value) : fallback_value;
+}
+
+float PAD::ControllerSettingInfo::FloatStepValue() const
+{
+ static constexpr float fallback_value = 0.1f;
+ return step_value ? StringUtil::FromChars(step_value).value_or(fallback_value) : fallback_value;
+}
+
std::string PAD::GetConfigSection(u32 pad_index)
{
return fmt::format("Pad{}", pad_index + 1);
@@ -350,9 +406,31 @@ static const PAD::ControllerBindingInfo s_dualshock2_binds[] = {
{"SmallMotor", "Small (High Frequency) Motor", PAD::ControllerBindingType::Motor, GenericInputBinding::SmallMotor},
};
+static const PAD::ControllerSettingInfo s_dualshock2_settings[] = {
+ {PAD::ControllerSettingInfo::Type::Float, "Deadzone", "Analog Deadzone",
+ "Sets the analog stick deadzone, i.e. the fraction of the stick movement which will be ignored.",
+ "0.00", "0.00", "1.00", "0.01"},
+ {PAD::ControllerSettingInfo::Type::Float, "AxisScale", "Analog Sensitivity",
+ "Sets the analog stick axis scaling factor. A value between 1.30 and 1.40 is recommended when using recent "
+ "controllers, e.g. DualShock 4, Xbox One Controller.",
+ "1.33", "0.01", "2.00", "0.01"},
+ {PAD::ControllerSettingInfo::Type::Float, "LargeMotorScale", "Large Motor Vibration Scale",
+ "Increases or decreases the intensity of low frequency vibration sent by the game.",
+ "1.00", "0.00", "2.00", "0.01"},
+ {PAD::ControllerSettingInfo::Type::Float, "SmallMotorScale", "Small Motor Vibration Scale",
+ "Increases or decreases the intensity of high frequency vibration sent by the game.",
+ "1.00", "0.00", "2.00", "0.01"},
+};
+
static const PAD::ControllerInfo s_controller_info[] = {
- {"None", "Not Connected", nullptr, 0, PAD::ControllerType::NotConnected, PAD::VibrationCapabilities::NoVibration},
- {"DualShock2", "DualShock 2", s_dualshock2_binds, std::size(s_dualshock2_binds), PAD::ControllerType::DualShock2, PAD::VibrationCapabilities::LargeSmallMotors},
+ {PAD::ControllerType::NotConnected, "None", "Not Connected",
+ nullptr, 0,
+ nullptr, 0,
+ PAD::VibrationCapabilities::NoVibration},
+ {PAD::ControllerType::DualShock2, "DualShock2", "DualShock 2",
+ s_dualshock2_binds, std::size(s_dualshock2_binds),
+ s_dualshock2_settings, std::size(s_dualshock2_settings),
+ PAD::VibrationCapabilities::LargeSmallMotors},
};
const PAD::ControllerInfo* PAD::GetControllerInfo(ControllerType type)
diff --git a/pcsx2/PAD/Host/PAD.h b/pcsx2/PAD/Host/PAD.h
index 88b22e0d09..f0d6581216 100644
--- a/pcsx2/PAD/Host/PAD.h
+++ b/pcsx2/PAD/Host/PAD.h
@@ -71,13 +71,48 @@ namespace PAD
GenericInputBinding generic_mapping;
};
+ struct ControllerSettingInfo
+ {
+ enum class Type
+ {
+ Boolean,
+ Integer,
+ Float,
+ String,
+ Path,
+ };
+
+ Type type;
+ const char* key;
+ const char* visible_name;
+ const char* description;
+ const char* default_value;
+ const char* min_value;
+ const char* max_value;
+ const char* step_value;
+
+ const char* StringDefaultValue() const;
+ bool BooleanDefaultValue() const;
+ s32 IntegerDefaultValue() const;
+ s32 IntegerMinValue() const;
+ s32 IntegerMaxValue() const;
+ s32 IntegerStepValue() const;
+ float FloatDefaultValue() const;
+ float FloatMinValue() const;
+ float FloatMaxValue() const;
+ float FloatStepValue() const;
+ };
+
+
struct ControllerInfo
{
+ ControllerType type;
const char* name;
const char* display_name;
const ControllerBindingInfo* bindings;
u32 num_bindings;
- ControllerType type;
+ const ControllerSettingInfo* settings;
+ u32 num_settings;
PAD::VibrationCapabilities vibration_caps;
};