diff --git a/scwx-qt/res/icons/font-awesome-6/keyboard-regular.svg b/scwx-qt/res/icons/font-awesome-6/keyboard-regular.svg
new file mode 100644
index 00000000..faff1aec
--- /dev/null
+++ b/scwx-qt/res/icons/font-awesome-6/keyboard-regular.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake
index 179c7bc9..282a1ffc 100644
--- a/scwx-qt/scwx-qt.cmake
+++ b/scwx-qt/scwx-qt.cmake
@@ -88,6 +88,7 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp
 set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp
                 source/scwx/qt/manager/download_manager.hpp
                 source/scwx/qt/manager/font_manager.hpp
+                source/scwx/qt/manager/hotkey_manager.hpp
                 source/scwx/qt/manager/media_manager.hpp
                 source/scwx/qt/manager/placefile_manager.hpp
                 source/scwx/qt/manager/position_manager.hpp
@@ -101,6 +102,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp
 set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp
                 source/scwx/qt/manager/download_manager.cpp
                 source/scwx/qt/manager/font_manager.cpp
+                source/scwx/qt/manager/hotkey_manager.cpp
                 source/scwx/qt/manager/media_manager.cpp
                 source/scwx/qt/manager/placefile_manager.cpp
                 source/scwx/qt/manager/position_manager.cpp
@@ -162,6 +164,7 @@ set(SRC_REQUEST source/scwx/qt/request/download_request.cpp
                 source/scwx/qt/request/nexrad_file_request.cpp)
 set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp
                  source/scwx/qt/settings/general_settings.hpp
+                 source/scwx/qt/settings/hotkey_settings.hpp
                  source/scwx/qt/settings/map_settings.hpp
                  source/scwx/qt/settings/palette_settings.hpp
                  source/scwx/qt/settings/product_settings.hpp
