diff options
author | dec05eba <dec05eba@protonmail.com> | 2025-01-23 21:23:19 +0100 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2025-01-23 21:25:45 +0100 |
commit | 1d9d4d6398e989d0628cb4dab0273293b79b4816 (patch) | |
tree | d5c4bd8c6f27d84156177831f1c0e6beb0de762a /src | |
parent | 47ada4d79844d9a98d9689d0de0c92864e0fc372 (diff) |
Make hotkeys reconfigurable, faster hotkey startup time, fix some keyboard locale issues
Diffstat (limited to 'src')
-rw-r--r-- | src/Config.cpp | 34 | ||||
-rw-r--r-- | src/GlobalHotkeysLinux.cpp | 140 | ||||
-rw-r--r-- | src/GlobalHotkeysX11.cpp | 39 | ||||
-rw-r--r-- | src/Overlay.cpp | 177 | ||||
-rw-r--r-- | src/WindowUtils.cpp | 100 | ||||
-rw-r--r-- | src/gui/GlobalSettingsPage.cpp | 434 | ||||
-rw-r--r-- | src/main.cpp | 96 |
7 files changed, 786 insertions, 234 deletions
diff --git a/src/Config.cpp b/src/Config.cpp index 8dbe32d..16b371f 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -1,21 +1,21 @@ #include "../include/Config.hpp" #include "../include/Utils.hpp" #include "../include/GsrInfo.hpp" +#include "../include/GlobalHotkeys.hpp" #include <variant> #include <limits.h> #include <inttypes.h> #include <libgen.h> #include <iostream> +#include <mglpp/window/Keyboard.hpp> #define FORMAT_I32 "%" PRIi32 #define FORMAT_I64 "%" PRIi64 #define FORMAT_U32 "%" PRIu32 -#define CONFIG_FILE_VERSION 1 - namespace gsr { bool ConfigHotkey::operator==(const ConfigHotkey &other) const { - return keysym == other.keysym && modifiers == other.modifiers; + return key == other.key && modifiers == other.modifiers; } bool ConfigHotkey::operator!=(const ConfigHotkey &other) const { @@ -25,19 +25,26 @@ namespace gsr { Config::Config(const SupportedCaptureOptions &capture_options) { const std::string default_save_directory = get_videos_dir(); + streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT}; streaming_config.record_options.video_quality = "custom"; streaming_config.record_options.audio_tracks.push_back("default_output"); streaming_config.record_options.video_bitrate = 15000; + record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT}; + record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT}; record_config.save_directory = default_save_directory; record_config.record_options.audio_tracks.push_back("default_output"); record_config.record_options.video_bitrate = 45000; + replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT}; + replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT}; replay_config.record_options.video_quality = "custom"; replay_config.save_directory = default_save_directory; replay_config.record_options.audio_tracks.push_back("default_output"); replay_config.record_options.video_bitrate = 45000; + main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT}; + if(!capture_options.monitors.empty()) { streaming_config.record_options.record_area_option = capture_options.monitors.front().name; record_config.record_options.record_area_option = capture_options.monitors.front().name; @@ -61,6 +68,7 @@ namespace gsr { {"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option}, {"main.joystick_hotkeys_enable_option", &config.main_config.joystick_hotkeys_enable_option}, {"main.tint_color", &config.main_config.tint_color}, + {"main.show_hide_hotkey", &config.main_config.show_hide_hotkey}, {"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option}, {"streaming.record_options.record_area_width", &config.streaming_config.record_options.record_area_width}, @@ -89,7 +97,7 @@ namespace gsr { {"streaming.twitch.key", &config.streaming_config.twitch.stream_key}, {"streaming.custom.url", &config.streaming_config.custom.url}, {"streaming.custom.container", &config.streaming_config.custom.container}, - {"streaming.start_stop_recording_hotkey", &config.streaming_config.start_stop_recording_hotkey}, + {"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey}, {"record.record_options.record_area_option", &config.record_config.record_options.record_area_option}, {"record.record_options.record_area_width", &config.record_config.record_options.record_area_width}, @@ -116,8 +124,8 @@ namespace gsr { {"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications}, {"record.save_directory", &config.record_config.save_directory}, {"record.container", &config.record_config.container}, - {"record.start_stop_recording_hotkey", &config.record_config.start_stop_recording_hotkey}, - {"record.pause_unpause_recording_hotkey", &config.record_config.pause_unpause_recording_hotkey}, + {"record.start_stop_hotkey", &config.record_config.start_stop_hotkey}, + {"record.pause_unpause_hotkey", &config.record_config.pause_unpause_hotkey}, {"replay.record_options.record_area_option", &config.replay_config.record_options.record_area_option}, {"replay.record_options.record_area_width", &config.replay_config.record_options.record_area_width}, @@ -147,8 +155,8 @@ namespace gsr { {"replay.save_directory", &config.replay_config.save_directory}, {"replay.container", &config.replay_config.container}, {"replay.time", &config.replay_config.replay_time}, - {"replay.start_stop_recording_hotkey", &config.replay_config.start_stop_recording_hotkey}, - {"replay.save_recording_hotkey", &config.replay_config.save_recording_hotkey} + {"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey}, + {"replay.save_hotkey", &config.replay_config.save_hotkey} }; } @@ -229,9 +237,9 @@ namespace gsr { } else if(std::holds_alternative<ConfigHotkey*>(it->second)) { std::string value_str(key_value->value); ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it->second); - if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->keysym, &config_hotkey->modifiers) != 2) { + if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->key, &config_hotkey->modifiers) != 2) { fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data()); - config_hotkey->keysym = 0; + config_hotkey->key = 0; config_hotkey->modifiers = 0; } } else if(std::holds_alternative<std::vector<std::string>*>(it->second)) { @@ -242,7 +250,7 @@ namespace gsr { return true; }); - if(config->main_config.config_file_version != CONFIG_FILE_VERSION) { + if(config->main_config.config_file_version != GSR_CONFIG_FILE_VERSION) { fprintf(stderr, "Info: the config file is outdated, resetting it\n"); config = std::nullopt; } @@ -251,7 +259,7 @@ namespace gsr { } void save_config(Config &config) { - config.main_config.config_file_version = CONFIG_FILE_VERSION; + config.main_config.config_file_version = GSR_CONFIG_FILE_VERSION; const std::string config_path = get_config_dir() + "/config_ui"; @@ -280,7 +288,7 @@ namespace gsr { fprintf(file, "%.*s " FORMAT_I32 "\n", (int)it.first.size(), it.first.data(), *std::get<int32_t*>(it.second)); } else if(std::holds_alternative<ConfigHotkey*>(it.second)) { const ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it.second); - fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->keysym, config_hotkey->modifiers); + fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->key, config_hotkey->modifiers); } else if(std::holds_alternative<std::vector<std::string>*>(it.second)) { std::vector<std::string> *array = std::get<std::vector<std::string>*>(it.second); for(const std::string &value : *array) { diff --git a/src/GlobalHotkeysLinux.cpp b/src/GlobalHotkeysLinux.cpp index 29cf952..c779777 100644 --- a/src/GlobalHotkeysLinux.cpp +++ b/src/GlobalHotkeysLinux.cpp @@ -5,6 +5,12 @@ #include <limits.h> #include <string.h> +extern "C" { +#include <mgl/mgl.h> +} +#include <X11/Xlib.h> +#include <linux/input-event-codes.h> + #define PIPE_READ 0 #define PIPE_WRITE 1 @@ -17,16 +23,55 @@ namespace gsr { return "--all"; } + static inline uint8_t x11_keycode_to_linux_keycode(uint8_t code) { + return code - 8; + } + + static std::vector<uint8_t> modifiers_to_linux_keys(uint32_t modifiers) { + std::vector<uint8_t> result; + if(modifiers & HOTKEY_MOD_LSHIFT) + result.push_back(KEY_LEFTSHIFT); + if(modifiers & HOTKEY_MOD_RSHIFT) + result.push_back(KEY_RIGHTSHIFT); + if(modifiers & HOTKEY_MOD_LCTRL) + result.push_back(KEY_LEFTCTRL); + if(modifiers & HOTKEY_MOD_RCTRL) + result.push_back(KEY_RIGHTCTRL); + if(modifiers & HOTKEY_MOD_LALT) + result.push_back(KEY_LEFTALT); + if(modifiers & HOTKEY_MOD_RALT) + result.push_back(KEY_RIGHTALT); + if(modifiers & HOTKEY_MOD_LSUPER) + result.push_back(KEY_LEFTMETA); + if(modifiers & HOTKEY_MOD_RSUPER) + result.push_back(KEY_RIGHTMETA); + return result; + } + + static std::string linux_keys_to_command_string(const uint8_t *keys, size_t size) { + std::string result; + for(size_t i = 0; i < size; ++i) { + if(!result.empty()) + result += "+"; + result += std::to_string(keys[i]); + } + return result; + } + GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) { for(int i = 0; i < 2; ++i) { - pipes[i] = -1; + read_pipes[i] = -1; + write_pipes[i] = -1; } } GlobalHotkeysLinux::~GlobalHotkeysLinux() { for(int i = 0; i < 2; ++i) { - if(pipes[i] > 0) - close(pipes[i]); + if(read_pipes[i] > 0) + close(read_pipes[i]); + + if(write_pipes[i] > 0) + close(write_pipes[i]); } if(read_file) @@ -58,21 +103,36 @@ namespace gsr { if(process_id > 0) return false; - if(pipe(pipes) == -1) + if(pipe(read_pipes) == -1) return false; + if(pipe(write_pipes) == -1) { + for(int i = 0; i < 2; ++i) { + close(read_pipes[i]); + read_pipes[i] = -1; + } + return false; + } + const pid_t pid = vfork(); if(pid == -1) { perror("Failed to vfork"); for(int i = 0; i < 2; ++i) { - close(pipes[i]); - pipes[i] = -1; + close(read_pipes[i]); + close(write_pipes[i]); + read_pipes[i] = -1; + write_pipes[i] = -1; } return false; } else if(pid == 0) { /* child */ - dup2(pipes[PIPE_WRITE], STDOUT_FILENO); + dup2(read_pipes[PIPE_WRITE], STDOUT_FILENO); for(int i = 0; i < 2; ++i) { - close(pipes[i]); + close(read_pipes[i]); + } + + dup2(write_pipes[PIPE_READ], STDIN_FILENO); + for(int i = 0; i < 2; ++i) { + close(write_pipes[i]); } if(inside_flatpak) { @@ -87,24 +147,70 @@ namespace gsr { _exit(127); } else { /* parent */ process_id = pid; - close(pipes[PIPE_WRITE]); - pipes[PIPE_WRITE] = -1; - const int fdl = fcntl(pipes[PIPE_READ], F_GETFL); - fcntl(pipes[PIPE_READ], F_SETFL, fdl | O_NONBLOCK); + close(read_pipes[PIPE_WRITE]); + read_pipes[PIPE_WRITE] = -1; - read_file = fdopen(pipes[PIPE_READ], "r"); + close(write_pipes[PIPE_READ]); + write_pipes[PIPE_READ] = -1; + + fcntl(read_pipes[PIPE_READ], F_SETFL, fcntl(read_pipes[PIPE_READ], F_GETFL) | O_NONBLOCK); + read_file = fdopen(read_pipes[PIPE_READ], "r"); if(read_file) - pipes[PIPE_READ] = -1; + read_pipes[PIPE_READ] = -1; else - fprintf(stderr, "fdopen failed, error: %s\n", strerror(errno)); + fprintf(stderr, "fdopen failed for read, error: %s\n", strerror(errno)); + } + + return true; + } + + bool GlobalHotkeysLinux::bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) { + if(bound_actions_by_id.find(id) != bound_actions_by_id.end()) + return false; + + if(id.find(' ') != std::string::npos || id.find('\n') != std::string::npos) { + fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: id \"%s\" contains either space or newline\n", id.c_str()); + return false; + } + + if(hotkey.key == 0) { + //fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a key\n"); + return false; } + if(hotkey.modifiers == 0) { + //fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a modifier\n"); + return false; + } + + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + const uint8_t keycode = x11_keycode_to_linux_keycode(XKeysymToKeycode(display, hotkey.key)); + const std::vector<uint8_t> modifiers = modifiers_to_linux_keys(hotkey.modifiers); + const std::string modifiers_command = linux_keys_to_command_string(modifiers.data(), modifiers.size()); + + char command[256]; + const int command_size = snprintf(command, sizeof(command), "bind %s %d+%s\n", id.c_str(), (int)keycode, modifiers_command.c_str()); + if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) { + fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno)); + return false; + } + + bound_actions_by_id[id] = std::move(callback); return true; } - bool GlobalHotkeysLinux::bind_action(const std::string &id, GlobalHotkeyCallback callback) { - return bound_actions_by_id.insert(std::make_pair(id, std::move(callback))).second; + void GlobalHotkeysLinux::unbind_all_keys() { + if(bound_actions_by_id.empty()) + return; + + char command[32]; + const int command_size = snprintf(command, sizeof(command), "unbind_all\n"); + if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) { + fprintf(stderr, "Error: GlobalHotkeysLinux::unbind_all_keys: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno)); + } + bound_actions_by_id.clear(); } void GlobalHotkeysLinux::poll_events() { diff --git a/src/GlobalHotkeysX11.cpp b/src/GlobalHotkeysX11.cpp index c8d198c..9af2607 100644 --- a/src/GlobalHotkeysX11.cpp +++ b/src/GlobalHotkeysX11.cpp @@ -50,6 +50,27 @@ namespace gsr { return mask; } + static uint32_t modifiers_to_x11_modifiers(uint32_t modifiers) { + uint32_t result = 0; + if(modifiers & HOTKEY_MOD_LSHIFT) + result |= ShiftMask; + if(modifiers & HOTKEY_MOD_RSHIFT) + result |= ShiftMask; + if(modifiers & HOTKEY_MOD_LCTRL) + result |= ControlMask; + if(modifiers & HOTKEY_MOD_RCTRL) + result |= ControlMask; + if(modifiers & HOTKEY_MOD_LALT) + result |= Mod1Mask; + if(modifiers & HOTKEY_MOD_RALT) + result |= Mod5Mask; + if(modifiers & HOTKEY_MOD_LSUPER) + result |= Mod4Mask; + if(modifiers & HOTKEY_MOD_RSUPER) + result |= Mod4Mask; + return result; + } + GlobalHotkeysX11::GlobalHotkeysX11() { dpy = XOpenDisplay(NULL); if(!dpy) @@ -74,16 +95,17 @@ namespace gsr { x_failed = false; XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error); + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers); unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(int i = 0; i < 4; ++i) { - XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync); } XSync(dpy, False); if(x_failed) { for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } XSync(dpy, False); XSetErrorHandler(prev_xerror); @@ -106,10 +128,11 @@ namespace gsr { x_failed = false; XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error); + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers); unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } XSync(dpy, False); @@ -127,8 +150,9 @@ namespace gsr { unsigned int numlock_mask = x11_get_numlock_mask(dpy); unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask }; for(auto it = bound_keys_by_id.begin(); it != bound_keys_by_id.end();) { + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers); for(int i = 0; i < 4; ++i) { - XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy)); + XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy)); } } bound_keys_by_id.clear(); @@ -145,7 +169,7 @@ namespace gsr { XNextEvent(dpy, &xev); if(xev.type == KeyPress) { const KeySym key_sym = XLookupKeysym(&xev.xkey, 0); - call_hotkey_callback({ key_sym, xev.xkey.state }); + call_hotkey_callback({ (uint32_t)key_sym, xev.xkey.state }); } } } @@ -157,7 +181,7 @@ namespace gsr { // Note: not all keys are mapped in mgl_key_to_key_sym. If more hotkeys are added or changed then add the key mapping there const KeySym key_sym = mgl_key_to_key_sym(event.key.code); const uint32_t modifiers = mgl_key_modifiers_to_x11_modifier_mask(event.key); - return !call_hotkey_callback(Hotkey{key_sym, modifiers}); + return !call_hotkey_callback(Hotkey{(uint32_t)key_sym, modifiers}); } static unsigned int key_state_without_locks(unsigned int key_state) { @@ -165,8 +189,9 @@ namespace gsr { } bool GlobalHotkeysX11::call_hotkey_callback(Hotkey hotkey) const { + const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers); for(const auto &[key, val] : bound_keys_by_id) { - if(val.hotkey.key == hotkey.key && key_state_without_locks(val.hotkey.modifiers) == key_state_without_locks(hotkey.modifiers)) { + if(val.hotkey.key == hotkey.key && key_state_without_locks(modifiers_to_x11_modifiers(val.hotkey.modifiers)) == key_state_without_locks(modifiers_x11)) { val.callback(key); return true; } diff --git a/src/Overlay.cpp b/src/Overlay.cpp index 2f1725d..a8490ab 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -336,6 +336,79 @@ namespace gsr { return true; } + static Hotkey config_hotkey_to_hotkey(ConfigHotkey config_hotkey) { + return { + (uint32_t)mgl::Keyboard::key_to_x11_keysym((mgl::Keyboard::Key)config_hotkey.key), + config_hotkey.modifiers + }; + } + + static void bind_linux_hotkeys(GlobalHotkeysLinux *global_hotkeys, Overlay *overlay) { + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().main_config.show_hide_hotkey), + "show_hide", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_show(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().record_config.start_stop_hotkey), + "record", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_record(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().record_config.pause_unpause_hotkey), + "pause", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_pause(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().streaming_config.start_stop_hotkey), + "stream", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_stream(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.start_stop_hotkey), + "replay_start", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_replay(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.save_hotkey), + "replay_save", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + } + + static std::unique_ptr<GlobalHotkeysLinux> register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) { + auto global_hotkeys = std::make_unique<GlobalHotkeysLinux>(grab_type); + if(!global_hotkeys->start()) + fprintf(stderr, "error: failed to start global hotkeys\n"); + + bind_linux_hotkeys(global_hotkeys.get(), overlay); + return global_hotkeys; + } + + static std::unique_ptr<GlobalHotkeysJoystick> register_joystick_hotkeys(Overlay *overlay) { + auto global_hotkeys_js = std::make_unique<GlobalHotkeysJoystick>(); + if(!global_hotkeys_js->start()) + fprintf(stderr, "Warning: failed to start joystick hotkeys\n"); + + global_hotkeys_js->bind_action("save_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + + return global_hotkeys_js; + } + Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) : resources_path(std::move(resources_path)), gsr_info(std::move(gsr_info)), @@ -366,6 +439,20 @@ namespace gsr { if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup") on_press_start_replay(true); + + if(config.main_config.hotkeys_enable_option == "enable_hotkeys") + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); + else if(config.main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices") + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL); + + if(config.main_config.joystick_hotkeys_enable_option == "enable_hotkeys") + global_hotkeys_js = register_joystick_hotkeys(this); + + x11_mapping_display = XOpenDisplay(nullptr); + if(x11_mapping_display) + XKeysymToKeycode(x11_mapping_display, XK_F1); // If we dont call we will never get a MappingNotify + else + fprintf(stderr, "Warning: XOpenDisplay failed to mapping notify\n"); } Overlay::~Overlay() { @@ -393,6 +480,9 @@ namespace gsr { close_gpu_screen_recorder_output(); deinit_color_theme(); + + if(x11_mapping_display) + XCloseDisplay(x11_mapping_display); } void Overlay::xi_setup() { @@ -535,7 +625,32 @@ namespace gsr { } } - void Overlay::handle_events(gsr::GlobalHotkeys *global_hotkeys) { + void Overlay::handle_keyboard_mapping_event() { + if(!x11_mapping_display) + return; + + bool mapping_updated = false; + while(XPending(x11_mapping_display)) { + XNextEvent(x11_mapping_display, &x11_mapping_xev); + if(x11_mapping_xev.type == MappingNotify) { + XRefreshKeyboardMapping(&x11_mapping_xev.xmapping); + mapping_updated = true; + } + } + + if(mapping_updated) + rebind_all_keyboard_hotkeys(); + } + + void Overlay::handle_events() { + if(global_hotkeys) + global_hotkeys->poll_events(); + + if(global_hotkeys_js) + global_hotkeys_js->poll_events(); + + handle_keyboard_mapping_event(); + if(!visible || !window) return; @@ -742,29 +857,30 @@ namespace gsr { const mgl::vec2i monitor_position_query_value = (x11_cursor_window || gsr_info.system_info.display_server != DisplayServer::WAYLAND) ? cursor_position : create_window_get_center_position(display); const Monitor *focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value); - if(is_wlroots) { + + // Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused. + // If the focused window is a wayland application then don't use override redirect and instead create + // a fullscreen window for the ui. + const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots; + + if(prevent_game_minimizing) { window_pos = focused_monitor->position; window_size = focused_monitor->size; } else { window_pos = {0, 0}; - window_size = {32, 32}; + window_size = focused_monitor->size / 2; } - // Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused. - // If the focused window is a wayland application then don't use override redirect and instead create - // a fullscreen window for the ui. - const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window; - mgl::Window::CreateParams window_create_params; window_create_params.size = window_size; - if(is_wlroots || prevent_game_minimizing) { + if(prevent_game_minimizing) { window_create_params.min_size = window_size; window_create_params.max_size = window_size; } - window_create_params.position = window_pos; + window_create_params.position = focused_monitor->position + focused_monitor->size / 2 - window_size / 2; window_create_params.hidden = prevent_game_minimizing; window_create_params.override_redirect = prevent_game_minimizing; - window_create_params.background_color = bg_color; + window_create_params.background_color = mgl::Color(0, 0, 0, 0); window_create_params.support_alpha = true; window_create_params.hide_decorations = true; // MGL_WINDOW_TYPE_DIALOG is needed for kde plasma wayland in some cases, otherwise the window will pop up on another activity @@ -786,6 +902,7 @@ namespace gsr { data = 1; XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); + const auto original_window_size = window_size; window_pos = focused_monitor->position; window_size = focused_monitor->size; if(!init_theme(resources_path)) { @@ -795,11 +912,11 @@ namespace gsr { } get_theme().set_window_size(window_size); - if(is_wlroots || prevent_game_minimizing) { + if(prevent_game_minimizing) { window->set_size(window_size); window->set_size_limits(window_size, window_size); - window->set_position(window_pos); } + window->set_position(focused_monitor->position + focused_monitor->size / 2 - original_window_size / 2); mgl_window *win = window->internal_window(); win->cursor_position.x = cursor_position.x - window_pos.x; @@ -927,7 +1044,8 @@ namespace gsr { button->set_bg_hover_color(mgl::Color(0, 0, 0, 255)); button->set_icon(&get_theme().settings_small_texture); button->on_click = [&]() { - auto settings_page = std::make_unique<GlobalSettingsPage>(&gsr_info, config, &page_stack); + auto settings_page = std::make_unique<GlobalSettingsPage>(this, &gsr_info, config, &page_stack); + settings_page->on_startup_changed = [&](bool enable, int exit_status) { if(exit_status == 0) return; @@ -949,11 +1067,21 @@ namespace gsr { }; settings_page->on_keyboard_hotkey_changed = [this](const char *hotkey_option) { - on_keyboard_hotkey_changed(hotkey_option); + global_hotkeys.reset(); + if(strcmp(hotkey_option, "enable_hotkeys") == 0) + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL); + else if(strcmp(hotkey_option, "enable_hotkeys_virtual_devices") == 0) + global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::VIRTUAL); + else if(strcmp(hotkey_option, "disable_hotkeys") == 0) + global_hotkeys.reset(); }; settings_page->on_joystick_hotkey_changed = [this](const char *hotkey_option) { - on_joystick_hotkey_changed(hotkey_option); + global_hotkeys_js.reset(); + if(strcmp(hotkey_option, "enable_hotkeys") == 0) + global_hotkeys_js = register_joystick_hotkeys(this); + else if(strcmp(hotkey_option, "disable_hotkeys") == 0) + global_hotkeys_js.reset(); }; page_stack.push(std::move(settings_page)); @@ -990,7 +1118,8 @@ namespace gsr { // The focused application can be an xwayland application but the cursor can hover over a wayland application. // This is even the case when hovering over the titlebar of the xwayland application. - if(prevent_game_minimizing) + const bool fake_cursor = is_wlroots ? x11_cursor_window != None : prevent_game_minimizing; + if(fake_cursor) xi_setup(); //window->set_fullscreen(true); @@ -1047,6 +1176,8 @@ namespace gsr { if(paused) update_ui_recording_paused(); + // Wayland compositors have retarded fullscreen animations that we cant disable in a proper way + // without messing up window position. show_overlay_timeout_seconds = prevent_game_minimizing ? 0.0 : 0.15; show_overlay_clock.restart(); draw(); @@ -1233,6 +1364,18 @@ namespace gsr { return config; } + void Overlay::unbind_all_keyboard_hotkeys() { + if(global_hotkeys) + global_hotkeys->unbind_all_keys(); + } + + void Overlay::rebind_all_keyboard_hotkeys() { + unbind_all_keyboard_hotkeys(); + // TODO: Check if type is GlobalHotkeysLinux + if(global_hotkeys) + bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this); + } + void Overlay::update_notification_process_status() { if(notification_process <= 0) return; diff --git a/src/WindowUtils.cpp b/src/WindowUtils.cpp index ea9dfbd..d8e3508 100644 --- a/src/WindowUtils.cpp +++ b/src/WindowUtils.cpp @@ -98,15 +98,16 @@ namespace gsr { return found_window; } - static Window get_window_at_cursor_position(Display *dpy) { + mgl::vec2i get_cursor_position(Display *dpy, Window *window) { Window root_window = None; - Window window = None; + *window = None; int dummy_i; unsigned int dummy_u; - XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &dummy_i, &dummy_i, &dummy_u); + mgl::vec2i root_pos; + XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u); if(window) - window = window_get_target_window_child(dpy, window); - return window; + *window = window_get_target_window_child(dpy, *window); + return root_pos; } Window get_focused_window(Display *dpy, WindowCaptureType cap_type) { @@ -136,7 +137,7 @@ namespace gsr { return focused_window; } - focused_window = get_window_at_cursor_position(dpy); + get_cursor_position(dpy, &focused_window); if(focused_window && focused_window != DefaultRootWindow(dpy)) return focused_window; @@ -235,35 +236,6 @@ namespace gsr { return result; } - mgl::vec2i get_cursor_position(Display *dpy, Window *window) { - Window root_window = None; - *window = None; - int dummy_i; - unsigned int dummy_u; - mgl::vec2i root_pos; - XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u); - - // This dumb shit is done to satisfy gnome wayland. Only set |window| if a valid x11 window is focused - if(window) { - XWindowAttributes attr; - if(XGetWindowAttributes(dpy, *window, &attr) && attr.override_redirect) - *window = None; - - int revert_to = 0; - Window input_focus_window = None; - if(XGetInputFocus(dpy, &input_focus_window, &revert_to)) { - if(input_focus_window) { - if(XGetWindowAttributes(dpy, input_focus_window, &attr) && attr.override_redirect) - *window = None; - } else { - *window = None; - } - } - } - - return root_pos; - } - typedef struct { unsigned long flags; unsigned long functions; @@ -334,7 +306,7 @@ namespace gsr { poll_fd.fd = x_fd; poll_fd.events = POLLIN; poll_fd.revents = 0; - const int fds_ready = poll(&poll_fd, 1, 1000); + const int fds_ready = poll(&poll_fd, 1, 200); if(fds_ready == 0) { fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n"); break; @@ -342,15 +314,18 @@ namespace gsr { continue; } - XNextEvent(display, &xev); - if(xev.type == ConfigureNotify && xev.xconfigure.window == window) { - got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0; - position.x = xev.xconfigure.x + xev.xconfigure.width / 2; - position.y = xev.xconfigure.y + xev.xconfigure.height / 2; - break; + while(XPending(display)) { + XNextEvent(display, &xev); + if(xev.type == ConfigureNotify && xev.xconfigure.window == window) { + got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0; + position.x = xev.xconfigure.x + xev.xconfigure.width / 2; + position.y = xev.xconfigure.y + xev.xconfigure.height / 2; + goto done; + } } } + done: XDestroyWindow(display, window); XFlush(display); @@ -395,7 +370,7 @@ namespace gsr { poll_fd.fd = x_fd; poll_fd.events = POLLIN; poll_fd.revents = 0; - const int fds_ready = poll(&poll_fd, 1, 1000); + const int fds_ready = poll(&poll_fd, 1, 200); if(fds_ready == 0) { fprintf(stderr, "Error: timed out waiting for MapNotify/ConfigureNotify after XCreateWindow\n"); break; @@ -403,27 +378,30 @@ namespace gsr { continue; } - XNextEvent(display, &xev); - if(xev.type == MapNotify && xev.xmap.window == window) { - int x = 0; - int y = 0; - Window w = None; - XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w); - - got_data = x > 0 && y > 0; - position.x = x + size / 2; - position.y = y + size / 2; - if(got_data) - break; - } else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) { - got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0; - position.x = xev.xconfigure.x + xev.xconfigure.width / 2; - position.y = xev.xconfigure.y + xev.xconfigure.height / 2; - if(got_data) - break; + while(XPending(display)) { + XNextEvent(display, &xev); + if(xev.type == MapNotify && xev.xmap.window == window) { + int x = 0; + int y = 0; + Window w = None; + XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w); + + got_data = x > 0 && y > 0; + position.x = x + size / 2; + position.y = y + size / 2; + if(got_data) + goto done; + } else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) { + got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0; + position.x = xev.xconfigure.x + xev.xconfigure.width / 2; + position.y = xev.xconfigure.y + xev.xconfigure.height / 2; + if(got_data) + goto done; + } } } + done: XDestroyWindow(display, window); XFlush(display); diff --git a/src/gui/GlobalSettingsPage.cpp b/src/gui/GlobalSettingsPage.cpp index 8a2a162..d4a87c2 100644 --- a/src/gui/GlobalSettingsPage.cpp +++ b/src/gui/GlobalSettingsPage.cpp @@ -1,5 +1,7 @@ #include "../../include/gui/GlobalSettingsPage.hpp" +#include "../../include/Overlay.hpp" +#include "../../include/GlobalHotkeys.hpp" #include "../../include/Theme.hpp" #include "../../include/Process.hpp" #include "../../include/gui/GsrPage.hpp" @@ -10,6 +12,16 @@ #include "../../include/gui/Label.hpp" #include "../../include/gui/RadioButton.hpp" #include "../../include/gui/LineSeparator.hpp" +#include "../../include/gui/CustomRendererWidget.hpp" + +#include <assert.h> +#include <X11/Xlib.h> +extern "C" { +#include <mgl/mgl.h> +} +#include <mglpp/window/Window.hpp> +#include <mglpp/graphics/Rectangle.hpp> +#include <mglpp/graphics/Text.hpp> #ifndef GSR_UI_VERSION #define GSR_UI_VERSION "unknown" @@ -40,8 +52,64 @@ namespace gsr { return "unknown"; } - GlobalSettingsPage::GlobalSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : + static uint32_t mgl_modifier_to_hotkey_modifier(mgl::Keyboard::Key modifier_key) { + switch(modifier_key) { + case mgl::Keyboard::LControl: return HOTKEY_MOD_LCTRL; + case mgl::Keyboard::LShift: return HOTKEY_MOD_LSHIFT; + case mgl::Keyboard::LAlt: return HOTKEY_MOD_LALT; + case mgl::Keyboard::LSystem: return HOTKEY_MOD_LSUPER; + case mgl::Keyboard::RControl: return HOTKEY_MOD_RCTRL; + case mgl::Keyboard::RShift: return HOTKEY_MOD_RSHIFT; + case mgl::Keyboard::RAlt: return HOTKEY_MOD_RALT; + case mgl::Keyboard::RSystem: return HOTKEY_MOD_RSUPER; + default: return 0; + } + return 0; + } + + static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) { + std::vector<mgl::Keyboard::Key> result; + if(modifiers & HOTKEY_MOD_LCTRL) + result.push_back(mgl::Keyboard::LControl); + if(modifiers & HOTKEY_MOD_LSHIFT) + result.push_back(mgl::Keyboard::LShift); + if(modifiers & HOTKEY_MOD_LALT) + result.push_back(mgl::Keyboard::LAlt); + if(modifiers & HOTKEY_MOD_LSUPER) + result.push_back(mgl::Keyboard::LSystem); + if(modifiers & HOTKEY_MOD_RCTRL) + result.push_back(mgl::Keyboard::RControl); + if(modifiers & HOTKEY_MOD_RSHIFT) + result.push_back(mgl::Keyboard::RShift); + if(modifiers & HOTKEY_MOD_RALT) + result.push_back(mgl::Keyboard::RAlt); + if(modifiers & HOTKEY_MOD_RSUPER) + result.push_back(mgl::Keyboard::RSystem); + return result; + } + + static std::string config_hotkey_to_string(ConfigHotkey config_hotkey) { + std::string result; + + const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(config_hotkey.modifiers); + for(const mgl::Keyboard::Key modifier_key : modifier_keys) { + if(!result.empty()) + result += " + "; + result += mgl::Keyboard::key_to_string(modifier_key); + } + + if(config_hotkey.key != 0) { + if(!result.empty()) + result += " + "; + result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)config_hotkey.key); + } + + return result; + } + + GlobalSettingsPage::GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()), + overlay(overlay), config(config), gsr_info(gsr_info), page_stack(page_stack) @@ -57,6 +125,45 @@ namespace gsr { add_widgets(); load(); + + auto hotkey_overlay = std::make_unique<CustomRendererWidget>(get_size()); + hotkey_overlay->draw_handler = [this](mgl::Window &window, mgl::vec2f, mgl::vec2f) { + Button *configure_hotkey_button = configure_hotkey_get_button_by_active_type(); + if(!configure_hotkey_button) + return; + + mgl::Text title_text("Press a key combination to use for the hotkey \"Start/stop recording\":", get_theme().title_font); + mgl::Text hotkey_text(configure_hotkey_button->get_text(), get_theme().top_bar_font); + mgl::Text description_text("The hotkey has to contain one or more of these keys: Alt, Ctrl, Shift and Super. Press Esc to cancel.", get_theme().body_font); + const float text_max_width = std::max(title_text.get_bounds().size.x, std::max(hotkey_text.get_bounds().size.x, description_text.get_bounds().size.x)); + + const float padding_horizontal = int(get_theme().window_height * 0.01f); + const float padding_vertical = int(get_theme().window_height * 0.01f); + + const mgl::vec2f bg_size = mgl::vec2f(text_max_width + padding_horizontal*2.0f, get_theme().window_height * 0.1f).floor(); + mgl::Rectangle bg_rect(mgl::vec2f(get_theme().window_width*0.5f - bg_size.x*0.5f, get_theme().window_height*0.5f - bg_size.y*0.5f).floor(), bg_size); + bg_rect.set_color(get_color_theme().page_bg_color); + window.draw(bg_rect); + + const mgl::vec2f tint_size = mgl::vec2f(bg_size.x, 0.004f * get_theme().window_height).floor(); + mgl::Rectangle tint_rect(bg_rect.get_position() - mgl::vec2f(0.0f, tint_size.y), tint_size); + tint_rect.set_color(get_color_theme().tint_color); + window.draw(tint_rect); + + title_text.set_position(mgl::vec2f(bg_rect.get_position() + mgl::vec2f(bg_rect.get_size().x*0.5f - title_text.get_bounds().size.x*0.5f, padding_vertical)).floor()); + window.draw(title_text); + + //const float description_bottom = description_text.get_position().y + description_text.get_bounds().size.y; + //const float remaining_height = (bg_rect.get_position().y + bg_rect.get_size().y) - description_bottom; + hotkey_text.set_position(mgl::vec2f(bg_rect.get_position() + bg_rect.get_size()*0.5f - hotkey_text.get_bounds().size*0.5f).floor()); + window.draw(hotkey_text); + + description_text.set_position(mgl::vec2f(bg_rect.get_position() + mgl::vec2f(bg_rect.get_size().x*0.5f - description_text.get_bounds().size.x*0.5f, bg_rect.get_size().y - description_text.get_bounds().size.y - padding_vertical)).floor()); + window.draw(description_text); + }; + hotkey_overlay->set_visible(false); + hotkey_overlay_ptr = hotkey_overlay.get(); + add_widget(std::move(hotkey_overlay)); } std::unique_ptr<Subsection> GlobalSettingsPage::create_appearance_subsection(ScrollablePage *parent_page) { @@ -134,6 +241,117 @@ namespace gsr { return enable_hotkeys_radio_button; } + std::unique_ptr<List> GlobalSettingsPage::create_show_hide_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Show/hide UI:", get_color_theme().text_color)); + auto show_hide_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + show_hide_button_ptr = show_hide_button.get(); + list->add_widget(std::move(show_hide_button)); + + show_hide_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::SHOW_HIDE); + }; + + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_replay_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Turn replay on/off:", get_color_theme().text_color)); + auto turn_replay_on_off_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + turn_replay_on_off_button_ptr = turn_replay_on_off_button.get(); + list->add_widget(std::move(turn_replay_on_off_button)); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save replay:", get_color_theme().text_color)); + auto save_replay_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_replay_button_ptr = save_replay_button.get(); + list->add_widget(std::move(save_replay_button)); + + turn_replay_on_off_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_START_STOP); + }; + + save_replay_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop recording:", get_color_theme().text_color)); + auto start_stop_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + start_stop_recording_button_ptr = start_stop_recording_button.get(); + list->add_widget(std::move(start_stop_recording_button)); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Pause/unpause recording:", get_color_theme().text_color)); + auto pause_unpause_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + pause_unpause_recording_button_ptr = pause_unpause_recording_button.get(); + list->add_widget(std::move(pause_unpause_recording_button)); + + start_stop_recording_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP); + }; + + pause_unpause_recording_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_stream_hotkey_options() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop streaming:", get_color_theme().text_color)); + auto start_stop_streaming_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + start_stop_streaming_button_ptr = start_stop_streaming_button.get(); + list->add_widget(std::move(start_stop_streaming_button)); + + start_stop_streaming_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::STREAM_START_STOP); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + + // auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + // clear_hotkeys_button->on_click = [this] { + // config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0}; + // config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0}; + // load_hotkeys(); + // overlay->rebind_all_keyboard_hotkeys(); + // }; + // list->add_widget(std::move(clear_hotkeys_button)); + + auto reset_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Reset hotkeys to default", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + reset_hotkeys_button->on_click = [this] { + config.streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT}; + config.record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT}; + config.record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT}; + config.replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT}; + config.replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT}; + config.main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT}; + load_hotkeys(); + overlay->rebind_all_keyboard_hotkeys(); + }; + list->add_widget(std::move(reset_hotkeys_button)); + + return list; + } + std::unique_ptr<Subsection> GlobalSettingsPage::create_hotkey_subsection(ScrollablePage *parent_page) { auto list = std::make_unique<List>(List::Orientation::VERTICAL); List *list_ptr = list.get(); @@ -144,7 +362,12 @@ namespace gsr { list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable controller hotkeys?", get_color_theme().text_color)); list_ptr->add_widget(create_enable_joystick_hotkeys_button()); list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x)); - list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Double-click the share button to save a replay", get_color_theme().text_color)); + list_ptr->add_widget(create_show_hide_hotkey_options()); + list_ptr->add_widget(create_replay_hotkey_options()); + list_ptr->add_widget(create_record_hotkey_options()); + list_ptr->add_widget(create_stream_hotkey_options()); + list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Double-click the controller share button to save a replay", get_color_theme().text_color)); + list_ptr->add_widget(create_hotkey_control_buttons()); return subsection; } @@ -168,32 +391,30 @@ namespace gsr { std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) { const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL); + list->add_widget(create_exit_program_button()); + if(inside_flatpak) + list->add_widget(create_go_back_to_old_ui_button()); + return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; auto list = std::make_unique<List>(List::Orientation::VERTICAL); - List *list_ptr = list.get(); - auto subsection = std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); - - { - auto buttons_list = std::make_unique<List>(List::Orientation::HORIZONTAL); - buttons_list->add_widget(create_exit_program_button()); - if(inside_flatpak) - buttons_list->add_widget(create_go_back_to_old_ui_button()); - list_ptr->add_widget(std::move(buttons_list)); - } - list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x)); - { - char str[256]; - snprintf(str, sizeof(str), "UI version: %s", GSR_UI_VERSION); - list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); - - if(inside_flatpak) { - snprintf(str, sizeof(str), "Flatpak version: %s", GSR_FLATPAK_VERSION); - list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); - } - snprintf(str, sizeof(str), "GPU vendor: %s", gpu_vendor_to_string(gsr_info->gpu_info.vendor)); - list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + char str[128]; + snprintf(str, sizeof(str), "UI version: %s", GSR_UI_VERSION); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + + if(inside_flatpak) { + snprintf(str, sizeof(str), "Flatpak version: %s", GSR_FLATPAK_VERSION); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); } - return subsection; + + snprintf(str, sizeof(str), "GPU vendor: %s", gpu_vendor_to_string(gsr_info->gpu_info.vendor)); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color)); + + return std::make_unique<Subsection>("Application info", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f)); } void GlobalSettingsPage::add_widgets() { @@ -205,6 +426,7 @@ namespace gsr { settings_list->add_widget(create_startup_subsection(scrollable_page.get())); settings_list->add_widget(create_hotkey_subsection(scrollable_page.get())); settings_list->add_widget(create_application_options_subsection(scrollable_page.get())); + settings_list->add_widget(create_application_info_subsection(scrollable_page.get())); scrollable_page->add_widget(std::move(settings_list)); content_page_ptr->add_widget(std::move(scrollable_page)); @@ -227,12 +449,174 @@ namespace gsr { enable_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false); enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false); + + load_hotkeys(); + } + + void GlobalSettingsPage::load_hotkeys() { + turn_replay_on_off_button_ptr->set_text(config_hotkey_to_string(config.replay_config.start_stop_hotkey)); + save_replay_button_ptr->set_text(config_hotkey_to_string(config.replay_config.save_hotkey)); + + start_stop_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.start_stop_hotkey)); + pause_unpause_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.pause_unpause_hotkey)); + + start_stop_streaming_button_ptr->set_text(config_hotkey_to_string(config.streaming_config.start_stop_hotkey)); + + show_hide_button_ptr->set_text(config_hotkey_to_string(config.main_config.show_hide_hotkey)); } void GlobalSettingsPage::save() { + configure_hotkey_cancel(); config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id(); config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id(); config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id(); save_config(config); } + + bool GlobalSettingsPage::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) { + if(!StaticPage::on_event(event, window, offset)) + return false; + + if(configure_hotkey_type == ConfigureHotkeyType::NONE) + return true; + + Button *configure_hotkey_button = configure_hotkey_get_button_by_active_type(); + if(!configure_hotkey_button) + return true; + + if(event.type == mgl::Event::KeyPressed) { + if(event.key.code == mgl::Keyboard::Escape) + return false; + + if(mgl::Keyboard::key_is_modifier(event.key.code)) { + configure_config_hotkey.modifiers |= mgl_modifier_to_hotkey_modifier(event.key.code); + configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey)); + } else if(configure_config_hotkey.modifiers != 0) { + configure_config_hotkey.key = event.key.code; + configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey)); + configure_hotkey_stop_and_save(); + } + + return false; + } else if(event.type == mgl::Event::KeyReleased) { + if(event.key.code == mgl::Keyboard::Escape) { + configure_hotkey_cancel(); + return false; + } + + if(mgl::Keyboard::key_is_modifier(event.key.code)) { + configure_config_hotkey.modifiers &= ~mgl_modifier_to_hotkey_modifier(event.key.code); + configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey)); + } + + return false; + } + + return true; + } + + Button* GlobalSettingsPage::configure_hotkey_get_button_by_active_type() { + switch(configure_hotkey_type) { + case ConfigureHotkeyType::NONE: + return nullptr; + case ConfigureHotkeyType::REPLAY_START_STOP: + return turn_replay_on_off_button_ptr; + case ConfigureHotkeyType::REPLAY_SAVE: + return save_replay_button_ptr; + case ConfigureHotkeyType::RECORD_START_STOP: + return start_stop_recording_button_ptr; + case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE: + return pause_unpause_recording_button_ptr; + case ConfigureHotkeyType::STREAM_START_STOP: + return start_stop_streaming_button_ptr; + case ConfigureHotkeyType::SHOW_HIDE: + return show_hide_button_ptr; + } + return nullptr; + } + + ConfigHotkey* GlobalSettingsPage::configure_hotkey_get_config_by_active_type() { + switch(configure_hotkey_type) { + case ConfigureHotkeyType::NONE: + return nullptr; + case ConfigureHotkeyType::REPLAY_START_STOP: + return &config.replay_config.start_stop_hotkey; + case ConfigureHotkeyType::REPLAY_SAVE: + return &config.replay_config.save_hotkey; + case ConfigureHotkeyType::RECORD_START_STOP: + return &config.record_config.start_stop_hotkey; + case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE: + return &config.record_config.pause_unpause_hotkey; + case ConfigureHotkeyType::STREAM_START_STOP: + return &config.streaming_config.start_stop_hotkey; + case ConfigureHotkeyType::SHOW_HIDE: + return &config.main_config.show_hide_hotkey; + } + return nullptr; + } + + void GlobalSettingsPage::for_each_config_hotkey(std::function<void(ConfigHotkey *config_hotkey)> callback) { + ConfigHotkey *config_hotkeys[] = { + &config.replay_config.start_stop_hotkey, + &config.replay_config.save_hotkey, + &config.record_config.start_stop_hotkey, + &config.record_config.pause_unpause_hotkey, + &config.streaming_config.start_stop_hotkey, + &config.main_config.show_hide_hotkey + }; + for(ConfigHotkey *config_hotkey : config_hotkeys) { + callback(config_hotkey); + } + } + + void GlobalSettingsPage::configure_hotkey_start(ConfigureHotkeyType hotkey_type) { + assert(hotkey_type != ConfigureHotkeyType::NONE); + configure_config_hotkey = {0, 0}; + configure_hotkey_type = hotkey_type; + + content_page_ptr->set_visible(false); + hotkey_overlay_ptr->set_visible(true); + overlay->unbind_all_keyboard_hotkeys(); + } + + void GlobalSettingsPage::configure_hotkey_cancel() { + Button *config_hotkey_button = configure_hotkey_get_button_by_active_type(); + ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type(); + if(config_hotkey_button && config_hotkey) + config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey)); + + configure_config_hotkey = {0, 0}; + configure_hotkey_type = ConfigureHotkeyType::NONE; + content_page_ptr->set_visible(true); + hotkey_overlay_ptr->set_visible(false); + overlay->rebind_all_keyboard_hotkeys(); + } + + void GlobalSettingsPage::configure_hotkey_stop_and_save() { + Button *config_hotkey_button = configure_hotkey_get_button_by_active_type(); + ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type(); + if(config_hotkey_button && config_hotkey) { + bool hotkey_used_by_another_action = false; + for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) { + if(config_hotkey_item != config_hotkey && *config_hotkey_item == configure_config_hotkey) + hotkey_used_by_another_action = true; + }); + + if(hotkey_used_by_another_action) { + const std::string error_msg = "The hotkey \"" + config_hotkey_to_string(configure_config_hotkey) + " is already used for something else"; + overlay->show_notification(error_msg.c_str(), 3.0, mgl::Color(255, 0, 0, 255), mgl::Color(255, 0, 0, 255), NotificationType::NONE); + config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey)); + configure_config_hotkey = {0, 0}; + return; + } + + *config_hotkey = configure_config_hotkey; + } + + configure_config_hotkey = {0, 0}; + configure_hotkey_type = ConfigureHotkeyType::NONE; + content_page_ptr->set_visible(true); + hotkey_overlay_ptr->set_visible(false); + overlay->rebind_all_keyboard_hotkeys(); + } }
\ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index efb3583..17a73a5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,18 +1,14 @@ #include "../include/GsrInfo.hpp" #include "../include/Overlay.hpp" -#include "../include/GlobalHotkeysLinux.hpp" -#include "../include/GlobalHotkeysJoystick.hpp" #include "../include/gui/Utils.hpp" #include "../include/Process.hpp" #include "../include/Rpc.hpp" #include <unistd.h> #include <signal.h> -#include <thread> #include <string.h> #include <limits.h> -#include <X11/keysym.h> #include <mglpp/mglpp.hpp> #include <mglpp/system/Clock.hpp> @@ -41,57 +37,6 @@ static void disable_prime_run() { unsetenv("DRI_PRIME"); } -static std::unique_ptr<gsr::GlobalHotkeysLinux> register_linux_hotkeys(gsr::Overlay *overlay, gsr::GlobalHotkeysLinux::GrabType grab_type) { - auto global_hotkeys = std::make_unique<gsr::GlobalHotkeysLinux>(grab_type); - if(!global_hotkeys->start()) - fprintf(stderr, "error: failed to start global hotkeys\n"); - - global_hotkeys->bind_action("show_hide", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_show(); - }); - - global_hotkeys->bind_action("record", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_record(); - }); - - global_hotkeys->bind_action("pause", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_pause(); - }); - - global_hotkeys->bind_action("stream", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_stream(); - }); - - global_hotkeys->bind_action("replay_start", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->toggle_replay(); - }); - - global_hotkeys->bind_action("replay_save", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->save_replay(); - }); - - return global_hotkeys; -} - -static std::unique_ptr<gsr::GlobalHotkeysJoystick> register_joystick_hotkeys(gsr::Overlay *overlay) { - auto global_hotkeys_js = std::make_unique<gsr::GlobalHotkeysJoystick>(); - if(!global_hotkeys_js->start()) - fprintf(stderr, "Warning: failed to start joystick hotkeys\n"); - - global_hotkeys_js->bind_action("save_replay", [overlay](const std::string &id) { - fprintf(stderr, "pressed %s\n", id.c_str()); - overlay->save_replay(); - }); - - return global_hotkeys_js; -} - static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) { rpc->add_handler("show_ui", [overlay](const std::string &name) { fprintf(stderr, "rpc command executed: %s\n", name.c_str()); @@ -320,34 +265,6 @@ int main(int argc, char **argv) { rpc_add_commands(rpc.get(), overlay.get()); - std::unique_ptr<gsr::GlobalHotkeys> global_hotkeys = nullptr; - if(overlay->get_config().main_config.hotkeys_enable_option == "enable_hotkeys") - global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::ALL); - else if(overlay->get_config().main_config.hotkeys_enable_option == "enable_hotkeys_virtual_devices") - global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::VIRTUAL); - - overlay->on_keyboard_hotkey_changed = [&](const char *hotkey_option) { - global_hotkeys.reset(); - if(strcmp(hotkey_option, "enable_hotkeys") == 0) - global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::ALL); - else if(strcmp(hotkey_option, "enable_hotkeys_virtual_devices") == 0) - global_hotkeys = register_linux_hotkeys(overlay.get(), gsr::GlobalHotkeysLinux::GrabType::VIRTUAL); - else if(strcmp(hotkey_option, "disable_hotkeys") == 0) - global_hotkeys.reset(); - }; - - std::unique_ptr<gsr::GlobalHotkeysJoystick> global_hotkeys_js = nullptr; - if(overlay->get_config().main_config.joystick_hotkeys_enable_option == "enable_hotkeys") - global_hotkeys_js = register_joystick_hotkeys(overlay.get()); - - overlay->on_joystick_hotkey_changed = [&](const char *hotkey_option) { - global_hotkeys_js.reset(); - if(strcmp(hotkey_option, "enable_hotkeys") == 0) - global_hotkeys_js = register_joystick_hotkeys(overlay.get()); - else if(strcmp(hotkey_option, "disable_hotkeys") == 0) - global_hotkeys_js.reset(); - }; - // TODO: Add hotkeys in Overlay when using x11 global hotkeys. The hotkeys in Overlay should duplicate each key that is used for x11 global hotkeys. std::string exit_reason; @@ -358,24 +275,15 @@ int main(int argc, char **argv) { gsr::set_frame_delta_seconds(frame_delta_seconds); rpc->poll(); - - if(global_hotkeys) - global_hotkeys->poll_events(); - - if(global_hotkeys_js) - global_hotkeys_js->poll_events(); - - overlay->handle_events(global_hotkeys.get()); + overlay->handle_events(); if(!overlay->draw()) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + usleep(100 * 1000); // 100ms mgl_ping_display_server(); } } fprintf(stderr, "Info: shutting down!\n"); rpc.reset(); - global_hotkeys.reset(); - global_hotkeys_js.reset(); overlay.reset(); mgl_deinit(); |