diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index 75063a7d2..c4a412b8b 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -12,6 +12,10 @@ add_executable(duckstation-qt
gamelistsettingswidget.ui
gamelistwidget.cpp
gamelistwidget.h
+ hotkeysettingswidget.cpp
+ hotkeysettingswidget.h
+ inputbindingwidgets.cpp
+ inputbindingwidgets.h
main.cpp
mainwindow.cpp
mainwindow.h
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index 9672b9354..613cab401 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -37,6 +37,8 @@
+
+
@@ -53,6 +55,8 @@
+
+
@@ -105,6 +109,8 @@
+
+
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters
index 8fc04a2da..ff3a57363 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj.filters
+++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters
@@ -25,11 +25,15 @@
+
+
+
+
diff --git a/src/duckstation-qt/hotkeysettingswidget.cpp b/src/duckstation-qt/hotkeysettingswidget.cpp
new file mode 100644
index 000000000..e76087927
--- /dev/null
+++ b/src/duckstation-qt/hotkeysettingswidget.cpp
@@ -0,0 +1,63 @@
+#include "hotkeysettingswidget.h"
+#include "core/controller.h"
+#include "core/settings.h"
+#include "inputbindingwidgets.h"
+#include "qthostinterface.h"
+#include "qtutils.h"
+#include
+#include
+#include
+#include
+
+HotkeySettingsWidget::HotkeySettingsWidget(QtHostInterface* host_interface, QWidget* parent /* = nullptr */)
+ : QWidget(parent), m_host_interface(host_interface)
+{
+ createUi();
+}
+
+HotkeySettingsWidget::~HotkeySettingsWidget() = default;
+
+void HotkeySettingsWidget::createUi()
+{
+ QGridLayout* layout = new QGridLayout(this);
+
+ m_tab_widget = new QTabWidget(this);
+
+ createButtons();
+
+ layout->addWidget(m_tab_widget, 0, 0, 1, 1);
+
+ setLayout(layout);
+}
+
+void HotkeySettingsWidget::createButtons()
+{
+ std::vector hotkeys = m_host_interface->getHotkeyList();
+
+ for (const QtHostInterface::HotkeyInfo& hi : hotkeys)
+ {
+ auto iter = m_categories.find(hi.category);
+ if (iter == m_categories.end())
+ {
+ QWidget* container = new QWidget(m_tab_widget);
+ QVBoxLayout* vlayout = new QVBoxLayout(container);
+ QGridLayout* layout = new QGridLayout();
+ layout->setContentsMargins(0, 0, 0, 0);
+ vlayout->addLayout(layout);
+ vlayout->addStretch(1);
+ iter = m_categories.insert(hi.category, Category{container, layout});
+ m_tab_widget->addTab(container, hi.category);
+ }
+
+ QWidget* container = iter->container;
+ QGridLayout* layout = iter->layout;
+ const int layout_count = layout->count() / 2;
+ const int target_column = (layout_count / ROWS_PER_COLUMN) * 2;
+ const int target_row = layout_count % ROWS_PER_COLUMN;
+
+ const QString setting_name = QStringLiteral("Hotkeys/%1").arg(hi.name);
+ layout->addWidget(new QLabel(hi.display_name, container), target_row, target_column);
+ layout->addWidget(new InputButtonBindingWidget(m_host_interface, setting_name, container), target_row,
+ target_column + 1);
+ }
+}
diff --git a/src/duckstation-qt/hotkeysettingswidget.h b/src/duckstation-qt/hotkeysettingswidget.h
new file mode 100644
index 000000000..8a9c9f767
--- /dev/null
+++ b/src/duckstation-qt/hotkeysettingswidget.h
@@ -0,0 +1,39 @@
+#pragma once
+#include "core/types.h"
+#include
+#include
+#include
+#include
+
+class QtHostInterface;
+class QGridLayout;
+
+class HotkeySettingsWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ HotkeySettingsWidget(QtHostInterface* host_interface, QWidget* parent = nullptr);
+ ~HotkeySettingsWidget();
+
+private:
+ enum : u32
+ {
+ ROWS_PER_COLUMN = 10
+ };
+
+ void createUi();
+ void createButtons();
+
+ QtHostInterface* m_host_interface;
+
+ QTabWidget* m_tab_widget;
+
+ struct Category
+ {
+ QWidget* container;
+ QGridLayout* layout;
+ };
+ QMap m_categories;
+};
+
diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp
new file mode 100644
index 000000000..ea7901751
--- /dev/null
+++ b/src/duckstation-qt/inputbindingwidgets.cpp
@@ -0,0 +1,85 @@
+#include "inputbindingwidgets.h"
+#include "core/settings.h"
+#include "qthostinterface.h"
+#include "qtutils.h"
+#include
+#include
+
+InputButtonBindingWidget::InputButtonBindingWidget(QtHostInterface* host_interface, QString setting_name,
+ QWidget* parent)
+ : QPushButton(parent), m_host_interface(host_interface), m_setting_name(std::move(setting_name))
+{
+ 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/inputbindingwidgets.h b/src/duckstation-qt/inputbindingwidgets.h
new file mode 100644
index 000000000..dd0722ff4
--- /dev/null
+++ b/src/duckstation-qt/inputbindingwidgets.h
@@ -0,0 +1,35 @@
+#pragma once
+#include "core/types.h"
+#include
+
+class QTimer;
+
+class QtHostInterface;
+
+class InputButtonBindingWidget : public QPushButton
+{
+ Q_OBJECT
+
+public:
+ InputButtonBindingWidget(QtHostInterface* host_interface, QString setting_name, 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;
+ QTimer* m_input_listen_timer = nullptr;
+ u32 m_input_listen_remaining_seconds = 0;
+};
diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui
index 8d5510876..aa89c0c21 100644
--- a/src/duckstation-qt/mainwindow.ui
+++ b/src/duckstation-qt/mainwindow.ui
@@ -72,8 +72,9 @@
-
+
+
@@ -219,13 +220,13 @@
&Port Settings...
-
+
:/icons/applications-other.png:/icons/applications-other.png
- &CPU Settings...
+ &Hotkey Settings...
diff --git a/src/duckstation-qt/portsettingswidget.cpp b/src/duckstation-qt/portsettingswidget.cpp
index c83736252..203e52bdf 100644
--- a/src/duckstation-qt/portsettingswidget.cpp
+++ b/src/duckstation-qt/portsettingswidget.cpp
@@ -1,6 +1,7 @@
#include "portsettingswidget.h"
#include "core/controller.h"
#include "core/settings.h"
+#include "inputbindingwidgets.h"
#include "qthostinterface.h"
#include "qtutils.h"
#include
@@ -94,7 +95,7 @@ void PortSettingsWidget::createPortBindingSettingsUi(int index, PortSettingsUI*
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);
+ InputButtonBindingWidget* button = new InputButtonBindingWidget(m_host_interface, setting_name, container);
layout->addWidget(label, start_row + current_row, current_column);
layout->addWidget(button, start_row + current_row, current_column + 1);
@@ -129,83 +130,3 @@ void PortSettingsWidget::onControllerTypeChanged(int index)
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
index 151a422f2..911f1766f 100644
--- a/src/duckstation-qt/portsettingswidget.h
+++ b/src/duckstation-qt/portsettingswidget.h
@@ -45,33 +45,3 @@ private:
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 4cadfa96a..2125e29fa 100644
--- a/src/duckstation-qt/qthostinterface.cpp
+++ b/src/duckstation-qt/qthostinterface.cpp
@@ -1,5 +1,6 @@
#include "qthostinterface.h"
#include "YBaseLib/Log.h"
+#include "YBaseLib/String.h"
#include "common/null_audio_stream.h"
#include "core/controller.h"
#include "core/game_list.h"
@@ -198,6 +199,12 @@ void QtHostInterface::doUpdateInputMap()
{
m_keyboard_input_handlers.clear();
+ updateControllerInputMap();
+ updateHotkeyInputMap();
+}
+
+void QtHostInterface::updateControllerInputMap()
+{
for (u32 controller_index = 0; controller_index < 2; controller_index++)
{
const ControllerType ctype = m_settings.controller_types[controller_index];
@@ -212,38 +219,102 @@ void QtHostInterface::doUpdateInputMap()
if (!var.isValid())
continue;
- auto handler = [this, controller_index, button_code](bool pressed) {
+ addButtonToInputMap(var.toString(), [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;
- }
+ });
}
}
}
+std::vector QtHostInterface::getHotkeyList() const
+{
+ std::vector hotkeys = {
+ {QStringLiteral("FastForward"), QStringLiteral("Toggle Fast Forward"), QStringLiteral("General")},
+ {QStringLiteral("Fullscreen"), QStringLiteral("Toggle Fullscreen"), QStringLiteral("General")},
+ {QStringLiteral("Pause"), QStringLiteral("Toggle Pause"), QStringLiteral("General")}};
+
+ for (u32 i = 1; i <= NUM_SAVE_STATE_HOTKEYS; i++)
+ {
+ hotkeys.push_back(
+ {QStringLiteral("LoadState%1").arg(i), QStringLiteral("Load State %1").arg(i), QStringLiteral("Save States")});
+ }
+ for (u32 i = 1; i <= NUM_SAVE_STATE_HOTKEYS; i++)
+ {
+ hotkeys.push_back(
+ {QStringLiteral("SaveState%1").arg(i), QStringLiteral("Save State %1").arg(i), QStringLiteral("Save States")});
+ }
+
+ return hotkeys;
+}
+
+void QtHostInterface::updateHotkeyInputMap()
+{
+ auto hk = [this](const QString& hotkey_name, InputButtonHandler handler) {
+ QVariant var = m_qsettings.value(QStringLiteral("Hotkeys/%1").arg(hotkey_name));
+ if (!var.isValid())
+ return;
+
+ addButtonToInputMap(var.toString(), std::move(handler));
+ };
+
+ hk(QStringLiteral("FastForward"), [this](bool pressed) {
+ m_speed_limiter_temp_disabled = pressed;
+ HostInterface::UpdateSpeedLimiterState();
+ });
+
+ hk(QStringLiteral("Fullscreen"), [this](bool pressed) {
+ if (!pressed)
+ toggleFullscreen();
+ });
+
+ hk(QStringLiteral("Pause"), [this](bool pressed) {
+ if (!pressed)
+ pauseSystem(!m_paused);
+ });
+
+ for (u32 i = 1; i <= NUM_SAVE_STATE_HOTKEYS; i++)
+ {
+ hk(QStringLiteral("LoadState%1").arg(i), [this, i](bool pressed) {
+ if (!pressed)
+ HostInterface::LoadState(TinyString::FromFormat("savestate_%u.bin", i));
+ });
+
+ hk(QStringLiteral("SaveState%1").arg(i), [this, i](bool pressed) {
+ if (!pressed)
+ HostInterface::SaveState(TinyString::FromFormat("savestate_%u.bin", i));
+ });
+ }
+}
+
+void QtHostInterface::addButtonToInputMap(const QString& binding, InputButtonHandler handler)
+{
+ const QString device = binding.section('/', 0, 0);
+ const QString button = binding.section('/', 1, 1);
+ if (device == QStringLiteral("Keyboard"))
+ {
+ std::optional key_id = QtUtils::GetKeyIdForIdentifier(button);
+ if (!key_id.has_value())
+ {
+ qWarning() << "Unknown keyboard key " << button;
+ return;
+ }
+
+ m_keyboard_input_handlers.emplace(key_id.value(), std::move(handler));
+ }
+ else
+ {
+ qWarning() << "Unknown input device: " << device;
+ return;
+ }
+}
+
+void QtHostInterface::updateFullscreen() {}
+
void QtHostInterface::powerOffSystem()
{
if (!isOnWorkerThread())
@@ -295,6 +366,18 @@ void QtHostInterface::pauseSystem(bool paused)
void QtHostInterface::changeDisc(QString new_disc_filename) {}
+void QtHostInterface::toggleFullscreen()
+{
+ if (!isOnWorkerThread())
+ {
+ QMetaObject::invokeMethod(this, "toggleFullscreen", Qt::QueuedConnection);
+ return;
+ }
+
+ m_settings.display_fullscreen = !m_settings.display_fullscreen;
+ updateFullscreen();
+}
+
void QtHostInterface::doBootSystem(QString initial_filename, QString initial_save_state_filename)
{
if (!m_display_window->initializeDeviceContext())
diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h
index d0eeb1506..6a9e6e790 100644
--- a/src/duckstation-qt/qthostinterface.h
+++ b/src/duckstation-qt/qthostinterface.h
@@ -8,6 +8,8 @@
#include
#include