@@ -176,6 +179,7 @@ set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp
                  source/scwx/qt/settings/ui_settings.hpp)
 set(SRC_SETTINGS source/scwx/qt/settings/audio_settings.cpp
                  source/scwx/qt/settings/general_settings.cpp
+                 source/scwx/qt/settings/hotkey_settings.cpp
                  source/scwx/qt/settings/map_settings.cpp
                  source/scwx/qt/settings/palette_settings.cpp
                  source/scwx/qt/settings/product_settings.cpp
@@ -191,6 +195,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp
               source/scwx/qt/types/event_types.hpp
               source/scwx/qt/types/font_types.hpp
               source/scwx/qt/types/github_types.hpp
+              source/scwx/qt/types/hotkey_types.hpp
               source/scwx/qt/types/icon_types.hpp
               source/scwx/qt/types/imgui_font.hpp
               source/scwx/qt/types/layer_types.hpp
@@ -205,6 +210,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp
               source/scwx/qt/types/time_types.hpp)
 set(SRC_TYPES source/scwx/qt/types/alert_types.cpp
               source/scwx/qt/types/github_types.cpp
+              source/scwx/qt/types/hotkey_types.cpp
               source/scwx/qt/types/icon_types.cpp
               source/scwx/qt/types/imgui_font.cpp
               source/scwx/qt/types/layer_types.cpp
@@ -225,6 +231,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
            source/scwx/qt/ui/county_dialog.hpp
            source/scwx/qt/ui/download_dialog.hpp
            source/scwx/qt/ui/flow_layout.hpp
+           source/scwx/qt/ui/hotkey_edit.hpp
            source/scwx/qt/ui/imgui_debug_dialog.hpp
            source/scwx/qt/ui/imgui_debug_widget.hpp
            source/scwx/qt/ui/layer_dialog.hpp
@@ -247,6 +254,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
            source/scwx/qt/ui/county_dialog.cpp
            source/scwx/qt/ui/download_dialog.cpp
            source/scwx/qt/ui/flow_layout.cpp
+           source/scwx/qt/ui/hotkey_edit.cpp
            source/scwx/qt/ui/imgui_debug_dialog.cpp
            source/scwx/qt/ui/imgui_debug_widget.cpp
            source/scwx/qt/ui/layer_dialog.cpp
@@ -276,6 +284,10 @@ set(UI_UI  source/scwx/qt/ui/about_dialog.ui
            source/scwx/qt/ui/radar_site_dialog.ui
            source/scwx/qt/ui/settings_dialog.ui
            source/scwx/qt/ui/update_dialog.ui)
+set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/hotkey_settings_widget.hpp
+                    source/scwx/qt/ui/settings/settings_page_widget.hpp)
+set(SRC_UI_SETTINGS source/scwx/qt/ui/settings/hotkey_settings_widget.cpp
+                    source/scwx/qt/ui/settings/settings_page_widget.cpp)
 set(HDR_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.hpp
                  source/scwx/qt/ui/setup/finish_page.hpp
                  source/scwx/qt/ui/setup/map_layout_page.hpp
@@ -387,6 +399,8 @@ set(PROJECT_SOURCES ${HDR_MAIN}
                     ${HDR_UI}
                     ${SRC_UI}
                     ${UI_UI}
+                    ${HDR_UI_SETTINGS}
+                    ${SRC_UI_SETTINGS}
                     ${HDR_UI_SETUP}
                     ${SRC_UI_SETUP}
                     ${HDR_UTIL}
@@ -400,41 +414,43 @@ set(PROJECT_SOURCES ${HDR_MAIN}
                     ${CMAKE_FILES})
 set(EXECUTABLE_SOURCES ${SRC_EXE_MAIN})
 
-source_group("Header Files\\main"      FILES ${HDR_MAIN})
-source_group("Source Files\\main"      FILES ${SRC_MAIN})
-source_group("Header Files\\config"    FILES ${HDR_CONFIG})
-source_group("Source Files\\config"    FILES ${SRC_CONFIG})
-source_group("Source Files\\external"  FILES ${SRC_EXTERNAL})
-source_group("Header Files\\gl"        FILES ${HDR_GL})
-source_group("Source Files\\gl"        FILES ${SRC_GL})
-source_group("Header Files\\gl\\draw"  FILES ${HDR_GL_DRAW})
-source_group("Source Files\\gl\\draw"  FILES ${SRC_GL_DRAW})
-source_group("Header Files\\manager"   FILES ${HDR_MANAGER})
-source_group("Source Files\\manager"   FILES ${SRC_MANAGER})
-source_group("UI Files\\main"          FILES ${UI_MAIN})
-source_group("Header Files\\map"       FILES ${HDR_MAP})
-source_group("Source Files\\map"       FILES ${SRC_MAP})
-source_group("Header Files\\model"     FILES ${HDR_MODEL})
-source_group("Source Files\\model"     FILES ${SRC_MODEL})
-source_group("Header Files\\request"   FILES ${HDR_REQUEST})
-source_group("Source Files\\request"   FILES ${SRC_REQUEST})
-source_group("Header Files\\settings"  FILES ${HDR_SETTINGS})
-source_group("Source Files\\settings"  FILES ${SRC_SETTINGS})
-source_group("Header Files\\types"     FILES ${HDR_TYPES})
-source_group("Source Files\\types"     FILES ${SRC_TYPES})
-source_group("Header Files\\ui"        FILES ${HDR_UI})
-source_group("Source Files\\ui"        FILES ${SRC_UI})
-source_group("Header Files\\ui\\setup" FILES ${HDR_UI_SETUP})
-source_group("Source Files\\ui\\setup" FILES ${SRC_UI_SETUP})
-source_group("UI Files\\ui"            FILES ${UI_UI})
-source_group("Header Files\\util"      FILES ${HDR_UTIL})
-source_group("Source Files\\util"      FILES ${SRC_UTIL})
-source_group("Header Files\\view"      FILES ${HDR_VIEW})
-source_group("Source Files\\view"      FILES ${SRC_VIEW})
-source_group("OpenGL Shaders"          FILES ${SHADER_FILES})
-source_group("Resources"               FILES ${RESOURCE_FILES})
-source_group("Resources\\json"         FILES ${JSON_FILES})
-source_group("I18N Files"              FILES ${TS_FILES})
+source_group("Header Files\\main"         FILES ${HDR_MAIN})
+source_group("Source Files\\main"         FILES ${SRC_MAIN})
+source_group("Header Files\\config"       FILES ${HDR_CONFIG})
+source_group("Source Files\\config"       FILES ${SRC_CONFIG})
+source_group("Source Files\\external"     FILES ${SRC_EXTERNAL})
+source_group("Header Files\\gl"           FILES ${HDR_GL})
+source_group("Source Files\\gl"           FILES ${SRC_GL})
+source_group("Header Files\\gl\\draw"     FILES ${HDR_GL_DRAW})
+source_group("Source Files\\gl\\draw"     FILES ${SRC_GL_DRAW})
+source_group("Header Files\\manager"      FILES ${HDR_MANAGER})
+source_group("Source Files\\manager"      FILES ${SRC_MANAGER})
+source_group("UI Files\\main"             FILES ${UI_MAIN})
+source_group("Header Files\\map"          FILES ${HDR_MAP})
+source_group("Source Files\\map"          FILES ${SRC_MAP})
+source_group("Header Files\\model"        FILES ${HDR_MODEL})
+source_group("Source Files\\model"        FILES ${SRC_MODEL})
+source_group("Header Files\\request"      FILES ${HDR_REQUEST})
+source_group("Source Files\\request"      FILES ${SRC_REQUEST})
+source_group("Header Files\\settings"     FILES ${HDR_SETTINGS})
+source_group("Source Files\\settings"     FILES ${SRC_SETTINGS})
+source_group("Header Files\\types"        FILES ${HDR_TYPES})
+source_group("Source Files\\types"        FILES ${SRC_TYPES})
+source_group("Header Files\\ui"           FILES ${HDR_UI})
+source_group("Source Files\\ui"           FILES ${SRC_UI})
+source_group("Header Files\\ui\\settings" FILES ${HDR_UI_SETTINGS})
+source_group("Source Files\\ui\\settings" FILES ${SRC_UI_SETTINGS})
+source_group("Header Files\\ui\\setup"    FILES ${HDR_UI_SETUP})
+source_group("Source Files\\ui\\setup"    FILES ${SRC_UI_SETUP})
+source_group("UI Files\\ui"               FILES ${UI_UI})
+source_group("Header Files\\util"         FILES ${HDR_UTIL})
+source_group("Source Files\\util"         FILES ${SRC_UTIL})
+source_group("Header Files\\view"         FILES ${HDR_VIEW})
+source_group("Source Files\\view"         FILES ${SRC_VIEW})
+source_group("OpenGL Shaders"             FILES ${SHADER_FILES})
+source_group("Resources"                  FILES ${RESOURCE_FILES})
+source_group("Resources\\json"            FILES ${JSON_FILES})
+source_group("I18N Files"                 FILES ${TS_FILES})
 
 add_library(scwx-qt OBJECT ${PROJECT_SOURCES})
 set_property(TARGET scwx-qt PROPERTY AUTOMOC ON)
diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc
index d917c977..79d50f23 100644
--- a/scwx-qt/scwx-qt.qrc
+++ b/scwx-qt/scwx-qt.qrc
@@ -39,6 +39,7 @@
         res/icons/font-awesome-6/gears-solid.svg
         res/icons/font-awesome-6/github.svg
         res/icons/font-awesome-6/house-solid.svg
+        res/icons/font-awesome-6/keyboard-regular.svg
         res/icons/font-awesome-6/layer-group-solid.svg
         res/icons/font-awesome-6/palette-solid.svg
         res/icons/font-awesome-6/pause-solid.svg
diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp
index 3249577a..93ee8f4b 100644
--- a/scwx-qt/source/scwx/qt/main/main_window.cpp
+++ b/scwx-qt/source/scwx/qt/main/main_window.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -42,6 +43,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -197,7 +199,9 @@ public:
 
    QTimer clockTimer_ {};
 
-   std::shared_ptr     alertManager_;
+   std::shared_ptr  alertManager_;
+   std::shared_ptr hotkeyManager_ {
+      manager::HotkeyManager::Instance()};
    std::shared_ptr placefileManager_;
    std::shared_ptr  positionManager_;
    std::shared_ptr textEventManager_;
@@ -398,6 +402,24 @@ MainWindow::~MainWindow()
    delete ui;
 }
 
+void MainWindow::keyPressEvent(QKeyEvent* ev)
+{
+   if (p->hotkeyManager_->HandleKeyPress(ev))
+   {
+      p->activeMap_->update();
+      ev->accept();
+   }
+}
+
+void MainWindow::keyReleaseEvent(QKeyEvent* ev)
+{
+   if (p->hotkeyManager_->HandleKeyRelease(ev))
+   {
+      p->activeMap_->update();
+      ev->accept();
+   }
+}
+
 void MainWindow::showEvent(QShowEvent* event)
 {
    QMainWindow::showEvent(event);
diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp
index 21592538..d0adf225 100644
--- a/scwx-qt/source/scwx/qt/main/main_window.hpp
+++ b/scwx-qt/source/scwx/qt/main/main_window.hpp
@@ -26,6 +26,8 @@ public:
    MainWindow(QWidget* parent = nullptr);
    ~MainWindow();
 
+   void keyPressEvent(QKeyEvent* ev) override final;
+   void keyReleaseEvent(QKeyEvent* ev) override final;
    void showEvent(QShowEvent* event) override;
 
 signals:
diff --git a/scwx-qt/source/scwx/qt/manager/hotkey_manager.cpp b/scwx-qt/source/scwx/qt/manager/hotkey_manager.cpp
new file mode 100644
index 00000000..28575438
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/manager/hotkey_manager.cpp
@@ -0,0 +1,128 @@
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace manager
+{
+
+static const std::string logPrefix_ = "scwx::qt::manager::hotkey_manager";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
+
+class HotkeyManager::Impl
+{
+public:
+   explicit Impl()
+   {
+      auto& hotkeySettings = settings::HotkeySettings::Instance();
+
+      for (auto hotkey : types::HotkeyIterator())
+      {
+         auto& hotkeyVariable = hotkeySettings.hotkey(hotkey);
+
+         UpdateHotkey(hotkey, hotkeyVariable.GetValue());
+
+         callbacks_.emplace_back(hotkeyVariable,
+                                 hotkeyVariable.RegisterValueChangedCallback(
+                                    [this, hotkey](const std::string& value)
+                                    { UpdateHotkey(hotkey, value); }));
+      }
+   }
+
+   ~Impl()
+   {
+      for (auto& callback : callbacks_)
+      {
+         callback.first.UnregisterValueChangedCallback(callback.second);
+      }
+   }
+
+   void UpdateHotkey(types::Hotkey hotkey, const std::string& value);
+
+   std::vector<
+      std::pair&, boost::uuids::uuid>>
+                                                           callbacks_ {};
+   boost::container::flat_map hotkeys_ {};
+};
+
+HotkeyManager::HotkeyManager() : p(std::make_unique()) {}
+HotkeyManager::~HotkeyManager() = default;
+
+void HotkeyManager::Impl::UpdateHotkey(types::Hotkey      hotkey,
+                                       const std::string& value)
+{
+   hotkeys_.insert_or_assign(hotkey,
+                             QKeySequence {QString::fromStdString(value)});
+}
+
+bool HotkeyManager::HandleKeyPress(QKeyEvent* ev)
+{
+   logger_->trace("HandleKeyPress: {}, {}",
+                  ev->keyCombination().toCombined(),
+                  ev->isAutoRepeat());
+
+   bool hotkeyPressed = false;
+
+   for (auto& hotkey : p->hotkeys_)
+   {
+      if (hotkey.second.count() == 1 &&
+          hotkey.second[0] == ev->keyCombination())
+      {
+         hotkeyPressed = true;
+         Q_EMIT HotkeyPressed(hotkey.first, ev->isAutoRepeat());
+      }
+   }
+
+   return hotkeyPressed;
+}
+
+bool HotkeyManager::HandleKeyRelease(QKeyEvent* ev)
+{
+   logger_->trace("HandleKeyRelease: {}", ev->keyCombination().toCombined());
+
+   bool hotkeyReleased = false;
+
+   for (auto& hotkey : p->hotkeys_)
+   {
+      if (hotkey.second.count() == 1 &&
+          hotkey.second[0] == ev->keyCombination())
+      {
+         hotkeyReleased = true;
+         Q_EMIT HotkeyReleased(hotkey.first);
+      }
+   }
+
+   return hotkeyReleased;
+}
+
+std::shared_ptr HotkeyManager::Instance()
+{
+   static std::weak_ptr hotkeyManagerReference_ {};
+   static std::mutex                   instanceMutex_ {};
+
+   std::unique_lock lock(instanceMutex_);
+
+   std::shared_ptr hotkeyManager =
+      hotkeyManagerReference_.lock();
+
+   if (hotkeyManager == nullptr)
+   {
+      hotkeyManager           = std::make_shared();
+      hotkeyManagerReference_ = hotkeyManager;
+   }
+
+   return hotkeyManager;
+}
+
+} // namespace manager
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/manager/hotkey_manager.hpp b/scwx-qt/source/scwx/qt/manager/hotkey_manager.hpp
new file mode 100644
index 00000000..b4780714
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/manager/hotkey_manager.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include 
+
+#include 
+
+#include 
+
+class QKeyEvent;
+
+namespace scwx
+{
+namespace qt
+{
+namespace manager
+{
+
+class HotkeyManager : public QObject
+{
+   Q_OBJECT
+   Q_DISABLE_COPY_MOVE(HotkeyManager)
+
+public:
+   explicit HotkeyManager();
+   ~HotkeyManager();
+
+   bool HandleKeyPress(QKeyEvent* event);
+   bool HandleKeyRelease(QKeyEvent* event);
+
+   static std::shared_ptr Instance();
+
+signals:
+   void HotkeyPressed(scwx::qt::types::Hotkey hotkey, bool isAutoRepeat);
+   void HotkeyReleased(scwx::qt::types::Hotkey hotkey);
+
+private:
+   class Impl;
+   std::unique_ptr p;
+};
+
+} // namespace manager
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp
index 8a244054..e5e806ea 100644
--- a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp
+++ b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp
@@ -2,6 +2,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -132,6 +133,7 @@ boost::json::value SettingsManager::Impl::ConvertSettingsToJson()
 
    settings::GeneralSettings::Instance().WriteJson(settingsJson);
    settings::AudioSettings::Instance().WriteJson(settingsJson);
+   settings::HotkeySettings::Instance().WriteJson(settingsJson);
    settings::MapSettings::Instance().WriteJson(settingsJson);
    settings::PaletteSettings::Instance().WriteJson(settingsJson);
    settings::ProductSettings::Instance().WriteJson(settingsJson);
@@ -147,6 +149,7 @@ void SettingsManager::Impl::GenerateDefaultSettings()
 
    settings::GeneralSettings::Instance().SetDefaults();
    settings::AudioSettings::Instance().SetDefaults();
+   settings::HotkeySettings::Instance().SetDefaults();
    settings::MapSettings::Instance().SetDefaults();
    settings::PaletteSettings::Instance().SetDefaults();
    settings::ProductSettings::Instance().SetDefaults();
@@ -163,6 +166,7 @@ bool SettingsManager::Impl::LoadSettings(
 
    jsonDirty |= !settings::GeneralSettings::Instance().ReadJson(settingsJson);
    jsonDirty |= !settings::AudioSettings::Instance().ReadJson(settingsJson);
+   jsonDirty |= !settings::HotkeySettings::Instance().ReadJson(settingsJson);
    jsonDirty |= !settings::MapSettings::Instance().ReadJson(settingsJson);
    jsonDirty |= !settings::PaletteSettings::Instance().ReadJson(settingsJson);
    jsonDirty |= !settings::ProductSettings::Instance().ReadJson(settingsJson);
diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp
index e64ef32b..9fb5c253 100644
--- a/scwx-qt/source/scwx/qt/map/map_widget.cpp
+++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp
@@ -1,6 +1,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -39,6 +40,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -147,6 +149,9 @@ public:
                           const std::string& before);
    void ConnectMapSignals();
    void ConnectSignals();
+   void HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat);
+   void HandleHotkeyReleased(types::Hotkey hotkey);
+   void HandleHotkeyUpdates();
    void ImGuiCheckFonts();
    void InitializeNewRadarProductView(const std::string& colorPalette);
    void RadarProductManagerConnect();
@@ -190,6 +195,8 @@ public:
    std::shared_ptr layerModel_ {
       model::LayerModel::Instance()};
 
+   std::shared_ptr hotkeyManager_ {
+      manager::HotkeyManager::Instance()};
    std::shared_ptr placefileManager_ {
       manager::PlacefileManager::Instance()};
    std::shared_ptr radarProductManager_;
@@ -227,6 +234,9 @@ public:
    double prevBearing_;
    double prevPitch_;
 
+   std::set               activeHotkeys_ {};
+   std::chrono::system_clock::time_point prevHotkeyTime_ {};
+
 public slots:
    void Update();
 };
@@ -331,6 +341,160 @@ void MapWidgetImpl::ConnectSignals()
            [this](const QModelIndex& /* parent */, //
                   int /* first */,
                   int /* last */) { AddLayers(); });
+
+   connect(hotkeyManager_.get(),
+           &manager::HotkeyManager::HotkeyPressed,
+           this,
+           &MapWidgetImpl::HandleHotkeyPressed);
+   connect(hotkeyManager_.get(),
+           &manager::HotkeyManager::HotkeyReleased,
+           this,
+           &MapWidgetImpl::HandleHotkeyReleased);
+}
+
+void MapWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat)
+{
+   Q_UNUSED(isAutoRepeat);
+
+   switch (hotkey)
+   {
+   case types::Hotkey::ChangeMapStyle:
+      if (context_->settings().isActive_)
+      {
+         widget_->changeStyle();
+      }
+      break;
+
+   case types::Hotkey::CopyCursorCoordinates:
+      if (hasMouse_)
+      {
+         QClipboard* clipboard  = QGuiApplication::clipboard();
+         auto        coordinate = map_->coordinateForPixel(lastPos_);
+         std::string text =
+            fmt::format("{}, {}", coordinate.first, coordinate.second);
+         clipboard->setText(QString::fromStdString(text));
+      }
+      break;
+
+   case types::Hotkey::CopyMapCoordinates:
+      if (context_->settings().isActive_)
+      {
+         QClipboard* clipboard  = QGuiApplication::clipboard();
+         auto        coordinate = map_->coordinate();
+         std::string text =
+            fmt::format("{}, {}", coordinate.first, coordinate.second);
+         clipboard->setText(QString::fromStdString(text));
+      }
+      break;
+
+   default:
+      break;
+   }
+
+   activeHotkeys_.insert(hotkey);
+}
+
+void MapWidgetImpl::HandleHotkeyReleased(types::Hotkey hotkey)
+{
+   // Erase the hotkey from the active set regardless of whether this is the
+   // active map
+   activeHotkeys_.erase(hotkey);
+}
+
+void MapWidgetImpl::HandleHotkeyUpdates()
+{
+   using namespace std::chrono_literals;
+
+   static constexpr float  kMapPanFactor    = 0.2f;
+   static constexpr float  kMapRotateFactor = 0.2f;
+   static constexpr double kMapScaleFactor  = 1000.0;
+
+   std::chrono::system_clock::time_point hotkeyTime =
+      std::chrono::system_clock::now();
+   std::chrono::milliseconds hotkeyElapsed =
+      std::min(std::chrono::duration_cast(
+                  hotkeyTime - prevHotkeyTime_),
+               100ms);
+
+   prevHotkeyTime_ = hotkeyTime;
+
+   if (!context_->settings().isActive_)
+   {
+      // Don't attempt to handle a hotkey if this is not the active map
+      return;
+   }
+
+   for (auto& hotkey : activeHotkeys_)
+   {
+      switch (hotkey)
+      {
+      case types::Hotkey::MapPanUp:
+      {
+         QPointF delta {0.0f, kMapPanFactor * hotkeyElapsed.count()};
+         map_->moveBy(delta);
+         break;
+      }
+
+      case types::Hotkey::MapPanDown:
+      {
+         QPointF delta {0.0f, -kMapPanFactor * hotkeyElapsed.count()};
+         map_->moveBy(delta);
+         break;
+      }
+
+      case types::Hotkey::MapPanLeft:
+      {
+         QPointF delta {kMapPanFactor * hotkeyElapsed.count(), 0.0f};
+         map_->moveBy(delta);
+         break;
+      }
+
+      case types::Hotkey::MapPanRight:
+      {
+         QPointF delta {-kMapPanFactor * hotkeyElapsed.count(), 0.0f};
+         map_->moveBy(delta);
+         break;
+      }
+
+      case types::Hotkey::MapRotateClockwise:
+      {
+         QPointF delta {-kMapRotateFactor * hotkeyElapsed.count(), 0.0f};
+         map_->rotateBy({}, delta);
+         break;
+      }
+
+      case types::Hotkey::MapRotateCounterclockwise:
+      {
+         QPointF delta {kMapRotateFactor * hotkeyElapsed.count(), 0.0f};
+         map_->rotateBy({}, delta);
+         break;
+      }
+
+      case types::Hotkey::MapZoomIn:
+      {
+         auto    widgetSize = widget_->size();
+         QPointF center     = {widgetSize.width() * 0.5f,
+                               widgetSize.height() * 0.5f};
+         double  scale = std::pow(2.0, hotkeyElapsed.count() / kMapScaleFactor);
+         map_->scaleBy(scale, center);
+         break;
+      }
+
+      case types::Hotkey::MapZoomOut:
+      {
+         auto    widgetSize = widget_->size();
+         QPointF center     = {widgetSize.width() * 0.5f,
+                               widgetSize.height() * 0.5f};
+         double  scale =
+            1.0 / std::pow(2.0, hotkeyElapsed.count() / kMapScaleFactor);
+         map_->scaleBy(scale, center);
+         break;
+      }
+
+      default:
+         break;
+      }
+   }
 }
 
 common::Level3ProductCategoryMap MapWidget::GetAvailableLevel3Categories()
