From 57a4a2ae0fc35906723ffc9f788f3cf7dd9c4ba5 Mon Sep 17 00:00:00 2001
From: Adityarup Laha <30696515+adityaruplaha@users.noreply.github.com>
Date: Sat, 16 Feb 2019 16:19:29 +0100
Subject: [PATCH] yuzu: Make hotkeys configurable via the GUI

* Adds a new Hotkeys tab in the Controls group.
* Double-click a Hotkey to rebind it.
---
 src/yuzu/CMakeLists.txt                       |   6 +-
 src/yuzu/applets/profile_select.cpp           |   1 +
 src/yuzu/applets/profile_select.h             |   2 +-
 src/yuzu/configuration/config.cpp             |  59 ++++++---
 src/yuzu/configuration/config.h               |   3 +
 src/yuzu/configuration/configure.ui           |  19 ++-
 src/yuzu/configuration/configure_dialog.cpp   |  16 ++-
 src/yuzu/configuration/configure_dialog.h     |   3 +-
 src/yuzu/configuration/configure_general.cpp  |   4 -
 src/yuzu/configuration/configure_general.h    |   1 -
 src/yuzu/configuration/configure_general.ui   |  24 ----
 src/yuzu/configuration/configure_hotkeys.cpp  | 121 ++++++++++++++++++
 src/yuzu/configuration/configure_hotkeys.h    |  48 +++++++
 src/yuzu/configuration/configure_hotkeys.ui   |  42 ++++++
 src/yuzu/hotkeys.cpp                          |  73 ++++-------
 src/yuzu/hotkeys.h                            |  42 +++---
 src/yuzu/hotkeys.ui                           |  46 -------
 src/yuzu/main.cpp                             |  53 ++++----
 src/yuzu/main.h                               |   2 +-
 src/yuzu/ui_settings.cpp                      |   1 -
 src/yuzu/ui_settings.h                        |   7 +-
 .../util/sequence_dialog/sequence_dialog.cpp  |  37 ++++++
 .../util/sequence_dialog/sequence_dialog.h    |  24 ++++
 23 files changed, 426 insertions(+), 208 deletions(-)
 create mode 100644 src/yuzu/configuration/configure_hotkeys.cpp
 create mode 100644 src/yuzu/configuration/configure_hotkeys.h
 create mode 100644 src/yuzu/configuration/configure_hotkeys.ui
 delete mode 100644 src/yuzu/hotkeys.ui
 create mode 100644 src/yuzu/util/sequence_dialog/sequence_dialog.cpp
 create mode 100644 src/yuzu/util/sequence_dialog/sequence_dialog.h

diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 4cab599b4d..732a1bf893 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -31,6 +31,8 @@ add_executable(yuzu
     configuration/configure_general.h
     configuration/configure_graphics.cpp
     configuration/configure_graphics.h
+    configuration/configure_hotkeys.cpp
+    configuration/configure_hotkeys.h
     configuration/configure_input.cpp
     configuration/configure_input.h
     configuration/configure_input_player.cpp
@@ -78,6 +80,8 @@ add_executable(yuzu
     ui_settings.h
     util/limitable_input_dialog.cpp
     util/limitable_input_dialog.h
+    util/sequence_dialog/sequence_dialog.cpp
+    util/sequence_dialog/sequence_dialog.h
     util/spinbox.cpp
     util/spinbox.h
     util/util.cpp
@@ -95,6 +99,7 @@ set(UIS
     configuration/configure_gamelist.ui
     configuration/configure_general.ui
     configuration/configure_graphics.ui
+    configuration/configure_hotkeys.ui
     configuration/configure_input.ui
     configuration/configure_input_player.ui
     configuration/configure_input_simple.ui
@@ -105,7 +110,6 @@ set(UIS
     configuration/configure_touchscreen_advanced.ui
     configuration/configure_web.ui
     compatdb.ui
-    hotkeys.ui
     loading_screen.ui
     main.ui
 )
diff --git a/src/yuzu/applets/profile_select.cpp b/src/yuzu/applets/profile_select.cpp
index 5c1b65a2cd..3a08245476 100644
--- a/src/yuzu/applets/profile_select.cpp
+++ b/src/yuzu/applets/profile_select.cpp
@@ -4,6 +4,7 @@
 
 #include <mutex>
 #include <QDialogButtonBox>
+#include <QHeaderView>
 #include <QLabel>
 #include <QLineEdit>
 #include <QScrollArea>
diff --git a/src/yuzu/applets/profile_select.h b/src/yuzu/applets/profile_select.h
index 868573324f..1c2922e549 100644
--- a/src/yuzu/applets/profile_select.h
+++ b/src/yuzu/applets/profile_select.h
@@ -7,6 +7,7 @@
 #include <vector>
 #include <QDialog>
 #include <QList>
+#include <QTreeView>
 #include "core/frontend/applets/profile_select.h"
 
 class GMainWindow;
@@ -16,7 +17,6 @@ class QLabel;
 class QScrollArea;
 class QStandardItem;
 class QStandardItemModel;
-class QTreeView;
 class QVBoxLayout;
 
 class QtProfileSelectionDialog final : public QDialog {
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 4650f96a38..2e8ebfc128 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -2,6 +2,8 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include <array>
+#include <QKeySequence>
 #include <QSettings>
 #include "common/file_util.h"
 #include "configure_input_simple.h"
@@ -9,7 +11,6 @@
 #include "core/hle/service/hid/controllers/npad.h"
 #include "input_common/main.h"
 #include "yuzu/configuration/config.h"
-#include "yuzu/ui_settings.h"
 
 Config::Config() {
     // TODO: Don't hardcode the path; let the frontend decide where to put the config files.
@@ -17,7 +18,6 @@ Config::Config() {
     FileUtil::CreateFullPath(qt_config_loc);
     qt_config =
         std::make_unique<QSettings>(QString::fromStdString(qt_config_loc), QSettings::IniFormat);
-
     Reload();
 }
 
@@ -205,6 +205,27 @@ const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> Config::default
     Qt::Key_Control, Qt::Key_Shift, Qt::Key_AltGr, Qt::Key_ApplicationRight,
 };
 
+// This shouldn't have anything except static initializers (no functions). So
+// QKeySequnce(...).toString() is NOT ALLOWED HERE.
+// This must be in alphabetical order according to action name as it must have the same order as
+// UISetting::values.shortcuts, which is alphabetically ordered.
+const std::array<UISettings::Shortcut, 15> Config::default_hotkeys{
+    {{"Capture Screenshot", "Main Window", {"Ctrl+P", Qt::ApplicationShortcut}},
+     {"Continue/Pause Emulation", "Main Window", {"F4", Qt::WindowShortcut}},
+     {"Decrease Speed Limit", "Main Window", {"-", Qt::ApplicationShortcut}},
+     {"Exit yuzu", "Main Window", {"Ctrl+Q", Qt::WindowShortcut}},
+     {"Exit Fullscreen", "Main Window", {"Esc", Qt::WindowShortcut}},
+     {"Fullscreen", "Main Window", {"F11", Qt::WindowShortcut}},
+     {"Increase Speed Limit", "Main Window", {"+", Qt::ApplicationShortcut}},
+     {"Load Amiibo", "Main Window", {"F2", Qt::ApplicationShortcut}},
+     {"Load File", "Main Window", {"Ctrl+O", Qt::WindowShortcut}},
+     {"Restart Emulation", "Main Window", {"F6", Qt::WindowShortcut}},
+     {"Stop Emulation", "Main Window", {"F5", Qt::WindowShortcut}},
+     {"Toggle Filter Bar", "Main Window", {"Ctrl+F", Qt::WindowShortcut}},
+     {"Toggle Speed Limit", "Main Window", {"Ctrl+Z", Qt::ApplicationShortcut}},
+     {"Toggle Status Bar", "Main Window", {"Ctrl+S", Qt::WindowShortcut}},
+     {"Change Docked Mode", "Main Window", {"F10", Qt::ApplicationShortcut}}}};
+
 void Config::ReadPlayerValues() {
     for (std::size_t p = 0; p < Settings::values.players.size(); ++p) {
         auto& player = Settings::values.players[p];
@@ -509,20 +530,15 @@ void Config::ReadValues() {
     qt_config->endGroup();
 
     qt_config->beginGroup("Shortcuts");
-    QStringList groups = qt_config->childGroups();
-    for (auto group : groups) {
+    for (auto [name, group, shortcut] : default_hotkeys) {
+        auto [keyseq, context] = shortcut;
         qt_config->beginGroup(group);
-
-        QStringList hotkeys = qt_config->childGroups();
-        for (auto hotkey : hotkeys) {
-            qt_config->beginGroup(hotkey);
-            UISettings::values.shortcuts.emplace_back(UISettings::Shortcut(
-                group + "/" + hotkey,
-                UISettings::ContextualShortcut(ReadSetting("KeySeq").toString(),
-                                               ReadSetting("Context").toInt())));
-            qt_config->endGroup();
-        }
-
+        qt_config->beginGroup(name);
+        UISettings::values.shortcuts.push_back(
+            {name,
+             group,
+             {ReadSetting("KeySeq", keyseq).toString(), ReadSetting("Context", context).toInt()}});
+        qt_config->endGroup();
         qt_config->endGroup();
     }
     qt_config->endGroup();
@@ -760,9 +776,16 @@ void Config::SaveValues() {
     qt_config->endGroup();
 
     qt_config->beginGroup("Shortcuts");
-    for (auto shortcut : UISettings::values.shortcuts) {
-        WriteSetting(shortcut.first + "/KeySeq", shortcut.second.first);
-        WriteSetting(shortcut.first + "/Context", shortcut.second.second);
+    // Lengths of UISettings::values.shortcuts & default_hotkeys are same.
+    // However, their ordering must also be the same.
+    for (std::size_t i = 0; i < default_hotkeys.size(); i++) {
+        auto [name, group, shortcut] = UISettings::values.shortcuts[i];
+        qt_config->beginGroup(group);
+        qt_config->beginGroup(name);
+        WriteSetting("KeySeq", shortcut.first, default_hotkeys[i].shortcut.first);
+        WriteSetting("Context", shortcut.second, default_hotkeys[i].shortcut.second);
+        qt_config->endGroup();
+        qt_config->endGroup();
     }
     qt_config->endGroup();
 
diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h
index f4185db18f..221d2364c0 100644
--- a/src/yuzu/configuration/config.h
+++ b/src/yuzu/configuration/config.h
@@ -9,6 +9,7 @@
 #include <string>
 #include <QVariant>
 #include "core/settings.h"
+#include "yuzu/ui_settings.h"
 
 class QSettings;
 
@@ -47,6 +48,8 @@ private:
     void WriteSetting(const QString& name, const QVariant& value);
     void WriteSetting(const QString& name, const QVariant& value, const QVariant& default_value);
 
+    static const std::array<UISettings::Shortcut, 15> default_hotkeys;
+
     std::unique_ptr<QSettings> qt_config;
     std::string qt_config_loc;
 };
diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui
index 3f03f0b77f..267717bc97 100644
--- a/src/yuzu/configuration/configure.ui
+++ b/src/yuzu/configuration/configure.ui
@@ -7,9 +7,15 @@
     <x>0</x>
     <y>0</y>
     <width>382</width>
-    <height>241</height>
+    <height>650</height>
    </rect>
   </property>
+  <property name="minimumSize">
+   <size>
+    <width>0</width>
+    <height>650</height>
+   </size>
+  </property>
   <property name="windowTitle">
    <string>yuzu Configuration</string>
   </property>
@@ -62,6 +68,11 @@
          <string>Input</string>
         </attribute>
        </widget>
+       <widget class="ConfigureHotkeys" name="hotkeysTab">
+        <attribute name="title">
+         <string>Hotkeys</string>
+        </attribute>
+       </widget>
        <widget class="ConfigureGraphics" name="graphicsTab">
         <attribute name="title">
          <string>Graphics</string>
@@ -150,6 +161,12 @@
    <header>configuration/configure_input_simple.h</header>
    <container>1</container>
   </customwidget>
+  <customwidget>
+   <class>ConfigureHotkeys</class>
+   <extends>QWidget</extends>
+   <header>configuration/configure_hotkeys.h</header>
+   <container>1</container>
+  </customwidget>
  </customwidgets>
  <resources/>
  <connections>
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp
index 7770504055..51bd1f1211 100644
--- a/src/yuzu/configuration/configure_dialog.cpp
+++ b/src/yuzu/configuration/configure_dialog.cpp
@@ -8,20 +8,22 @@
 #include "ui_configure.h"
 #include "yuzu/configuration/config.h"
 #include "yuzu/configuration/configure_dialog.h"
+#include "yuzu/configuration/configure_input_player.h"
 #include "yuzu/hotkeys.h"
 
-ConfigureDialog::ConfigureDialog(QWidget* parent, const HotkeyRegistry& registry)
-    : QDialog(parent), ui(new Ui::ConfigureDialog) {
+ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry)
+    : QDialog(parent), registry(registry), ui(new Ui::ConfigureDialog) {
     ui->setupUi(this);
-    ui->generalTab->PopulateHotkeyList(registry);
+    ui->hotkeysTab->Populate(registry);
     this->setConfiguration();
     this->PopulateSelectionList();
     connect(ui->selectorList, &QListWidget::itemSelectionChanged, this,
             &ConfigureDialog::UpdateVisibleTabs);
-
     adjustSize();
-
     ui->selectorList->setCurrentRow(0);
+
+    // Synchronise lists upon initialisation
+    ui->hotkeysTab->EmitHotkeysChanged();
 }
 
 ConfigureDialog::~ConfigureDialog() = default;
@@ -34,6 +36,7 @@ void ConfigureDialog::applyConfiguration() {
     ui->systemTab->applyConfiguration();
     ui->profileManagerTab->applyConfiguration();
     ui->inputTab->applyConfiguration();
+    ui->hotkeysTab->applyConfiguration(registry);
     ui->graphicsTab->applyConfiguration();
     ui->audioTab->applyConfiguration();
     ui->debugTab->applyConfiguration();
@@ -47,7 +50,7 @@ void ConfigureDialog::PopulateSelectionList() {
         {{tr("General"), {tr("General"), tr("Web"), tr("Debug"), tr("Game List")}},
          {tr("System"), {tr("System"), tr("Profiles"), tr("Audio")}},
          {tr("Graphics"), {tr("Graphics")}},
-         {tr("Controls"), {tr("Input")}}}};
+         {tr("Controls"), {tr("Input"), tr("Hotkeys")}}}};
 
     for (const auto& entry : items) {
         auto* const item = new QListWidgetItem(entry.first);
@@ -66,6 +69,7 @@ void ConfigureDialog::UpdateVisibleTabs() {
                                                  {tr("System"), ui->systemTab},
                                                  {tr("Profiles"), ui->profileManagerTab},
                                                  {tr("Input"), ui->inputTab},
+                                                 {tr("Hotkeys"), ui->hotkeysTab},
                                                  {tr("Graphics"), ui->graphicsTab},
                                                  {tr("Audio"), ui->audioTab},
                                                  {tr("Debug"), ui->debugTab},
diff --git a/src/yuzu/configuration/configure_dialog.h b/src/yuzu/configuration/configure_dialog.h
index 243d9fa097..2363ba5849 100644
--- a/src/yuzu/configuration/configure_dialog.h
+++ b/src/yuzu/configuration/configure_dialog.h
@@ -17,7 +17,7 @@ class ConfigureDialog : public QDialog {
     Q_OBJECT
 
 public:
-    explicit ConfigureDialog(QWidget* parent, const HotkeyRegistry& registry);
+    explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry);
     ~ConfigureDialog() override;
 
     void applyConfiguration();
@@ -28,4 +28,5 @@ private:
     void PopulateSelectionList();
 
     std::unique_ptr<Ui::ConfigureDialog> ui;
+    HotkeyRegistry& registry;
 };
diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp
index 4116b6cd74..81a60da082 100644
--- a/src/yuzu/configuration/configure_general.cpp
+++ b/src/yuzu/configuration/configure_general.cpp
@@ -36,10 +36,6 @@ void ConfigureGeneral::setConfiguration() {
     ui->enable_nfc->setChecked(Settings::values.enable_nfc);
 }
 
-void ConfigureGeneral::PopulateHotkeyList(const HotkeyRegistry& registry) {
-    ui->widget->Populate(registry);
-}
-
 void ConfigureGeneral::applyConfiguration() {
     UISettings::values.gamedir_deepscan = ui->toggle_deepscan->isChecked();
     UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
diff --git a/src/yuzu/configuration/configure_general.h b/src/yuzu/configuration/configure_general.h
index 59738af40b..df41d995bc 100644
--- a/src/yuzu/configuration/configure_general.h
+++ b/src/yuzu/configuration/configure_general.h
@@ -20,7 +20,6 @@ public:
     explicit ConfigureGeneral(QWidget* parent = nullptr);
     ~ConfigureGeneral() override;
 
-    void PopulateHotkeyList(const HotkeyRegistry& registry);
     void applyConfiguration();
 
 private:
diff --git a/src/yuzu/configuration/configure_general.ui b/src/yuzu/configuration/configure_general.ui
index dff0ad5d0d..879ef747fb 100644
--- a/src/yuzu/configuration/configure_general.ui
+++ b/src/yuzu/configuration/configure_general.ui
@@ -117,22 +117,6 @@
        </layout>
       </widget>
      </item>
-     <item>
-      <widget class="QGroupBox" name="HotKeysGroupBox">
-       <property name="title">
-        <string>Hotkeys</string>
-       </property>
-       <layout class="QHBoxLayout" name="HotKeysHorizontalLayout">
-        <item>
-         <layout class="QVBoxLayout" name="HotKeysVerticalLayout">
-          <item>
-           <widget class="GHotkeysDialog" name="widget" native="true"/>
-          </item>
-         </layout>
-        </item>
-       </layout>
-      </widget>
-     </item>
      <item>
       <spacer name="verticalSpacer">
        <property name="orientation">
@@ -150,14 +134,6 @@
    </item>
   </layout>
  </widget>
- <customwidgets>
-  <customwidget>
-   <class>GHotkeysDialog</class>
-   <extends>QWidget</extends>
-   <header>hotkeys.h</header>
-   <container>1</container>
-  </customwidget>
- </customwidgets>
  <resources/>
  <connections/>
 </ui>
diff --git a/src/yuzu/configuration/configure_hotkeys.cpp b/src/yuzu/configuration/configure_hotkeys.cpp
new file mode 100644
index 0000000000..bfb5625353
--- /dev/null
+++ b/src/yuzu/configuration/configure_hotkeys.cpp
@@ -0,0 +1,121 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QMessageBox>
+#include <QStandardItemModel>
+#include "core/settings.h"
+#include "ui_configure_hotkeys.h"
+#include "yuzu/configuration/configure_hotkeys.h"
+#include "yuzu/hotkeys.h"
+#include "yuzu/util/sequence_dialog/sequence_dialog.h"
+
+ConfigureHotkeys::ConfigureHotkeys(QWidget* parent)
+    : QWidget(parent), ui(std::make_unique<Ui::ConfigureHotkeys>()) {
+    ui->setupUi(this);
+    setFocusPolicy(Qt::ClickFocus);
+
+    model = new QStandardItemModel(this);
+    model->setColumnCount(3);
+    model->setHorizontalHeaderLabels({tr("Action"), tr("Hotkey"), tr("Context")});
+
+    connect(ui->hotkey_list, &QTreeView::doubleClicked, this, &ConfigureHotkeys::Configure);
+    ui->hotkey_list->setModel(model);
+
+    // TODO(Kloen): Make context configurable as well (hiding the column for now)
+    ui->hotkey_list->hideColumn(2);
+
+    ui->hotkey_list->setColumnWidth(0, 200);
+    ui->hotkey_list->resizeColumnToContents(1);
+}
+
+ConfigureHotkeys::~ConfigureHotkeys() = default;
+
+void ConfigureHotkeys::EmitHotkeysChanged() {
+    emit HotkeysChanged(GetUsedKeyList());
+}
+
+QList<QKeySequence> ConfigureHotkeys::GetUsedKeyList() const {
+    QList<QKeySequence> list;
+    for (int r = 0; r < model->rowCount(); r++) {
+        const QStandardItem* parent = model->item(r, 0);
+        for (int r2 = 0; r2 < parent->rowCount(); r2++) {
+            const QStandardItem* keyseq = parent->child(r2, 1);
+            list << QKeySequence::fromString(keyseq->text(), QKeySequence::NativeText);
+        }
+    }
+    return list;
+}
+
+void ConfigureHotkeys::Populate(const HotkeyRegistry& registry) {
+    for (const auto& group : registry.hotkey_groups) {
+        auto* parent_item = new QStandardItem(group.first);
+        parent_item->setEditable(false);
+        for (const auto& hotkey : group.second) {
+            auto* action = new QStandardItem(hotkey.first);
+            auto* keyseq =
+                new QStandardItem(hotkey.second.keyseq.toString(QKeySequence::NativeText));
+            action->setEditable(false);
+            keyseq->setEditable(false);
+            parent_item->appendRow({action, keyseq});
+        }
+        model->appendRow(parent_item);
+    }
+
+    ui->hotkey_list->expandAll();
+}
+
+void ConfigureHotkeys::Configure(QModelIndex index) {
+    if (index.parent() == QModelIndex())
+        return;
+
+    index = index.sibling(index.row(), 1);
+    auto* model = ui->hotkey_list->model();
+    auto previous_key = model->data(index);
+
+    auto* hotkey_dialog = new SequenceDialog;
+    int return_code = hotkey_dialog->exec();
+
+    auto key_sequence = hotkey_dialog->GetSequence();
+
+    if (return_code == QDialog::Rejected || key_sequence.isEmpty())
+        return;
+
+    if (IsUsedKey(key_sequence) && key_sequence != QKeySequence(previous_key.toString())) {
+        QMessageBox::critical(this, tr("Error in inputted key"),
+                              tr("You're using a key that's already bound."));
+    } else {
+        model->setData(index, key_sequence.toString(QKeySequence::NativeText));
+        EmitHotkeysChanged();
+    }
+}
+
+bool ConfigureHotkeys::IsUsedKey(QKeySequence key_sequence) {
+    return GetUsedKeyList().contains(key_sequence);
+}
+
+void ConfigureHotkeys::applyConfiguration(HotkeyRegistry& registry) {
+    for (int key_id = 0; key_id < model->rowCount(); key_id++) {
+        const QStandardItem* parent = model->item(key_id, 0);
+        for (int key_column_id = 0; key_column_id < parent->rowCount(); key_column_id++) {
+            const QStandardItem* action = parent->child(key_column_id, 0);
+            const QStandardItem* keyseq = parent->child(key_column_id, 1);
+            for (auto& [group, sub_actions] : registry.hotkey_groups) {
+                if (group != parent->text())
+                    continue;
+                for (auto& [action_name, hotkey] : sub_actions) {
+                    if (action_name != action->text())
+                        continue;
+                    hotkey.keyseq = QKeySequence(keyseq->text());
+                }
+            }
+        }
+    }
+
+    registry.SaveHotkeys();
+    Settings::Apply();
+}
+
+void ConfigureHotkeys::retranslateUi() {
+    ui->retranslateUi(this);
+}
diff --git a/src/yuzu/configuration/configure_hotkeys.h b/src/yuzu/configuration/configure_hotkeys.h
new file mode 100644
index 0000000000..cd203aad67
--- /dev/null
+++ b/src/yuzu/configuration/configure_hotkeys.h
@@ -0,0 +1,48 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QWidget>
+#include "core/settings.h"
+
+namespace Ui {
+class ConfigureHotkeys;
+}
+
+class HotkeyRegistry;
+class QStandardItemModel;
+
+class ConfigureHotkeys : public QWidget {
+    Q_OBJECT
+
+public:
+    explicit ConfigureHotkeys(QWidget* parent = nullptr);
+    ~ConfigureHotkeys() override;
+
+    void applyConfiguration(HotkeyRegistry& registry);
+    void retranslateUi();
+
+    void EmitHotkeysChanged();
+
+    /**
+     * Populates the hotkey list widget using data from the provided registry.
+     * Called everytime the Configure dialog is opened.
+     * @param registry The HotkeyRegistry whose data is used to populate the list.
+     */
+    void Populate(const HotkeyRegistry& registry);
+
+signals:
+    void HotkeysChanged(QList<QKeySequence> new_key_list);
+
+private:
+    void Configure(QModelIndex index);
+    bool IsUsedKey(QKeySequence key_sequence);
+    QList<QKeySequence> GetUsedKeyList() const;
+
+    std::unique_ptr<Ui::ConfigureHotkeys> ui;
+
+    QStandardItemModel* model;
+};
diff --git a/src/yuzu/configuration/configure_hotkeys.ui b/src/yuzu/configuration/configure_hotkeys.ui
new file mode 100644
index 0000000000..0d0b70f385
--- /dev/null
+++ b/src/yuzu/configuration/configure_hotkeys.ui
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ConfigureHotkeys</class>
+ <widget class="QWidget" name="ConfigureHotkeys">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>363</width>
+    <height>388</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Hotkey Settings</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_2">
+     <item>
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Double-click on a binding to change it.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QTreeView" name="hotkey_list">
+       <property name="editTriggers">
+        <set>QAbstractItemView::NoEditTriggers</set>
+       </property>
+       <property name="sortingEnabled">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
\ No newline at end of file
diff --git a/src/yuzu/hotkeys.cpp b/src/yuzu/hotkeys.cpp
index dce3997746..4582e7f215 100644
--- a/src/yuzu/hotkeys.cpp
+++ b/src/yuzu/hotkeys.cpp
@@ -2,7 +2,6 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
-#include <map>
 #include <QKeySequence>
 #include <QShortcut>
 #include <QTreeWidgetItem>
@@ -13,47 +12,32 @@
 HotkeyRegistry::HotkeyRegistry() = default;
 HotkeyRegistry::~HotkeyRegistry() = default;
 
-void HotkeyRegistry::LoadHotkeys() {
-    // Make sure NOT to use a reference here because it would become invalid once we call
-    // beginGroup()
-    for (auto shortcut : UISettings::values.shortcuts) {
-        const QStringList cat = shortcut.first.split('/');
-        Q_ASSERT(cat.size() >= 2);
-
-        // RegisterHotkey assigns default keybindings, so use old values as default parameters
-        Hotkey& hk = hotkey_groups[cat[0]][cat[1]];
-        if (!shortcut.second.first.isEmpty()) {
-            hk.keyseq = QKeySequence::fromString(shortcut.second.first);
-            hk.context = static_cast<Qt::ShortcutContext>(shortcut.second.second);
-        }
-        if (hk.shortcut)
-            hk.shortcut->setKey(hk.keyseq);
-    }
-}
-
 void HotkeyRegistry::SaveHotkeys() {
     UISettings::values.shortcuts.clear();
     for (const auto& group : hotkey_groups) {
         for (const auto& hotkey : group.second) {
-            UISettings::values.shortcuts.emplace_back(
-                UISettings::Shortcut(group.first + '/' + hotkey.first,
-                                     UISettings::ContextualShortcut(hotkey.second.keyseq.toString(),
-                                                                    hotkey.second.context)));
+            UISettings::values.shortcuts.push_back(
+                {hotkey.first, group.first,
+                 UISettings::ContextualShortcut(hotkey.second.keyseq.toString(),
+                                                hotkey.second.context)});
         }
     }
 }
 
-void HotkeyRegistry::RegisterHotkey(const QString& group, const QString& action,
-                                    const QKeySequence& default_keyseq,
-                                    Qt::ShortcutContext default_context) {
-    auto& hotkey_group = hotkey_groups[group];
-    if (hotkey_group.find(action) != hotkey_group.end()) {
-        return;
+void HotkeyRegistry::LoadHotkeys() {
+    // Make sure NOT to use a reference here because it would become invalid once we call
+    // beginGroup()
+    for (auto shortcut : UISettings::values.shortcuts) {
+        Hotkey& hk = hotkey_groups[shortcut.group][shortcut.name];
+        if (!shortcut.shortcut.first.isEmpty()) {
+            hk.keyseq = QKeySequence::fromString(shortcut.shortcut.first, QKeySequence::NativeText);
+            hk.context = static_cast<Qt::ShortcutContext>(shortcut.shortcut.second);
+        }
+        if (hk.shortcut) {
+            hk.shortcut->disconnect();
+            hk.shortcut->setKey(hk.keyseq);
+        }
     }
-
-    auto& hotkey_action = hotkey_groups[group][action];
-    hotkey_action.keyseq = default_keyseq;
-    hotkey_action.context = default_context;
 }
 
 QShortcut* HotkeyRegistry::GetHotkey(const QString& group, const QString& action, QWidget* widget) {
@@ -65,24 +49,11 @@ QShortcut* HotkeyRegistry::GetHotkey(const QString& group, const QString& action
     return hk.shortcut;
 }
 
-GHotkeysDialog::GHotkeysDialog(QWidget* parent) : QWidget(parent) {
-    ui.setupUi(this);
+QKeySequence HotkeyRegistry::GetKeySequence(const QString& group, const QString& action) {
+    return hotkey_groups[group][action].keyseq;
 }
 
-void GHotkeysDialog::Populate(const HotkeyRegistry& registry) {
-    for (const auto& group : registry.hotkey_groups) {
-        QTreeWidgetItem* toplevel_item = new QTreeWidgetItem(QStringList(group.first));
-        for (const auto& hotkey : group.second) {
-            QStringList columns;
-            columns << hotkey.first << hotkey.second.keyseq.toString();
-            QTreeWidgetItem* item = new QTreeWidgetItem(columns);
-            toplevel_item->addChild(item);
-        }
-        ui.treeWidget->addTopLevelItem(toplevel_item);
-    }
-    // TODO: Make context configurable as well (hiding the column for now)
-    ui.treeWidget->setColumnCount(2);
-
-    ui.treeWidget->resizeColumnToContents(0);
-    ui.treeWidget->resizeColumnToContents(1);
+Qt::ShortcutContext HotkeyRegistry::GetShortcutContext(const QString& group,
+                                                       const QString& action) {
+    return hotkey_groups[group][action].context;
 }
diff --git a/src/yuzu/hotkeys.h b/src/yuzu/hotkeys.h
index f38e6c0028..4f526dc7e8 100644
--- a/src/yuzu/hotkeys.h
+++ b/src/yuzu/hotkeys.h
@@ -5,7 +5,6 @@
 #pragma once
 
 #include <map>
-#include "ui_hotkeys.h"
 
 class QDialog;
 class QKeySequence;
@@ -14,7 +13,7 @@ class QShortcut;
 
 class HotkeyRegistry final {
 public:
-    friend class GHotkeysDialog;
+    friend class ConfigureHotkeys;
 
     explicit HotkeyRegistry();
     ~HotkeyRegistry();
@@ -49,22 +48,27 @@ public:
     QShortcut* GetHotkey(const QString& group, const QString& action, QWidget* widget);
 
     /**
-     * Register a hotkey.
+     * Returns a QKeySequence object whose signal can be connected to QAction::setShortcut.
      *
-     * @param group General group this hotkey belongs to (e.g. "Main Window", "Debugger")
-     * @param action Name of the action (e.g. "Start Emulation", "Load Image")
-     * @param default_keyseq Default key sequence to assign if the hotkey wasn't present in the
-     *                       settings file before
-     * @param default_context Default context to assign if the hotkey wasn't present in the settings
-     *                        file before
-     * @warning Both the group and action strings will be displayed in the hotkey settings dialog
+     * @param group  General group this hotkey belongs to (e.g. "Main Window", "Debugger").
+     * @param action Name of the action (e.g. "Start Emulation", "Load Image").
      */
-    void RegisterHotkey(const QString& group, const QString& action,
-                        const QKeySequence& default_keyseq = {},
-                        Qt::ShortcutContext default_context = Qt::WindowShortcut);
+    QKeySequence GetKeySequence(const QString& group, const QString& action);
+
+    /**
+     * Returns a Qt::ShortcutContext object who can be connected to other
+     * QAction::setShortcutContext.
+     *
+     * @param group  General group this shortcut context belongs to (e.g. "Main Window",
+     * "Debugger").
+     * @param action Name of the action (e.g. "Start Emulation", "Load Image").
+     */
+    Qt::ShortcutContext GetShortcutContext(const QString& group, const QString& action);
 
 private:
     struct Hotkey {
+        Hotkey() : shortcut(nullptr), context(Qt::WindowShortcut) {}
+
         QKeySequence keyseq;
         QShortcut* shortcut = nullptr;
         Qt::ShortcutContext context = Qt::WindowShortcut;
@@ -75,15 +79,3 @@ private:
 
     HotkeyGroupMap hotkey_groups;
 };
-
-class GHotkeysDialog : public QWidget {
-    Q_OBJECT
-
-public:
-    explicit GHotkeysDialog(QWidget* parent = nullptr);
-
-    void Populate(const HotkeyRegistry& registry);
-
-private:
-    Ui::hotkeys ui;
-};
diff --git a/src/yuzu/hotkeys.ui b/src/yuzu/hotkeys.ui
deleted file mode 100644
index 050fe064e1..0000000000
--- a/src/yuzu/hotkeys.ui
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>hotkeys</class>
- <widget class="QWidget" name="hotkeys">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>363</width>
-    <height>388</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>Hotkey Settings</string>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <item>
-    <widget class="QTreeWidget" name="treeWidget">
-     <property name="selectionBehavior">
-      <enum>QAbstractItemView::SelectItems</enum>
-     </property>
-     <property name="headerHidden">
-      <bool>false</bool>
-     </property>
-     <column>
-      <property name="text">
-       <string>Action</string>
-      </property>
-     </column>
-     <column>
-      <property name="text">
-       <string>Hotkey</string>
-      </property>
-     </column>
-     <column>
-      <property name="text">
-       <string>Context</string>
-      </property>
-     </column>
-    </widget>
-   </item>
-  </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 41ba3c4c60..0bda2239f6 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -511,33 +511,34 @@ void GMainWindow::InitializeRecentFileMenuActions() {
 }
 
 void GMainWindow::InitializeHotkeys() {
-    hotkey_registry.RegisterHotkey("Main Window", "Load File", QKeySequence::Open);
-    hotkey_registry.RegisterHotkey("Main Window", "Start Emulation");
-    hotkey_registry.RegisterHotkey("Main Window", "Continue/Pause", QKeySequence(Qt::Key_F4));
-    hotkey_registry.RegisterHotkey("Main Window", "Restart", QKeySequence(Qt::Key_F5));
-    hotkey_registry.RegisterHotkey("Main Window", "Fullscreen", QKeySequence::FullScreen);
-    hotkey_registry.RegisterHotkey("Main Window", "Exit Fullscreen", QKeySequence(Qt::Key_Escape),
-                                   Qt::ApplicationShortcut);
-    hotkey_registry.RegisterHotkey("Main Window", "Toggle Speed Limit", QKeySequence("CTRL+Z"),
-                                   Qt::ApplicationShortcut);
-    hotkey_registry.RegisterHotkey("Main Window", "Increase Speed Limit", QKeySequence("+"),
-                                   Qt::ApplicationShortcut);
-    hotkey_registry.RegisterHotkey("Main Window", "Decrease Speed Limit", QKeySequence("-"),
-                                   Qt::ApplicationShortcut);
-    hotkey_registry.RegisterHotkey("Main Window", "Load Amiibo", QKeySequence(Qt::Key_F2),
-                                   Qt::ApplicationShortcut);
-    hotkey_registry.RegisterHotkey("Main Window", "Capture Screenshot",
-                                   QKeySequence(QKeySequence::Print));
-    hotkey_registry.RegisterHotkey("Main Window", "Change Docked Mode", QKeySequence(Qt::Key_F10));
-
     hotkey_registry.LoadHotkeys();
 
+    ui.action_Load_File->setShortcut(hotkey_registry.GetKeySequence("Main Window", "Load File"));
+    ui.action_Load_File->setShortcutContext(
+        hotkey_registry.GetShortcutContext("Main Window", "Load File"));
+
+    ui.action_Exit->setShortcut(hotkey_registry.GetKeySequence("Main Window", "Exit yuzu"));
+    ui.action_Exit->setShortcutContext(
+        hotkey_registry.GetShortcutContext("Main Window", "Exit yuzu"));
+
+    ui.action_Stop->setShortcut(hotkey_registry.GetKeySequence("Main Window", "Stop Emulation"));
+    ui.action_Stop->setShortcutContext(
+        hotkey_registry.GetShortcutContext("Main Window", "Stop Emulation"));
+
+    ui.action_Show_Filter_Bar->setShortcut(
+        hotkey_registry.GetKeySequence("Main Window", "Toggle Filter Bar"));
+    ui.action_Show_Filter_Bar->setShortcutContext(
+        hotkey_registry.GetShortcutContext("Main Window", "Toggle Filter Bar"));
+
+    ui.action_Show_Status_Bar->setShortcut(
+        hotkey_registry.GetKeySequence("Main Window", "Toggle Status Bar"));
+    ui.action_Show_Status_Bar->setShortcutContext(
+        hotkey_registry.GetShortcutContext("Main Window", "Toggle Status Bar"));
+
     connect(hotkey_registry.GetHotkey("Main Window", "Load File", this), &QShortcut::activated,
             this, &GMainWindow::OnMenuLoadFile);
-    connect(hotkey_registry.GetHotkey("Main Window", "Start Emulation", this),
-            &QShortcut::activated, this, &GMainWindow::OnStartGame);
-    connect(hotkey_registry.GetHotkey("Main Window", "Continue/Pause", this), &QShortcut::activated,
-            this, [&] {
+    connect(hotkey_registry.GetHotkey("Main Window", "Continue/Pause Emulation", this),
+            &QShortcut::activated, this, [&] {
                 if (emulation_running) {
                     if (emu_thread->IsRunning()) {
                         OnPauseGame();
@@ -546,8 +547,8 @@ void GMainWindow::InitializeHotkeys() {
                     }
                 }
             });
-    connect(hotkey_registry.GetHotkey("Main Window", "Restart", this), &QShortcut::activated, this,
-            [this] {
+    connect(hotkey_registry.GetHotkey("Main Window", "Restart Emulation", this),
+            &QShortcut::activated, this, [this] {
                 if (!Core::System::GetInstance().IsPoweredOn())
                     return;
                 BootGame(QString(game_path));
@@ -692,7 +693,6 @@ void GMainWindow::ConnectMenuEvents() {
             &GMainWindow::ToggleWindowMode);
     connect(ui.action_Display_Dock_Widget_Headers, &QAction::triggered, this,
             &GMainWindow::OnDisplayTitleBars);
-    ui.action_Show_Filter_Bar->setShortcut(tr("CTRL+F"));
     connect(ui.action_Show_Filter_Bar, &QAction::triggered, this, &GMainWindow::OnToggleFilterBar);
     connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
 
@@ -1624,6 +1624,7 @@ void GMainWindow::OnConfigure() {
     auto result = configureDialog.exec();
     if (result == QDialog::Accepted) {
         configureDialog.applyConfiguration();
+        InitializeHotkeys();
         if (UISettings::values.theme != old_theme)
             UpdateUITheme();
         if (UISettings::values.enable_discord_presence != old_discord_presence)
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index e07c892cf2..90cd79bca4 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -120,7 +120,6 @@ private:
     void InitializeWidgets();
     void InitializeDebugWidgets();
     void InitializeRecentFileMenuActions();
-    void InitializeHotkeys();
 
     void SetDefaultUIGeometry();
     void RestoreUIState();
@@ -195,6 +194,7 @@ private slots:
     void OnAbout();
     void OnToggleFilterBar();
     void OnDisplayTitleBars(bool);
+    void InitializeHotkeys();
     void ToggleFullscreen();
     void ShowFullscreen();
     void HideFullscreen();
diff --git a/src/yuzu/ui_settings.cpp b/src/yuzu/ui_settings.cpp
index a314493fcc..4bdc302e0f 100644
--- a/src/yuzu/ui_settings.cpp
+++ b/src/yuzu/ui_settings.cpp
@@ -12,5 +12,4 @@ const Themes themes{{
 }};
 
 Values values = {};
-
 } // namespace UISettings
diff --git a/src/yuzu/ui_settings.h b/src/yuzu/ui_settings.h
index 82aaeedb0a..45e705b619 100644
--- a/src/yuzu/ui_settings.h
+++ b/src/yuzu/ui_settings.h
@@ -15,7 +15,12 @@
 namespace UISettings {
 
 using ContextualShortcut = std::pair<QString, int>;
-using Shortcut = std::pair<QString, ContextualShortcut>;
+
+struct Shortcut {
+    QString name;
+    QString group;
+    ContextualShortcut shortcut;
+};
 
 using Themes = std::array<std::pair<const char*, const char*>, 2>;
 extern const Themes themes;
diff --git a/src/yuzu/util/sequence_dialog/sequence_dialog.cpp b/src/yuzu/util/sequence_dialog/sequence_dialog.cpp
new file mode 100644
index 0000000000..d3edf6ec3d
--- /dev/null
+++ b/src/yuzu/util/sequence_dialog/sequence_dialog.cpp
@@ -0,0 +1,37 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <QDialogButtonBox>
+#include <QKeySequenceEdit>
+#include <QVBoxLayout>
+#include "yuzu/util/sequence_dialog/sequence_dialog.h"
+
+SequenceDialog::SequenceDialog(QWidget* parent) : QDialog(parent) {
+    setWindowTitle(tr("Enter a hotkey"));
+    auto* layout = new QVBoxLayout(this);
+    key_sequence = new QKeySequenceEdit;
+    layout->addWidget(key_sequence);
+    auto* buttons =
+        new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal);
+    buttons->setCenterButtons(true);
+    layout->addWidget(buttons);
+    connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+    connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+    setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+}
+
+SequenceDialog::~SequenceDialog() = default;
+
+QKeySequence SequenceDialog::GetSequence() const {
+    // Only the first key is returned. The other 3, if present, are ignored.
+    return QKeySequence(key_sequence->keySequence()[0]);
+}
+
+bool SequenceDialog::focusNextPrevChild(bool next) {
+    return false;
+}
+
+void SequenceDialog::closeEvent(QCloseEvent*) {
+    reject();
+}
diff --git a/src/yuzu/util/sequence_dialog/sequence_dialog.h b/src/yuzu/util/sequence_dialog/sequence_dialog.h
new file mode 100644
index 0000000000..969c777407
--- /dev/null
+++ b/src/yuzu/util/sequence_dialog/sequence_dialog.h
@@ -0,0 +1,24 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <QDialog>
+
+class QKeySequenceEdit;
+
+class SequenceDialog : public QDialog {
+    Q_OBJECT
+
+public:
+    explicit SequenceDialog(QWidget* parent = nullptr);
+    ~SequenceDialog() override;
+
+    QKeySequence GetSequence() const;
+    void closeEvent(QCloseEvent*) override;
+
+private:
+    QKeySequenceEdit* key_sequence;
+    bool focusNextPrevChild(bool next) override;
+};