From 77d3357e8b2d3685f267b50f1a6e0eb25a1505ff Mon Sep 17 00:00:00 2001 From: Nyjako Date: Mon, 17 Mar 2025 20:44:47 +0100 Subject: [PATCH 1/9] Added UIOHook library --- .gitmodules | 3 +++ CMakeLists.txt | 9 ++++++++ cmake/Finduiohook.cmake | 46 +++++++++++++++++++++++++++++++++++++++++ subprojects/libuiohook | 1 + 4 files changed, 59 insertions(+) create mode 100644 cmake/Finduiohook.cmake create mode 160000 subprojects/libuiohook diff --git a/.gitmodules b/.gitmodules index e0d062ad..95d378f3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "subprojects/qrcodegen"] path = subprojects/qrcodegen url = https://github.com/nayuki/QR-Code-generator +[submodule "subprojects/libuiohook"] + path = subprojects/libuiohook + url = https://github.com/kwhat/libuiohook diff --git a/CMakeLists.txt b/CMakeLists.txt index 49bc2ece..8cc60e63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,15 @@ endif () find_package(spdlog REQUIRED) target_link_libraries(abaddon spdlog::spdlog) +find_package(uiohook QUIET) +if (NOT uiohook_FOUND) + message("uiohook was not found and will be included as a submodule") + add_subdirectory(subprojects/libuiohook) + target_link_libraries(abaddon uiohook) +else () + target_link_libraries(abaddon uiohook) +endif () + target_link_libraries(abaddon ${SQLite3_LIBRARIES}) target_link_libraries(abaddon ${GTKMM_LIBRARIES}) target_link_libraries(abaddon ${ZLIB_LIBRARY}) diff --git a/cmake/Finduiohook.cmake b/cmake/Finduiohook.cmake new file mode 100644 index 00000000..960c4528 --- /dev/null +++ b/cmake/Finduiohook.cmake @@ -0,0 +1,46 @@ +function(add_imported_library library headers) + add_library(uiohook::uiohook UNKNOWN IMPORTED) + set_target_properties(uiohook::uiohook PROPERTIES + IMPORTED_LOCATION "${library}" + INTERFACE_INCLUDE_DIRECTORIES "${headers}" + ) + + set(uiohook_FOUND 1 CACHE INTERNAL "uiohook found" FORCE) + set(uiohook_LIBRARIES "${library}" CACHE STRING "Path to uiohook library" FORCE) + set(uiohook_INCLUDES "${headers}" CACHE STRING "Path to uiohook headers" FORCE) + mark_as_advanced(FORCE uiohook_LIBRARIES) + mark_as_advanced(FORCE uiohook_INCLUDES) +endfunction() + +if(uiohook_LIBRARIES AND uiohook_INCLUDES) + add_imported_library(${uiohook_LIBRARIES} ${uiohook_INCLUDES}) + return() +endif() + +set(_uiohook_DIR "${CMAKE_CURRENT_SOURCE_DIR}/subprojects/libuiohook") + +find_library(uiohook_LIBRARY_PATH + NAMES libuiohook uiohook + PATHS + "${_uiohook_DIR}/lib" + /usr/lib +) + +find_path(uiohook_HEADER_PATH + NAMES uiohook.h + PATHS + "${_uiohook_DIR}/include" + /usr/include +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + uiohook DEFAULT_MSG uiohook_LIBRARY_PATH uiohook_HEADER_PATH +) + +if(uiohook_FOUND) + add_imported_library( + "${uiohook_LIBRARY_PATH}" + "${uiohook_HEADER_PATH}" + ) +endif() diff --git a/subprojects/libuiohook b/subprojects/libuiohook new file mode 160000 index 00000000..2a2d40f9 --- /dev/null +++ b/subprojects/libuiohook @@ -0,0 +1 @@ +Subproject commit 2a2d40f96178481d98980da0a8c1db8a41937d7c From 397eb1b20aa300602d23351fbda56b9e79cc0e18 Mon Sep 17 00:00:00 2001 From: Nyjako Date: Mon, 17 Mar 2025 21:19:51 +0100 Subject: [PATCH 2/9] Added simple hotkey support Added hotkey for mute ALT+M Added hotkey for deafen ALT+D --- README.md | 2 + src/misc/GlobalHotkeyManager.cpp | 74 +++++++++++++++++++++++++++++++ src/misc/GlobalHotkeyManager.hpp | 40 +++++++++++++++++ src/windows/voice/voicewindow.cpp | 18 ++++++++ src/windows/voice/voicewindow.hpp | 3 ++ 5 files changed, 137 insertions(+) create mode 100644 src/misc/GlobalHotkeyManager.cpp create mode 100644 src/misc/GlobalHotkeyManager.hpp diff --git a/README.md b/README.md index 44a00558..880bf4b8 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Current features: * Emojis2 * Thread support3 * Animated avatars, server icons, emojis (can be turned off) +* Global hotkeys support 1 - Abaddon tries its best (though is not perfect) to make Discord think it's a legitimate web client. Some of the things done to do this @@ -173,6 +174,7 @@ spam filter's wrath: * [libopus](https://opus-codec.org/) (optional, required for voice) * [libsodium](https://doc.libsodium.org/) (optional, required for voice) * [rnnoise](https://gitlab.xiph.org/xiph/rnnoise) (optional, provided as submodule, noise suppression and improved VAD) +* [libuiohook](https://github.com/kwhat/libuiohook) (provided as submodule, global hotkeys) ### TODO: diff --git a/src/misc/GlobalHotkeyManager.cpp b/src/misc/GlobalHotkeyManager.cpp new file mode 100644 index 00000000..65ab6d8f --- /dev/null +++ b/src/misc/GlobalHotkeyManager.cpp @@ -0,0 +1,74 @@ +#include "GlobalHotkeyManager.hpp" +#include + +// Singleton instance +GlobalHotkeyManager& GlobalHotkeyManager::instance() { + static GlobalHotkeyManager instance; + return instance; +} + +GlobalHotkeyManager::GlobalHotkeyManager() : m_nextId(1) { + hook_set_dispatch_proc(&GlobalHotkeyManager::hook_callback); + + // Run hook in separate thread to not block gtk + std::thread([this]() { + if (hook_run() != UIOHOOK_SUCCESS) { + std::cerr << "Failed to start libuiohook" << std::endl; + } + }).detach(); +} + +GlobalHotkeyManager::~GlobalHotkeyManager() +{ + hook_stop(); +} + +struct GlobalHotkeyManager::Hotkey { + uint16_t keycode; + uint32_t modifiers; + HotkeyCallback callback; +}; + +int GlobalHotkeyManager::registerHotkey(uint16_t keycode, uint32_t modifiers, HotkeyCallback callback) { + std::lock_guard lock(m_mutex); + int id = m_nextId++; + m_callbacks[id] = { + .keycode = keycode, + .modifiers = modifiers, + .callback = callback + }; + + return id; +} + +GlobalHotkeyManager::Hotkey* GlobalHotkeyManager::find_hotkey(uint16_t keycode, uint32_t modifiers) { + auto it = std::find_if(m_callbacks.begin(), m_callbacks.end(), + [keycode, modifiers](const auto& pair) { + return pair.second.keycode == keycode && (modifiers & pair.second.modifiers); + }); + + if (it != m_callbacks.end()) { + return &it->second; + } + + return nullptr; +} + +void GlobalHotkeyManager::unregisterHotkey(int id) { + std::lock_guard lock(m_mutex); + m_callbacks.erase(id); +} + +void GlobalHotkeyManager::hook_callback(uiohook_event* const event) { + GlobalHotkeyManager::instance().handleEvent(event); +} + +void GlobalHotkeyManager::handleEvent(uiohook_event* const event) { + if (event->type == EVENT_KEY_PRESSED) { + Hotkey *hk = find_hotkey(event->data.keyboard.keycode, event->mask); + if (hk != nullptr) { + std::lock_guard lock(m_mutex); + hk->callback(); + } + } +} \ No newline at end of file diff --git a/src/misc/GlobalHotkeyManager.hpp b/src/misc/GlobalHotkeyManager.hpp new file mode 100644 index 00000000..0ac660bf --- /dev/null +++ b/src/misc/GlobalHotkeyManager.hpp @@ -0,0 +1,40 @@ +#ifndef GLOBALHOTKEYMANAGER_H +#define GLOBALHOTKEYMANAGER_H + +#pragma once + +#include +#include +#include +#include "uiohook.h" + +// hotkey callback type +using HotkeyCallback = std::function; + +class GlobalHotkeyManager +{ +public: + static GlobalHotkeyManager& instance(); + int registerHotkey(uint16_t keycode, uint32_t modifiers, HotkeyCallback callback); + void unregisterHotkey(int id); + +private: + GlobalHotkeyManager(); + ~GlobalHotkeyManager(); + + // no copy/move + GlobalHotkeyManager(const GlobalHotkeyManager&) = delete; + GlobalHotkeyManager& operator=(const GlobalHotkeyManager&) = delete; + + struct Hotkey; + Hotkey* find_hotkey(uint16_t keycode, uint32_t modifiers); + + static void hook_callback(uiohook_event* const event); + void handleEvent(uiohook_event* const event); + + std::mutex m_mutex; + std::map m_callbacks; + int m_nextId; +}; + +#endif \ No newline at end of file diff --git a/src/windows/voice/voicewindow.cpp b/src/windows/voice/voicewindow.cpp index a9e9682e..d2692234 100644 --- a/src/windows/voice/voicewindow.cpp +++ b/src/windows/voice/voicewindow.cpp @@ -1,3 +1,5 @@ +#include "misc/GlobalHotkeyManager.hpp" +#include "uiohook.h" #include "util.hpp" #ifdef WITH_VOICE @@ -11,6 +13,7 @@ #include "voicewindowaudiencelistentry.hpp" #include "voicewindowspeakerlistentry.hpp" #include "windows/voicesettingswindow.hpp" +#include "misc/GlobalHotkeyManager.hpp" // clang-format on @@ -54,6 +57,16 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); + // TODO: Load shortcuts from config file + m_mute_hotkey = GlobalHotkeyManager::instance().registerHotkey(VC_M, MASK_ALT, [this]() { + // This is probably stupid there is for sure some way to call event + // but I'm not really familiar with gtk and this works well. + m_mute.set_active( !m_mute.get_active() ); + }); + m_deafen_hotkey = GlobalHotkeyManager::instance().registerHotkey(VC_D, MASK_ALT, [this]() { + m_deafen.set_active( !m_deafen.get_active() ); + }); + m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); m_scroll.set_hexpand(true); m_scroll.set_vexpand(true); @@ -271,6 +284,11 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) UpdateStageCommand(); } +VoiceWindow::~VoiceWindow() { + GlobalHotkeyManager::instance().unregisterHotkey(m_mute_hotkey); + GlobalHotkeyManager::instance().unregisterHotkey(m_deafen_hotkey); +} + void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { auto &discord = Abaddon::Get().GetDiscordClient(); const auto me = discord.GetUserData().ID; diff --git a/src/windows/voice/voicewindow.hpp b/src/windows/voice/voicewindow.hpp index 05033d9b..08686edd 100644 --- a/src/windows/voice/voicewindow.hpp +++ b/src/windows/voice/voicewindow.hpp @@ -21,6 +21,7 @@ class VoiceWindow : public Gtk::Window { public: VoiceWindow(Snowflake channel_id); + ~VoiceWindow(); private: void SetUsers(const std::unordered_set &user_ids); @@ -49,7 +50,9 @@ class VoiceWindow : public Gtk::Window { Gtk::Box m_controls; Gtk::CheckButton m_mute; + int m_mute_hotkey; Gtk::CheckButton m_deafen; + int m_deafen_hotkey; Gtk::ScrolledWindow m_scroll; Gtk::VBox m_listing; From e9565ed855fb78f345ff10889e0f77a99a1d7718 Mon Sep 17 00:00:00 2001 From: Nyjako Date: Tue, 18 Mar 2025 12:52:25 +0100 Subject: [PATCH 3/9] Fixed some parts of reviewed code Added `ENABLE_GLOBAL_HOTKEY` option in cmake (ON by default) Removed designated initializer from struct Replaced `std::cerr` with `spdlog::get("ui")->error` Added missing mutex lock Used `Glib::Dispatcher` for executing callbacks (Hope done correctly) For now mute/deafen hotkey are still placed in voicewindow --- CMakeLists.txt | 19 +++++++++------ src/misc/GlobalHotkeyManager.cpp | 39 ++++++++++++++++++++++++------- src/misc/GlobalHotkeyManager.hpp | 8 +++++++ src/windows/voice/voicewindow.cpp | 9 ++++++- src/windows/voice/voicewindow.hpp | 5 +++- 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cc60e63..3154d9f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ option(USE_KEYCHAIN "Store the token in the keychain (default)" ON) option(ENABLE_NOTIFICATION_SOUNDS "Enable notification sounds (default)" ON) option(ENABLE_RNNOISE "Enable RNNoise for voice activity detection (default)" ON) option(ENABLE_QRCODE_LOGIN "Enable QR code login (default)" ON) +option(ENABLE_GLOBAL_HOTKEY "Enable global hotkeys for mute and deafen (default)" ON) find_package(nlohmann_json REQUIRED) find_package(CURL) @@ -113,13 +114,17 @@ endif () find_package(spdlog REQUIRED) target_link_libraries(abaddon spdlog::spdlog) -find_package(uiohook QUIET) -if (NOT uiohook_FOUND) - message("uiohook was not found and will be included as a submodule") - add_subdirectory(subprojects/libuiohook) - target_link_libraries(abaddon uiohook) -else () - target_link_libraries(abaddon uiohook) +if (ENABLE_GLOBAL_HOTKEY) + target_compile_definitions(abaddon PRIVATE WITH_HOTKEYS) + + find_package(uiohook QUIET) + if (NOT uiohook_FOUND) + message("uiohook was not found and will be included as a submodule") + add_subdirectory(subprojects/libuiohook) + target_link_libraries(abaddon uiohook) + else () + target_link_libraries(abaddon uiohook) + endif () endif () target_link_libraries(abaddon ${SQLite3_LIBRARIES}) diff --git a/src/misc/GlobalHotkeyManager.cpp b/src/misc/GlobalHotkeyManager.cpp index 65ab6d8f..013ffea8 100644 --- a/src/misc/GlobalHotkeyManager.cpp +++ b/src/misc/GlobalHotkeyManager.cpp @@ -1,5 +1,7 @@ +#include "sigc++/functors/mem_fun.h" +#ifdef WITH_HOTKEYS #include "GlobalHotkeyManager.hpp" -#include +#include "spdlog/spdlog.h" // Singleton instance GlobalHotkeyManager& GlobalHotkeyManager::instance() { @@ -8,16 +10,32 @@ GlobalHotkeyManager& GlobalHotkeyManager::instance() { } GlobalHotkeyManager::GlobalHotkeyManager() : m_nextId(1) { + m_dispatcher.connect(sigc::mem_fun(*this, &GlobalHotkeyManager::processCallbacks)); + hook_set_dispatch_proc(&GlobalHotkeyManager::hook_callback); // Run hook in separate thread to not block gtk std::thread([this]() { if (hook_run() != UIOHOOK_SUCCESS) { - std::cerr << "Failed to start libuiohook" << std::endl; + spdlog::get("ui")->error("Failed to start libuiohook"); } }).detach(); } +void GlobalHotkeyManager::processCallbacks() +{ + std::queue callbacksToRun; + { + std::lock_guard lock(m_queueMutex); + std::swap(callbacksToRun, m_pendingCallbacks); + } + while (!callbacksToRun.empty()) { + auto callback = callbacksToRun.front(); + callbacksToRun.pop(); + callback(); + } +} + GlobalHotkeyManager::~GlobalHotkeyManager() { hook_stop(); @@ -33,15 +51,16 @@ int GlobalHotkeyManager::registerHotkey(uint16_t keycode, uint32_t modifiers, Ho std::lock_guard lock(m_mutex); int id = m_nextId++; m_callbacks[id] = { - .keycode = keycode, - .modifiers = modifiers, - .callback = callback + keycode, + modifiers, + callback }; return id; } GlobalHotkeyManager::Hotkey* GlobalHotkeyManager::find_hotkey(uint16_t keycode, uint32_t modifiers) { + std::lock_guard lock(m_mutex); auto it = std::find_if(m_callbacks.begin(), m_callbacks.end(), [keycode, modifiers](const auto& pair) { return pair.second.keycode == keycode && (modifiers & pair.second.modifiers); @@ -67,8 +86,12 @@ void GlobalHotkeyManager::handleEvent(uiohook_event* const event) { if (event->type == EVENT_KEY_PRESSED) { Hotkey *hk = find_hotkey(event->data.keyboard.keycode, event->mask); if (hk != nullptr) { - std::lock_guard lock(m_mutex); - hk->callback(); + { + std::lock_guard lock(m_queueMutex); + m_pendingCallbacks.push(hk->callback); + } + m_dispatcher.emit(); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/misc/GlobalHotkeyManager.hpp b/src/misc/GlobalHotkeyManager.hpp index 0ac660bf..374445b3 100644 --- a/src/misc/GlobalHotkeyManager.hpp +++ b/src/misc/GlobalHotkeyManager.hpp @@ -2,10 +2,12 @@ #define GLOBALHOTKEYMANAGER_H #pragma once +#ifdef WITH_HOTKEYS #include #include #include +#include #include "uiohook.h" // hotkey callback type @@ -31,10 +33,16 @@ class GlobalHotkeyManager static void hook_callback(uiohook_event* const event); void handleEvent(uiohook_event* const event); + void processCallbacks(); + + Glib::Dispatcher m_dispatcher; + std::queue m_pendingCallbacks; + std::mutex m_queueMutex; std::mutex m_mutex; std::map m_callbacks; int m_nextId; }; +#endif #endif \ No newline at end of file diff --git a/src/windows/voice/voicewindow.cpp b/src/windows/voice/voicewindow.cpp index d2692234..7ab947c9 100644 --- a/src/windows/voice/voicewindow.cpp +++ b/src/windows/voice/voicewindow.cpp @@ -1,5 +1,4 @@ #include "misc/GlobalHotkeyManager.hpp" -#include "uiohook.h" #include "util.hpp" #ifdef WITH_VOICE @@ -13,7 +12,11 @@ #include "voicewindowaudiencelistentry.hpp" #include "voicewindowspeakerlistentry.hpp" #include "windows/voicesettingswindow.hpp" + +#ifdef WITH_HOTKEYS #include "misc/GlobalHotkeyManager.hpp" +#include "uiohook.h" +#endif // clang-format on @@ -57,6 +60,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); +#ifdef WITH_HOTKEYS // TODO: Load shortcuts from config file m_mute_hotkey = GlobalHotkeyManager::instance().registerHotkey(VC_M, MASK_ALT, [this]() { // This is probably stupid there is for sure some way to call event @@ -66,6 +70,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_deafen_hotkey = GlobalHotkeyManager::instance().registerHotkey(VC_D, MASK_ALT, [this]() { m_deafen.set_active( !m_deafen.get_active() ); }); +#endif m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); m_scroll.set_hexpand(true); @@ -285,8 +290,10 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) } VoiceWindow::~VoiceWindow() { +#ifdef WITH_HOTKEYS GlobalHotkeyManager::instance().unregisterHotkey(m_mute_hotkey); GlobalHotkeyManager::instance().unregisterHotkey(m_deafen_hotkey); +#endif } void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { diff --git a/src/windows/voice/voicewindow.hpp b/src/windows/voice/voicewindow.hpp index 08686edd..0a50bd43 100644 --- a/src/windows/voice/voicewindow.hpp +++ b/src/windows/voice/voicewindow.hpp @@ -50,9 +50,12 @@ class VoiceWindow : public Gtk::Window { Gtk::Box m_controls; Gtk::CheckButton m_mute; - int m_mute_hotkey; Gtk::CheckButton m_deafen; + +#ifdef WITH_HOTKEYS + int m_mute_hotkey; int m_deafen_hotkey; +#endif Gtk::ScrolledWindow m_scroll; Gtk::VBox m_listing; From fdeb8ee92df531ac3e202f6198e5bbc7372ae3a4 Mon Sep 17 00:00:00 2001 From: Nyjako Date: Wed, 19 Mar 2025 07:15:52 +0100 Subject: [PATCH 4/9] Added function to convert GDK keyvals to libuihooks key codes I might have added a bit to much but it might be useful later with ui helper --- src/misc/GlobalHotkeyManager.cpp | 222 +++++++++++++++++++++++++++++- src/misc/GlobalHotkeyManager.hpp | 8 ++ src/windows/voice/voicewindow.cpp | 2 +- 3 files changed, 230 insertions(+), 2 deletions(-) diff --git a/src/misc/GlobalHotkeyManager.cpp b/src/misc/GlobalHotkeyManager.cpp index 013ffea8..0fea51a9 100644 --- a/src/misc/GlobalHotkeyManager.cpp +++ b/src/misc/GlobalHotkeyManager.cpp @@ -1,7 +1,9 @@ -#include "sigc++/functors/mem_fun.h" #ifdef WITH_HOTKEYS +#include "sigc++/functors/mem_fun.h" #include "GlobalHotkeyManager.hpp" #include "spdlog/spdlog.h" +#include "gdk/gdkkeysyms.h" +#include "uiohook.h" // Singleton instance GlobalHotkeyManager& GlobalHotkeyManager::instance() { @@ -48,7 +50,13 @@ struct GlobalHotkeyManager::Hotkey { }; int GlobalHotkeyManager::registerHotkey(uint16_t keycode, uint32_t modifiers, HotkeyCallback callback) { + if (find_hotkey(keycode, modifiers) != nullptr) { + spdlog::get("ui")->error("Unable to register hotkey: Keycode {} with mask {} already in use.", keycode, modifiers); + return -1; + } + std::lock_guard lock(m_mutex); + int id = m_nextId++; m_callbacks[id] = { keycode, @@ -59,6 +67,20 @@ int GlobalHotkeyManager::registerHotkey(uint16_t keycode, uint32_t modifiers, Ho return id; } +int GlobalHotkeyManager::registerHotkey(const char *shortcut_str, HotkeyCallback callback) { + auto parse_result = parse_and_convert_shortcut(shortcut_str); + + if (!parse_result.has_value()) { + return -1; + } + + auto values = parse_result.value(); + uint16_t keycode = std::get<0>(values); + uint32_t modifiers = std::get<1>(values); + + return registerHotkey(keycode, modifiers, callback); +} + GlobalHotkeyManager::Hotkey* GlobalHotkeyManager::find_hotkey(uint16_t keycode, uint32_t modifiers) { std::lock_guard lock(m_mutex); auto it = std::find_if(m_callbacks.begin(), m_callbacks.end(), @@ -78,6 +100,204 @@ void GlobalHotkeyManager::unregisterHotkey(int id) { m_callbacks.erase(id); } +uint16_t GlobalHotkeyManager::convert_gdk_keyval_to_uihooks_key(guint keyval) { + switch (keyval) { + // Function Keys + case GDK_KEY_F1: return VC_F1; + case GDK_KEY_F2: return VC_F2; + case GDK_KEY_F3: return VC_F3; + case GDK_KEY_F4: return VC_F4; + case GDK_KEY_F5: return VC_F5; + case GDK_KEY_F6: return VC_F6; + case GDK_KEY_F7: return VC_F7; + case GDK_KEY_F8: return VC_F8; + case GDK_KEY_F9: return VC_F9; + case GDK_KEY_F10: return VC_F10; + case GDK_KEY_F11: return VC_F11; + case GDK_KEY_F12: return VC_F12; + case GDK_KEY_F13: return VC_F13; + case GDK_KEY_F14: return VC_F14; + case GDK_KEY_F15: return VC_F15; + case GDK_KEY_F16: return VC_F16; + case GDK_KEY_F17: return VC_F17; + case GDK_KEY_F18: return VC_F18; + case GDK_KEY_F19: return VC_F19; + case GDK_KEY_F20: return VC_F20; + case GDK_KEY_F21: return VC_F21; + case GDK_KEY_F22: return VC_F22; + case GDK_KEY_F23: return VC_F23; + case GDK_KEY_F24: return VC_F24; + + // Alphanumeric + case GDK_KEY_grave: return VC_BACKQUOTE; + case GDK_KEY_0: return VC_0; + case GDK_KEY_1: return VC_1; + case GDK_KEY_2: return VC_2; + case GDK_KEY_3: return VC_3; + case GDK_KEY_4: return VC_4; + case GDK_KEY_5: return VC_5; + case GDK_KEY_6: return VC_6; + case GDK_KEY_7: return VC_7; + case GDK_KEY_8: return VC_8; + case GDK_KEY_9: return VC_9; + + case GDK_KEY_minus: return VC_MINUS; + case GDK_KEY_equal: return VC_EQUALS; + case GDK_KEY_BackSpace: return VC_BACKSPACE; + + case GDK_KEY_Tab: return VC_TAB; + case GDK_KEY_Caps_Lock: return VC_CAPS_LOCK; + + case GDK_KEY_bracketleft: return VC_OPEN_BRACKET; + case GDK_KEY_bracketright: return VC_CLOSE_BRACKET; + case GDK_KEY_backslash: return VC_BACK_SLASH; + + case GDK_KEY_semicolon: return VC_SEMICOLON; + case GDK_KEY_apostrophe: return VC_QUOTE; + case GDK_KEY_Return: return VC_ENTER; + + case GDK_KEY_comma: return VC_COMMA; + case GDK_KEY_period: return VC_PERIOD; + case GDK_KEY_slash: return VC_SLASH; + + case GDK_KEY_space: return VC_SPACE; + + case GDK_KEY_A: case GDK_KEY_a: return VC_A; + case GDK_KEY_B: case GDK_KEY_b: return VC_B; + case GDK_KEY_C: case GDK_KEY_c: return VC_C; + case GDK_KEY_D: case GDK_KEY_d: return VC_D; + case GDK_KEY_E: case GDK_KEY_e: return VC_E; + case GDK_KEY_F: case GDK_KEY_f: return VC_F; + case GDK_KEY_G: case GDK_KEY_g: return VC_G; + case GDK_KEY_H: case GDK_KEY_h: return VC_H; + case GDK_KEY_I: case GDK_KEY_i: return VC_I; + case GDK_KEY_J: case GDK_KEY_j: return VC_J; + case GDK_KEY_K: case GDK_KEY_k: return VC_K; + case GDK_KEY_L: case GDK_KEY_l: return VC_L; + case GDK_KEY_M: case GDK_KEY_m: return VC_M; + case GDK_KEY_N: case GDK_KEY_n: return VC_N; + case GDK_KEY_O: case GDK_KEY_o: return VC_O; + case GDK_KEY_P: case GDK_KEY_p: return VC_P; + case GDK_KEY_Q: case GDK_KEY_q: return VC_Q; + case GDK_KEY_R: case GDK_KEY_r: return VC_R; + case GDK_KEY_S: case GDK_KEY_s: return VC_S; + case GDK_KEY_T: case GDK_KEY_t: return VC_T; + case GDK_KEY_U: case GDK_KEY_u: return VC_U; + case GDK_KEY_V: case GDK_KEY_v: return VC_V; + case GDK_KEY_W: case GDK_KEY_w: return VC_W; + case GDK_KEY_X: case GDK_KEY_x: return VC_X; + case GDK_KEY_Y: case GDK_KEY_y: return VC_Y; + case GDK_KEY_Z: case GDK_KEY_z: return VC_Z; + + case GDK_KEY_Print: return VC_PRINTSCREEN; + case GDK_KEY_Scroll_Lock: return VC_SCROLL_LOCK; + case GDK_KEY_Pause: return VC_PAUSE; + + case GDK_KEY_less: return VC_LESSER_GREATER; + + // Edit Key + case GDK_KEY_Insert: return VC_INSERT; + case GDK_KEY_Delete: return VC_DELETE; + case GDK_KEY_Home: return VC_HOME; + case GDK_KEY_End: return VC_END; + case GDK_KEY_Page_Up: return VC_PAGE_UP; + case GDK_KEY_Page_Down: return VC_PAGE_DOWN; + + // Cursor Key + case GDK_KEY_Up: return VC_UP; + case GDK_KEY_Left: return VC_LEFT; + case GDK_KEY_Clear: return VC_CLEAR; + case GDK_KEY_Right: return VC_RIGHT; + case GDK_KEY_Down: return VC_DOWN; + + // Numeric + case GDK_KEY_Num_Lock: return VC_NUM_LOCK; + case GDK_KEY_KP_Divide: return VC_KP_DIVIDE; + case GDK_KEY_KP_Multiply: return VC_KP_MULTIPLY; + case GDK_KEY_KP_Subtract: return VC_KP_SUBTRACT; + case GDK_KEY_KP_Equal: return VC_KP_EQUALS; + case GDK_KEY_KP_Add: return VC_KP_ADD; + case GDK_KEY_KP_Enter: return VC_KP_ENTER; + case GDK_KEY_KP_Separator: return VC_KP_SEPARATOR; + + case GDK_KEY_KP_0: return VC_KP_0; + case GDK_KEY_KP_1: return VC_KP_1; + case GDK_KEY_KP_2: return VC_KP_2; + case GDK_KEY_KP_3: return VC_KP_3; + case GDK_KEY_KP_4: return VC_KP_4; + case GDK_KEY_KP_5: return VC_KP_5; + case GDK_KEY_KP_6: return VC_KP_6; + case GDK_KEY_KP_7: return VC_KP_7; + case GDK_KEY_KP_8: return VC_KP_8; + case GDK_KEY_KP_9: return VC_KP_9; + + case GDK_KEY_KP_End: return VC_KP_END; + case GDK_KEY_KP_Down: return VC_KP_DOWN; + case GDK_KEY_KP_Page_Down: return VC_KP_PAGE_DOWN; + case GDK_KEY_KP_Left: return VC_KP_LEFT; + case GDK_KEY_KP_Begin: return VC_KP_CLEAR; + case GDK_KEY_KP_Right: return VC_KP_RIGHT; + case GDK_KEY_KP_Home: return VC_KP_HOME; + case GDK_KEY_KP_Up: return VC_KP_UP; + case GDK_KEY_KP_Page_Up: return VC_KP_PAGE_UP; + case GDK_KEY_KP_Insert: return VC_KP_INSERT; + case GDK_KEY_KP_Delete: return VC_KP_DELETE; + + // Modifier and Control Keys + case GDK_KEY_Shift_L: return VC_SHIFT_L; + case GDK_KEY_Shift_R: return VC_SHIFT_R; + case GDK_KEY_Control_L: return VC_CONTROL_L; + case GDK_KEY_Control_R: return VC_CONTROL_R; + case GDK_KEY_Alt_L: return VC_ALT_L; + case GDK_KEY_Alt_R: return VC_ALT_R; + case GDK_KEY_Meta_L: return VC_META_L; + case GDK_KEY_Meta_R: return VC_META_R; + case GDK_KEY_Menu: return VC_CONTEXT_MENU; + + // Media Control Keys + case GDK_KEY_AudioPlay: return VC_MEDIA_PLAY; + case GDK_KEY_AudioStop: return VC_MEDIA_STOP; + case GDK_KEY_AudioPrev: return VC_MEDIA_PREVIOUS; + case GDK_KEY_AudioNext: return VC_MEDIA_NEXT; + + default: return VC_UNDEFINED; + } +} + +uint32_t GlobalHotkeyManager::convert_gdk_modifiers_to_uihooks(GdkModifierType mods) { + uint16_t hook_modifiers = 0; + + if (mods & GDK_SHIFT_MASK) hook_modifiers |= MASK_SHIFT; + if (mods & GDK_CONTROL_MASK) hook_modifiers |= MASK_CTRL; + if (mods & GDK_MOD1_MASK) hook_modifiers |= MASK_ALT; + if (mods & GDK_META_MASK) hook_modifiers |= MASK_META; + // I don't think more masks would be necesary + + return hook_modifiers; +} + +std::optional> GlobalHotkeyManager::parse_and_convert_shortcut(const char *shortcut_str) { + guint gdk_key = 0; + GdkModifierType gdk_mods; + + gtk_accelerator_parse(shortcut_str, &gdk_key, &gdk_mods); + + if (gdk_key == 0) { + spdlog::get("ui")->warn("Failed to parse shortcut: {}", shortcut_str); + return std::nullopt; + } + + uint16_t key = convert_gdk_keyval_to_uihooks_key(gdk_key); + uint32_t mods = convert_gdk_modifiers_to_uihooks(gdk_mods); + + if (key == VC_UNDEFINED) { + spdlog::get("ui")->warn("Failed to parse key code: {}", gdk_key); + return std::nullopt; + } + + return std::make_tuple(key, mods); +} + void GlobalHotkeyManager::hook_callback(uiohook_event* const event) { GlobalHotkeyManager::instance().handleEvent(event); } diff --git a/src/misc/GlobalHotkeyManager.hpp b/src/misc/GlobalHotkeyManager.hpp index 374445b3..b1184a2f 100644 --- a/src/misc/GlobalHotkeyManager.hpp +++ b/src/misc/GlobalHotkeyManager.hpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include #include "uiohook.h" @@ -18,6 +20,7 @@ class GlobalHotkeyManager public: static GlobalHotkeyManager& instance(); int registerHotkey(uint16_t keycode, uint32_t modifiers, HotkeyCallback callback); + int registerHotkey(const char *shortcut_str, HotkeyCallback callback); void unregisterHotkey(int id); private: @@ -35,6 +38,11 @@ class GlobalHotkeyManager void handleEvent(uiohook_event* const event); void processCallbacks(); + // Converting gtk to uiohook codes + uint16_t convert_gdk_keyval_to_uihooks_key(guint keyval); + uint32_t convert_gdk_modifiers_to_uihooks(GdkModifierType mods); + std::optional> parse_and_convert_shortcut(const char *shortcut_str); + Glib::Dispatcher m_dispatcher; std::queue m_pendingCallbacks; std::mutex m_queueMutex; diff --git a/src/windows/voice/voicewindow.cpp b/src/windows/voice/voicewindow.cpp index 7ab947c9..3b748d94 100644 --- a/src/windows/voice/voicewindow.cpp +++ b/src/windows/voice/voicewindow.cpp @@ -62,7 +62,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) #ifdef WITH_HOTKEYS // TODO: Load shortcuts from config file - m_mute_hotkey = GlobalHotkeyManager::instance().registerHotkey(VC_M, MASK_ALT, [this]() { + m_mute_hotkey = GlobalHotkeyManager::instance().registerHotkey("M", [this]() { // This is probably stupid there is for sure some way to call event // but I'm not really familiar with gtk and this works well. m_mute.set_active( !m_mute.get_active() ); From 38febea773902ffcb318f16c393c11777f08c5b2 Mon Sep 17 00:00:00 2001 From: Nyjako Date: Wed, 19 Mar 2025 13:19:30 +0100 Subject: [PATCH 5/9] Moved `GlobalHotkeyManager` to `Abaddon` singleton --- src/abaddon.cpp | 9 +++++++++ src/abaddon.hpp | 12 ++++++++++++ src/misc/GlobalHotkeyManager.cpp | 28 +++++++++++++++++----------- src/misc/GlobalHotkeyManager.hpp | 13 +++++++------ src/windows/voice/voicewindow.cpp | 8 ++++---- 5 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 653327cd..98882607 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -57,6 +57,9 @@ Abaddon::Abaddon() #ifdef WITH_VOICE , m_audio(GetSettings().Backends) #endif +#ifdef WITH_HOTKEYS + , m_HotkeyManager() +#endif { LoadFromSettings(); @@ -1135,6 +1138,12 @@ AudioManager &Abaddon::GetAudio() { } #endif +#ifdef WITH_HOTKEYS + GlobalHotkeyManager& Abaddon::HotkeyManager() { + return Get().m_HotkeyManager; + } +#endif + void Abaddon::on_tray_click() { m_main_window->set_visible(!m_main_window->is_visible()); } diff --git a/src/abaddon.hpp b/src/abaddon.hpp index 6093523f..9ffad01e 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -14,6 +14,10 @@ #include "notifications/notifications.hpp" #include "audio/manager.hpp" +#ifdef WITH_HOTKEYS +#include "misc/GlobalHotkeyManager.hpp" +#endif + #define APP_TITLE "Abaddon" class AudioManager; @@ -101,6 +105,10 @@ class Abaddon { void ShowVoiceWindow(); #endif +#ifdef WITH_HOTKEYS + static GlobalHotkeyManager& HotkeyManager(); +#endif + SettingsManager::Settings &GetSettings(); Glib::RefPtr GetStyleProvider(); @@ -179,6 +187,10 @@ class Abaddon { Gtk::Window *m_voice_window = nullptr; #endif +#ifdef WITH_HOTKEYS + GlobalHotkeyManager m_HotkeyManager; +#endif + mutable std::mutex m_mutex; Glib::RefPtr m_gtk_app; Glib::RefPtr m_css_provider; diff --git a/src/misc/GlobalHotkeyManager.cpp b/src/misc/GlobalHotkeyManager.cpp index 0fea51a9..570187e5 100644 --- a/src/misc/GlobalHotkeyManager.cpp +++ b/src/misc/GlobalHotkeyManager.cpp @@ -5,15 +5,13 @@ #include "gdk/gdkkeysyms.h" #include "uiohook.h" -// Singleton instance -GlobalHotkeyManager& GlobalHotkeyManager::instance() { - static GlobalHotkeyManager instance; - return instance; -} +// Linker will complain otherwise +GlobalHotkeyManager* GlobalHotkeyManager::s_instance = nullptr; GlobalHotkeyManager::GlobalHotkeyManager() : m_nextId(1) { m_dispatcher.connect(sigc::mem_fun(*this, &GlobalHotkeyManager::processCallbacks)); + s_instance = this; hook_set_dispatch_proc(&GlobalHotkeyManager::hook_callback); // Run hook in separate thread to not block gtk @@ -38,17 +36,23 @@ void GlobalHotkeyManager::processCallbacks() } } -GlobalHotkeyManager::~GlobalHotkeyManager() -{ - hook_stop(); -} - struct GlobalHotkeyManager::Hotkey { uint16_t keycode; uint32_t modifiers; HotkeyCallback callback; }; +GlobalHotkeyManager::~GlobalHotkeyManager() +{ + // hook_stop() stop event processing + // but I will leave this to prevent dangling references + for (std::pair callback : m_callbacks) { + unregisterHotkey(callback.first); + } + hook_stop(); + s_instance = nullptr; +} + int GlobalHotkeyManager::registerHotkey(uint16_t keycode, uint32_t modifiers, HotkeyCallback callback) { if (find_hotkey(keycode, modifiers) != nullptr) { spdlog::get("ui")->error("Unable to register hotkey: Keycode {} with mask {} already in use.", keycode, modifiers); @@ -299,7 +303,9 @@ std::optional> GlobalHotkeyManager::parse_and_con } void GlobalHotkeyManager::hook_callback(uiohook_event* const event) { - GlobalHotkeyManager::instance().handleEvent(event); + if (s_instance) { + s_instance->handleEvent(event); + } } void GlobalHotkeyManager::handleEvent(uiohook_event* const event) { diff --git a/src/misc/GlobalHotkeyManager.hpp b/src/misc/GlobalHotkeyManager.hpp index b1184a2f..9bf97454 100644 --- a/src/misc/GlobalHotkeyManager.hpp +++ b/src/misc/GlobalHotkeyManager.hpp @@ -18,15 +18,15 @@ using HotkeyCallback = std::function; class GlobalHotkeyManager { public: - static GlobalHotkeyManager& instance(); + GlobalHotkeyManager(); + ~GlobalHotkeyManager(); + int registerHotkey(uint16_t keycode, uint32_t modifiers, HotkeyCallback callback); int registerHotkey(const char *shortcut_str, HotkeyCallback callback); void unregisterHotkey(int id); - + void handleEvent(uiohook_event* const event); private: - GlobalHotkeyManager(); - ~GlobalHotkeyManager(); - + // no copy/move GlobalHotkeyManager(const GlobalHotkeyManager&) = delete; GlobalHotkeyManager& operator=(const GlobalHotkeyManager&) = delete; @@ -35,7 +35,6 @@ class GlobalHotkeyManager Hotkey* find_hotkey(uint16_t keycode, uint32_t modifiers); static void hook_callback(uiohook_event* const event); - void handleEvent(uiohook_event* const event); void processCallbacks(); // Converting gtk to uiohook codes @@ -43,6 +42,8 @@ class GlobalHotkeyManager uint32_t convert_gdk_modifiers_to_uihooks(GdkModifierType mods); std::optional> parse_and_convert_shortcut(const char *shortcut_str); + static GlobalHotkeyManager* s_instance; + Glib::Dispatcher m_dispatcher; std::queue m_pendingCallbacks; std::mutex m_queueMutex; diff --git a/src/windows/voice/voicewindow.cpp b/src/windows/voice/voicewindow.cpp index 3b748d94..e6151686 100644 --- a/src/windows/voice/voicewindow.cpp +++ b/src/windows/voice/voicewindow.cpp @@ -62,12 +62,12 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) #ifdef WITH_HOTKEYS // TODO: Load shortcuts from config file - m_mute_hotkey = GlobalHotkeyManager::instance().registerHotkey("M", [this]() { + m_mute_hotkey = Abaddon::HotkeyManager().registerHotkey("M", [this]() { // This is probably stupid there is for sure some way to call event // but I'm not really familiar with gtk and this works well. m_mute.set_active( !m_mute.get_active() ); }); - m_deafen_hotkey = GlobalHotkeyManager::instance().registerHotkey(VC_D, MASK_ALT, [this]() { + m_deafen_hotkey = Abaddon::HotkeyManager().registerHotkey(VC_D, MASK_ALT, [this]() { m_deafen.set_active( !m_deafen.get_active() ); }); #endif @@ -291,8 +291,8 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) VoiceWindow::~VoiceWindow() { #ifdef WITH_HOTKEYS - GlobalHotkeyManager::instance().unregisterHotkey(m_mute_hotkey); - GlobalHotkeyManager::instance().unregisterHotkey(m_deafen_hotkey); + Abaddon::HotkeyManager().unregisterHotkey(m_mute_hotkey); + Abaddon::HotkeyManager().unregisterHotkey(m_deafen_hotkey); #endif } From d0369d43982080ba528106a1f63f66524b389c2e Mon Sep 17 00:00:00 2001 From: Nyjako Date: Wed, 19 Mar 2025 20:57:07 +0100 Subject: [PATCH 6/9] Moved `hotkeys` out of `VoiceWindow` Moved hotkeys from `VoiceWindow` into `Abaddon` class Added getters and setters in `VoiceWindow` --- src/abaddon.cpp | 42 +++++++++++++++++++++++++++++++ src/abaddon.hpp | 6 +++++ src/windows/voice/voicewindow.cpp | 37 +++++++++++---------------- src/windows/voice/voicewindow.hpp | 10 +++----- 4 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/abaddon.cpp b/src/abaddon.cpp index 98882607..d1bc6631 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -492,6 +492,37 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) { void Abaddon::OnVoiceConnected() { m_audio.StartCaptureDevice(); ShowVoiceWindow(); + +#ifdef WITH_HOTKEYS + // TODO: load hotkeys from config + m_mute_hotkey_id = m_HotkeyManager.registerHotkey("M", [this]() { + if (m_voice_window != nullptr) { + auto voice_window = dynamic_cast(m_voice_window); + if (voice_window) { + m_is_mute = !voice_window->GetMute(); + voice_window->SetMute( m_is_mute ); + return; + } + } + m_is_mute = !m_is_mute; + m_discord.SetVoiceMuted( m_is_mute ); + m_audio.SetCapture(!m_is_mute); + }); + + m_deafen_hotkey_id = m_HotkeyManager.registerHotkey(VC_D, MASK_ALT, [this]() { + if (m_voice_window != nullptr) { + auto voice_window = dynamic_cast(m_voice_window); + if (voice_window) { + m_is_deaf = !voice_window->GetDeaf(); + voice_window->SetDeaf( m_is_deaf ); + return; + } + } + m_is_deaf = !m_is_deaf; + m_discord.SetVoiceDeafened( m_is_deaf ); + m_audio.SetPlayback(!m_is_deaf); + }); +#endif } void Abaddon::OnVoiceDisconnected() { @@ -500,6 +531,11 @@ void Abaddon::OnVoiceDisconnected() { if (m_voice_window != nullptr) { m_voice_window->close(); } + +#ifdef WITH_HOTKEYS + HotkeyManager().unregisterHotkey(m_mute_hotkey_id); + HotkeyManager().unregisterHotkey(m_deafen_hotkey_id); +#endif } void Abaddon::ShowVoiceWindow() { @@ -509,11 +545,17 @@ void Abaddon::ShowVoiceWindow() { m_voice_window = wnd; wnd->signal_mute().connect([this](bool is_mute) { +#ifdef WITH_HOTKEYS + m_is_mute = is_mute; +#endif m_discord.SetVoiceMuted(is_mute); m_audio.SetCapture(!is_mute); }); wnd->signal_deafen().connect([this](bool is_deaf) { +#ifdef WITH_HOTKEYS + m_is_deaf = is_deaf; +#endif m_discord.SetVoiceDeafened(is_deaf); m_audio.SetPlayback(!is_deaf); }); diff --git a/src/abaddon.hpp b/src/abaddon.hpp index 9ffad01e..11c2c9c7 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -185,6 +185,12 @@ class Abaddon { #ifdef WITH_VOICE AudioManager m_audio; Gtk::Window *m_voice_window = nullptr; +#ifdef WITH_HOTKEYS + int m_mute_hotkey_id; + int m_deafen_hotkey_id; + bool m_is_mute = false; + bool m_is_deaf = false; +#endif #endif #ifdef WITH_HOTKEYS diff --git a/src/windows/voice/voicewindow.cpp b/src/windows/voice/voicewindow.cpp index e6151686..51fcae72 100644 --- a/src/windows/voice/voicewindow.cpp +++ b/src/windows/voice/voicewindow.cpp @@ -1,4 +1,3 @@ -#include "misc/GlobalHotkeyManager.hpp" #include "util.hpp" #ifdef WITH_VOICE @@ -13,11 +12,6 @@ #include "voicewindowspeakerlistentry.hpp" #include "windows/voicesettingswindow.hpp" -#ifdef WITH_HOTKEYS -#include "misc/GlobalHotkeyManager.hpp" -#include "uiohook.h" -#endif - // clang-format on VoiceWindow::VoiceWindow(Snowflake channel_id) @@ -60,18 +54,6 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_mute.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnMuteChanged)); m_deafen.signal_toggled().connect(sigc::mem_fun(*this, &VoiceWindow::OnDeafenChanged)); -#ifdef WITH_HOTKEYS - // TODO: Load shortcuts from config file - m_mute_hotkey = Abaddon::HotkeyManager().registerHotkey("M", [this]() { - // This is probably stupid there is for sure some way to call event - // but I'm not really familiar with gtk and this works well. - m_mute.set_active( !m_mute.get_active() ); - }); - m_deafen_hotkey = Abaddon::HotkeyManager().registerHotkey(VC_D, MASK_ALT, [this]() { - m_deafen.set_active( !m_deafen.get_active() ); - }); -#endif - m_scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); m_scroll.set_hexpand(true); m_scroll.set_vexpand(true); @@ -289,11 +271,20 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) UpdateStageCommand(); } -VoiceWindow::~VoiceWindow() { -#ifdef WITH_HOTKEYS - Abaddon::HotkeyManager().unregisterHotkey(m_mute_hotkey); - Abaddon::HotkeyManager().unregisterHotkey(m_deafen_hotkey); -#endif +void VoiceWindow::SetMute(bool is_mute) { + m_mute.set_active(is_mute); +} + +void VoiceWindow::SetDeaf(bool is_deaf) { + m_deafen.set_active(is_deaf); +} + +bool VoiceWindow::GetMute() { + return m_mute.get_active(); +} + +bool VoiceWindow::GetDeaf() { + return m_deafen.get_active(); } void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { diff --git a/src/windows/voice/voicewindow.hpp b/src/windows/voice/voicewindow.hpp index 0a50bd43..7113eafb 100644 --- a/src/windows/voice/voicewindow.hpp +++ b/src/windows/voice/voicewindow.hpp @@ -21,8 +21,11 @@ class VoiceWindow : public Gtk::Window { public: VoiceWindow(Snowflake channel_id); - ~VoiceWindow(); + void SetMute(bool is_mute); + void SetDeaf(bool is_deaf); + bool GetMute(); + bool GetDeaf(); private: void SetUsers(const std::unordered_set &user_ids); @@ -51,11 +54,6 @@ class VoiceWindow : public Gtk::Window { Gtk::CheckButton m_mute; Gtk::CheckButton m_deafen; - -#ifdef WITH_HOTKEYS - int m_mute_hotkey; - int m_deafen_hotkey; -#endif Gtk::ScrolledWindow m_scroll; Gtk::VBox m_listing; From 8cab24e09514a6ceff5293d5f8d36c51e2359cbc Mon Sep 17 00:00:00 2001 From: Nyjako Date: Thu, 20 Mar 2025 00:15:18 +0100 Subject: [PATCH 7/9] Added hotkeys to `abaddon.ini` Hotkeys can now be assigned in `abaddon.ini` under `[hotkeys]` Moved GDK keyval mapping to bottom of file Added hotkeys to readme file --- README.md | 9 ++- src/abaddon.cpp | 5 +- src/misc/GlobalHotkeyManager.cpp | 107 ++++++++++++++++--------------- src/settings.cpp | 2 + src/settings.hpp | 4 ++ 5 files changed, 70 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 880bf4b8..f49840c6 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ spam filter's wrath: * [libopus](https://opus-codec.org/) (optional, required for voice) * [libsodium](https://doc.libsodium.org/) (optional, required for voice) * [rnnoise](https://gitlab.xiph.org/xiph/rnnoise) (optional, provided as submodule, noise suppression and improved VAD) -* [libuiohook](https://github.com/kwhat/libuiohook) (provided as submodule, global hotkeys) +* [libuiohook](https://github.com/kwhat/libuiohook) (optional, provided as submodule, global hotkeys) ### TODO: @@ -354,6 +354,13 @@ For example, memory_db would be set by adding `memory_db = true` under the line | `vad` | string | rnnoise if enabled, gate otherwise | Method used for voice activity detection. Changeable in UI | | `backends` | string | empty | Change backend priority when initializing miniaudio: `wasapi;dsound;winmm;coreaudio;sndio;audio4;oss;pulseaudio;alsa;jack` | +#### hotkeys + +| Setting | Type | Default | Description | +|----------|--------|-----------|-------------------------------------------| +| `mute` | string | \M | Shortcut to mute microphone in voice call | +| `deafen` | string | \D | Shortcut to deafen audio in voice call | + #### windows | Setting | Type | Default | Description | diff --git a/src/abaddon.cpp b/src/abaddon.cpp index d1bc6631..fd7affc3 100644 --- a/src/abaddon.cpp +++ b/src/abaddon.cpp @@ -494,8 +494,7 @@ void Abaddon::OnVoiceConnected() { ShowVoiceWindow(); #ifdef WITH_HOTKEYS - // TODO: load hotkeys from config - m_mute_hotkey_id = m_HotkeyManager.registerHotkey("M", [this]() { + m_mute_hotkey_id = m_HotkeyManager.registerHotkey(GetSettings().ToggleMute.c_str(), [this]() { if (m_voice_window != nullptr) { auto voice_window = dynamic_cast(m_voice_window); if (voice_window) { @@ -509,7 +508,7 @@ void Abaddon::OnVoiceConnected() { m_audio.SetCapture(!m_is_mute); }); - m_deafen_hotkey_id = m_HotkeyManager.registerHotkey(VC_D, MASK_ALT, [this]() { + m_deafen_hotkey_id = m_HotkeyManager.registerHotkey(GetSettings().ToggleDeafen.c_str(), [this]() { if (m_voice_window != nullptr) { auto voice_window = dynamic_cast(m_voice_window); if (voice_window) { diff --git a/src/misc/GlobalHotkeyManager.cpp b/src/misc/GlobalHotkeyManager.cpp index 570187e5..886d95a5 100644 --- a/src/misc/GlobalHotkeyManager.cpp +++ b/src/misc/GlobalHotkeyManager.cpp @@ -86,6 +86,7 @@ int GlobalHotkeyManager::registerHotkey(const char *shortcut_str, HotkeyCallback } GlobalHotkeyManager::Hotkey* GlobalHotkeyManager::find_hotkey(uint16_t keycode, uint32_t modifiers) { + // FIXME: When using multiple modifiers it does not care if just one is pressed or both std::lock_guard lock(m_mutex); auto it = std::find_if(m_callbacks.begin(), m_callbacks.end(), [keycode, modifiers](const auto& pair) { @@ -104,6 +105,59 @@ void GlobalHotkeyManager::unregisterHotkey(int id) { m_callbacks.erase(id); } +void GlobalHotkeyManager::hook_callback(uiohook_event* const event) { + if (s_instance) { + s_instance->handleEvent(event); + } +} + +void GlobalHotkeyManager::handleEvent(uiohook_event* const event) { + if (event->type == EVENT_KEY_PRESSED) { + Hotkey *hk = find_hotkey(event->data.keyboard.keycode, event->mask); + if (hk != nullptr) { + { + std::lock_guard lock(m_queueMutex); + m_pendingCallbacks.push(hk->callback); + } + m_dispatcher.emit(); + } + } +} + +std::optional> GlobalHotkeyManager::parse_and_convert_shortcut(const char *shortcut_str) { + guint gdk_key = 0; + GdkModifierType gdk_mods; + + gtk_accelerator_parse(shortcut_str, &gdk_key, &gdk_mods); + + if (gdk_key == 0) { + spdlog::get("ui")->warn("Failed to parse shortcut: {}", shortcut_str); + return std::nullopt; + } + + uint16_t key = convert_gdk_keyval_to_uihooks_key(gdk_key); + uint32_t mods = convert_gdk_modifiers_to_uihooks(gdk_mods); + + if (key == VC_UNDEFINED) { + spdlog::get("ui")->warn("Failed to parse key code: {}", gdk_key); + return std::nullopt; + } + + return std::make_tuple(key, mods); +} + +uint32_t GlobalHotkeyManager::convert_gdk_modifiers_to_uihooks(GdkModifierType mods) { + uint16_t hook_modifiers = 0; + + if (mods & GDK_SHIFT_MASK) hook_modifiers |= MASK_SHIFT; + if (mods & GDK_CONTROL_MASK) hook_modifiers |= MASK_CTRL; + if (mods & GDK_MOD1_MASK) hook_modifiers |= MASK_ALT; + if (mods & GDK_META_MASK) hook_modifiers |= MASK_META; + // I don't think more masks would be necesary + + return hook_modifiers; +} + uint16_t GlobalHotkeyManager::convert_gdk_keyval_to_uihooks_key(guint keyval) { switch (keyval) { // Function Keys @@ -267,57 +321,4 @@ uint16_t GlobalHotkeyManager::convert_gdk_keyval_to_uihooks_key(guint keyval) { default: return VC_UNDEFINED; } } - -uint32_t GlobalHotkeyManager::convert_gdk_modifiers_to_uihooks(GdkModifierType mods) { - uint16_t hook_modifiers = 0; - - if (mods & GDK_SHIFT_MASK) hook_modifiers |= MASK_SHIFT; - if (mods & GDK_CONTROL_MASK) hook_modifiers |= MASK_CTRL; - if (mods & GDK_MOD1_MASK) hook_modifiers |= MASK_ALT; - if (mods & GDK_META_MASK) hook_modifiers |= MASK_META; - // I don't think more masks would be necesary - - return hook_modifiers; -} - -std::optional> GlobalHotkeyManager::parse_and_convert_shortcut(const char *shortcut_str) { - guint gdk_key = 0; - GdkModifierType gdk_mods; - - gtk_accelerator_parse(shortcut_str, &gdk_key, &gdk_mods); - - if (gdk_key == 0) { - spdlog::get("ui")->warn("Failed to parse shortcut: {}", shortcut_str); - return std::nullopt; - } - - uint16_t key = convert_gdk_keyval_to_uihooks_key(gdk_key); - uint32_t mods = convert_gdk_modifiers_to_uihooks(gdk_mods); - - if (key == VC_UNDEFINED) { - spdlog::get("ui")->warn("Failed to parse key code: {}", gdk_key); - return std::nullopt; - } - - return std::make_tuple(key, mods); -} - -void GlobalHotkeyManager::hook_callback(uiohook_event* const event) { - if (s_instance) { - s_instance->handleEvent(event); - } -} - -void GlobalHotkeyManager::handleEvent(uiohook_event* const event) { - if (event->type == EVENT_KEY_PRESSED) { - Hotkey *hk = find_hotkey(event->data.keyboard.keycode, event->mask); - if (hk != nullptr) { - { - std::lock_guard lock(m_queueMutex); - m_pendingCallbacks.push(hk->callback); - } - m_dispatcher.emit(); - } - } -} #endif \ No newline at end of file diff --git a/src/settings.cpp b/src/settings.cpp index e9b6fc76..63f4e620 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -130,6 +130,8 @@ void SettingsManager::DefineSettings() { AddSetting("voice", "vad", "gate"s, &Settings::VAD); #endif AddSetting("voice", "backends", ""s, &Settings::Backends); + AddSetting("hotkeys", "mute", "M"s, &Settings::ToggleMute); + AddSetting("hotkeys", "deafen", "D"s, &Settings::ToggleDeafen); } void SettingsManager::ReadSettings() { diff --git a/src/settings.hpp b/src/settings.hpp index 5805452b..8b014593 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -53,6 +53,10 @@ class SettingsManager { std::string VAD; std::string Backends; + // [hotkeys] + std::string ToggleMute; + std::string ToggleDeafen; + // [windows] bool HideConsole; }; From 71841bffc211536e695b1265af9581a87243be4d Mon Sep 17 00:00:00 2001 From: Nyjako Date: Thu, 20 Mar 2025 07:05:32 +0100 Subject: [PATCH 8/9] Fixed multiple modifiers in single hotkey You can now use for example `M` --- src/misc/GlobalHotkeyManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/misc/GlobalHotkeyManager.cpp b/src/misc/GlobalHotkeyManager.cpp index 886d95a5..dcfd8dce 100644 --- a/src/misc/GlobalHotkeyManager.cpp +++ b/src/misc/GlobalHotkeyManager.cpp @@ -86,11 +86,11 @@ int GlobalHotkeyManager::registerHotkey(const char *shortcut_str, HotkeyCallback } GlobalHotkeyManager::Hotkey* GlobalHotkeyManager::find_hotkey(uint16_t keycode, uint32_t modifiers) { - // FIXME: When using multiple modifiers it does not care if just one is pressed or both std::lock_guard lock(m_mutex); auto it = std::find_if(m_callbacks.begin(), m_callbacks.end(), [keycode, modifiers](const auto& pair) { - return pair.second.keycode == keycode && (modifiers & pair.second.modifiers); + return (pair.second.keycode == keycode) && + ((modifiers & 0x0F) == (pair.second.modifiers & 0x0F)); }); if (it != m_callbacks.end()) { From e02402df8a1252d6a19fd92def1f5df82be4f3c0 Mon Sep 17 00:00:00 2001 From: Nyjako Date: Thu, 20 Mar 2025 18:50:20 +0100 Subject: [PATCH 9/9] Reverted last commit --- src/misc/GlobalHotkeyManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/misc/GlobalHotkeyManager.cpp b/src/misc/GlobalHotkeyManager.cpp index dcfd8dce..886d95a5 100644 --- a/src/misc/GlobalHotkeyManager.cpp +++ b/src/misc/GlobalHotkeyManager.cpp @@ -86,11 +86,11 @@ int GlobalHotkeyManager::registerHotkey(const char *shortcut_str, HotkeyCallback } GlobalHotkeyManager::Hotkey* GlobalHotkeyManager::find_hotkey(uint16_t keycode, uint32_t modifiers) { + // FIXME: When using multiple modifiers it does not care if just one is pressed or both std::lock_guard lock(m_mutex); auto it = std::find_if(m_callbacks.begin(), m_callbacks.end(), [keycode, modifiers](const auto& pair) { - return (pair.second.keycode == keycode) && - ((modifiers & 0x0F) == (pair.second.modifiers & 0x0F)); + return pair.second.keycode == keycode && (modifiers & pair.second.modifiers); }); if (it != m_callbacks.end()) {