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