@@ -1061,16 +1225,18 @@ void MapWidget::leaveEvent(QEvent* /* ev */)
 
 void MapWidget::keyPressEvent(QKeyEvent* ev)
 {
-   switch (ev->key())
+   if (p->hotkeyManager_->HandleKeyPress(ev))
    {
-   case Qt::Key_S:
-      changeStyle();
-      break;
-   default:
-      break;
+      ev->accept();
    }
+}
 
-   ev->accept();
+void MapWidget::keyReleaseEvent(QKeyEvent* ev)
+{
+   if (p->hotkeyManager_->HandleKeyRelease(ev))
+   {
+      ev->accept();
+   }
 }
 
 void MapWidget::mousePressEvent(QMouseEvent* ev)
@@ -1197,6 +1363,9 @@ void MapWidget::paintGL()
 
    p->frameDraws_++;
 
+   // Handle hotkey updates
+   p->HandleHotkeyUpdates();
+
    // Setup ImGui Frame
    ImGui::SetCurrentContext(p->imGuiContext_);
 
diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp
index 7776e1c9..1b2c1220 100644
--- a/scwx-qt/source/scwx/qt/map/map_widget.hpp
+++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp
@@ -125,6 +125,7 @@ private:
    bool event(QEvent* e) override;
    void enterEvent(QEnterEvent* ev) override final;
    void keyPressEvent(QKeyEvent* ev) override final;
