From 8a5e955ba31381c86857e2ca2ab4dbece676280e Mon Sep 17 00:00:00 2001
From: Connor McLaughlin <stenzek@gmail.com>
Date: Wed, 15 Apr 2020 01:44:16 +1000
Subject: [PATCH] Qt: Add input profile support

---
 src/duckstation-qt/inputbindingwidgets.cpp |   6 ++
 src/duckstation-qt/inputbindingwidgets.h   |   1 +
 src/duckstation-qt/portsettingswidget.cpp  | 116 +++++++++++++++++++--
 src/duckstation-qt/portsettingswidget.h    |   9 +-
 src/duckstation-qt/qthostinterface.cpp     |  21 ++++
 src/duckstation-qt/qthostinterface.h       |   8 ++
 6 files changed, 152 insertions(+), 9 deletions(-)

diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp
index 54d64e66a..eeacaaa12 100644
--- a/src/duckstation-qt/inputbindingwidgets.cpp
+++ b/src/duckstation-qt/inputbindingwidgets.cpp
@@ -77,6 +77,12 @@ void InputBindingWidget::clearBinding()
   setText(m_current_binding_value);
 }
 
+void InputBindingWidget::reloadBinding()
+{
+  m_current_binding_value = m_host_interface->getSettingValue(m_setting_name).toString();
+  setText(m_current_binding_value);
+}
+
 void InputBindingWidget::onPressed()
 {
   if (isListeningForInput())
diff --git a/src/duckstation-qt/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h
index c9c0668f4..e4058d098 100644
--- a/src/duckstation-qt/inputbindingwidgets.h
+++ b/src/duckstation-qt/inputbindingwidgets.h
@@ -20,6 +20,7 @@ public:
 public Q_SLOTS:
   void beginRebindAll();
   void clearBinding();
+  void reloadBinding();
 
 protected Q_SLOTS:
   void onPressed();
diff --git a/src/duckstation-qt/portsettingswidget.cpp b/src/duckstation-qt/portsettingswidget.cpp
index e3871845b..31403bd58 100644
--- a/src/duckstation-qt/portsettingswidget.cpp
+++ b/src/duckstation-qt/portsettingswidget.cpp
@@ -7,16 +7,21 @@
 #include "settingwidgetbinder.h"
 #include <QtCore/QSignalBlocker>
 #include <QtCore/QTimer>
+#include <QtGui/QCursor>
 #include <QtGui/QKeyEvent>
 #include <QtWidgets/QFileDialog>
+#include <QtWidgets/QMenu>
 #include <QtWidgets/QMessageBox>
 
 static constexpr char MEMORY_CARD_IMAGE_FILTER[] = "All Memory Card Types (*.mcd *.mcr *.mc)";
+static constexpr char INPUT_PROFILE_FILTER[] = "Input Profiles (*.ini)";
 
 PortSettingsWidget::PortSettingsWidget(QtHostInterface* host_interface, QWidget* parent /* = nullptr */)
   : QWidget(parent), m_host_interface(host_interface)
 {
   createUi();
+
+  connect(host_interface, &QtHostInterface::inputProfileLoaded, this, &PortSettingsWidget::onProfileLoaded);
 }
 
 PortSettingsWidget::~PortSettingsWidget() = default;
@@ -35,6 +40,38 @@ void PortSettingsWidget::createUi()
   setLayout(layout);
 }
 
