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..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,6 +114,19 @@ endif () find_package(spdlog REQUIRED) target_link_libraries(abaddon spdlog::spdlog) +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}) target_link_libraries(abaddon ${GTKMM_LIBRARIES}) target_link_libraries(abaddon ${ZLIB_LIBRARY}) diff --git a/README.md b/README.md index 44a00558..f49840c6 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) (optional, provided as submodule, global hotkeys) ### TODO: @@ -352,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/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/src/abaddon.cpp b/src/abaddon.cpp index 653327cd..fd7affc3 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(); @@ -489,6 +492,36 @@ void Abaddon::DiscordOnThreadUpdate(const ThreadUpdateData &data) { void Abaddon::OnVoiceConnected() { m_audio.StartCaptureDevice(); ShowVoiceWindow(); + +#ifdef WITH_HOTKEYS + 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) { + 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(GetSettings().ToggleDeafen.c_str(), [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() { @@ -497,6 +530,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() { @@ -506,11 +544,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); }); @@ -1135,6 +1179,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..11c2c9c7 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(); @@ -177,6 +185,16 @@ 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 + GlobalHotkeyManager m_HotkeyManager; #endif mutable std::mutex m_mutex; diff --git a/src/misc/GlobalHotkeyManager.cpp b/src/misc/GlobalHotkeyManager.cpp new file mode 100644 index 00000000..886d95a5 --- /dev/null +++ b/src/misc/GlobalHotkeyManager.cpp @@ -0,0 +1,324 @@ +#ifdef WITH_HOTKEYS +#include "sigc++/functors/mem_fun.h" +#include "GlobalHotkeyManager.hpp" +#include "spdlog/spdlog.h" +#include "gdk/gdkkeysyms.h" +#include "uiohook.h" + +// 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 + std::thread([this]() { + if (hook_run() != UIOHOOK_SUCCESS) { + 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(); + } +} + +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); + return -1; + } + + std::lock_guard lock(m_mutex); + + int id = m_nextId++; + m_callbacks[id] = { + keycode, + modifiers, + callback + }; + + 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) { + // 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); + }); + + 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) { + 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 + 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; + } +} +#endif \ No newline at end of file diff --git a/src/misc/GlobalHotkeyManager.hpp b/src/misc/GlobalHotkeyManager.hpp new file mode 100644 index 00000000..9bf97454 --- /dev/null +++ b/src/misc/GlobalHotkeyManager.hpp @@ -0,0 +1,57 @@ +#ifndef GLOBALHOTKEYMANAGER_H +#define GLOBALHOTKEYMANAGER_H + +#pragma once +#ifdef WITH_HOTKEYS + +#include +#include +#include +#include +#include +#include +#include "uiohook.h" + +// hotkey callback type +using HotkeyCallback = std::function; + +class GlobalHotkeyManager +{ +public: + 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: + + // 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 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); + + static GlobalHotkeyManager* s_instance; + + 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/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; }; diff --git a/src/windows/voice/voicewindow.cpp b/src/windows/voice/voicewindow.cpp index a9e9682e..51fcae72 100644 --- a/src/windows/voice/voicewindow.cpp +++ b/src/windows/voice/voicewindow.cpp @@ -271,6 +271,22 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) UpdateStageCommand(); } +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) { 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..7113eafb 100644 --- a/src/windows/voice/voicewindow.hpp +++ b/src/windows/voice/voicewindow.hpp @@ -22,6 +22,10 @@ class VoiceWindow : public Gtk::Window { public: VoiceWindow(Snowflake channel_id); + void SetMute(bool is_mute); + void SetDeaf(bool is_deaf); + bool GetMute(); + bool GetDeaf(); private: void SetUsers(const std::unordered_set &user_ids); 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