+   void keyReleaseEvent(QKeyEvent* ev) override final;
    void leaveEvent(QEvent* ev) override final;
    void mousePressEvent(QMouseEvent* ev) override final;
    void mouseMoveEvent(QMouseEvent* ev) override final;
diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp
new file mode 100644
index 00000000..0b22f9e9
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp
@@ -0,0 +1,126 @@
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace settings
+{
+
+static const std::string logPrefix_ = "scwx::qt::settings::hotkey_settings";
+
+static const std::unordered_map kDefaultHotkeys_ {
+   {types::Hotkey::ChangeMapStyle, QKeySequence {Qt::Key::Key_Z}},
+   {types::Hotkey::CopyCursorCoordinates,
+    QKeySequence {QKeyCombination {Qt::KeyboardModifier::ControlModifier,
+                                   Qt::Key::Key_C}}},
+   {types::Hotkey::CopyMapCoordinates,
+    QKeySequence {QKeyCombination {Qt::KeyboardModifier::ControlModifier |
+                                      Qt::KeyboardModifier::ShiftModifier,
+                                   Qt::Key::Key_C}}},
+   {types::Hotkey::MapPanUp, QKeySequence {Qt::Key::Key_W}},
+   {types::Hotkey::MapPanDown, QKeySequence {Qt::Key::Key_S}},
+   {types::Hotkey::MapPanLeft, QKeySequence {Qt::Key::Key_A}},
+   {types::Hotkey::MapPanRight, QKeySequence {Qt::Key::Key_D}},
+   {types::Hotkey::MapRotateClockwise, QKeySequence {Qt::Key::Key_E}},
+   {types::Hotkey::MapRotateCounterclockwise, QKeySequence {Qt::Key::Key_Q}},
+   {types::Hotkey::MapZoomIn, QKeySequence {Qt::Key::Key_Equal}},
+   {types::Hotkey::MapZoomOut, QKeySequence {Qt::Key::Key_Minus}},
+   {types::Hotkey::ProductTiltDecrease,
+    QKeySequence {Qt::Key::Key_BracketLeft}},
+   {types::Hotkey::ProductTiltIncrease,
+    QKeySequence {Qt::Key::Key_BracketRight}},
+   {types::Hotkey::TimelineStepBegin,
+    QKeySequence {QKeyCombination {Qt::KeyboardModifier::ControlModifier,
+                                   Qt::Key::Key_Left}}},
+   {types::Hotkey::TimelineStepBack, QKeySequence {Qt::Key::Key_Left}},
+   {types::Hotkey::TimelinePlay, QKeySequence {Qt::Key::Key_Space}},
+   {types::Hotkey::TimelineStepNext, QKeySequence {Qt::Key::Key_Right}},
+   {types::Hotkey::TimelineStepEnd,
+    QKeySequence {QKeyCombination {Qt::KeyboardModifier::ControlModifier,
+                                   Qt::Key::Key_Right}}},
+   {types::Hotkey::Unknown, QKeySequence {}}};
+
+static bool IsHotkeyValid(const std::string& value);
+
+class HotkeySettings::Impl
+{
+public:
+   explicit Impl()
+   {
+      for (const auto& hotkey : types::HotkeyIterator())
+      {
+         const std::string& name = types::GetHotkeyShortName(hotkey);
+         const std::string  defaultValue =
+            kDefaultHotkeys_.at(hotkey).toString().toStdString();
+
+         auto result =
+            hotkey_.emplace(hotkey, SettingsVariable {name});
+
+         SettingsVariable& settingsVariable = result.first->second;
+
+         settingsVariable.SetDefault(defaultValue);
+         settingsVariable.SetValidator(&IsHotkeyValid);
+
+         variables_.push_back(&settingsVariable);
+      }
+
+      // Add an empty hotkey (not part of registered variables) for error
+      // handling
+      hotkey_.emplace(types::Hotkey::Unknown,
+                      SettingsVariable {"?"});
+   }
+
+   ~Impl() {}
+
+   std::unordered_map> hotkey_ {};
+   std::vector variables_ {};
+};
+
+HotkeySettings::HotkeySettings() :
+    SettingsCategory("hotkeys"), p(std::make_unique())
+{
+   RegisterVariables(p->variables_);
+   SetDefaults();
+
+   p->variables_.clear();
+}
+HotkeySettings::~HotkeySettings() = default;
+
+HotkeySettings::HotkeySettings(HotkeySettings&&) noexcept            = default;
+HotkeySettings& HotkeySettings::operator=(HotkeySettings&&) noexcept = default;
+
+SettingsVariable&
+HotkeySettings::hotkey(scwx::qt::types::Hotkey hotkey) const
+{
+   auto hotkeyVariable = p->hotkey_.find(hotkey);
+   if (hotkeyVariable == p->hotkey_.cend())
+   {
+      hotkeyVariable = p->hotkey_.find(types::Hotkey::Unknown);
+   }
+   return hotkeyVariable->second;
+}
+
+HotkeySettings& HotkeySettings::Instance()
+{
+   static HotkeySettings hotkeySettings_;
+   return hotkeySettings_;
+}
+
+bool operator==(const HotkeySettings& lhs, const HotkeySettings& rhs)
+{
+   return (lhs.p->hotkey_ == rhs.p->hotkey_);
+}
+
+static bool IsHotkeyValid(const std::string& value)
+{
+   return QKeySequence::fromString(QString::fromStdString(value))
+             .toString()
+             .toStdString() == value;
+}
+
+} // namespace settings
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp
new file mode 100644
index 00000000..9fa56cc4
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace settings
+{
+
+class HotkeySettings : public SettingsCategory
+{
+public:
+   explicit HotkeySettings();
+   ~HotkeySettings();
+
+   HotkeySettings(const HotkeySettings&)            = delete;
+   HotkeySettings& operator=(const HotkeySettings&) = delete;
+
+   HotkeySettings(HotkeySettings&&) noexcept;
+   HotkeySettings& operator=(HotkeySettings&&) noexcept;
+
+   SettingsVariable& hotkey(scwx::qt::types::Hotkey hotkey) const;
+
+   static HotkeySettings& Instance();
+
+   friend bool operator==(const HotkeySettings& lhs, const HotkeySettings& rhs);
+
+private:
+   class Impl;
+   std::unique_ptr p;
+};
+
+} // namespace settings
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp
index a20e7dfa..2bf38e80 100644
--- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp
+++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -155,7 +156,27 @@ void SettingsInterface::SetEditWidget(QWidget* widget)
       return;
    }
 
