diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index eec9bf139..3f9f16d9f 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -18,6 +18,8 @@ add_executable(duckstation-qt
mainwindow.ui
opengldisplaywindow.cpp
opengldisplaywindow.h
+ portsettingswidget.cpp
+ portsettingswidget.h
qthostinterface.cpp
qthostinterface.h
qtsettingsinterface.cpp
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index c2479fb40..38e54dc7d 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -41,12 +41,14 @@
+
+
@@ -100,6 +102,7 @@
+
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
index 38de9472d..eea141aec 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj.filters
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -19,6 +19,8 @@
+
+
@@ -41,6 +43,7 @@
+
diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp
index 4e6956d3d..bca619558 100644
--- a/src/duckstation-qt/mainwindow.cpp
+++ b/src/duckstation-qt/mainwindow.cpp
@@ -159,6 +159,8 @@ void MainWindow::connectSignals()
connect(m_ui.actionSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::Count); });
connect(m_ui.actionGameListSettings, &QAction::triggered,
[this]() { doSettings(SettingsDialog::Category::GameListSettings); });
+ connect(m_ui.actionPortSettings, &QAction::triggered,
+ [this]() { doSettings(SettingsDialog::Category::PortSettings); });
connect(m_ui.actionCPUSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::CPUSettings); });
connect(m_ui.actionGPUSettings, &QAction::triggered, [this]() { doSettings(SettingsDialog::Category::GPUSettings); });
connect(m_ui.actionAudioSettings, &QAction::triggered,
diff --git a/src/duckstation-qt/opengldisplaywindow.cpp b/src/duckstation-qt/opengldisplaywindow.cpp
index ba1c72402..c1e0984d9 100644
--- a/src/duckstation-qt/opengldisplaywindow.cpp
+++ b/src/duckstation-qt/opengldisplaywindow.cpp
@@ -1,6 +1,8 @@
#include "opengldisplaywindow.h"
#include "YBaseLib/Assert.h"
#include "YBaseLib/Log.h"
+#include "qthostinterface.h"
+#include
#include
#include
Log_SetChannel(OpenGLDisplayWindow);
@@ -56,7 +58,8 @@ private:
u32 m_height;
};
-OpenGLDisplayWindow::OpenGLDisplayWindow(QWindow* parent) : QWindow(parent)
+OpenGLDisplayWindow::OpenGLDisplayWindow(QtHostInterface* host_interface, QWindow* parent)
+ : QWindow(parent), m_host_interface(host_interface)
{
setSurfaceType(QWindow::OpenGLSurface);
}
@@ -151,6 +154,16 @@ std::tuple OpenGLDisplayWindow::GetWindowSize() const
void OpenGLDisplayWindow::WindowResized() {}
+void OpenGLDisplayWindow::keyPressEvent(QKeyEvent* event)
+{
+ m_host_interface->handleKeyEvent(event->key(), true);
+}
+
+void OpenGLDisplayWindow::keyReleaseEvent(QKeyEvent* event)
+{
+ m_host_interface->handleKeyEvent(event->key(), false);
+}
+
const char* OpenGLDisplayWindow::GetGLSLVersionString() const
{
return m_is_gles ? "#version 300 es" : "#version 130\n";
@@ -278,7 +291,7 @@ bool OpenGLDisplayWindow::initializeGLContext()
return false;
}
-#if 1
+#if 0
if (GLAD_GL_KHR_debug)
{
glad_glDebugMessageCallbackKHR(GLDebugCallback, nullptr);
diff --git a/src/duckstation-qt/opengldisplaywindow.h b/src/duckstation-qt/opengldisplaywindow.h
index 0749fb2a3..516ac7543 100644
--- a/src/duckstation-qt/opengldisplaywindow.h
+++ b/src/duckstation-qt/opengldisplaywindow.h
@@ -9,12 +9,14 @@
#include
#include
+class QtHostInterface;
+
class OpenGLDisplayWindow final : public QWindow, public HostDisplay
{
Q_OBJECT
public:
- explicit OpenGLDisplayWindow(QWindow* parent);
+ OpenGLDisplayWindow(QtHostInterface* host_interface, QWindow* parent);
~OpenGLDisplayWindow();
bool createGLContext(QThread* worker_thread);
@@ -43,6 +45,10 @@ public:
std::tuple GetWindowSize() const override;
void WindowResized() override;
+protected:
+ void keyPressEvent(QKeyEvent* event);
+ void keyReleaseEvent(QKeyEvent* event);
+
private:
const char* GetGLSLVersionString() const;
std::string GetGLSLVersionHeader() const;
@@ -53,6 +59,8 @@ private:
void Render();
void RenderDisplay();
+ QtHostInterface* m_host_interface;
+
QOpenGLContext* m_gl_context = nullptr;
GL::Program m_display_program;
diff --git a/src/duckstation-qt/portsettingswidget.cpp b/src/duckstation-qt/portsettingswidget.cpp
new file mode 100644
index 000000000..c83736252
--- /dev/null
+++ b/src/duckstation-qt/portsettingswidget.cpp
@@ -0,0 +1,211 @@
+#include "portsettingswidget.h"
+#include "core/controller.h"
+#include "core/settings.h"
+#include "qthostinterface.h"
+#include "qtutils.h"
+#include
+#include
+
+PortSettingsWidget::PortSettingsWidget(QtHostInterface* host_interface, QWidget* parent /* = nullptr */)
+ : QWidget(parent), m_host_interface(host_interface)
+{
+ createUi();
+}
+
+PortSettingsWidget::~PortSettingsWidget() = default;
+
+void PortSettingsWidget::createUi()
+{
+ QGridLayout* layout = new QGridLayout(this);
+
+ m_tab_widget = new QTabWidget(this);
+ for (int i = 0; i < static_cast(m_port_ui.size()); i++)
+ createPortSettingsUi(i, &m_port_ui[i]);
+
+ layout->addWidget(m_tab_widget, 0, 0, 1, 1);
+
+ setLayout(layout);
+}
+
+void PortSettingsWidget::createPortSettingsUi(int index, PortSettingsUI* ui)
+{
+ const Settings& settings = m_host_interface->GetCoreSettings();
+
+ ui->widget = new QWidget(m_tab_widget);
+ ui->layout = new QVBoxLayout(ui->widget);
+
+ QHBoxLayout* memory_card_layout = new QHBoxLayout();
+ ui->memory_card_path = new QLineEdit(QString::fromStdString(settings.memory_card_paths[index]), ui->widget);
+ memory_card_layout->addWidget(ui->memory_card_path);
+ ui->memory_card_path_browse = new QPushButton(tr("Browse..."), ui->widget);
+ memory_card_layout->addWidget(ui->memory_card_path_browse);
+ ui->layout->addWidget(new QLabel(tr("Memory Card Path:"), ui->widget));
+ ui->layout->addLayout(memory_card_layout);
+
+ ui->controller_type = new QComboBox(ui->widget);
+ for (int i = 0; i < static_cast(ControllerType::Count); i++)
+ {
+ ui->controller_type->addItem(
+ QString::fromLocal8Bit(Settings::GetControllerTypeDisplayName(static_cast(i))));
+ }
+ ui->controller_type->setCurrentIndex(static_cast(settings.controller_types[index]));
+ connect(ui->controller_type, static_cast(&QComboBox::currentIndexChanged),
+ [this, index]() { onControllerTypeChanged(index); });
+ ui->layout->addWidget(new QLabel(tr("Controller Type:"), ui->widget));
+ ui->layout->addWidget(ui->controller_type);
+
+ createPortBindingSettingsUi(index, ui);
+
+ ui->layout->addStretch(1);
+
+ ui->widget->setLayout(ui->layout);
+
+ m_tab_widget->addTab(ui->widget, tr("Port %1").arg(index + 1));
+}
+
+void PortSettingsWidget::createPortBindingSettingsUi(int index, PortSettingsUI* ui)
+{
+ QWidget* container = new QWidget(ui->widget);
+ QGridLayout* layout = new QGridLayout(container);
+ layout->setContentsMargins(0, 0, 0, 0);
+ const ControllerType ctype = m_host_interface->GetCoreSettings().controller_types[index];
+ const auto buttons = Controller::GetButtonNames(ctype);
+
+ if (!buttons.empty())
+ {
+ QFrame* line = new QFrame(container);
+ line->setFrameShape(QFrame::HLine);
+ line->setFrameShadow(QFrame::Sunken);
+ layout->addWidget(line, 0, 0, 1, 4);
+ layout->addWidget(new QLabel(tr("Button Bindings:"), container), 1, 0, 1, 4);
+
+ const int start_row = 2;
+ const int num_rows = (static_cast(buttons.size()) + 1) / 2;
+ int current_row = 0;
+ int current_column = 0;
+ for (const auto& [button_name, button_code] : buttons)
+ {
+ if (current_row == num_rows)
+ {
+ current_row = 0;
+ current_column += 2;
+ }
+
+ const QString button_name_q = QString::fromStdString(button_name);
+ const QString setting_name = QStringLiteral("Controller%1/Button%2").arg(index + 1).arg(button_name_q);
+ QLabel* label = new QLabel(button_name_q, container);
+ InputButtonBindingWidget* button = new InputButtonBindingWidget(m_host_interface, setting_name, ctype, container);
+ layout->addWidget(label, start_row + current_row, current_column);
+ layout->addWidget(button, start_row + current_row, current_column + 1);
+
+ current_row++;
+ }
+ }
+
+ if (ui->button_binding_container)
+ {
+ QLayoutItem* old_item = ui->layout->replaceWidget(ui->button_binding_container, container);
+ Q_ASSERT(old_item != nullptr);
+
+ delete old_item;
+ delete ui->button_binding_container;
+ }
+ else
+ {
+ ui->layout->addWidget(container);
+ }
+ ui->button_binding_container = container;
+}
+
+void PortSettingsWidget::onControllerTypeChanged(int index)
+{
+ const int type_index = m_port_ui[index].controller_type->currentIndex();
+ if (type_index < 0 || type_index >= static_cast(ControllerType::Count))
+ return;
+
+ m_host_interface->GetCoreSettings().controller_types[index] = static_cast(type_index);
+ m_host_interface->getQSettings().setValue(
+ QStringLiteral("Controller%1/Type").arg(index + 1),
+ QString::fromStdString(Settings::GetControllerTypeName(static_cast(type_index))));
+ createPortBindingSettingsUi(index, &m_port_ui[index]);
+}
+
+InputButtonBindingWidget::InputButtonBindingWidget(QtHostInterface* host_interface, QString setting_name,
+ ControllerType controller_type, QWidget* parent)
+ : QPushButton(parent), m_host_interface(host_interface), m_setting_name(std::move(setting_name)),
+ m_controller_type(controller_type)
+{
+ m_current_binding_value = m_host_interface->getQSettings().value(m_setting_name).toString();
+ setText(m_current_binding_value);
+
+ connect(this, &QPushButton::pressed, this, &InputButtonBindingWidget::onPressed);
+}
+
+InputButtonBindingWidget::~InputButtonBindingWidget() = default;
+
+void InputButtonBindingWidget::keyPressEvent(QKeyEvent* event)
+{
+ // ignore the key press if we're listening for input
+ if (isListeningForInput())
+ return;
+
+ QPushButton::keyPressEvent(event);
+}
+
+void InputButtonBindingWidget::keyReleaseEvent(QKeyEvent* event)
+{
+ if (!isListeningForInput())
+ {
+ QPushButton::keyReleaseEvent(event);
+ return;
+ }
+
+ QString key_name = QtUtils::GetKeyIdentifier(event->key());
+ if (!key_name.isEmpty())
+ {
+ // TODO: Update input map
+ m_current_binding_value = QStringLiteral("Keyboard/%1").arg(key_name);
+ m_host_interface->getQSettings().setValue(m_setting_name, m_current_binding_value);
+ }
+
+ stopListeningForInput();
+}
+
+void InputButtonBindingWidget::onPressed()
+{
+ if (isListeningForInput())
+ stopListeningForInput();
+
+ startListeningForInput();
+}
+
+void InputButtonBindingWidget::onInputListenTimerTimeout()
+{
+ m_input_listen_remaining_seconds--;
+ if (m_input_listen_remaining_seconds == 0)
+ {
+ stopListeningForInput();
+ return;
+ }
+
+ setText(tr("Push Button... [%1]").arg(m_input_listen_remaining_seconds));
+}
+
+void InputButtonBindingWidget::startListeningForInput()
+{
+ m_input_listen_timer = new QTimer(this);
+ m_input_listen_timer->setSingleShot(false);
+ m_input_listen_timer->start(1000);
+
+ m_input_listen_timer->connect(m_input_listen_timer, &QTimer::timeout, this,
+ &InputButtonBindingWidget::onInputListenTimerTimeout);
+ m_input_listen_remaining_seconds = 5;
+ setText(tr("Push Button... [%1]").arg(m_input_listen_remaining_seconds));
+}
+
+void InputButtonBindingWidget::stopListeningForInput()
+{
+ setText(m_current_binding_value);
+ delete m_input_listen_timer;
+ m_input_listen_timer = nullptr;
+}
diff --git a/src/duckstation-qt/portsettingswidget.h b/src/duckstation-qt/portsettingswidget.h
new file mode 100644
index 000000000..151a422f2
--- /dev/null
+++ b/src/duckstation-qt/portsettingswidget.h
@@ -0,0 +1,77 @@
+#pragma once
+#include "core/types.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class QTimer;
+
+class QtHostInterface;
+class InputButtonBindingWidget;
+
+class PortSettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ PortSettingsWidget(QtHostInterface* host_interface, QWidget* parent = nullptr);
+ ~PortSettingsWidget();
+
+private:
+ QtHostInterface* m_host_interface;
+
+ QTabWidget* m_tab_widget;
+
+ struct PortSettingsUI
+ {
+ QWidget* widget;
+ QVBoxLayout* layout;
+ QComboBox* controller_type;
+ QLineEdit* memory_card_path;
+ QPushButton* memory_card_path_browse;
+ QWidget* button_binding_container;
+ };
+
+ void createUi();
+ void createPortSettingsUi(int index, PortSettingsUI* ui);
+ void createPortBindingSettingsUi(int index, PortSettingsUI* ui);
+ void onControllerTypeChanged(int index);
+
+ std::array m_port_ui = {};
+};
+
+class InputButtonBindingWidget : public QPushButton
+{
+ Q_OBJECT
+
+public:
+ InputButtonBindingWidget(QtHostInterface* host_interface, QString setting_name, ControllerType controller_type,
+ QWidget* parent);
+ ~InputButtonBindingWidget();
+
+protected:
+ void keyPressEvent(QKeyEvent* event) override;
+ void keyReleaseEvent(QKeyEvent* event) override;
+
+private Q_SLOTS:
+ void onPressed();
+ void onInputListenTimerTimeout();
+
+private:
+ bool isListeningForInput() const { return m_input_listen_timer != nullptr; }
+ void startListeningForInput();
+ void stopListeningForInput();
+
+ QtHostInterface* m_host_interface;
+ QString m_setting_name;
+ QString m_current_binding_value;
+ ControllerType m_controller_type;
+ QTimer* m_input_listen_timer = nullptr;
+ u32 m_input_listen_remaining_seconds = 0;
+};
diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp
index b7d36548a..98074e296 100644
--- a/src/duckstation-qt/qthostinterface.cpp
+++ b/src/duckstation-qt/qthostinterface.cpp
@@ -1,11 +1,14 @@
#include "qthostinterface.h"
#include "YBaseLib/Log.h"
#include "common/null_audio_stream.h"
+#include "core/controller.h"
#include "core/game_list.h"
#include "core/gpu.h"
#include "core/system.h"
#include "qtsettingsinterface.h"
+#include "qtutils.h"
#include
+#include
#include
#include
Log_SetChannel(QtHostInterface);
@@ -15,6 +18,7 @@ QtHostInterface::QtHostInterface(QObject* parent)
{
checkSettings();
createGameList();
+ doUpdateInputMap();
createThread();
}
@@ -37,6 +41,24 @@ void QtHostInterface::ReportMessage(const char* message)
void QtHostInterface::setDefaultSettings()
{
m_settings.SetDefaults();
+
+ // default input settings for Qt
+ m_settings.controller_types[0] = ControllerType::DigitalController;
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonUp"), QStringLiteral("Keyboard/W"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonDown"), QStringLiteral("Keyboard/S"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonLeft"), QStringLiteral("Keyboard/A"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonRight"), QStringLiteral("Keyboard/D"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonSelect"), QStringLiteral("Keyboard/Backspace"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonStart"), QStringLiteral("Keyboard/Return"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonTriangle"), QStringLiteral("Keyboard/8"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonCross"), QStringLiteral("Keyboard/2"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonSquare"), QStringLiteral("Keyboard/4"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonCircle"), QStringLiteral("Keyboard/6"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonL1"), QStringLiteral("Keyboard/Q"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonL2"), QStringLiteral("Keyboard/1"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonR1"), QStringLiteral("Keyboard/E"));
+ m_qsettings.setValue(QStringLiteral("Controller1/ButtonR2"), QStringLiteral("Keyboard/3"));
+
updateQSettings();
}
@@ -107,7 +129,7 @@ void QtHostInterface::refreshGameList(bool invalidate_cache /*= false*/)
QWidget* QtHostInterface::createDisplayWidget(QWidget* parent)
{
- m_opengl_display_window = new OpenGLDisplayWindow(nullptr);
+ m_opengl_display_window = new OpenGLDisplayWindow(this, nullptr);
m_display.release();
m_display = std::unique_ptr(static_cast(m_opengl_display_window));
return QWidget::createWindowContainer(m_opengl_display_window, parent);
@@ -134,6 +156,87 @@ void QtHostInterface::bootSystem(QString initial_filename, QString initial_save_
Q_ARG(QString, initial_save_state_filename));
}
+void QtHostInterface::handleKeyEvent(int key, bool pressed)
+{
+ if (!isOnWorkerThread())
+ {
+ QMetaObject::invokeMethod(this, "doHandleKeyEvent", Qt::QueuedConnection, Q_ARG(int, key), Q_ARG(bool, pressed));
+ return;
+ }
+
+ doHandleKeyEvent(key, pressed);
+}
+
+void QtHostInterface::doHandleKeyEvent(int key, bool pressed)
+{
+ const auto iter = m_keyboard_input_handlers.find(key);
+ if (iter == m_keyboard_input_handlers.end())
+ return;
+
+ iter->second(pressed);
+}
+
+void QtHostInterface::updateInputMap()
+{
+ if (!isOnWorkerThread())
+ {
+ QMetaObject::invokeMethod(this, "doUpdateInputMap", Qt::QueuedConnection);
+ return;
+ }
+
+ doUpdateInputMap();
+}
+
+void QtHostInterface::doUpdateInputMap()
+{
+ m_keyboard_input_handlers.clear();
+
+ for (u32 controller_index = 0; controller_index < 2; controller_index++)
+ {
+ const ControllerType ctype = m_settings.controller_types[controller_index];
+ if (ctype == ControllerType::None)
+ continue;
+
+ const auto button_names = Controller::GetButtonNames(ctype);
+ for (const auto& [button_name, button_code] : button_names)
+ {
+ QVariant var = m_qsettings.value(
+ QStringLiteral("Controller%1/Button%2").arg(controller_index + 1).arg(QString::fromStdString(button_name)));
+ if (!var.isValid())
+ continue;
+
+ auto handler = [this, controller_index, button_code](bool pressed) {
+ if (!m_system)
+ return;
+
+ Controller* controller = m_system->GetController(controller_index);
+ if (controller)
+ controller->SetButtonState(button_code, pressed);
+ };
+
+ const QString value = var.toString();
+ const QString device = value.section('/', 0, 0);
+ const QString button = value.section('/', 1, 1);
+ if (device == QStringLiteral("Keyboard"))
+ {
+ std::optional key_id = QtUtils::GetKeyIdForIdentifier(button);
+ if (!key_id.has_value())
+ {
+ qWarning() << "Unknown keyboard key " << button;
+ continue;
+ }
+
+ m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler));
+ }
+ else
+ {
+ qWarning() << "Unknown input device: " << device;
+ continue;
+ }
+ }
+ }
+}
+
void QtHostInterface::powerOffSystem()
{
if (!isOnWorkerThread())
diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h
index d6034bba1..024b96c3a 100644
--- a/src/duckstation-qt/qthostinterface.h
+++ b/src/duckstation-qt/qthostinterface.h
@@ -1,11 +1,13 @@
#pragma once
-#include
-#include
-#include
-#include
-#include
#include "core/host_interface.h"
#include "opengldisplaywindow.h"
+#include
+#include
+#include
+#include
+#include
+#include