From ff2c33e3a1659560a3e6d4c5e2c099b240d788ef Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 19 Jul 2024 21:25:57 +0200 Subject: Add support for wayland global hotkeys (global shortcuts desktop portal), only kde really supports this. Refactor x11 hotkeys (use list...), add separate key for start/stop recording/pause --- src/config.hpp | 8 + src/global_shortcuts.c | 334 ++++++++++++++++++++ src/global_shortcuts.h | 41 +++ src/main.cpp | 806 +++++++++++++++++++++++++++++-------------------- 4 files changed, 861 insertions(+), 328 deletions(-) create mode 100644 src/global_shortcuts.c create mode 100644 src/global_shortcuts.h (limited to 'src') diff --git a/src/config.hpp b/src/config.hpp index 1bea56c..86fb4de 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -59,13 +59,16 @@ struct StreamingConfig { TwitchStreamConfig twitch; CustomStreamConfig custom; ConfigHotkey start_recording_hotkey; + ConfigHotkey stop_recording_hotkey; }; struct RecordConfig { std::string save_directory; std::string container; ConfigHotkey start_recording_hotkey; + ConfigHotkey stop_recording_hotkey; ConfigHotkey pause_recording_hotkey; + ConfigHotkey unpause_recording_hotkey; }; struct ReplayConfig { @@ -73,6 +76,7 @@ struct ReplayConfig { std::string container; int32_t replay_time = 30; ConfigHotkey start_recording_hotkey; + ConfigHotkey stop_recording_hotkey; ConfigHotkey save_recording_hotkey; }; @@ -321,16 +325,20 @@ static std::map get_config_options(Config &config) { {"streaming.custom.url", {CONFIG_TYPE_STRING, &config.streaming_config.custom.url}}, {"streaming.custom.container", {CONFIG_TYPE_STRING, &config.streaming_config.custom.container}}, {"streaming.start_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.streaming_config.start_recording_hotkey}}, + {"streaming.stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.streaming_config.stop_recording_hotkey}}, {"record.save_directory", {CONFIG_TYPE_STRING, &config.record_config.save_directory}}, {"record.container", {CONFIG_TYPE_STRING, &config.record_config.container}}, {"record.start_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.start_recording_hotkey}}, + {"record.stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.stop_recording_hotkey}}, {"record.pause_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.pause_recording_hotkey}}, + {"record.unpause_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.unpause_recording_hotkey}}, {"replay.save_directory", {CONFIG_TYPE_STRING, &config.replay_config.save_directory}}, {"replay.container", {CONFIG_TYPE_STRING, &config.replay_config.container}}, {"replay.time", {CONFIG_TYPE_I32, &config.replay_config.replay_time}}, {"replay.start_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.replay_config.start_recording_hotkey}}, + {"replay.stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.replay_config.stop_recording_hotkey}}, {"replay.save_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.replay_config.save_recording_hotkey}} }; } diff --git a/src/global_shortcuts.c b/src/global_shortcuts.c new file mode 100644 index 0000000..7037eb8 --- /dev/null +++ b/src/global_shortcuts.c @@ -0,0 +1,334 @@ +#include "global_shortcuts.h" +#include +#include +#include +#include +#include + +/* TODO: Remove G_DBUS_CALL_FLAGS_NO_AUTO_START and G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START? also in gpu screen recorder equivalent */ +/* TODO: More error handling and clean up resources after done */ +/* TODO: Use GArray instead of GVariant where possible */ + +static bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) { + /* TODO: Use other functions on other platforms than linux */ + if(getrandom(buffer, buffer_size, 0) < buffer_size) { + fprintf(stderr, "gsr error: generate_random_characters: failed to get random bytes, error: %s\n", strerror(errno)); + return false; + } + + for(int i = 0; i < buffer_size; ++i) { + unsigned char c = *(unsigned char*)&buffer[i]; + buffer[i] = alphabet[c % alphabet_size]; + } + + return true; +} + +static void gsr_dbus_portal_get_unique_handle_token(gsr_global_shortcuts *self, char *buffer, int size) { + snprintf(buffer, size, "gpu_screen_recorder_gtk_handle_%s_%u", self->random_str, self->handle_counter++); +} + +/* Assumes shortcuts is an array */ +static void handle_shortcuts_data(GVariant *shortcuts, gsr_shortcut_callback callback, void *userdata) { + for(guint i = 0; i < g_variant_n_children(shortcuts); ++i) { + gchar *shortcut_id = NULL; + GVariant *shortcut_values = NULL; + g_variant_get_child(shortcuts, i, "(s@a{sv})", &shortcut_id, &shortcut_values); + + if(!shortcut_id || !shortcut_values) + continue; + + // gchar *description = NULL; + // g_variant_lookup(shortcut_values, "description", "s", &description); + + gchar *trigger_description = NULL; + g_variant_lookup(shortcut_values, "trigger_description", "s", &trigger_description); + + gsr_shortcut shortcut; + shortcut.id = shortcut_id; + shortcut.trigger_description = trigger_description ? trigger_description : ""; + callback(shortcut, userdata); + } +} + +typedef struct { + gsr_global_shortcuts *self; + gsr_shortcut_callback callback; + void *userdata; +} signal_list_bind_userdata; + +static void dbus_signal_list_bind(GDBusProxy *proxy, gchar *sender_name, gchar *signal_name, GVariant *parameters, signal_list_bind_userdata *userdata) { + (void)proxy; + (void)sender_name; + if(g_strcmp0(signal_name, "Response") != 0) + goto done; + + guint32 response = 0; + GVariant *results = NULL; + g_variant_get(parameters, "(u@a{sv})", &response, &results); + + if(response != 0 || !results) + goto done; + + GVariant *shortcuts = g_variant_lookup_value(results, "shortcuts", G_VARIANT_TYPE("a(sa{sv})")); + if(!shortcuts) + goto done; + + handle_shortcuts_data(shortcuts, userdata->callback, userdata->userdata); + + done: + free(userdata); +} + +typedef struct { + gsr_global_shortcuts *self; + gsr_deactivated_callback deactivated_callback; + gsr_shortcut_callback shortcut_changed_callback; + void *userdata; +} signal_userdata; + +static void signal_callback(GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer userdata) +{ + (void)connection; + (void)sender_name; + (void)object_path; + (void)interface_name; + (void)signal_name; + (void)parameters; + signal_userdata *cu = userdata; + + /* Button released */ + if(strcmp(signal_name, "Deactivated") == 0) { + gchar *session_handle = NULL; + gchar *shortcut_id = NULL; + gchar *timestamp = NULL; + GVariant *options = NULL; + g_variant_get(parameters, "(ost@a{sv})", &session_handle, &shortcut_id, ×tamp, &options); + + if(session_handle && shortcut_id && strcmp(session_handle, cu->self->session_handle) == 0) + cu->deactivated_callback(shortcut_id, cu->userdata); + } else if(strcmp(signal_name, "ShortcutsChanged") == 0) { + gchar *session_handle = NULL; + GVariant *shortcuts = NULL; + g_variant_get(parameters, "(o@a(sa{sv}))", &session_handle, &shortcuts); + + if(session_handle && shortcuts && strcmp(session_handle, cu->self->session_handle) == 0) + handle_shortcuts_data(shortcuts, cu->shortcut_changed_callback, cu->userdata); + } +} + +typedef struct { + gsr_global_shortcuts *self; + gsr_init_callback callback; + void *userdata; +} signal_create_session_userdata; + +static void dbus_signal_create_session(GDBusProxy *proxy, gchar *sender_name, gchar *signal_name, GVariant *parameters, signal_create_session_userdata *cu) { + (void)proxy; + (void)sender_name; + if(g_strcmp0(signal_name, "Response") != 0) + goto done; + + guint32 response = 0; + GVariant *results = NULL; + g_variant_get(parameters, "(u@a{sv})", &response, &results); + + if(response != 0 || !results) + goto done; + + gchar *session_handle = NULL; + if(g_variant_lookup(results, "session_handle", "s", &session_handle) && session_handle) { + cu->self->session_handle = strdup(session_handle); + cu->self->session_created = true; + cu->callback(cu->userdata); + } + + done: + free(cu); +} + +static bool gsr_global_shortcuts_create_session(gsr_global_shortcuts *self, gsr_init_callback callback, void *userdata) { + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + char session_handle_token[64]; + snprintf(session_handle_token, sizeof(session_handle_token), "gpu_screen_recorder_gtk"); + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(handle_token)); + g_variant_builder_add(&builder, "{sv}", "session_handle_token", g_variant_new_string(session_handle_token)); + GVariant *aa = g_variant_builder_end(&builder); + + GVariant *ret = g_dbus_connection_call_sync(self->gdbus_con, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "CreateSession", g_variant_new_tuple(&aa, 1), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, 1000, NULL, NULL); + + if(ret) { + const gchar *val = NULL; + g_variant_get(ret, "(&o)", &val); + if(!val) + return false; + //g_variant_unref(ret); + + GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, NULL, "org.freedesktop.portal.Desktop", val, "org.freedesktop.portal.Request", NULL, NULL); + if(!proxy) + return false; + //g_object_unref(proxy); + + signal_create_session_userdata *cu = malloc(sizeof(signal_create_session_userdata)); + cu->self = self; + cu->callback = callback; + cu->userdata = userdata; + g_signal_connect(proxy, "g-signal", G_CALLBACK(dbus_signal_create_session), cu); + return true; + } else { + return false; + } +} + +bool gsr_global_shortcuts_init(gsr_global_shortcuts *self, gsr_init_callback callback, void *userdata) { + memset(self, 0, sizeof(*self)); + + self->random_str[DBUS_RANDOM_STR_SIZE] = '\0'; + if(!generate_random_characters(self->random_str, DBUS_RANDOM_STR_SIZE, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 62)) { + fprintf(stderr, "gsr error: gsr_global_shortcuts_init: failed to generate random string\n"); + return false; + } + + self->gdbus_con = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); + if(!self->gdbus_con) { + fprintf(stderr, "gsr error: gsr_global_shortcuts_init: g_bus_get_sync failed\n"); + return false; + } + + if(!gsr_global_shortcuts_create_session(self, callback, userdata)) { + gsr_global_shortcuts_deinit(self); + return false; + } + + return true; +} + +void gsr_global_shortcuts_deinit(gsr_global_shortcuts *self) { + if(self->gdbus_con) { + /* TODO: Re-add this. Right now it causes errors as the connection is already closed, but checking if it's already closed here has no effect */ + //g_dbus_connection_close(self->gdbus_con, NULL, NULL, NULL); + self->gdbus_con = NULL; + } + + if(self->session_handle) { + free(self->session_handle); + self->session_handle = NULL; + } +} + +bool gsr_global_shortcuts_list_shortcuts(gsr_global_shortcuts *self, gsr_shortcut_callback callback, void *userdata) { + if(!self->session_created) + return false; + + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + GVariant *session_handle_obj = g_variant_new_object_path(self->session_handle); + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(handle_token)); + GVariant *aa = g_variant_builder_end(&builder); + + GVariant *args[2] = { session_handle_obj, aa }; + + GVariant *ret = g_dbus_connection_call_sync(self->gdbus_con, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "ListShortcuts", g_variant_new_tuple(args, 2), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, 1000, NULL, NULL); + if(ret) { + const gchar *val = NULL; + g_variant_get(ret, "(&o)", &val); + if(!val) + return false; + + GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, NULL, "org.freedesktop.portal.Desktop", val, "org.freedesktop.portal.Request", NULL, NULL); + if(!proxy) + return false; + //g_object_unref(proxy); + + signal_list_bind_userdata *cu = malloc(sizeof(signal_list_bind_userdata)); + cu->self = self; + cu->callback = callback; + cu->userdata = userdata; + g_signal_connect(proxy, "g-signal", G_CALLBACK(dbus_signal_list_bind), cu); + return true; + } else { + return false; + } +} + +bool gsr_global_shortcuts_bind_shortcuts(gsr_global_shortcuts *self, const gsr_bind_shortcut *shortcuts, int num_shortcuts, gsr_shortcut_callback callback, void *userdata) { + if(!self->session_created) + return false; + + char handle_token[64]; + gsr_dbus_portal_get_unique_handle_token(self, handle_token, sizeof(handle_token)); + + GVariant *session_handle_obj = g_variant_new_object_path(self->session_handle); + + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a(sa{sv})")); + + for(int i = 0; i < num_shortcuts; ++i) { + GVariantBuilder shortcuts_builder; + g_variant_builder_init(&shortcuts_builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&shortcuts_builder, "{sv}", "description", g_variant_new_string(shortcuts[i].description)); + g_variant_builder_add(&shortcuts_builder, "{sv}", "preferred_trigger", g_variant_new_string(shortcuts[i].shortcut.trigger_description)); + GVariant *shortcuts_data = g_variant_builder_end(&shortcuts_builder); + GVariant *ss_l[2] = { g_variant_new_string(shortcuts[i].shortcut.id), shortcuts_data }; + g_variant_builder_add_value(&builder, g_variant_new_tuple(ss_l, 2)); + } + GVariant *aa = g_variant_builder_end(&builder); + + GVariantBuilder builder_zzz; + g_variant_builder_init(&builder_zzz, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&builder_zzz, "{sv}", "handle_token", g_variant_new_string(handle_token)); + GVariant *bb = g_variant_builder_end(&builder_zzz); + + GVariant *parent_window = g_variant_new_string(""); + GVariant *args[4] = { session_handle_obj, aa, parent_window, bb }; + + GVariant *ret = g_dbus_connection_call_sync(self->gdbus_con, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "BindShortcuts", g_variant_new_tuple(args, 4), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, NULL); + if(ret) { + const gchar *val = NULL; + g_variant_get(ret, "(&o)", &val); + if(!val) + return false; + + GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, NULL, "org.freedesktop.portal.Desktop", val, "org.freedesktop.portal.Request", NULL, NULL); + if(!proxy) + return false; + //g_object_unref(proxy); + + signal_list_bind_userdata *cu = malloc(sizeof(signal_list_bind_userdata)); + cu->self = self; + cu->callback = callback; + cu->userdata = userdata; + g_signal_connect(proxy, "g-signal", G_CALLBACK(dbus_signal_list_bind), cu); + return true; + } else { + return false; + } +} + +bool gsr_global_shortcuts_subscribe_activated_signal(gsr_global_shortcuts *self, gsr_deactivated_callback deactivated_callback, gsr_shortcut_callback shortcut_changed_callback, void *userdata) { + if(!self->session_created) + return false; + + signal_userdata *cu = malloc(sizeof(signal_userdata)); + cu->self = self; + cu->deactivated_callback = deactivated_callback; + cu->shortcut_changed_callback = shortcut_changed_callback; + cu->userdata = userdata; + g_dbus_connection_signal_subscribe(self->gdbus_con, "org.freedesktop.portal.Desktop", "org.freedesktop.portal.GlobalShortcuts", NULL, "/org/freedesktop/portal/desktop", NULL, G_DBUS_SIGNAL_FLAGS_NONE, signal_callback, cu, free); + return true; +} diff --git a/src/global_shortcuts.h b/src/global_shortcuts.h new file mode 100644 index 0000000..f27a4de --- /dev/null +++ b/src/global_shortcuts.h @@ -0,0 +1,41 @@ +#ifndef GLOBAL_SHORTCUTS_H +#define GLOBAL_SHORTCUTS_H + +/* Global shortcuts via desktop portal */ + +#include +#include + +#define DBUS_RANDOM_STR_SIZE 16 + +typedef struct { + const char *id; + const char *trigger_description; +} gsr_shortcut; + +typedef struct { + const char *description; + gsr_shortcut shortcut; +} gsr_bind_shortcut; + +typedef void (*gsr_init_callback)(void *userdata); +typedef void (*gsr_shortcut_callback)(gsr_shortcut shortcut, void *userdata); +typedef void (*gsr_deactivated_callback)(const char *id, void *userdata); + +typedef struct { + GDBusConnection *gdbus_con; + char *session_handle; + bool session_created; + char random_str[DBUS_RANDOM_STR_SIZE + 1]; + unsigned int handle_counter; +} gsr_global_shortcuts; + +bool gsr_global_shortcuts_init(gsr_global_shortcuts *self, gsr_init_callback callback, void *userdata); +void gsr_global_shortcuts_deinit(gsr_global_shortcuts *self); + +bool gsr_global_shortcuts_list_shortcuts(gsr_global_shortcuts *self, gsr_shortcut_callback callback, void *userdata); +bool gsr_global_shortcuts_bind_shortcuts(gsr_global_shortcuts *self, const gsr_bind_shortcut *shortcuts, int num_shortcuts, gsr_shortcut_callback callback, void *userdata); + +bool gsr_global_shortcuts_subscribe_activated_signal(gsr_global_shortcuts *self, gsr_deactivated_callback deactivated_callback, gsr_shortcut_callback shortcut_changed_callback, void *userdata); + +#endif /* GLOBAL_SHORTCUTS_H */ diff --git a/src/main.cpp b/src/main.cpp index ee9fbe2..829320e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,7 @@ #include "config.hpp" +extern "C" { +#include "global_shortcuts.h" +} #include #include #include @@ -17,6 +20,13 @@ #include #include +// Start/stop recording also means start/stop streaming and start/stop replay +#define SHORTCUT_ID_START_RECORDING "gpu_screen_recorder_start_recording" +#define SHORTCUT_ID_STOP_RECORDING "gpu_screen_recorder_stop_recording" +#define SHORTCUT_ID_PAUSE_RECORDING "gpu_screen_recorder_pause_recording" +#define SHORTCUT_ID_UNPAUSE_RECORDING "gpu_screen_recorder_unpause_recording" +#define SHORTCUT_ID_SAVE_REPLAY "gpu_screen_recorder_save_replay" + typedef struct { Display *display; GtkApplication *app; @@ -76,11 +86,15 @@ static GtkSpinButton *replay_time_entry; static GtkButton *select_window_button; static GtkWidget *audio_input_used_list; static GtkWidget *add_audio_input_button; -static GtkWidget *record_hotkey_button; -static GtkWidget *pause_unpause_hotkey_button; -static GtkWidget *replay_start_stop_hotkey_button; +static GtkWidget *record_start_hotkey_button; +static GtkWidget *record_stop_hotkey_button; +static GtkWidget *pause_hotkey_button; +static GtkWidget *unpause_hotkey_button; +static GtkWidget *replay_start_hotkey_button; +static GtkWidget *replay_stop_hotkey_button; static GtkWidget *replay_save_hotkey_button; -static GtkWidget *streaming_hotkey_button; +static GtkWidget *streaming_start_hotkey_button; +static GtkWidget *streaming_stop_hotkey_button; static GtkWidget *merge_audio_tracks_button; static GtkWidget *show_notification_button; static GtkWidget *record_cursor_button; @@ -135,6 +149,9 @@ static double notification_start_seconds = 0.0; static AppIndicator *app_indicator; +static gsr_global_shortcuts global_shortcuts; +static bool global_shortcuts_initialized = false; + struct AudioInput { std::string name; std::string description; @@ -156,22 +173,48 @@ enum class HotkeyMode { static HotkeyMode hotkey_mode = HotkeyMode::NoAction; +typedef gboolean (*hotkey_trigger_handler)(GtkButton *button, gpointer userdata); struct Hotkey { uint32_t modkey_mask = 0; KeySym keysym = None; GtkWidget *hotkey_entry = nullptr; GtkWidget *hotkey_active_label = nullptr; + ConfigHotkey *config = nullptr; + bool grab_success = false; + GtkWidget *page = nullptr; + bool *state = nullptr; + bool expected_state_to_trigger = false; + hotkey_trigger_handler trigger_handler = nullptr; + GtkButton *associated_button = nullptr; + const char *shortcut_id = nullptr; }; static Hotkey *current_hotkey = nullptr; static Hotkey pressed_hotkey; static Hotkey latest_hotkey; -static Hotkey streaming_hotkey; -static Hotkey record_hotkey; -static Hotkey pause_unpause_hotkey; -static Hotkey replay_start_stop_hotkey; +static Hotkey streaming_start_hotkey; +static Hotkey streaming_stop_hotkey; +static Hotkey record_start_hotkey; +static Hotkey record_stop_hotkey; +static Hotkey pause_hotkey; +static Hotkey unpause_hotkey; +static Hotkey replay_start_hotkey; +static Hotkey replay_stop_hotkey; static Hotkey replay_save_hotkey; +static Hotkey *hotkeys[] = { + &streaming_start_hotkey, + &streaming_stop_hotkey, + &record_start_hotkey, + &record_stop_hotkey, + &pause_hotkey, + &unpause_hotkey, + &replay_start_hotkey, + &replay_stop_hotkey, + &replay_save_hotkey +}; +static int num_hotkeys = 9; + struct SupportedVideoCodecs { bool h264 = false; bool hevc = false; @@ -645,8 +688,7 @@ static bool is_video_capture_option_enabled(const char *str) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) enabled = strcmp(str, "window") != 0 && strcmp(str, "focused") != 0; - const bool is_portal = strcmp(str, "portal") == 0; - if(is_portal && !gsr_info.supported_capture_options.portal) + if(strcmp(str, "portal") == 0 && !gsr_info.supported_capture_options.portal) enabled = false; return enabled; @@ -906,30 +948,19 @@ static void save_configs() { config.streaming_config.twitch.stream_key = gtk_entry_get_text(twitch_stream_id_entry); config.streaming_config.custom.url = gtk_entry_get_text(custom_stream_url_entry); config.streaming_config.custom.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(custom_stream_container)); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { - config.streaming_config.start_recording_hotkey.keysym = streaming_hotkey.keysym; - config.streaming_config.start_recording_hotkey.modifiers = streaming_hotkey.modkey_mask; - } config.record_config.save_directory = gtk_button_get_label(record_file_chooser_button); config.record_config.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(record_container)); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { - config.record_config.start_recording_hotkey.keysym = record_hotkey.keysym; - config.record_config.start_recording_hotkey.modifiers = record_hotkey.modkey_mask; - - config.record_config.pause_recording_hotkey.keysym = pause_unpause_hotkey.keysym; - config.record_config.pause_recording_hotkey.modifiers = pause_unpause_hotkey.modkey_mask; - } config.replay_config.save_directory = gtk_button_get_label(replay_file_chooser_button); config.replay_config.container = gtk_combo_box_get_active_id(GTK_COMBO_BOX(replay_container)); config.replay_config.replay_time = gtk_spin_button_get_value_as_int(replay_time_entry); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { - config.replay_config.start_recording_hotkey.keysym = replay_start_stop_hotkey.keysym; - config.replay_config.start_recording_hotkey.modifiers = replay_start_stop_hotkey.modkey_mask; - config.replay_config.save_recording_hotkey.keysym = replay_save_hotkey.keysym; - config.replay_config.save_recording_hotkey.modifiers = replay_save_hotkey.modkey_mask; + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { + for(int i = 0; i < num_hotkeys; ++i) { + hotkeys[i]->config->keysym = hotkeys[i]->keysym; + hotkeys[i]->config->modifiers = hotkeys[i]->modkey_mask; + } } save_config(config); @@ -1221,8 +1252,10 @@ static bool grab_ungrab_hotkey_combo(Display *display, Hotkey hotkey, bool grab) if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return true; - if(hotkey.keysym == None && hotkey.modkey_mask == 0) + if(hotkey.keysym == None && hotkey.modkey_mask == 0) { + fprintf(stderr, "hotkey is empty\n"); return true; + } unsigned int numlockmask = 0; KeyCode numlock_keycode = XKeysymToKeycode(display, XK_Num_Lock); @@ -1279,11 +1312,9 @@ static void ungrab_keys(Display *display) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return; - grab_ungrab_hotkey_combo(display, streaming_hotkey, false); - grab_ungrab_hotkey_combo(display, record_hotkey, false); - grab_ungrab_hotkey_combo(display, pause_unpause_hotkey, false); - grab_ungrab_hotkey_combo(display, replay_start_stop_hotkey, false); - grab_ungrab_hotkey_combo(display, replay_save_hotkey, false); + for(int i = 0; i < num_hotkeys; ++i) { + grab_ungrab_hotkey_combo(display, *hotkeys[i], false); + } } static void set_hotkey_text_from_hotkey_data(GtkEntry *entry, Hotkey hotkey) { @@ -1326,38 +1357,33 @@ static void set_hotkey_text_from_hotkey_data(GtkEntry *entry, Hotkey hotkey) { gtk_entry_set_text(entry, hotkey_combo_str.c_str()); } -struct HotkeyResult { - bool record_hotkey_success = false; - bool pause_unpause_hotkey_success = false; - bool streaming_hotkey_success = false; - bool replay_start_stop_hotkey_success = false; - bool replay_save_hotkey_success = false; -}; +static bool replace_grabbed_keys_depending_on_active_page() { + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) + return true; + + for(int i = 0; i < num_hotkeys; ++i) { + hotkeys[i]->grab_success = false; + } -static HotkeyResult replace_grabbed_keys_depending_on_active_page() { - HotkeyResult hotkey_result; ungrab_keys(gdk_x11_get_default_xdisplay()); const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata.stack); - if(visible_page == page_navigation_userdata.recording_page) { - bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), record_hotkey, true); - bool grab_pause_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), pause_unpause_hotkey, true); - hotkey_mode = HotkeyMode::Record; - - hotkey_result.record_hotkey_success = grab_record_success; - hotkey_result.pause_unpause_hotkey_success = grab_pause_success; - } else if(visible_page == page_navigation_userdata.streaming_page) { - bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), streaming_hotkey, true); - hotkey_mode = HotkeyMode::Record; - hotkey_result.streaming_hotkey_success = grab_record_success; - } else if(visible_page == page_navigation_userdata.replay_page) { - bool grab_record_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), replay_start_stop_hotkey, true); - bool grab_save_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), replay_save_hotkey, true); - hotkey_mode = HotkeyMode::Record; - - hotkey_result.replay_start_stop_hotkey_success = grab_record_success; - hotkey_result.replay_save_hotkey_success = grab_save_success; + + bool keys_successfully_grabbed = true; + for(int i = 0; i < num_hotkeys; ++i) { + if(visible_page == hotkeys[i]->page) { + hotkey_mode = HotkeyMode::Record; + hotkeys[i]->grab_success = grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), *hotkeys[i], true); + if(hotkeys[i]->grab_success) { + set_hotkey_text_from_hotkey_data(GTK_ENTRY(hotkeys[i]->hotkey_entry), *hotkeys[i]); + } else { + gtk_entry_set_text(GTK_ENTRY(hotkeys[i]->hotkey_entry), ""); + hotkeys[i]->keysym = 0; + hotkeys[i]->modkey_mask = 0; + keys_successfully_grabbed = false; + } + } } - return hotkey_result; + return keys_successfully_grabbed; } static bool is_monitor_capture_drm() { @@ -1404,19 +1430,8 @@ static gboolean on_start_replay_click(GtkButton*, gpointer userdata) { gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->replay_page); app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(page_navigation_userdata->app, SystrayPage::REPLAY))); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { - HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); - if(!hotkey_result.replay_start_stop_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(replay_start_stop_hotkey.hotkey_entry), ""); - replay_start_stop_hotkey.keysym = 0; - replay_start_stop_hotkey.modkey_mask = 0; - } - if(!hotkey_result.replay_save_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey.hotkey_entry), ""); - replay_save_hotkey.keysym = 0; - replay_save_hotkey.modkey_mask = 0; - } - } + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) + replace_grabbed_keys_depending_on_active_page(); return true; } @@ -1429,19 +1444,8 @@ static gboolean on_start_recording_click(GtkButton*, gpointer userdata) { gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->recording_page); app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(page_navigation_userdata->app, SystrayPage::RECORDING))); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { - HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); - if(!hotkey_result.record_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(record_hotkey.hotkey_entry), ""); - record_hotkey.keysym = 0; - record_hotkey.modkey_mask = 0; - } - if(!hotkey_result.pause_unpause_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(pause_unpause_hotkey.hotkey_entry), ""); - pause_unpause_hotkey.keysym = 0; - pause_unpause_hotkey.modkey_mask = 0; - } - } + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) + replace_grabbed_keys_depending_on_active_page(); return true; } @@ -1473,14 +1477,8 @@ static gboolean on_start_streaming_click(GtkButton*, gpointer userdata) { gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->streaming_page); app_indicator_set_menu(app_indicator, GTK_MENU(create_systray_menu(page_navigation_userdata->app, SystrayPage::STREAMING))); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { - HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); - if(!hotkey_result.streaming_hotkey_success) { - gtk_entry_set_text(GTK_ENTRY(streaming_hotkey.hotkey_entry), ""); - streaming_hotkey.keysym = 0; - streaming_hotkey.modkey_mask = 0; - } - } + if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) + replace_grabbed_keys_depending_on_active_page(); return true; } @@ -1774,6 +1772,9 @@ static gboolean on_replay_save_button_click(GtkButton*, gpointer userdata) { } static gboolean on_pause_unpause_button_click(GtkButton*, gpointer) { + if(!recording) + return true; + if(gpu_screen_recorder_process == -1) return true; @@ -2215,16 +2216,12 @@ static bool is_cuda_installed() { return lib != nullptr; } -typedef gboolean (*KeyPressHandler)(GtkButton *button, gpointer userdata); -static void keypress_toggle_recording(bool recording_state, GtkButton *record_button, KeyPressHandler keypress_handler, GtkApplication *app) { - if(!gtk_widget_get_sensitive(GTK_WIDGET(record_button))) - return; - - if(!recording_state) { - keypress_handler(record_button, app); - } else if(recording_state) { - keypress_handler(record_button, app); +static bool is_hotkey_already_bound_to_another_action_on_same_page(const Hotkey *current_hotkey, const Hotkey new_hotkey, GtkWidget *page) { + for(int i = 0; i < num_hotkeys; ++i) { + if(hotkeys[i] != current_hotkey && hotkeys[i]->page == page && hotkeys[i]->keysym == new_hotkey.keysym && hotkeys[i]->modkey_mask == new_hotkey.modkey_mask) + return true; } + return false; } static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpointer userdata) { @@ -2244,22 +2241,12 @@ static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpoi if(hotkey_mode == HotkeyMode::Record && ev->type == KeyRelease) { const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata->stack); - if(visible_page == page_navigation_userdata->recording_page) { - if(key_sym == record_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(record_hotkey.modkey_mask)) { - keypress_toggle_recording(recording, start_recording_button, on_start_recording_button_click, page_navigation_userdata->app); - } else if(key_sym == pause_unpause_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(pause_unpause_hotkey.modkey_mask) && recording) { - on_pause_unpause_button_click(nullptr, page_navigation_userdata->app); - } - } else if(visible_page == page_navigation_userdata->streaming_page) { - if(key_sym == streaming_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(streaming_hotkey.modkey_mask)) { - keypress_toggle_recording(streaming, start_streaming_button, on_start_streaming_button_click, page_navigation_userdata->app); - } - } else if(visible_page == page_navigation_userdata->replay_page) { - if(key_sym == replay_start_stop_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(replay_start_stop_hotkey.modkey_mask)) { - keypress_toggle_recording(replaying, start_replay_button, on_start_replay_button_click, page_navigation_userdata->app); - } else if(key_sym == replay_save_hotkey.keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(replay_save_hotkey.modkey_mask) && replaying && gpu_screen_recorder_process != -1) { - on_replay_save_button_click(nullptr, page_navigation_userdata->app); - } + for(int i = 0; i < num_hotkeys; ++i) { + if(visible_page != hotkeys[i]->page) + continue; + + if(key_sym == hotkeys[i]->keysym && key_state_without_locks(ev->xkey.state) == key_mod_mask_to_x11_mask(hotkeys[i]->modkey_mask) && *hotkeys[i]->state == hotkeys[i]->expected_state_to_trigger) + hotkeys[i]->trigger_handler(hotkeys[i]->associated_button, page_navigation_userdata->app); } return GDK_FILTER_CONTINUE; } @@ -2279,6 +2266,7 @@ static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpoi if(ev->type == KeyPress && key_sym == XK_BackSpace) { if(pressed_hotkey.modkey_mask == None && pressed_hotkey.keysym == None) { ungrab_keyboard(display); + grab_ungrab_hotkey_combo(gdk_x11_get_default_xdisplay(), *current_hotkey, false); gtk_entry_set_text(GTK_ENTRY(current_hotkey->hotkey_entry), ""); current_hotkey->keysym = None; current_hotkey->modkey_mask = 0; @@ -2319,57 +2307,43 @@ static GdkFilterReturn hotkey_filter_callback(GdkXEvent *xevent, GdkEvent*, gpoi if(pressed_hotkey.modkey_mask == None && pressed_hotkey.keysym == None) { ungrab_keyboard(display); - ungrab_keys(gdk_x11_get_default_xdisplay()); - - bool hotkey_already_used_by_another_hotkey = false; - if(current_hotkey == &replay_start_stop_hotkey) - hotkey_already_used_by_another_hotkey = (latest_hotkey.keysym == replay_save_hotkey.keysym && latest_hotkey.modkey_mask == replay_save_hotkey.modkey_mask); - else if(current_hotkey == &replay_save_hotkey) - hotkey_already_used_by_another_hotkey = (latest_hotkey.keysym == replay_start_stop_hotkey.keysym && latest_hotkey.modkey_mask == replay_start_stop_hotkey.modkey_mask); - if(hotkey_already_used_by_another_hotkey) { - std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); - std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used for something else. Please choose another hotkey."; + if(is_hotkey_already_bound_to_another_action_on_same_page(current_hotkey, latest_hotkey, gtk_stack_get_visible_child(page_navigation_userdata->stack))) { + const std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); + const std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used for something else. Please choose another hotkey."; set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), *current_hotkey); GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); current_hotkey = nullptr; + hotkey_mode = HotkeyMode::Record; return GDK_FILTER_CONTINUE; } - Hotkey prev_current_hotkey = *current_hotkey; + const Hotkey prev_current_hotkey = *current_hotkey; current_hotkey->keysym = latest_hotkey.keysym; current_hotkey->modkey_mask = latest_hotkey.modkey_mask; - HotkeyResult hotkey_result = replace_grabbed_keys_depending_on_active_page(); - bool hotkey_success = false; - if(current_hotkey == &record_hotkey) - hotkey_success = hotkey_result.record_hotkey_success; - else if(current_hotkey == &pause_unpause_hotkey) - hotkey_success = hotkey_result.pause_unpause_hotkey_success; - else if(current_hotkey == &streaming_hotkey) - hotkey_success = hotkey_result.streaming_hotkey_success; - else if(current_hotkey == &replay_start_stop_hotkey) - hotkey_success = hotkey_result.replay_start_stop_hotkey_success; - else if(current_hotkey == &replay_save_hotkey) - hotkey_success = hotkey_result.replay_save_hotkey_success; - - if(hotkey_success) { + if(replace_grabbed_keys_depending_on_active_page()) { save_configs(); + current_hotkey = nullptr; + hotkey_mode = HotkeyMode::Record; + return GDK_FILTER_CONTINUE; } else { - *current_hotkey = prev_current_hotkey; - std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); - std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey."; + const std::string hotkey_text = gtk_entry_get_text(GTK_ENTRY(current_hotkey->hotkey_entry)); + const std::string error_text = "Hotkey " + hotkey_text + " can't be used because it's used by another program. Please choose another hotkey."; + + current_hotkey->keysym = prev_current_hotkey.keysym; + current_hotkey->modkey_mask = prev_current_hotkey.modkey_mask; set_hotkey_text_from_hotkey_data(GTK_ENTRY(current_hotkey->hotkey_entry), *current_hotkey); + GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", error_text.c_str()); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); - } - current_hotkey = nullptr; - return GDK_FILTER_CONTINUE; + return GDK_FILTER_CONTINUE; + } } } @@ -2380,32 +2354,27 @@ static gboolean on_hotkey_entry_click(GtkWidget *button, gpointer) { if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) return true; - hotkey_mode = HotkeyMode::NewHotkey; - pressed_hotkey.hotkey_entry = nullptr; pressed_hotkey.hotkey_active_label = nullptr; pressed_hotkey.keysym = None; pressed_hotkey.modkey_mask = 0; latest_hotkey = pressed_hotkey; - if(button == record_hotkey_button) { - current_hotkey = &record_hotkey; - } else if(button == pause_unpause_hotkey_button) { - current_hotkey = &pause_unpause_hotkey; - } else if(button == streaming_hotkey_button) { - current_hotkey = &streaming_hotkey; - } else if(button == replay_start_stop_hotkey_button) { - current_hotkey = &replay_start_stop_hotkey; - } else if(button == replay_save_hotkey_button) { - current_hotkey = &replay_save_hotkey; - } else { - current_hotkey = nullptr; + current_hotkey = nullptr; + for(int i = 0; i < num_hotkeys; ++i) { + if(button == hotkeys[i]->hotkey_entry) { + current_hotkey = hotkeys[i]; + break; + } } - if(current_hotkey) { - gtk_grab_add(current_hotkey->hotkey_entry); - gtk_widget_set_visible(current_hotkey->hotkey_active_label, true); - } + if(!current_hotkey) + return true; + + hotkey_mode = HotkeyMode::NewHotkey; + + gtk_grab_add(current_hotkey->hotkey_entry); + gtk_widget_set_visible(current_hotkey->hotkey_active_label, true); Display *display = gdk_x11_get_default_xdisplay(); XErrorHandler prev_error_handler = XSetErrorHandler(xerror_dummy); @@ -2642,11 +2611,11 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, "Window (Unavailable on Wayland)", -1); + gtk_list_store_set(store, &iter, 0, "Window (Not available on Wayland)", -1); gtk_list_store_set(store, &iter, 1, "window", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, "Follow focused window (Unavailable on Wayland)", -1); + gtk_list_store_set(store, &iter, 0, "Follow focused window (Not available on Wayland)", -1); gtk_list_store_set(store, &iter, 1, "focused", -1); } else { gtk_list_store_append(store, &iter); @@ -2698,9 +2667,15 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a } } - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, gsr_info.supported_capture_options.portal ? "Desktop portal (Experimental)" : "Desktop portal (Not supported on your system)", -1); - gtk_list_store_set(store, &iter, 1, "portal", -1); + if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_capture_options.portal ? "Desktop portal (Experimental)" : "Desktop portal (Not available on your system)", -1); + gtk_list_store_set(store, &iter, 1, "portal", -1); + } else { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, "Desktop portal (Not available on X11)", -1); + gtk_list_store_set(store, &iter, 1, "portal", -1); + } record_area_selection_menu = GTK_COMBO_BOX(gtk_combo_box_new_with_model(record_area_selection_model)); @@ -2823,32 +2798,32 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_list_store_set(store, &iter, 1, "auto", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.h264 ? "H264" : "H264 (Not supported on your system)", -1); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.h264 ? "H264" : "H264 (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "h264", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC" : "HEVC (Not supported on your system)", -1); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC" : "HEVC (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "hevc", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1" : "AV1 (Not supported on your system)", -1); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1" : "AV1 (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "av1", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.vp8 ? "VP8" : "VP8 (Not supported on your system)", -1); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.vp8 ? "VP8" : "VP8 (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "vp8", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.vp9 ? "VP9" : "VP9 (Not supported on your system)", -1); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.vp9 ? "VP9" : "VP9 (Not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "vp9", -1); if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC (HDR)" : "HEVC (HDR, not supported on your system)", -1); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.hevc ? "HEVC (HDR)" : "HEVC (HDR, not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "hevc_hdr", -1); gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1 (HDR)" : "AV1 (HDR, not supported on your system)", -1); + gtk_list_store_set(store, &iter, 0, gsr_info.supported_video_codecs.av1 ? "AV1 (HDR)" : "AV1 (HDR, not available on your system)", -1); gtk_list_store_set(store, &iter, 1, "av1_hdr", -1); } else { gtk_list_store_append(store, &iter); @@ -2970,39 +2945,107 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a return GTK_WIDGET(grid); } -static void add_wayland_global_hotkeys_ui(GtkGrid *grid, int &row, int width) { - GtkWidget *label = gtk_label_new("Hotkeys not supported because Wayland doesn't support global hotkeys"); - gtk_widget_set_hexpand(label, true); - gtk_grid_attach(grid, label, 0, row, width - 1, 1); +static void replace_meta_with_super(std::string &str) { + size_t index = str.find("meta"); + if(index != std::string::npos) + str.replace(index, 4, "Super"); - GtkButton *question_button = GTK_BUTTON(gtk_button_new_with_label("?")); - gtk_grid_attach(grid, GTK_WIDGET(question_button), width - 1, row, 1, 1); + index = str.find("Meta"); + if(index != std::string::npos) + str.replace(index, 4, "Super"); +} - row++; +static void shortcut_changed_callback(gsr_shortcut shortcut, void *userdata) { + (void)userdata; + std::string trigger = shortcut.trigger_description; + replace_meta_with_super(trigger); + fprintf(stderr, "shortcut: %s, %s\n", shortcut.id, trigger.c_str()); + for(int i = 0; i < num_hotkeys; ++i) { + if(strcmp(shortcut.id, hotkeys[i]->shortcut_id) == 0) { + gtk_entry_set_text(GTK_ENTRY(hotkeys[i]->hotkey_entry), trigger.c_str()); + } + } +} - g_signal_connect(question_button, "clicked", G_CALLBACK(+[](GtkButton *button, gpointer userdata){ - (void)button; - (void)userdata; - GtkWidget *dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, - "Wayland (desktop portal) doesn't support global hotkeys in any meaningful manner to most applications, including GPU Screen Recorder.\n" - "If you want to use global hotkeys in GPU Screen Recorder then either use X11 or bind the following commands in your Wayland desktop environment hotkey settings to keys:\n" - "Stop recording (saves video as well when not in replay mode):\n" - " killall -SIGINT gpu-screen-recorder\n" - "Save a replay:\n" - " killall -SIGUSR1 gpu-screen-recorder\n" - "Pause/unpause recording:\n" - " killall -SIGUSR2 gpu-screen-recorder\n"); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); +static void deactivated_callback(const char *description, void *userdata) { + (void)userdata; + fprintf(stderr, "deactivated callback: %s, recording: %s\n", description, recording ? "yes" : "no"); + const GtkWidget *visible_page = gtk_stack_get_visible_child(page_navigation_userdata.stack); + for(int i = 0; i < num_hotkeys; ++i) { + if(visible_page != hotkeys[i]->page) + continue; - return true; - }), nullptr); + if(strcmp(description, hotkeys[i]->shortcut_id) == 0 && *hotkeys[i]->state == hotkeys[i]->expected_state_to_trigger) + hotkeys[i]->trigger_handler(hotkeys[i]->associated_button, page_navigation_userdata.app); + } +} + +static gboolean on_register_hotkeys_button_clicked(GtkButton *button, gpointer userdata) { + (void)button; + (void)userdata; - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, width, 1); + /* + Modifier key names are defined here: https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-names.h. + Remove the XKB_MOD_NAME_ prefix from the name and use the remaining part. + Key names are defined here: https://github.com/xkbcommon/libxkbcommon/blob/master/include/xkbcommon/xkbcommon-keysyms.h. + Remove the XKB_KEY_ (or XKB_KEY_KP_) prefix from the name and user the remaining part. + */ + /* LOGO = Super key */ + /* Unfortunately global shortcuts cant handle same key for different shortcuts, even though GPU Screen Recorder has page specific hotkeys */ + const gsr_bind_shortcut shortcuts[5] = { + { + "Start recording/replay/streaming", + { SHORTCUT_ID_START_RECORDING, "LOGO+f1" } + }, + { + "Stop recording/replaying/streaming", + { SHORTCUT_ID_STOP_RECORDING, "LOGO+f2" } + }, + { + "Pause recording", + { SHORTCUT_ID_PAUSE_RECORDING, "LOGO+f5" } + }, + { + "Unpause recording", + { SHORTCUT_ID_UNPAUSE_RECORDING, "LOGO+f6" } + }, + { + "Save replay", + { SHORTCUT_ID_SAVE_REPLAY, "LOGO+f9" } + } + }; + + if(global_shortcuts_initialized) { + if(!gsr_global_shortcuts_bind_shortcuts(&global_shortcuts, shortcuts, 5, shortcut_changed_callback, NULL)) { + fprintf(stderr, "gsr error: failed to bind shortcuts\n"); + } + } + + return true; +} + +static void add_wayland_global_hotkeys_ui(GtkGrid *grid, int &row, int width) { + GtkGrid *aa_grid = GTK_GRID(gtk_grid_new()); + gtk_widget_set_halign(GTK_WIDGET(aa_grid), GTK_ALIGN_CENTER); + gtk_grid_attach(grid, GTK_WIDGET(aa_grid), 0, row++, width, 1); + gtk_grid_set_column_spacing(aa_grid, 10); + + gtk_grid_attach(aa_grid, gtk_label_new("On Wayland hotkeys are managed externally by the Wayland compositor, click here to register hotkeys:"), 0, 0, 1, 1); + + GtkButton *register_hotkeys_button = GTK_BUTTON(gtk_button_new_with_label("Register hotkeys")); + gtk_widget_set_hexpand(GTK_WIDGET(register_hotkeys_button), true); + //gtk_widget_set_halign(GTK_WIDGET(register_hotkeys_button), GTK_ALIGN_START); + g_signal_connect(register_hotkeys_button, "clicked", G_CALLBACK(on_register_hotkeys_button_clicked), nullptr); + gtk_grid_attach(aa_grid, GTK_WIDGET(register_hotkeys_button), 1, 0, 1, 1); + + row++; + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, width, 1); + row++; } static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { int row = 0; + const int num_columns = 7; std::string video_filepath = get_videos_dir(); @@ -3016,52 +3059,45 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { GtkWidget *hotkey_active_label = NULL; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { - add_wayland_global_hotkeys_ui(grid, row, 5); + add_wayland_global_hotkeys_ui(grid, row, num_columns); } else { hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel"); - gtk_grid_attach(grid, hotkey_active_label, 0, row++, 5, 1); + gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); + } + + { + gtk_grid_attach(grid, gtk_label_new("Press"), 0, row, 1, 1); - GtkWidget *a = gtk_label_new("Press"); - gtk_widget_set_halign(a, GTK_ALIGN_END); + replay_start_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(replay_start_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F1"); + g_signal_connect(replay_start_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_start_hotkey_button); + gtk_grid_attach(grid, replay_start_hotkey_button, 1, row, 1, 1); - GtkWidget *b = gtk_label_new("to start/stop the replay and"); - gtk_widget_set_halign(b, GTK_ALIGN_START); + gtk_grid_attach(grid, gtk_label_new("to start the replay and"), 2, row, 1, 1); - GtkWidget *c = gtk_label_new("to save"); - gtk_widget_set_halign(c, GTK_ALIGN_START); + replay_stop_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(replay_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F2"); + g_signal_connect(replay_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_stop_hotkey_button); + gtk_grid_attach(grid, replay_stop_hotkey_button, 3, row, 1, 1); - replay_start_stop_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(replay_start_stop_hotkey_button), "Alt + F1"); - g_signal_connect(replay_start_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_start_stop_hotkey_button); + gtk_grid_attach(grid, gtk_label_new("to stop the replay and"), 4, row, 1, 1); replay_save_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey_button), "Alt + F2"); - gtk_widget_set_halign(replay_save_hotkey_button, GTK_ALIGN_START); + gtk_entry_set_text(GTK_ENTRY(replay_save_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F2"); g_signal_connect(replay_save_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), replay_save_hotkey_button); + gtk_grid_attach(grid, replay_save_hotkey_button, 5, row, 1, 1); + + gtk_grid_attach(grid, gtk_label_new("to save the replay"), 6, row, 1, 1); - gtk_grid_attach(grid, a, 0, row, 1, 1); - gtk_grid_attach(grid, replay_start_stop_hotkey_button, 1, row, 1, 1); - gtk_grid_attach(grid, b, 2, row, 1, 1); - gtk_grid_attach(grid, replay_save_hotkey_button, 3, row, 1, 1); - gtk_grid_attach(grid, c, 4, row, 1, 1); ++row; - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 5, 1); } - replay_start_stop_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - replay_start_stop_hotkey.keysym = XK_F1; - replay_start_stop_hotkey.hotkey_entry = replay_start_stop_hotkey_button; - replay_start_stop_hotkey.hotkey_active_label = hotkey_active_label; - - replay_save_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - replay_save_hotkey.keysym = XK_F2; - replay_save_hotkey.hotkey_entry = replay_save_hotkey_button; - replay_save_hotkey.hotkey_active_label = hotkey_active_label; + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); GtkWidget *save_icon = gtk_image_new_from_icon_name("document-save", GTK_ICON_SIZE_BUTTON); GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(file_chooser_grid, 10); GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the replays?"); gtk_grid_attach(file_chooser_grid, file_chooser_label, 0, 0, 1, 1); @@ -3074,7 +3110,7 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(file_chooser_grid, GTK_WIDGET(replay_file_chooser_button), 1, 0, 1, 1); GtkGrid *container_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, row++, num_columns, 1); gtk_grid_attach(container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); replay_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); for(auto &supported_container : supported_containers) { @@ -3085,10 +3121,10 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { } gtk_widget_set_hexpand(GTK_WIDGET(replay_container), true); gtk_grid_attach(container_grid, GTK_WIDGET(replay_container), 1, 0, 1, 1); - gtk_combo_box_set_active(GTK_COMBO_BOX(replay_container), 0); // TODO: + gtk_combo_box_set_active(GTK_COMBO_BOX(replay_container), 0); GtkGrid *replay_time_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(replay_time_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(replay_time_grid), 0, row++, num_columns, 1); gtk_grid_attach(replay_time_grid, gtk_label_new("Replay time in seconds: "), 0, 0, 1, 1); replay_time_entry = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(5.0, 1200.0, 1.0)); gtk_spin_button_set_value(replay_time_entry, 30.0); @@ -3096,7 +3132,7 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(replay_time_grid, GTK_WIDGET(replay_time_entry), 1, 0, 1, 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); replay_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); @@ -3118,10 +3154,10 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_sensitive(GTK_WIDGET(replay_save_button), false); gtk_grid_attach(start_button_grid, GTK_WIDGET(replay_save_button), 2, 0, 1, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 5, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); replay_bottom_panel_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(replay_bottom_panel_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(replay_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(replay_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(replay_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(replay_bottom_panel_grid), GTK_ALIGN_END); @@ -3134,11 +3170,48 @@ static GtkWidget* create_replay_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_valign(replay_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(replay_bottom_panel_grid, replay_record_time_label, 1, 0, 1, 1); + replay_start_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + replay_start_hotkey.keysym = XK_F1; + replay_start_hotkey.hotkey_entry = replay_start_hotkey_button; + replay_start_hotkey.hotkey_active_label = hotkey_active_label; + replay_start_hotkey.config = &config.replay_config.start_recording_hotkey; + replay_start_hotkey.page = GTK_WIDGET(grid); + replay_start_hotkey.state = &replaying; + replay_start_hotkey.expected_state_to_trigger = false; + replay_start_hotkey.trigger_handler = on_start_replay_button_click; + replay_start_hotkey.associated_button = start_replay_button; + replay_start_hotkey.shortcut_id = SHORTCUT_ID_START_RECORDING; + + replay_stop_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + replay_stop_hotkey.keysym = XK_F2; + replay_stop_hotkey.hotkey_entry = replay_stop_hotkey_button; + replay_stop_hotkey.hotkey_active_label = hotkey_active_label; + replay_stop_hotkey.config = &config.replay_config.stop_recording_hotkey; + replay_stop_hotkey.page = GTK_WIDGET(grid); + replay_stop_hotkey.state = &replaying; + replay_stop_hotkey.expected_state_to_trigger = true; + replay_stop_hotkey.trigger_handler = on_start_replay_button_click; + replay_stop_hotkey.associated_button = start_replay_button; + replay_stop_hotkey.shortcut_id = SHORTCUT_ID_STOP_RECORDING; + + replay_save_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + replay_save_hotkey.keysym = XK_F3; + replay_save_hotkey.hotkey_entry = replay_save_hotkey_button; + replay_save_hotkey.hotkey_active_label = hotkey_active_label; + replay_save_hotkey.config = &config.replay_config.save_recording_hotkey; + replay_save_hotkey.page = GTK_WIDGET(grid); + replay_save_hotkey.state = &replaying; + replay_save_hotkey.expected_state_to_trigger = true; + replay_save_hotkey.trigger_handler = on_replay_save_button_click; + replay_save_hotkey.associated_button = replay_save_button; + replay_save_hotkey.shortcut_id = SHORTCUT_ID_SAVE_REPLAY; + return GTK_WIDGET(grid); } static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { int row = 0; + const int num_columns = 5; std::string video_filepath = get_videos_dir(); @@ -3152,52 +3225,58 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { GtkWidget *hotkey_active_label = NULL; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { - add_wayland_global_hotkeys_ui(grid, row, 5); + add_wayland_global_hotkeys_ui(grid, row, num_columns); } else { hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel"); - gtk_grid_attach(grid, hotkey_active_label, 0, row++, 5, 1); + gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); + } - GtkWidget *a = gtk_label_new("Press"); - gtk_widget_set_halign(a, GTK_ALIGN_END); + { + gtk_grid_attach(grid, gtk_label_new("Press"), 0, row, 1, 1); - GtkWidget *b = gtk_label_new("to start/stop recording and"); - gtk_widget_set_halign(b, GTK_ALIGN_START); + record_start_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(record_start_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F1"); + g_signal_connect(record_start_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), record_start_hotkey_button); + gtk_grid_attach(grid, record_start_hotkey_button, 1, row, 1, 1); - GtkWidget *c = gtk_label_new("to pause/unpause"); - gtk_widget_set_halign(c, GTK_ALIGN_START); + gtk_grid_attach(grid, gtk_label_new("to start recording and"), 2, row, 1, 1); - record_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(record_hotkey_button), "Alt + F1"); - g_signal_connect(record_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), record_hotkey_button); + record_stop_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(record_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F2"); + g_signal_connect(record_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), record_stop_hotkey_button); + gtk_grid_attach(grid, record_stop_hotkey_button, 3, row, 1, 1); - pause_unpause_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(pause_unpause_hotkey_button), "Alt + F2"); - gtk_widget_set_halign(pause_unpause_hotkey_button, GTK_ALIGN_START); - g_signal_connect(pause_unpause_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), pause_unpause_hotkey_button); + gtk_grid_attach(grid, gtk_label_new("to stop recording"), 4, row, 1, 1); - gtk_grid_attach(grid, a, 0, row, 1, 1); - gtk_grid_attach(grid, record_hotkey_button, 1, row, 1, 1); - gtk_grid_attach(grid, b, 2, row, 1, 1); - gtk_grid_attach(grid, pause_unpause_hotkey_button, 3, row, 1, 1); - gtk_grid_attach(grid, c, 4, row, 1, 1); ++row; - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 5, 1); } - record_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - record_hotkey.keysym = XK_F1; - record_hotkey.hotkey_entry = record_hotkey_button; - record_hotkey.hotkey_active_label = hotkey_active_label; + { + gtk_grid_attach(grid, gtk_label_new("Press"), 0, row, 1, 1); + + pause_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(pause_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F5"); + g_signal_connect(pause_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), pause_hotkey_button); + gtk_grid_attach(grid, pause_hotkey_button, 1, row, 1, 1); - pause_unpause_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - pause_unpause_hotkey.keysym = XK_F2; - pause_unpause_hotkey.hotkey_entry = pause_unpause_hotkey_button; - pause_unpause_hotkey.hotkey_active_label = hotkey_active_label; + gtk_grid_attach(grid, gtk_label_new("to pause recording and"), 2, row, 1, 1); + + unpause_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(unpause_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F6"); + g_signal_connect(unpause_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), unpause_hotkey_button); + gtk_grid_attach(grid, unpause_hotkey_button, 3, row, 1, 1); + + gtk_grid_attach(grid, gtk_label_new("to unpause recording"), 4, row, 1, 1); + + ++row; + } + + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); GtkWidget *save_icon = gtk_image_new_from_icon_name("document-save", GTK_ICON_SIZE_BUTTON); GtkGrid *file_chooser_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(file_chooser_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(file_chooser_grid, 10); GtkWidget *file_chooser_label = gtk_label_new("Where do you want to save the video?"); gtk_grid_attach(file_chooser_grid, GTK_WIDGET(file_chooser_label), 0, 0, 1, 1); @@ -3210,7 +3289,7 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(file_chooser_grid, GTK_WIDGET(record_file_chooser_button), 1, 0, 1, 1); GtkGrid *container_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(container_grid), 0, row++, num_columns, 1); gtk_grid_attach(container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); record_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); for(auto &supported_container : supported_containers) { @@ -3221,10 +3300,10 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { } gtk_widget_set_hexpand(GTK_WIDGET(record_container), true); gtk_grid_attach(container_grid, GTK_WIDGET(record_container), 1, 0, 1, 1); - gtk_combo_box_set_active(GTK_COMBO_BOX(record_container), 0); // TODO: + gtk_combo_box_set_active(GTK_COMBO_BOX(record_container), 0); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); record_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); @@ -3246,10 +3325,10 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_sensitive(GTK_WIDGET(pause_recording_button), false); gtk_grid_attach(start_button_grid, GTK_WIDGET(pause_recording_button), 2, 0, 1, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 5, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); recording_bottom_panel_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(recording_bottom_panel_grid), 0, row++, 5, 1); + gtk_grid_attach(grid, GTK_WIDGET(recording_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(recording_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(recording_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(recording_bottom_panel_grid), GTK_ALIGN_END); @@ -3262,11 +3341,60 @@ static GtkWidget* create_recording_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_valign(recording_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(recording_bottom_panel_grid, recording_record_time_label, 1, 0, 1, 1); + record_start_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + record_start_hotkey.keysym = XK_F1; + record_start_hotkey.hotkey_entry = record_start_hotkey_button; + record_start_hotkey.hotkey_active_label = hotkey_active_label; + record_start_hotkey.config = &config.record_config.start_recording_hotkey; + record_start_hotkey.page = GTK_WIDGET(grid); + record_start_hotkey.state = &recording; + record_start_hotkey.expected_state_to_trigger = false; + record_start_hotkey.trigger_handler = on_start_recording_button_click; + record_start_hotkey.associated_button = start_recording_button; + record_start_hotkey.shortcut_id = SHORTCUT_ID_START_RECORDING; + + record_stop_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + record_stop_hotkey.keysym = XK_F2; + record_stop_hotkey.hotkey_entry = record_stop_hotkey_button; + record_stop_hotkey.hotkey_active_label = hotkey_active_label; + record_stop_hotkey.config = &config.record_config.stop_recording_hotkey; + record_stop_hotkey.page = GTK_WIDGET(grid); + record_stop_hotkey.state = &recording; + record_stop_hotkey.expected_state_to_trigger = true; + record_stop_hotkey.trigger_handler = on_start_recording_button_click; + record_stop_hotkey.associated_button = start_recording_button; + record_stop_hotkey.shortcut_id = SHORTCUT_ID_STOP_RECORDING; + + pause_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + pause_hotkey.keysym = XK_F5; + pause_hotkey.hotkey_entry = pause_hotkey_button; + pause_hotkey.hotkey_active_label = hotkey_active_label; + pause_hotkey.config = &config.record_config.pause_recording_hotkey; + pause_hotkey.page = GTK_WIDGET(grid); + pause_hotkey.state = &paused; + pause_hotkey.expected_state_to_trigger = false; + pause_hotkey.trigger_handler = on_pause_unpause_button_click; + pause_hotkey.associated_button = pause_recording_button; + pause_hotkey.shortcut_id = SHORTCUT_ID_PAUSE_RECORDING; + + unpause_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + unpause_hotkey.keysym = XK_F6; + unpause_hotkey.hotkey_entry = unpause_hotkey_button; + unpause_hotkey.hotkey_active_label = hotkey_active_label; + unpause_hotkey.config = &config.record_config.unpause_recording_hotkey; + unpause_hotkey.page = GTK_WIDGET(grid); + unpause_hotkey.state = &paused; + unpause_hotkey.expected_state_to_trigger = true; + unpause_hotkey.trigger_handler = on_pause_unpause_button_click; + unpause_hotkey.associated_button = pause_recording_button; + unpause_hotkey.shortcut_id = SHORTCUT_ID_UNPAUSE_RECORDING; + return GTK_WIDGET(grid); } static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { int row = 0; + const int num_columns = 5; GtkGrid *grid = GTK_GRID(gtk_grid_new()); gtk_stack_add_named(stack, GTK_WIDGET(grid), "streaming"); @@ -3278,34 +3406,36 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { GtkWidget *hotkey_active_label = NULL; if(gsr_info.system_info.display_server == DisplayServer::WAYLAND) { - add_wayland_global_hotkeys_ui(grid, row, 3); + add_wayland_global_hotkeys_ui(grid, row, num_columns); } else { hotkey_active_label = gtk_label_new("Press a key combination to set a new hotkey, backspace to remove the hotkey or esc to cancel"); - gtk_grid_attach(grid, hotkey_active_label, 0, row++, 3, 1); + gtk_grid_attach(grid, hotkey_active_label, 0, row++, num_columns, 1); + } + + { + gtk_grid_attach(grid, gtk_label_new("Press"), 0, row, 1, 1); + + streaming_start_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(streaming_start_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F1"); + g_signal_connect(streaming_start_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), streaming_start_hotkey_button); + gtk_grid_attach(grid, streaming_start_hotkey_button, 1, row, 1, 1); + + gtk_grid_attach(grid, gtk_label_new("to start streaming and"), 2, row, 1, 1); - GtkWidget *a = gtk_label_new("Press"); - gtk_widget_set_halign(a, GTK_ALIGN_END); + streaming_stop_hotkey_button = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(streaming_stop_hotkey_button), gsr_info.system_info.display_server == DisplayServer::WAYLAND ? "" : "Super + F2"); + g_signal_connect(streaming_stop_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), streaming_stop_hotkey_button); + gtk_grid_attach(grid, streaming_stop_hotkey_button, 3, row, 1, 1); - GtkWidget *b = gtk_label_new("to start/stop streaming"); - gtk_widget_set_halign(b, GTK_ALIGN_START); + gtk_grid_attach(grid, gtk_label_new("to stop streaming"), 4, row, 1, 1); - streaming_hotkey_button = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(streaming_hotkey_button), "Alt + F1"); - g_signal_connect(streaming_hotkey_button, "button-press-event", G_CALLBACK(on_hotkey_entry_click), streaming_hotkey_button); - gtk_grid_attach(grid, a, 0, row, 1, 1); - gtk_grid_attach(grid, streaming_hotkey_button, 1, row, 1, 1); - gtk_grid_attach(grid, b, 2, row, 1, 1); ++row; - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 3, 1); } - streaming_hotkey.modkey_mask = modkey_to_mask(XK_Alt_L); - streaming_hotkey.keysym = XK_F1; - streaming_hotkey.hotkey_entry = streaming_hotkey_button; - streaming_hotkey.hotkey_active_label = hotkey_active_label; + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); GtkGrid *stream_service_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(stream_service_grid), 0, row++, num_columns, 1); gtk_grid_attach(stream_service_grid, gtk_label_new("Stream service: "), 0, 0, 1, 1); stream_service_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(stream_service_input_menu, "twitch", "Twitch"); @@ -3317,7 +3447,7 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_grid_attach(stream_service_grid, GTK_WIDGET(stream_service_input_menu), 1, 0, 1, 1); GtkGrid *stream_id_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(stream_id_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(stream_id_grid), 0, row++, num_columns, 1); stream_key_label = GTK_LABEL(gtk_label_new("Stream key: ")); gtk_grid_attach(stream_id_grid, GTK_WIDGET(stream_key_label), 0, 0, 1, 1); @@ -3335,7 +3465,7 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { } custom_stream_container_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(custom_stream_container_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(custom_stream_container_grid), 0, row++, num_columns, 1); gtk_grid_attach(custom_stream_container_grid, gtk_label_new("Container: "), 0, 0, 1, 1); custom_stream_container = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); for(auto &supported_container : supported_containers) { @@ -3349,7 +3479,7 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_combo_box_set_active(GTK_COMBO_BOX(custom_stream_container), 1); GtkGrid *start_button_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(start_button_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(start_button_grid, 10); stream_back_button = GTK_BUTTON(gtk_button_new_with_label("Back")); @@ -3364,10 +3494,10 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { g_signal_connect(start_streaming_button, "clicked", G_CALLBACK(on_start_streaming_button_click), app); gtk_grid_attach(start_button_grid, GTK_WIDGET(start_streaming_button), 1, 0, 1, 1); - gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, 3, 1); + gtk_grid_attach(grid, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row++, num_columns, 1); streaming_bottom_panel_grid = GTK_GRID(gtk_grid_new()); - gtk_grid_attach(grid, GTK_WIDGET(streaming_bottom_panel_grid), 0, row++, 3, 1); + gtk_grid_attach(grid, GTK_WIDGET(streaming_bottom_panel_grid), 0, row++, num_columns, 1); gtk_grid_set_column_spacing(streaming_bottom_panel_grid, 5); gtk_widget_set_opacity(GTK_WIDGET(streaming_bottom_panel_grid), 0.5); gtk_widget_set_halign(GTK_WIDGET(streaming_bottom_panel_grid), GTK_ALIGN_END); @@ -3380,6 +3510,30 @@ static GtkWidget* create_streaming_page(GtkApplication *app, GtkStack *stack) { gtk_widget_set_valign(streaming_record_time_label, GTK_ALIGN_CENTER); gtk_grid_attach(streaming_bottom_panel_grid, streaming_record_time_label, 1, 0, 1, 1); + streaming_start_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + streaming_start_hotkey.keysym = XK_F1; + streaming_start_hotkey.hotkey_entry = streaming_start_hotkey_button; + streaming_start_hotkey.hotkey_active_label = hotkey_active_label; + streaming_start_hotkey.config = &config.streaming_config.start_recording_hotkey; + streaming_start_hotkey.page = GTK_WIDGET(grid); + streaming_start_hotkey.state = &streaming; + streaming_start_hotkey.expected_state_to_trigger = false; + streaming_start_hotkey.trigger_handler = on_start_streaming_button_click; + streaming_start_hotkey.associated_button = start_streaming_button; + streaming_start_hotkey.shortcut_id = SHORTCUT_ID_START_RECORDING; + + streaming_stop_hotkey.modkey_mask = modkey_to_mask(XK_Super_L); + streaming_stop_hotkey.keysym = XK_F2; + streaming_stop_hotkey.hotkey_entry = streaming_stop_hotkey_button; + streaming_stop_hotkey.hotkey_active_label = hotkey_active_label; + streaming_stop_hotkey.config = &config.streaming_config.stop_recording_hotkey; + streaming_stop_hotkey.page = GTK_WIDGET(grid); + streaming_stop_hotkey.state = &streaming; + streaming_stop_hotkey.expected_state_to_trigger = true; + streaming_stop_hotkey.trigger_handler = on_start_streaming_button_click; + streaming_stop_hotkey.associated_button = start_streaming_button; + streaming_stop_hotkey.shortcut_id = SHORTCUT_ID_STOP_RECORDING; + return GTK_WIDGET(grid); } @@ -3494,7 +3648,7 @@ static void load_config() { // } else if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && gsr_info.gpu_info.vendor == GpuVendor::NVIDIA && strcmp(config.main_config.record_area_option.c_str(), "screen") == 0) { // - } else if(config.main_config.record_area_option == "portal" && gsr_info.supported_capture_options.portal) { + } else if(config.main_config.record_area_option == "portal" && gsr_info.supported_capture_options.portal && gsr_info.system_info.display_server == DisplayServer::WAYLAND) { // } else { bool found_monitor = false; @@ -3595,48 +3749,29 @@ static void load_config() { gtk_entry_set_text(twitch_stream_id_entry, config.streaming_config.twitch.stream_key.c_str()); gtk_entry_set_text(custom_stream_url_entry, config.streaming_config.custom.url.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(custom_stream_container), config.streaming_config.custom.container.c_str()); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && streaming_hotkey_button && !config_empty) { - streaming_hotkey.keysym = config.streaming_config.start_recording_hotkey.keysym; - streaming_hotkey.modkey_mask = config.streaming_config.start_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(streaming_hotkey_button), streaming_hotkey); - } gtk_button_set_label(record_file_chooser_button, config.record_config.save_directory.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(record_container), config.record_config.container.c_str()); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && record_hotkey_button && !config_empty) { - record_hotkey.keysym = config.record_config.start_recording_hotkey.keysym; - record_hotkey.modkey_mask = config.record_config.start_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(record_hotkey_button), record_hotkey); - } - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && pause_unpause_hotkey_button && !config_empty) { - pause_unpause_hotkey.keysym = config.record_config.pause_recording_hotkey.keysym; - pause_unpause_hotkey.modkey_mask = config.record_config.pause_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(pause_unpause_hotkey_button), pause_unpause_hotkey); - } gtk_button_set_label(replay_file_chooser_button, config.replay_config.save_directory.c_str()); gtk_combo_box_set_active_id(GTK_COMBO_BOX(replay_container), config.replay_config.container.c_str()); gtk_spin_button_set_value(replay_time_entry, config.replay_config.replay_time); - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && replay_start_stop_hotkey_button && !config_empty) { - replay_start_stop_hotkey.keysym = config.replay_config.start_recording_hotkey.keysym; - replay_start_stop_hotkey.modkey_mask = config.replay_config.start_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(replay_start_stop_hotkey_button), replay_start_stop_hotkey); - } - if(gsr_info.system_info.display_server != DisplayServer::WAYLAND && replay_save_hotkey_button && !config_empty) { - replay_save_hotkey.keysym = config.replay_config.save_recording_hotkey.keysym; - replay_save_hotkey.modkey_mask = config.replay_config.save_recording_hotkey.modifiers; - set_hotkey_text_from_hotkey_data(GTK_ENTRY(replay_save_hotkey_button), replay_save_hotkey); - } gtk_combo_box_set_active_id(GTK_COMBO_BOX(view_combo_box), config.main_config.advanced_view ? "advanced" : "simple"); view_combo_box_change_callback(GTK_COMBO_BOX(view_combo_box), view_combo_box); if(gsr_info.system_info.display_server != DisplayServer::WAYLAND) { - gtk_widget_set_visible(record_hotkey.hotkey_active_label, false); - gtk_widget_set_visible(pause_unpause_hotkey.hotkey_active_label, false); - gtk_widget_set_visible(streaming_hotkey.hotkey_active_label, false); - gtk_widget_set_visible(replay_start_stop_hotkey.hotkey_active_label, false); - gtk_widget_set_visible(replay_save_hotkey.hotkey_active_label, false); + if(!config_empty) { + for(int i = 0; i < num_hotkeys; ++i) { + hotkeys[i]->keysym = hotkeys[i]->config->keysym; + hotkeys[i]->modkey_mask = hotkeys[i]->config->modifiers; + set_hotkey_text_from_hotkey_data(GTK_ENTRY(hotkeys[i]->hotkey_entry), *hotkeys[i]); + } + } + + gtk_widget_set_visible(record_start_hotkey.hotkey_active_label, false); + gtk_widget_set_visible(streaming_start_hotkey.hotkey_active_label, false); + gtk_widget_set_visible(replay_start_hotkey.hotkey_active_label, false); } enable_stream_record_button_if_info_filled(); stream_service_item_change_callback(GTK_COMBO_BOX(stream_service_input_menu), nullptr); @@ -3655,6 +3790,14 @@ static void load_config() { } } +static void init_shortcuts_callback(void *userdata) { + (void)userdata; + if(!gsr_global_shortcuts_list_shortcuts(&global_shortcuts, shortcut_changed_callback, NULL)) { + fprintf(stderr, "gsr error: failed to list shortcuts\n"); + } + gsr_global_shortcuts_subscribe_activated_signal(&global_shortcuts, deactivated_callback, shortcut_changed_callback, NULL); +} + static const char* gpu_vendor_to_name(GpuVendor vendor) { switch(vendor) { case GpuVendor::AMD: return "AMD"; @@ -3799,6 +3942,13 @@ static void activate(GtkApplication *app, gpointer) { gtk_widget_show_all(window); load_config(); + + if(gsr_global_shortcuts_init(&global_shortcuts, init_shortcuts_callback, NULL)) { + global_shortcuts_initialized = true; + } else { + fprintf(stderr, "gsr error: failed to initialize global shortcuts\n"); + global_shortcuts_initialized = false; + } } int main(int argc, char **argv) { -- cgit v1.2.3