-   if (QLineEdit* lineEdit = dynamic_cast(widget))
+   if (ui::HotkeyEdit* hotkeyEdit = dynamic_cast(widget))
+   {
+      if constexpr (std::is_same_v)
+      {
+         QObject::connect(hotkeyEdit,
+                          &ui::HotkeyEdit::KeySequenceChanged,
+                          p->context_.get(),
+                          [this](const QKeySequence& sequence)
+                          {
+                             std::string value {
+                                sequence.toString().toStdString()};
+
+                             // Attempt to stage the value
+                             p->stagedValid_ = p->variable_->StageValue(value);
+                             p->UpdateResetButton();
+
+                             // TODO: Display invalid status
+                          });
+      }
+   }
+   else if (QLineEdit* lineEdit = dynamic_cast(widget))
    {
       if constexpr (std::is_same_v)
       {
@@ -487,7 +508,16 @@ void SettingsInterface::Impl::UpdateEditWidget()
    const T                value        = variable_->GetValue();
    const T&               currentValue = staged.has_value() ? *staged : value;
 
-   if (QLineEdit* lineEdit = dynamic_cast(editWidget_))
+   if (ui::HotkeyEdit* hotkeyEdit = dynamic_cast(editWidget_))
+   {
+      if constexpr (std::is_same_v)
+      {
+         QKeySequence keySequence =
+            QKeySequence::fromString(QString::fromStdString(currentValue));
+         hotkeyEdit->set_key_sequence(keySequence);
+      }
+   }
+   else if (QLineEdit* lineEdit = dynamic_cast(editWidget_))
    {
       SetWidgetText(lineEdit, currentValue);
    }
diff --git a/scwx-qt/source/scwx/qt/types/hotkey_types.cpp b/scwx-qt/source/scwx/qt/types/hotkey_types.cpp
new file mode 100644
index 00000000..c0989888
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/types/hotkey_types.cpp
@@ -0,0 +1,72 @@
+#include 
+#include 
+
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace types
+{
+
+static const std::unordered_map hotkeyShortName_ {
+   {Hotkey::ChangeMapStyle, "change_map_style"},
+   {Hotkey::CopyCursorCoordinates, "copy_cursor_coordinates"},
+   {Hotkey::CopyMapCoordinates, "copy_map_coordinates"},
+   {Hotkey::MapPanUp, "map_pan_up"},
+   {Hotkey::MapPanDown, "map_pan_down"},
+   {Hotkey::MapPanLeft, "map_pan_left"},
+   {Hotkey::MapPanRight, "map_pan_right"},
+   {Hotkey::MapRotateClockwise, "map_rotate_clockwise"},
+   {Hotkey::MapRotateCounterclockwise, "map_rotate_counterclockwise"},
+   {Hotkey::MapZoomIn, "map_zoom_in"},
+   {Hotkey::MapZoomOut, "map_zoom_out"},
+   {Hotkey::ProductTiltDecrease, "product_tilt_decrease"},
+   {Hotkey::ProductTiltIncrease, "product_tilt_increase"},
+   {Hotkey::TimelineStepBegin, "timeline_step_begin"},
+   {Hotkey::TimelineStepBack, "timeline_step_back"},
+   {Hotkey::TimelinePlay, "timeline_play"},
+   {Hotkey::TimelineStepNext, "timeline_step_next"},
+   {Hotkey::TimelineStepEnd, "timeline_step_end"},
+   {Hotkey::Unknown, "?"}};
+
+static const std::unordered_map hotkeyLongName_ {
+   {Hotkey::ChangeMapStyle, "Change Map Style"},
+   {Hotkey::CopyCursorCoordinates, "Copy Cursor Coordinates"},
+   {Hotkey::CopyMapCoordinates, "Copy Map Coordinates"},
+   {Hotkey::MapPanUp, "Map Pan Up"},
+   {Hotkey::MapPanDown, "Map Pan Down"},
+   {Hotkey::MapPanLeft, "Map Pan Left"},
+   {Hotkey::MapPanRight, "Map Pan Right"},
+   {Hotkey::MapRotateClockwise, "Map Rotate Clockwise"},
+   {Hotkey::MapRotateCounterclockwise, "Map Rotate Counterclockwise"},
+   {Hotkey::MapZoomIn, "Map Zoom In"},
+   {Hotkey::MapZoomOut, "Map Zoom Out"},
+   {Hotkey::ProductTiltDecrease, "Product Tilt Decrease"},
+   {Hotkey::ProductTiltIncrease, "Product Tilt Increase"},
+   {Hotkey::TimelineStepBegin, "Timeline Step Begin"},
+   {Hotkey::TimelineStepBack, "Timeline Step Back"},
+   {Hotkey::TimelinePlay, "Timeline Play/Pause"},
+   {Hotkey::TimelineStepNext, "Timeline Step Next"},
+   {Hotkey::TimelineStepEnd, "Timeline Step End"},
+   {Hotkey::Unknown, "?"}};
+
+SCWX_GET_ENUM(Hotkey, GetHotkeyFromShortName, hotkeyShortName_)
+SCWX_GET_ENUM(Hotkey, GetHotkeyFromLongName, hotkeyLongName_)
+
+const std::string& GetHotkeyShortName(Hotkey hotkey)
+{
+   return hotkeyShortName_.at(hotkey);
+}
+
+const std::string& GetHotkeyLongName(Hotkey hotkey)
+{
+   return hotkeyLongName_.at(hotkey);
+}
+
+} // namespace types
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/types/hotkey_types.hpp b/scwx-qt/source/scwx/qt/types/hotkey_types.hpp
new file mode 100644
index 00000000..434a01b7
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/types/hotkey_types.hpp
@@ -0,0 +1,47 @@
+#pragma once
+
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace types
+{
+
+enum class Hotkey
+{
+   ChangeMapStyle,
+   CopyCursorCoordinates,
+   CopyMapCoordinates,
+   MapPanUp,
+   MapPanDown,
+   MapPanLeft,
+   MapPanRight,
+   MapRotateClockwise,
+   MapRotateCounterclockwise,
+   MapZoomIn,
+   MapZoomOut,
+   ProductTiltDecrease,
+   ProductTiltIncrease,
+   TimelineStepBegin,
+   TimelineStepBack,
+   TimelinePlay,
+   TimelineStepNext,
+   TimelineStepEnd,
+   Unknown
+};
+typedef scwx::util::
+   Iterator
+      HotkeyIterator;
+
+Hotkey             GetHotkeyFromShortName(const std::string& name);
+Hotkey             GetHotkeyFromLongName(const std::string& name);
+const std::string& GetHotkeyShortName(Hotkey hotkey);
+const std::string& GetHotkeyLongName(Hotkey hotkey);
+
+} // namespace types
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp
index f19be01f..ccc6bdca 100644
--- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp
+++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp
@@ -1,6 +1,7 @@
 #include "animation_dock_widget.hpp"
 #include "ui_animation_dock_widget.h"
 
+#include 
 #include 
 #include 
 #include 
@@ -39,6 +40,9 @@ public:
 
    AnimationDockWidget* self_;
 
+   std::shared_ptr hotkeyManager_ {
+      manager::HotkeyManager::Instance()};
+
    types::AnimationState animationState_ {types::AnimationState::Pause};
    types::MapTime        viewType_ {types::MapTime::Live};
    bool                  isLive_ {true};
@@ -220,6 +224,39 @@ void AnimationDockWidgetImpl::ConnectSignals()
                     &QAbstractButton::clicked,
                     self_,
                     [this]() { Q_EMIT self_->AnimationStepEndSelected(); });
+
+   // Shortcuts
+   QObject::connect(hotkeyManager_.get(),
+                    &manager::HotkeyManager::HotkeyPressed,
+                    self_,
+                    [this](types::Hotkey hotkey, bool /* isAutoRepeat */)
+                    {
+                       switch (hotkey)
+                       {
+                       case types::Hotkey::TimelineStepBegin:
+                          Q_EMIT self_->AnimationStepBeginSelected();
+                          break;
+
+                       case types::Hotkey::TimelineStepBack:
+                          Q_EMIT self_->AnimationStepBackSelected();
+                          break;
+
+                       case types::Hotkey::TimelinePlay:
+                          Q_EMIT self_->AnimationPlaySelected();
+                          break;
+
+                       case types::Hotkey::TimelineStepNext:
+                          Q_EMIT self_->AnimationStepNextSelected();
+                          break;
+
+                       case types::Hotkey::TimelineStepEnd:
+                          Q_EMIT self_->AnimationStepEndSelected();
+                          break;
+
+                       default:
+                          break;
+                       }
+                    });
 }
 
 void AnimationDockWidget::UpdateAnimationState(types::AnimationState state)