+void PortSettingsWidget::onProfileLoaded()
+{
+  for (int i = 0; i < static_cast<int>(m_port_ui.size()); i++)
+  {
+    ControllerType ctype = Settings::ParseControllerTypeName(
+                             m_host_interface->getSettingValue(QStringLiteral("Controller%1/Type").arg(i + 1))
+                               .toString()
+                               .toStdString()
+                               .c_str())
+                             .value_or(ControllerType::None);
+
+    {
+      QSignalBlocker blocker(m_port_ui[i].controller_type);
+      m_port_ui[i].controller_type->setCurrentIndex(static_cast<int>(ctype));
+    }
+    createPortBindingSettingsUi(i, &m_port_ui[i], ctype);
+  }
+}
+
+void PortSettingsWidget::reloadBindingButtons()
+{
+  for (PortSettingsUI& ui : m_port_ui)
+  {
+    InputBindingWidget* widget = ui.first_button;
+    while (widget)
+    {
+      widget->reloadBinding();
+      widget = widget->getNextWidget();
+    }
+  }
+}
+
 void PortSettingsWidget::createPortSettingsUi(int index, PortSettingsUI* ui)
 {
   ui->widget = new QWidget(m_tab_widget);
@@ -171,7 +208,6 @@ void PortSettingsWidget::createPortBindingSettingsUi(int index, PortSettingsUI*
     start_row += num_rows;
   }
 
-
   const u32 num_motors = Controller::GetVibrationMotorCount(ctype);
   if (num_motors > 0)
   {
@@ -195,12 +231,23 @@ void PortSettingsWidget::createPortBindingSettingsUi(int index, PortSettingsUI*
 
   layout->addWidget(QtUtils::CreateHorizontalLine(ui->widget), start_row++, 0, 1, 4);
 
+  QHBoxLayout* left_hbox = new QHBoxLayout();
+  QPushButton* load_profile_button = new QPushButton(tr("Load Profile"), ui->widget);
+  connect(load_profile_button, &QPushButton::clicked, this, &PortSettingsWidget::onLoadProfileClicked);
+  left_hbox->addWidget(load_profile_button);
+
+  QPushButton* save_profile_button = new QPushButton(tr("Save Profile"), ui->widget);
+  connect(save_profile_button, &QPushButton::clicked, this, &PortSettingsWidget::onSaveProfileClicked);
+  left_hbox->addWidget(save_profile_button);
+
+  layout->addLayout(left_hbox, start_row, 0, 1, 2, Qt::AlignLeft);
+
   if (first_button)
   {
-    QHBoxLayout* hbox = new QHBoxLayout();
+    QHBoxLayout* right_hbox = new QHBoxLayout();
 
     QPushButton* clear_all_button = new QPushButton(tr("Clear All"), ui->widget);
-    clear_all_button->connect(clear_all_button, &QPushButton::pressed, [this, first_button]() {
+    clear_all_button->connect(clear_all_button, &QPushButton::clicked, [this, first_button]() {
       if (QMessageBox::question(this, tr("Clear Bindings"),
                                 tr("Are you sure you want to clear all bound controls? This cannot be reversed.")) !=
           QMessageBox::Yes)
@@ -217,7 +264,7 @@ void PortSettingsWidget::createPortBindingSettingsUi(int index, PortSettingsUI*
     });
 
     QPushButton* rebind_all_button = new QPushButton(tr("Rebind All"), ui->widget);
-    rebind_all_button->connect(rebind_all_button, &QPushButton::pressed, [this, first_button]() {
+    rebind_all_button->connect(rebind_all_button, &QPushButton::clicked, [this, first_button]() {
       if (QMessageBox::question(this, tr("Clear Bindings"), tr("Do you want to clear all currently-bound controls?")) ==
           QMessageBox::Yes)
       {
@@ -232,9 +279,9 @@ void PortSettingsWidget::createPortBindingSettingsUi(int index, PortSettingsUI*
       first_button->beginRebindAll();
     });
 
-    hbox->addWidget(clear_all_button);
-    hbox->addWidget(rebind_all_button);
-    layout->addLayout(hbox, start_row, 2, 1, 2, Qt::AlignRight);
+    right_hbox->addWidget(clear_all_button);
+    right_hbox->addWidget(rebind_all_button);
+    layout->addLayout(right_hbox, start_row, 2, 1, 2, Qt::AlignRight);
   }
 
   if (ui->button_binding_container)
@@ -243,13 +290,14 @@ void PortSettingsWidget::createPortBindingSettingsUi(int index, PortSettingsUI*
     Q_ASSERT(old_item != nullptr);
 
     delete old_item;
-    delete ui->button_binding_container;
+    ui->button_binding_container->deleteLater();
   }
   else
   {
     ui->layout->addWidget(container);
   }
   ui->button_binding_container = container;
+  ui->first_button = first_button;
 }
 
 void PortSettingsWidget::onControllerTypeChanged(int index)
@@ -282,3 +330,55 @@ void PortSettingsWidget::onEjectMemoryCardClicked(int index)
   m_host_interface->removeSettingValue(QStringLiteral("MemoryCards/Card%1Path").arg(index + 1));
   m_host_interface->applySettings();
 }
+
+void PortSettingsWidget::onLoadProfileClicked()
+{
+  const auto profile_names = m_host_interface->getInputProfileList();
+
+  QMenu menu;
+  for (const auto& [name, path] : profile_names)
+  {
+    QAction* action = menu.addAction(QString::fromStdString(name));
+    QString path_qstr = QString::fromStdString(path);
+    connect(action, &QAction::triggered, [this, path_qstr]() { m_host_interface->applyInputProfile(path_qstr); });
+  }
+
+  if (!profile_names.empty())
+    menu.addSeparator();
+
+  QAction* browse = menu.addAction(tr("Browse..."));
+  connect(browse, &QAction::triggered, [this]() {
+    QString path =
+      QFileDialog::getOpenFileName(this, tr("Select path to input profile ini"), QString(), tr(INPUT_PROFILE_FILTER));
+    if (!path.isEmpty())
+      m_host_interface->applyInputProfile(path);
+  });
+
+  menu.exec(QCursor::pos());
+}
+
+void PortSettingsWidget::onSaveProfileClicked()
+{
+  const auto profile_names = m_host_interface->getInputProfileList();
+
+  QMenu menu;
+  for (const auto& [name, path] : profile_names)
+  {
+    QAction* action = menu.addAction(QString::fromStdString(name));
+    QString path_qstr = QString::fromStdString(path);
+    connect(action, &QAction::triggered, [this, path_qstr]() { m_host_interface->saveInputProfile(path_qstr); });
+  }
+
+  if (!profile_names.empty())
+    menu.addSeparator();
+
+  QAction* browse = menu.addAction(tr("Browse..."));
+  connect(browse, &QAction::triggered, [this]() {
+    QString path =
+      QFileDialog::getSaveFileName(this, tr("Select path to input profile ini"), QString(), tr(INPUT_PROFILE_FILTER));
+    if (!path.isEmpty())
+      m_host_interface->saveInputProfile(path);
+  });
+
+  menu.exec(QCursor::pos());
+}
diff --git a/src/duckstation-qt/portsettingswidget.h b/src/duckstation-qt/portsettingswidget.h
index f540c5908..073c3fda9 100644
--- a/src/duckstation-qt/portsettingswidget.h
+++ b/src/duckstation-qt/portsettingswidget.h
@@ -13,7 +13,7 @@
 class QTimer;
 
 class QtHostInterface;
-class InputButtonBindingWidget;
+class InputBindingWidget;
 
 class PortSettingsWidget : public QWidget
 {
@@ -23,6 +23,9 @@ public:
   PortSettingsWidget(QtHostInterface* host_interface, QWidget* parent = nullptr);
   ~PortSettingsWidget();
 
+private Q_SLOTS:
+  void onProfileLoaded();
+
 private:
   QtHostInterface* m_host_interface;
 
@@ -35,14 +38,18 @@ private:
     QComboBox* controller_type;
     QLineEdit* memory_card_path;
     QWidget* button_binding_container;
+    InputBindingWidget* first_button;
   };
 
   void createUi();
+  void reloadBindingButtons();
   void createPortSettingsUi(int index, PortSettingsUI* ui);
   void createPortBindingSettingsUi(int index, PortSettingsUI* ui, ControllerType ctype);
   void onControllerTypeChanged(int index);
   void onBrowseMemoryCardPathClicked(int index);
   void onEjectMemoryCardClicked(int index);
+  void onLoadProfileClicked();
+  void onSaveProfileClicked();
 
   std::array<PortSettingsUI, 2> m_port_ui = {};
 };
diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp
index 8281388a7..900772d22 100644
--- a/src/duckstation-qt/qthostinterface.cpp
+++ b/src/duckstation-qt/qthostinterface.cpp
@@ -460,6 +460,27 @@ void QtHostInterface::updateInputMap()
   CommonHostInterface::UpdateInputMap(si);
 }
 
+void QtHostInterface::applyInputProfile(const QString& profile_path)
+{
+  if (!isOnWorkerThread())
+  {
+    QMetaObject::invokeMethod(this, "applyInputProfile", Qt::QueuedConnection, Q_ARG(const QString&, profile_path));
+    return;
+  }
+
+  std::lock_guard<std::recursive_mutex> lock(m_qsettings_mutex);
+  QtSettingsInterface si(m_qsettings.get());
+  ApplyInputProfile(profile_path.toUtf8().data(), si);
+  emit inputProfileLoaded();
+}
+
+void QtHostInterface::saveInputProfile(const QString& profile_name)
+{
+  std::lock_guard<std::recursive_mutex> lock(m_qsettings_mutex);
+  QtSettingsInterface si(m_qsettings.get());
+  SaveInputProfile(profile_name.toUtf8().data(), si);
+}
+
 void QtHostInterface::powerOffSystem()
 {
   if (!isOnWorkerThread())
diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h
index bc80f073d..ba226e755 100644
--- a/src/duckstation-qt/qthostinterface.h
+++ b/src/duckstation-qt/qthostinterface.h
@@ -72,6 +72,12 @@ public:
   /// Fills menu with save state info and handlers.
   void populateGameListContextMenu(const char* game_code, QWidget* parent_window, QMenu* menu);
 
+  ALWAYS_INLINE std::vector<std::pair<std::string, std::string>> getInputProfileList() const
+  {
+    return GetInputProfileList();
+  }
+  void saveInputProfile(const QString& profile_path);
+
 Q_SIGNALS:
   void errorReported(const QString& message);
   void messageReported(const QString& message);
@@ -90,11 +96,13 @@ Q_SIGNALS:
                                         float worst_frame_time);
   void runningGameChanged(const QString& filename, const QString& game_code, const QString& game_title);
   void exitRequested();
+  void inputProfileLoaded();
 
 public Q_SLOTS:
   void setDefaultSettings();
   void applySettings();
   void updateInputMap();
+  void applyInputProfile(const QString& profile_path);
   void handleKeyEvent(int key, bool pressed);
   void bootSystem(const SystemBootParameters& params);
   void resumeSystemFromState(const QString& filename, bool boot_on_failure);