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