diff --git a/scwx-qt/source/scwx/qt/ui/hotkey_edit.cpp b/scwx-qt/source/scwx/qt/ui/hotkey_edit.cpp
new file mode 100644
index 00000000..a3aa649d
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/hotkey_edit.cpp
@@ -0,0 +1,137 @@
+#include 
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+static const std::string logPrefix_ = "scwx::qt::ui::hotkey_edit";
+static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);
+
+class HotkeyEdit::Impl
+{
+public:
+   explicit Impl() {};
+   ~Impl() = default;
+
+   QKeySequence sequence_ {};
+};
+
+HotkeyEdit::HotkeyEdit(QWidget* parent) :
+    QLineEdit(parent), p {std::make_unique()}
+{
+   setReadOnly(true);
+   setClearButtonEnabled(true);
+
+   QAction* clearAction = findChild();
+   if (clearAction != nullptr)
+   {
+      clearAction->setEnabled(true);
+
+      connect(clearAction,
+              &QAction::triggered,
+              this,
+              [this](bool /* checked */)
+              {
+                 logger_->trace("clearAction");
+
+                 if (!p->sequence_.isEmpty())
+                 {
+                    // Clear saved sequence
+                    p->sequence_ = QKeySequence {};
+                    setText(p->sequence_.toString());
+                    Q_EMIT KeySequenceChanged({});
+                 }
+              });
+   }
+}
+
+HotkeyEdit::~HotkeyEdit() {}
+
+QKeySequence HotkeyEdit::key_sequence() const
+{
+   return p->sequence_;
+}
+
+void HotkeyEdit::set_key_sequence(const QKeySequence& sequence)
+{
+   if (sequence != p->sequence_)
+   {
+      p->sequence_ = sequence;
+      setText(sequence.toString());
+      Q_EMIT KeySequenceChanged(sequence);
+   }
+}
+
+void HotkeyEdit::focusInEvent(QFocusEvent* e)
+{
+   logger_->trace("focusInEvent");
+
+   // Replace text with placeholder prompting for input
+   setPlaceholderText("Press any key");
+   setText({});
+
+   QLineEdit::focusInEvent(e);
+}
+
+void HotkeyEdit::focusOutEvent(QFocusEvent* e)
+{
+   logger_->trace("focusOutEvent");
+
+   // Replace text with saved sequence
+   setPlaceholderText({});
+   setText(p->sequence_.toString());
+
+   QLineEdit::focusOutEvent(e);
+}
+
+void HotkeyEdit::keyPressEvent(QKeyEvent* e)
+{
+   logger_->trace("keyPressEvent");
+
+   QKeySequence sequence {};
+
+   switch (e->key())
+   {
+   case Qt::Key::Key_Shift:
+   case Qt::Key::Key_Control:
+   case Qt::Key::Key_Alt:
+   case Qt::Key::Key_Meta:
+   case Qt::Key::Key_Mode_switch:
+      // Record modifiers only in sequence
+      sequence = e->modifiers().toInt();
+      break;
+
+   default:
+      // Record modifiers and keys in sequence, and save sequence
+      sequence = e->modifiers().toInt() | e->key();
+
+      if (sequence != p->sequence_)
+      {
+         p->sequence_ = sequence;
+         Q_EMIT KeySequenceChanged(sequence);
+      }
+
+      clearFocus();
+      break;
+   }
+
+   setText(sequence.toString());
+}
+
+void HotkeyEdit::keyReleaseEvent(QKeyEvent*)
+{
+   logger_->trace("keyReleaseEvent");
+
+   // Modifiers were released prior to pressing a non-modifier key
+   setText({});
+}
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/hotkey_edit.hpp b/scwx-qt/source/scwx/qt/ui/hotkey_edit.hpp
new file mode 100644
index 00000000..316fa8f8
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/hotkey_edit.hpp
@@ -0,0 +1,41 @@
+#pragma once
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+class HotkeyEdit : public QLineEdit
+{
+   Q_OBJECT
+   Q_DISABLE_COPY_MOVE(HotkeyEdit)
+
+public:
+   explicit HotkeyEdit(QWidget* parent = nullptr);
+   ~HotkeyEdit();
+
+   QKeySequence key_sequence() const;
+
+   void set_key_sequence(const QKeySequence& sequence);
+
+protected:
+   void focusInEvent(QFocusEvent* e) override;
+   void focusOutEvent(QFocusEvent* e) override;
+   void keyPressEvent(QKeyEvent* e) override;
+   void keyReleaseEvent(QKeyEvent* e) override;
+
+signals:
+   void KeySequenceChanged(const QKeySequence& sequence);
+
+private:
+   class Impl;
+   std::unique_ptr p;
+};
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp
index 18e9ebf1..85b476c9 100644
--- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp
+++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp
@@ -1,6 +1,8 @@
 #include 
 #include 
+#include 
 #include 
+#include 
 
 #include 
 
@@ -16,6 +18,9 @@ namespace qt
 namespace ui
 {
 
+static const std::string logPrefix_ = "scwx::qt::ui::level2_settings_widget";
+static const auto        logger_    = util::Logger::Create(logPrefix_);
+
 class Level2SettingsWidgetImpl : public QObject
 {
    Q_OBJECT
@@ -46,9 +51,15 @@ public:
       settingsLayout->addWidget(declutterCheckBox_);
 
       settingsGroupBox_->setVisible(false);
+
+      QObject::connect(hotkeyManager_.get(),
+                       &manager::HotkeyManager::HotkeyPressed,
+                       this,
+                       &Level2SettingsWidgetImpl::HandleHotkeyPressed);
    }
    ~Level2SettingsWidgetImpl() = default;
 
+   void HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat);
    void NormalizeElevationButtons();
    void SelectElevation(float elevation);
 
@@ -63,6 +74,12 @@ public:
 
    QGroupBox* settingsGroupBox_;
    QCheckBox* declutterCheckBox_;
+
+   float        currentElevation_ {};
+   QToolButton* currentElevationButton_ {nullptr};
+
+   std::shared_ptr hotkeyManager_ {
+      manager::HotkeyManager::Instance()};
 };
 
 Level2SettingsWidget::Level2SettingsWidget(QWidget* parent) :
@@ -96,6 +113,71 @@ void Level2SettingsWidget::showEvent(QShowEvent* event)
    p->NormalizeElevationButtons();
 }
 
+void Level2SettingsWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey,
+                                                   bool          isAutoRepeat)
+{
+   if (hotkey != types::Hotkey::ProductTiltDecrease &&
+       hotkey != types::Hotkey::ProductTiltIncrease)
+   {
+      // Not handling this hotkey
+      return;
+   }
+
+   logger_->trace("Handling hotkey: {}, repeat: {}",
+                  types::GetHotkeyShortName(hotkey),
+                  isAutoRepeat);
+
+   if (!self_->isVisible() || currentElevationButton_ == nullptr)
+   {
+      // Level 2 product is not selected
+      return;
+   }
+
+   // Find the current elevation tilt
+   auto tiltIt = std::find(elevationButtons_.cbegin(),
+                           elevationButtons_.cend(),
+                           currentElevationButton_);
+
+   if (tiltIt == elevationButtons_.cend())
+   {
+      logger_->error("Could not locate level 2 tilt: {}", currentElevation_);
+      return;
+   }
+
+   if (hotkey == types::Hotkey::ProductTiltDecrease)
+   {
+      // Validate the current elevation tilt
+      if (tiltIt != elevationButtons_.cbegin())
+      {
+         // Get the previous elevation tilt
+         --tiltIt;
+
+         // Select the new elevation tilt
+         (*tiltIt)->click();
+      }
+      else
+      {
+         logger_->info("Level 2 tilt at lower limit");
+      }
+   }
+   else if (hotkey == types::Hotkey::ProductTiltIncrease)
+   {
+      // Get the next elevation tilt
+      ++tiltIt;
+
+      // Validate the next elevation tilt
+      if (tiltIt != elevationButtons_.cend())
+      {
+         // Select the new elevation tilt
+         (*tiltIt)->click();
+      }
+      else
+      {
+         logger_->info("Level 2 tilt at upper limit");
+      }
+   }
+}
+
 void Level2SettingsWidgetImpl::NormalizeElevationButtons()
 {
    // Set each elevation cut's tool button to the same size
@@ -135,12 +217,15 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation)
    QString buttonText {QString::number(elevation, 'f', 1) +
                        common::Characters::DEGREE};
 
+   QToolButton* newElevationButton = nullptr;
+
    std::for_each(p->elevationButtons_.cbegin(),
                  p->elevationButtons_.cend(),
                  [&](auto& toolButton)
                  {
                     if (toolButton->text() == buttonText)
                     {
+                       newElevationButton = toolButton;
                        toolButton->setCheckable(true);
                        toolButton->setChecked(true);
                     }
@@ -150,6 +235,9 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation)
                        toolButton->setCheckable(false);
                     }
                  });
+
+   p->currentElevation_       = elevation;
+   p->currentElevationButton_ = newElevationButton;
 }
 
 void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap)
diff --git a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp
index 99f22092..eb2d9f30 100644
--- a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp
+++ b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp
@@ -1,5 +1,6 @@
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -127,9 +128,15 @@ public:
 
       stiPastEnabled_.SetEditWidget(stiPastEnableCheckBox);
       stiForecastEnabled_.SetEditWidget(stiForecastEnableCheckBox);
+
+      QObject::connect(hotkeyManager_.get(),
+                       &manager::HotkeyManager::HotkeyPressed,
+                       this,
+                       &Level3ProductsWidgetImpl::HandleHotkeyPressed);
    }
    ~Level3ProductsWidgetImpl() = default;
 
+   void HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat);
    void NormalizeProductButtons();
    void SelectProductCategory(common::Level3ProductCategory category);
    void UpdateCategorySelection(common::Level3ProductCategory category);
@@ -144,11 +151,17 @@ public:
                       std::unordered_map>
       categoryMenuMap_;
 
+   std::shared_ptr hotkeyManager_ {
+      manager::HotkeyManager::Instance()};
+
    std::unordered_map> productTiltMap_;
 
    std::unordered_map awipsProductMap_;
    std::shared_mutex                         awipsProductMutex_;
 
+   std::string currentAwipsId_ {};
+   QAction*    currentProductTiltAction_ {nullptr};
+
    settings::SettingsInterface stiPastEnabled_ {};
    settings::SettingsInterface stiForecastEnabled_ {};
 };
@@ -167,6 +180,84 @@ void Level3ProductsWidget::showEvent(QShowEvent* event)
    p->NormalizeProductButtons();
 }
 
+void Level3ProductsWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey,
+                                                   bool          isAutoRepeat)
+{
+   if (hotkey != types::Hotkey::ProductTiltDecrease &&
+       hotkey != types::Hotkey::ProductTiltIncrease)
+   {
+      // Not handling this hotkey
+      return;
+   }
+
+   logger_->trace("Handling hotkey: {}, repeat: {}",
+                  types::GetHotkeyShortName(hotkey),
+                  isAutoRepeat);
+
+   std::string currentAwipsId           = currentAwipsId_;
+   QAction*    currentProductTiltAction = currentProductTiltAction_;
+
+   if (currentProductTiltAction == nullptr || currentAwipsId.empty() ||
+       currentAwipsId == "?")
+   {
+      // Level 3 product is not selected
+      return;
+   }
+
+   // Get product
+   std::string product = common::GetLevel3ProductByAwipsId(currentAwipsId);
+   if (product == "?")
+   {
+      logger_->error("Invalid AWIPS ID: {}", currentAwipsId);
+      return;
+   }
+
+   std::shared_lock lock {awipsProductMutex_};
+
+   // Find the current product tilt
+   auto productTiltsIt = productTiltMap_.find(product);
+   if (productTiltsIt == productTiltMap_.cend())
+   {
+      logger_->error("Could not find product tilt map: {}",
+                     common::GetLevel3ProductDescription(product));
+      return;
+   }
+
+   auto& productTilts  = productTiltsIt->second;
+   auto  productTiltIt = std::find(
+      productTilts.cbegin(), productTilts.cend(), currentProductTiltAction);
+   if (productTiltIt == productTilts.cend())
+   {
+      logger_->error("Could not locate product tilt: {}", currentAwipsId);
+      return;
+   }
+
+   std::ptrdiff_t productTiltIndex =
+      std::distance(productTilts.cbegin(), productTiltIt);
+
+   // Determine the new product tilt index
+   std::ptrdiff_t newProductTiltIndex =
+      (hotkey == types::Hotkey::ProductTiltDecrease) ? productTiltIndex - 1 :
+                                                       productTiltIndex + 1;
+
+   // Validate the new product tilt index
+   if (newProductTiltIndex < 0 ||
+       newProductTiltIndex >=
+          static_cast(productTilts.size()) ||
+       !productTilts.at(newProductTiltIndex)->isVisible())
+   {
+      const std::string direction =
+         (hotkey == types::Hotkey::ProductTiltDecrease) ? "lower" : "upper";
+
+      logger_->info("Product tilt at {} limit", direction);
+
+      return;
+   }
+
+   // Select the new tilt
+   productTilts.at(newProductTiltIndex)->trigger();
+}
+
 void Level3ProductsWidgetImpl::NormalizeProductButtons()
 {
    int level3MaxWidth = 0;
@@ -283,6 +374,8 @@ void Level3ProductsWidget::UpdateProductSelection(
    else
    {
       p->UpdateCategorySelection(common::Level3ProductCategory::Unknown);
+      p->currentAwipsId_.erase();
+      p->currentProductTiltAction_ = nullptr;
    }
 }
 
@@ -293,7 +386,7 @@ void Level3ProductsWidgetImpl::UpdateCategorySelection(
 
    std::for_each(categoryButtons_.cbegin(),
                  categoryButtons_.cend(),
-                 [&](auto& toolButton)
+                 [&, this](auto& toolButton)
                  {
                     if (toolButton->text().toStdString() == categoryName)
                     {
@@ -313,10 +406,25 @@ void Level3ProductsWidgetImpl::UpdateProductSelection(
 {
    std::shared_lock lock {awipsProductMutex_};
 
+   QAction* newProductTilt = nullptr;
+
    std::for_each(awipsProductMap_.cbegin(),
                  awipsProductMap_.cend(),
-                 [=](const auto& pair)
-                 { pair.first->setChecked(pair.second == awipsId); });
+                 [&, this](const auto& pair)
+                 {
+                    if (pair.second == awipsId)
+                    {
+                       newProductTilt = pair.first;
+                       pair.first->setChecked(true);
+                    }
+                    else
+                    {
+                       pair.first->setChecked(false);
+                    }
+                 });
+
+   currentAwipsId_           = awipsId;
+   currentProductTiltAction_ = newProductTilt;
 }
 
 } // namespace ui
diff --git a/scwx-qt/source/scwx/qt/ui/settings/hotkey_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/hotkey_settings_widget.cpp
new file mode 100644
index 00000000..a636b78a
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/settings/hotkey_settings_widget.cpp
@@ -0,0 +1,107 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+static const std::string logPrefix_ =
+   "scwx::qt::ui::settings::hotkey_settings_widget";
+
+class HotkeySettingsWidget::Impl
+{
+public:
+   explicit Impl(HotkeySettingsWidget* self)
+   {
+      auto& hotkeySettings = settings::HotkeySettings::Instance();
+
+      gridLayout_ = new QGridLayout(self);
+      contents_   = new QWidget(self);
+      contents_->setLayout(gridLayout_);
+
+      scrollArea_ = new QScrollArea(self);
+      scrollArea_->setHorizontalScrollBarPolicy(
+         Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
+      scrollArea_->setWidgetResizable(true);
+      scrollArea_->setWidget(contents_);
+
+      layout_ = new QVBoxLayout(self);
+      layout_->setContentsMargins(0, 0, 0, 0);
+      layout_->addWidget(scrollArea_);
+
+      self->setLayout(layout_);
+
+      int row = 0;
+
+      for (types::Hotkey hotkey : types::HotkeyIterator())
+      {
+         const std::string& labelText = types::GetHotkeyLongName(hotkey);
+
+         QLabel*      label = new QLabel(QObject::tr(labelText.c_str()), self);
+         HotkeyEdit*  hotkeyEdit  = new HotkeyEdit(self);
+         QToolButton* resetButton = new QToolButton(self);
+
+         resetButton->setIcon(
+            QIcon {":/res/icons/font-awesome-6/rotate-left-solid.svg"});
+         resetButton->setVisible(false);
+
+         gridLayout_->addWidget(label, row, 0);
+         gridLayout_->addWidget(hotkeyEdit, row, 1);
+         gridLayout_->addWidget(resetButton, row, 2);
+
+         // Create settings interface
+         auto result = hotkeys_.emplace(
+            hotkey, settings::SettingsInterface {});
+         auto& pair      = *result.first;
+         auto& interface = pair.second;
+
+         // Add to settings list
+         self->AddSettingsInterface(&interface);
+
+         auto& hotkeyVariable = hotkeySettings.hotkey(hotkey);
+         interface.SetSettingsVariable(hotkeyVariable);
+         interface.SetEditWidget(hotkeyEdit);
+         interface.SetResetButton(resetButton);
+
+         ++row;
+      }
+
+      QSpacerItem* spacer =
+         new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
+      gridLayout_->addItem(spacer, row, 0);
+   }
+   ~Impl() = default;
+
+   QWidget*     contents_;
+   QLayout*     layout_;
+   QScrollArea* scrollArea_ {};
+   QGridLayout* gridLayout_ {};
+
+   boost::unordered_flat_map>
+      hotkeys_ {};
+};
+
+HotkeySettingsWidget::HotkeySettingsWidget(QWidget* parent) :
+    SettingsPageWidget(parent), p {std::make_shared(this)}
+{
+}
+
+HotkeySettingsWidget::~HotkeySettingsWidget() = default;
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/settings/hotkey_settings_widget.hpp b/scwx-qt/source/scwx/qt/ui/settings/hotkey_settings_widget.hpp
new file mode 100644
index 00000000..9e8dd802
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/settings/hotkey_settings_widget.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+class HotkeySettingsWidget : public SettingsPageWidget
+{
+   Q_OBJECT
+
+public:
+   explicit HotkeySettingsWidget(QWidget* parent = nullptr);
+   ~HotkeySettingsWidget();
+
+private:
+   class Impl;
+   std::shared_ptr p;
+};
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp
new file mode 100644
index 00000000..d174fbbd
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp
@@ -0,0 +1,68 @@
+#include 
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+static const std::string logPrefix_ =
+   "scwx::qt::ui::settings::settings_page_widget";
+
+class SettingsPageWidget::Impl
+{
+public:
+   explicit Impl() {}
+   ~Impl() = default;
+
+   std::vector settings_;
+};
+
+SettingsPageWidget::SettingsPageWidget(QWidget* parent) :
+    QWidget(parent), p {std::make_shared()}
+{
+}
+
+SettingsPageWidget::~SettingsPageWidget() = default;
+
+void SettingsPageWidget::AddSettingsInterface(
+   settings::SettingsInterfaceBase* setting)
+{
+   p->settings_.push_back(setting);
+}
+
+bool SettingsPageWidget::CommitChanges()
+{
+   bool committed = false;
+
+   for (auto& setting : p->settings_)
+   {
+      committed |= setting->Commit();
+   }
+
+   return committed;
+}
+
+void SettingsPageWidget::DiscardChanges()
+{
+   for (auto& setting : p->settings_)
+   {
+      setting->Reset();
+   }
+}
+
+void SettingsPageWidget::ResetToDefault()
+{
+   for (auto& setting : p->settings_)
+   {
+      setting->StageDefault();
+   }
+}
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp
new file mode 100644
index 00000000..e2c2ea59
--- /dev/null
+++ b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+#include 
+
+#include 
+
+namespace scwx
+{
+namespace qt
+{
+namespace ui
+{
+
+class SettingsPageWidget : public QWidget
+{
+   Q_OBJECT
+
+public:
+   explicit SettingsPageWidget(QWidget* parent = nullptr);
+   ~SettingsPageWidget();
+
+   bool CommitChanges();
+   void DiscardChanges();
+   void ResetToDefault();
+
+protected:
+   void AddSettingsInterface(settings::SettingsInterfaceBase* setting);
+
+private:
+   class Impl;
+   std::shared_ptr p;
+};
+
+} // namespace ui
+} // namespace qt
+} // namespace scwx
diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp
index 635324cc..04d9331a 100644
--- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp
+++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp
@@ -22,6 +22,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -177,6 +178,7 @@ public:
    void SetupPalettesAlertsTab();
    void SetupAudioTab();
    void SetupTextTab();
+   void SetupHotkeysTab();
 
    void ShowColorDialog(QLineEdit* lineEdit, QFrame* frame = nullptr);
    void UpdateRadarDialogLocation(const std::string& id);
@@ -216,6 +218,9 @@ public:
    std::shared_ptr positionManager_ {
       manager::PositionManager::Instance()};
 
+   std::vector settingsPages_ {};
+   HotkeySettingsWidget*            hotkeySettingsWidget_ {};
+
    settings::SettingsInterface  defaultRadarSite_ {};
    settings::SettingsInterface gridWidth_ {};
    settings::SettingsInterface gridHeight_ {};
@@ -289,6 +294,9 @@ SettingsDialog::SettingsDialog(QWidget* parent) :
    // Text
    p->SetupTextTab();
 
+   // Hotkeys
+   p->SetupHotkeysTab();
+
    p->ConnectSignals();
 }
 
@@ -1171,6 +1179,16 @@ void SettingsDialogImpl::SetupTextTab()
       self_->ui->radarSiteHoverTextCheckBox);
 }
 
+void SettingsDialogImpl::SetupHotkeysTab()
+{
+   QVBoxLayout* layout = new QVBoxLayout(self_->ui->hotkeys);
+
+   hotkeySettingsWidget_ = new HotkeySettingsWidget(self_->ui->hotkeys);
+   layout->addWidget(hotkeySettingsWidget_);
+
+   settingsPages_.push_back(hotkeySettingsWidget_);
+}
+
 QImage SettingsDialogImpl::GenerateColorTableImage(
    std::shared_ptr colorTable,
    std::uint16_t                       min,
@@ -1343,6 +1361,11 @@ void SettingsDialogImpl::ApplyChanges()
       committed |= setting->Commit();
    }
 
+   for (auto& page : settingsPages_)
+   {
+      committed |= page->CommitChanges();
+   }
+
    if (committed)
    {
       manager::SettingsManager::Instance().SaveSettings();
@@ -1357,6 +1380,11 @@ void SettingsDialogImpl::DiscardChanges()
    {
       setting->Reset();
    }
+
+   for (auto& page : settingsPages_)
+   {
+      page->DiscardChanges();
+   }
 }
 
 void SettingsDialogImpl::ResetToDefault()
@@ -1367,6 +1395,11 @@ void SettingsDialogImpl::ResetToDefault()
    {
       setting->StageDefault();
    }
+
+   for (auto& page : settingsPages_)
+   {
+      page->ResetToDefault();
+   }
 }
 
 std::string SettingsDialogImpl::RadarSiteLabel(
diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui
index 7b026bc3..6790b659 100644
--- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui
+++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui
@@ -13,8 +13,8 @@
   
    Settings
   
-  
-   - 
+  
+   - 
     
      
       QFrame::StyledPanel
@@ -95,6 +95,15 @@
             :/res/icons/font-awesome-6/font-solid.svg:/res/icons/font-awesome-6/font-solid.svg
           
          +
- 
+          
+           Hotkeys
+          
+          
+           
+            :/res/icons/font-awesome-6/keyboard-regular.svg:/res/icons/font-awesome-6/keyboard-regular.svg
+          
+         @@ -209,6 +218,9 @@
- 
                
               +
- 
+               
+              
- 
                
                 
@@ -249,6 +261,17 @@
                 
                
               +
- 
+               
+                
+                 ...
+                
+                
+                 
+                  :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
+                
+               
+              
- 
                
               @@ -329,20 +352,6 @@
- 
                
               -
- 
-               
-              -
- 
-               
-                
-                 ...
-                
-                
-                 
-                  :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg
-                
-               
-              
- 
                
                 
@@ -429,8 +438,8 @@
                    
                     0
                     0
-                    63
-                    18
+                    498
+                    383
                    
                   
                   
@@ -1007,13 +1016,14 @@
            +
-- 
+   - 
     
      
       Qt::Horizontal
diff --git a/test/data b/test/data
index 2ba87405..260b3400 160000
--- a/test/data
+++ b/test/data
@@ -1 +1 @@
-Subproject commit 2ba8740516bbdc58c848bf71755b2f285aa47938
+Subproject commit 260b340030487b01ce9aa37135d949008c972f27