diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index b20c2fa476ce5c..52afae2519fd8b 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -48,6 +48,8 @@ target_sources( obs-hotkey.c obs-hotkey.h obs-hotkeys.h + obs-hotkey-portal.c + obs-hotkey-portal.h obs-interaction.h obs-internal.h obs-missing-files.c @@ -312,6 +314,7 @@ set( obs-encoder.h obs-hotkey.h obs-hotkeys.h + obs-hotkey-portal.h obs-interaction.h obs-missing-files.h obs-module.h diff --git a/libobs/obs-hotkey-portal.c b/libobs/obs-hotkey-portal.c new file mode 100644 index 00000000000000..5a43a8e84ee2e1 --- /dev/null +++ b/libobs/obs-hotkey-portal.c @@ -0,0 +1,607 @@ +/****************************************************************************** + Copyright (C) 2026 by Adam Fallon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "obs-hotkey-portal.h" +#include "obs-hotkey.h" +#include "obs.h" +#include "util/base.h" +#include "util/bmem.h" +#include "util/c99defs.h" +#include +#include +#include +#include +#include + +#define PORTAL_NAME "org.freedesktop.portal.Desktop" +#define PORTAL_PATH "/org/freedesktop/portal/desktop" +#define SHORTCUTS_IFACE "org.freedesktop.portal.GlobalShortcuts" +#define REQUEST_IFACE "org.freedesktop.portal.Request" +#define SESSION_IFACE "org.freedesktop.portal.Session" + +#undef HASH_FUNCTION +#define HASH_FUNCTION(s, len, hashv) (hashv) = *s % UINT_MAX +#define HASH_FIND_HKEY(head, id, out) HASH_FIND(hh, head, &(id), sizeof(size_t), out) + +static struct obs_hotkey_portal_state { + GDBusConnection *conn; + char *session_path; + bool bind_shortcuts_called_for_session; + // tracks the timestamp when pending_hotkeys was last added to + long add_to_pending_hotkeys_timestamp_us; + // stores list of hotkeys that need to be registered with the portal + GQueue *pending_hotkeys; + // maps portal id -> hotkey id + GHashTable *registered_hotkeys; +} *state = NULL; + +typedef struct obs_hotkey_portal_state obs_hotkey_portal_state_t; + +enum obs_hotkey_portal_registered_type { + SINGLE, + PAIR, +}; +typedef enum obs_hotkey_portal_registered_type obs_hotkey_portal_registered_type_t; + +struct obs_hotkey_portal_registered_hotkey { + obs_hotkey_portal_registered_type_t type; + size_t id; +}; +typedef struct obs_hotkey_portal_registered_hotkey obs_hotkey_portal_registered_hotkey_t; + +static void close_session(); +static void create_session(); + +gboolean destroy_registered_hotkey(gpointer key, gpointer value, gpointer user_data) +{ + UNUSED_PARAMETER(user_data); + bfree(key); + bfree(value); + return TRUE; +} + +void obs_hotkey_portal_free() +{ + if (state == NULL) + return; + g_dbus_connection_close_sync(state->conn, NULL, NULL); + g_object_unref(state->conn); + bfree(state->session_path); + g_queue_free_full(state->pending_hotkeys, bfree); + g_hash_table_foreach_remove(state->registered_hotkeys, destroy_registered_hotkey, NULL); + g_hash_table_destroy(state->registered_hotkeys); + bfree(state); + state = NULL; +} + +GString *get_formatted_sender(GDBusConnection *conn) +{ + g_autoptr(GString) temp = NULL; + GString *unique_name = NULL; + temp = g_string_new(g_dbus_connection_get_unique_name(conn)); + + g_string_replace(temp, ".", "_", 0); + + if (temp->len < 1) { + return NULL; + } + + unique_name = g_string_new(NULL); + g_string_printf(unique_name, "%s", &temp->str[1]); + return unique_name; +} + +char *get_hotkey_portal_id(obs_hotkey_t *hotkey) +{ + if (hotkey == NULL) { + return NULL; + } + + const char *append_to_id = NULL; + void *registerer = obs_weak_object_get_object(hotkey->registerer); + if (registerer) { + switch (obs_hotkey_get_registerer_type(hotkey)) { + case OBS_HOTKEY_REGISTERER_SOURCE: + append_to_id = obs_source_get_uuid(registerer); + obs_object_release(registerer); + break; + default: + append_to_id = obs_obj_get_id(obs_weak_object_get_object(hotkey->registerer)); + break; + } + } + + // append to id + char *id_dst; + if (append_to_id != NULL) { + id_dst = bzalloc(strlen(hotkey->name) + strlen(append_to_id) + 8); + sprintf(id_dst, "%s.%s", hotkey->name, append_to_id); + return id_dst; + } else { + id_dst = bstrdup(hotkey->name); + } + + obs_object_release(registerer); + return id_dst; +} + +char *get_hotkey_portal_description(obs_hotkey_t *hotkey) +{ + if (hotkey == NULL) { + return NULL; + } + + const char *append_to_description = NULL; + void *registerer = obs_weak_object_get_object(hotkey->registerer); + if (registerer) { + switch (obs_hotkey_get_registerer_type(hotkey)) { + case OBS_HOTKEY_REGISTERER_SOURCE: + append_to_description = obs_source_get_name(registerer); + break; + default: + break; + } + } + + char *description_dst; + if (append_to_description != NULL) { + description_dst = bzalloc(strlen(hotkey->description) + strlen(append_to_description) + 8); + sprintf(description_dst, "%s (%s)", hotkey->description, append_to_description); + } else { + description_dst = bstrdup(hotkey->description); + } + + obs_object_release(registerer); + return description_dst; +} + +void add_hotkey_to_builder(GVariantBuilder *shortcuts_builder, char *id, char *description) +{ + GVariantBuilder vardict_builder; + g_variant_builder_init(&vardict_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&vardict_builder, "{sv}", "description", g_variant_new_string(description)); + + // add shortcut to builder + GVariant *data[] = {g_variant_new_string(id), g_variant_builder_end(&vardict_builder)}; + g_variant_builder_add_value(shortcuts_builder, g_variant_new_tuple(data, 2)); +} + +// register all pending hotkeys, if any +static gboolean obs_hotkey_portal_finish_registration() +{ + if (!state || !state->session_path) { + return G_SOURCE_CONTINUE; + } + + // check if at least 0.5 seconds have passed since the last time we added to pending_hotkeys + long current_timestamp = g_get_monotonic_time(); + if (current_timestamp - state->add_to_pending_hotkeys_timestamp_us < 500000) { + return G_SOURCE_CONTINUE; + } + + // check if there are any pending hotkeys + if (g_queue_is_empty(state->pending_hotkeys)) { + return G_SOURCE_CONTINUE; + } + + // if BindShortcuts has already been called + if (state->bind_shortcuts_called_for_session) { + // destroy session + close_session(); + + // re-queue all hotkeys + GHashTableIter iter; + gpointer portal_id, dst; + g_hash_table_iter_init(&iter, state->registered_hotkeys); + while (g_hash_table_iter_next(&iter, &portal_id, &dst)) { + obs_hotkey_portal_registered_hotkey_t *hk = dst; + g_queue_push_head(state->pending_hotkeys, hk); + bfree(portal_id); + } + + g_hash_table_remove_all(state->registered_hotkeys); + + // open session back up + create_session(); + return G_SOURCE_CONTINUE; + } + + state->bind_shortcuts_called_for_session = true; + + // bind some shortcuts + GVariantBuilder shortcuts_builder; + g_variant_builder_init(&shortcuts_builder, G_VARIANT_TYPE("a(sa{sv})")); + + for (obs_hotkey_portal_registered_hotkey_t *queued_hk = g_queue_pop_head(state->pending_hotkeys); + queued_hk != NULL; queued_hk = g_queue_pop_head(state->pending_hotkeys)) { + + if (queued_hk->type == SINGLE) { + obs_hotkey_t *hotkey; + HASH_FIND_HKEY(obs->hotkeys.hotkeys, queued_hk->id, hotkey); + blog(LOG_INFO, "Registering hotkey id with portal: %lu", queued_hk->id); + + if (!hotkey) + continue; + + char *id = get_hotkey_portal_id(hotkey); + char *description = get_hotkey_portal_description(hotkey); + + blog(LOG_INFO, "Registering hotkey with portal: %s: %s", hotkey->name, hotkey->description); + + add_hotkey_to_builder(&shortcuts_builder, id, description); + g_hash_table_insert(state->registered_hotkeys, id, queued_hk); + + // id is now owned by state->registered_hotkeys + bfree(description); + } else if (queued_hk->type == PAIR) { + blog(LOG_INFO, "Registering hotkey pair id with portal: %lu", queued_hk->id); + // get pair + obs_hotkey_pair_t *pair; + HASH_FIND_HKEY(obs->hotkeys.hotkey_pairs, queued_hk->id, pair); + + if (!pair) + continue; + + // get both hotkeys in that pair + obs_hotkey_t *hotkey1, *hotkey2; + HASH_FIND_HKEY(obs->hotkeys.hotkeys, pair->id[0], hotkey1); + HASH_FIND_HKEY(obs->hotkeys.hotkeys, pair->id[1], hotkey2); + + if (!hotkey1 || !hotkey2) + continue; + + // get id + char *hotkey1_id = get_hotkey_portal_id(hotkey1); + char *hotkey2_id = get_hotkey_portal_id(hotkey2); + size_t joint_id_len = strlen(hotkey1_id) + strlen(hotkey2_id) + 2; + char *joint_id = bzalloc(joint_id_len); + snprintf(joint_id, joint_id_len, "%s.%s", hotkey1_id, hotkey2_id); + + // get description + char *hotkey1_desc = get_hotkey_portal_description(hotkey1); + char *hotkey2_desc = get_hotkey_portal_description(hotkey2); + size_t joint_desc_len = strlen(hotkey1_desc) + strlen(hotkey2_desc) + 2; + char *joint_desc = bzalloc(joint_desc_len); + snprintf(joint_desc, joint_desc_len, "%s/%s", hotkey1_desc, hotkey2_desc); + + // add hotkey + add_hotkey_to_builder(&shortcuts_builder, joint_id, joint_desc); + g_hash_table_insert(state->registered_hotkeys, joint_id, queued_hk); + + bfree(hotkey1_id); + bfree(hotkey1_desc); + bfree(hotkey2_id); + bfree(hotkey2_desc); + // joint_id is now owned by state->registered_hotkeys + bfree(joint_desc); + } else { + blog(LOG_ERROR, "Invalid hotkey type: %d", queued_hk->type); + } + } + + // options + GVariantBuilder options_builder; + char request_handle_token[64] = {}; + snprintf(request_handle_token, sizeof(request_handle_token), "obs_globalshortcuts_request%d", rand()); + + g_variant_builder_init(&options_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&options_builder, "{sv}", "handle_token", g_variant_new_string(request_handle_token)); + + GVariant *options[] = {g_variant_new_object_path(state->session_path), + g_variant_builder_end(&shortcuts_builder), g_variant_new_string(""), + g_variant_builder_end(&options_builder)}; + g_dbus_connection_call(state->conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "BindShortcuts", + g_variant_new_tuple(options, 4), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + + return G_SOURCE_CONTINUE; +} + +static void call_hotkey_cb(const char *received_id, bool pressed) +{ + char *iter_id = NULL; + + obs_hotkey_portal_registered_hotkey_t *received_hk; + received_hk = g_hash_table_lookup(state->registered_hotkeys, received_id); + + if (!received_hk) { + blog(LOG_ERROR, "Failed to find activated hotkey for portal"); + return; + } + + if (received_hk->type == SINGLE) { + obs_hotkey_t *hotkey; + HASH_FIND_HKEY(obs->hotkeys.hotkeys, received_hk->id, hotkey); + + if (!hotkey) + return; + if (pressed == hotkey->pressed) + return; + + hotkey->pressed = pressed; + + if (!obs->hotkeys.reroute_hotkeys) + hotkey->func(hotkey->data, hotkey->id, hotkey, pressed); + else if (obs->hotkeys.router_func) + obs->hotkeys.router_func(obs->hotkeys.router_func_data, hotkey->id, pressed); + + bfree(iter_id); + } else if (received_hk->type == PAIR) { + obs_hotkey_pair_t *pair; + HASH_FIND_HKEY(obs->hotkeys.hotkey_pairs, received_hk->id, pair); + + if (!pair) + return; + + for (size_t i = 0; i < 2; ++i) { + obs_hotkey_t *hk; + HASH_FIND_HKEY(obs->hotkeys.hotkeys, pair->id[i], hk); + if (!obs->hotkeys.reroute_hotkeys) + pair->func[i](hk->data, hk->id, hk, pressed); + else if (obs->hotkeys.router_func) + obs->hotkeys.router_func(obs->hotkeys.router_func_data, hk->id, pressed); + } + + } else { + blog(LOG_ERROR, "Invalid hotkey type: %d", received_hk->type); + } +} + +static void activated_cb(GDBusConnection *conn, const char *sender_name, const char *object_path, + const char *interface_name, const char *signal_name, GVariant *parameters, gpointer user_data) +{ + UNUSED_PARAMETER(conn); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(parameters); + UNUSED_PARAMETER(user_data); + g_autofree char *received_id = NULL; + g_variant_get(parameters, "(ost@a{sv})", NULL, &received_id, NULL, NULL); + call_hotkey_cb(received_id, true); +} + +static void deactivated_cb(GDBusConnection *conn, const char *sender_name, const char *object_path, + const char *interface_name, const char *signal_name, GVariant *parameters, + gpointer user_data) +{ + UNUSED_PARAMETER(conn); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(parameters); + UNUSED_PARAMETER(user_data); + g_autofree char *received_id = NULL; + g_variant_get(parameters, "(ost@a{sv})", NULL, &received_id, NULL, NULL); + call_hotkey_cb(received_id, false); +} + +static void shortcuts_changed_cb(GDBusConnection *conn, const char *sender_name, const char *object_path, + const char *interface_name, const char *signal_name, GVariant *parameters, + gpointer user_data) +{ + UNUSED_PARAMETER(conn); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(parameters); + UNUSED_PARAMETER(user_data); + + // TODO: update UI? + g_print("Shortcuts changed!\n"); +} + +static void create_session_cb(GDBusConnection *conn, const char *sender_name, const char *object_path, + const char *interface_name, const char *signal_name, GVariant *parameters, + gpointer user_data) +{ + if (!state || state->session_path) { + return; + } + + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + UNUSED_PARAMETER(user_data); + + int response; + g_autoptr(GVariant) options = NULL; + const char *session_handle = NULL; + + g_variant_get(parameters, "(u@a{sv})", &response, &options); + g_variant_lookup(options, "session_handle", "&s", &session_handle); + + // start listening to shortcuts + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Activated", PORTAL_PATH, NULL, + G_DBUS_SIGNAL_FLAGS_NONE, activated_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "Deactivated", PORTAL_PATH, NULL, + G_DBUS_SIGNAL_FLAGS_NONE, deactivated_cb, NULL, NULL); + g_dbus_connection_signal_subscribe(conn, PORTAL_NAME, SHORTCUTS_IFACE, "ShortcutsChanged", PORTAL_PATH, NULL, + G_DBUS_SIGNAL_FLAGS_NONE, shortcuts_changed_cb, NULL, NULL); + + state->session_path = bstrdup(session_handle); +} + +static void close_session() +{ + if (!state || !state->session_path || !state->conn) { + return; + } + + GError *error = NULL; + g_dbus_connection_call_sync(state->conn, PORTAL_NAME, state->session_path, SESSION_IFACE, "Close", NULL, NULL, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + + if (error != NULL) { + blog(LOG_ERROR, "Failed to destroy global shortcuts portal session: %s", error->message); + return; + } + bfree(state->session_path); + state->bind_shortcuts_called_for_session = false; + state->session_path = NULL; +} + +static void create_session() +{ + if (!state || state->session_path || !state->conn) { + return; + } + + state->bind_shortcuts_called_for_session = false; + + g_autoptr(GString) session_token = NULL; + g_autoptr(GString) request_handle_token = NULL; + GVariant *options = NULL; + g_autoptr(GString) request_path = NULL; + g_autoptr(GString) sender = NULL; + GVariantBuilder options_builder; + g_autoptr(GVariant) result = NULL; + GError *error = NULL; + + sender = get_formatted_sender(state->conn); + + // get tokens + session_token = g_string_new(NULL); + g_string_printf(session_token, "obs_globalshortcuts_session%d", rand()); + + request_handle_token = g_string_new(NULL); + g_string_printf(request_handle_token, "obs_globalshortcuts_request%d", rand()); + + // connect to session parameters + g_variant_builder_init(&options_builder, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&options_builder, "{sv}", "handle_token", + g_variant_new_string(request_handle_token->str)); + g_variant_builder_add(&options_builder, "{sv}", "session_handle_token", + g_variant_new_string(session_token->str)); + options = g_variant_builder_end(&options_builder); + + // start to listen to request object in advance + request_path = g_string_new(NULL); + g_string_printf(request_path, "/org/freedesktop/portal/desktop/request/%s/%s", sender->str, + request_handle_token->str); + g_print("Listening to request at path: %s\n", request_path->str); + g_dbus_connection_signal_subscribe(state->conn, PORTAL_NAME, REQUEST_IFACE, "Response", request_path->str, NULL, + G_DBUS_SIGNAL_FLAGS_NONE, create_session_cb, NULL, NULL); + + // finally, connect to session + result = g_dbus_connection_call_sync(state->conn, PORTAL_NAME, PORTAL_PATH, SHORTCUTS_IFACE, "CreateSession", + g_variant_new_tuple(&options, 1), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, + &error); + + if (result == NULL) { + blog(LOG_WARNING, "Failed to create global shortcuts portal session: %s", error->message); + return; + } +} + +static void obs_hotkey_portal_init() +{ + // only allow this function to run once + static bool init_run = false; + if (init_run) { + return; + } + init_run = true; + + g_autoptr(GDBusConnection) conn = NULL; + GError *error = NULL; + + // get connection + conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); + if (!conn) { + g_printerr("Failed to connect to dbus: %s\n", error->message); + return; + } + + // initialise state + state = bzalloc(sizeof(obs_hotkey_portal_state_t)); + state->conn = conn; + state->session_path = NULL; + state->pending_hotkeys = g_queue_new(); + state->registered_hotkeys = g_hash_table_new(g_str_hash, g_str_equal); + + create_session(); + + // schedule a timer to register all pending hotkeys every 0.1 seconds + g_timeout_add(500, (GSourceFunc)obs_hotkey_portal_finish_registration, NULL); +} + +void obs_hotkey_portal_register(obs_hotkey_t *hotkey) +{ + // init if haven't already + obs_hotkey_portal_init(); + if (state == NULL) { + blog(LOG_WARNING, + "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + return; + } + + // add to pending list + obs_hotkey_portal_registered_hotkey_t *hk; + hk = bzalloc(sizeof(obs_hotkey_portal_registered_hotkey_t)); + hk->type = SINGLE; + hk->id = hotkey->id; + + blog(LOG_WARNING, "Registering hotkey with id %lu to xdg-desktop-portal", hk->id); + + g_queue_push_tail(state->pending_hotkeys, hk); + state->add_to_pending_hotkeys_timestamp_us = g_get_monotonic_time(); +} + +void obs_hotkey_portal_register_pair(obs_hotkey_pair_t *pair) +{ + // init if haven't already + obs_hotkey_portal_init(); + if (state == NULL) { + blog(LOG_WARNING, + "Attempting to register a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + return; + } + + blog(LOG_WARNING, "Registering hotkey pair with id %lu to xdg-desktop-portal", pair->pair_id); + + // add to pending list + obs_hotkey_portal_registered_hotkey_t *hk; + hk = bzalloc(sizeof(obs_hotkey_portal_registered_hotkey_t)); + hk->type = PAIR; + hk->id = pair->pair_id; + + g_queue_push_tail(state->pending_hotkeys, hk); + state->add_to_pending_hotkeys_timestamp_us = g_get_monotonic_time(); +} + +void obs_hotkey_portal_unregister(obs_hotkey_id id) +{ + if (state == NULL) { + blog(LOG_WARNING, + "Attempting to unregister a hotkey with the xdg-desktop-portal, but this feature has not been initialised"); + return; + } + + blog(LOG_WARNING, "Unregistering hotkey with id %lu from xdg-desktop-portal", id); + + // remove from pending list (if it's there) + for (GList *l = state->pending_hotkeys->head; l != NULL; l = l->next) { + if (*(obs_hotkey_id *)l->data == id) { + g_queue_delete_link(state->pending_hotkeys, l); + break; + } + } +} diff --git a/libobs/obs-hotkey-portal.h b/libobs/obs-hotkey-portal.h new file mode 100644 index 00000000000000..0667164b57a7cd --- /dev/null +++ b/libobs/obs-hotkey-portal.h @@ -0,0 +1,45 @@ +/****************************************************************************** + Copyright (C) 2026 by Adam Fallon + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "obs-internal.h" +#include "obs-hotkey.h" +#include +#include + +// must be called before registering any hotkeys with the portal. +// does nothing if it has already been called +void obs_hotkey_portal_free(); + +// returns true if the portal session has been started +bool obs_hotkey_portal_session_active(); + +// queues hotkey to be added to portal +// should always be called, even if it has no effect (i.e. if unsupported on +// the current platform) +void obs_hotkey_portal_register(obs_hotkey_t *hotkey); +void obs_hotkey_portal_register_pair(obs_hotkey_pair_t *pair); + +// // specifies hotkeys which should be a considered a pair +// // must only be called after these two hotkeys have been registered +// void obs_hotkey_portal_register_pair(obs_hotkey_id first, obs_hotkey_id second); + +// unqueues hotkey to be registered +// should always be called, even if it has no effect (i.e. if unsupported on +// the current platform) +void obs_hotkey_portal_unregister(obs_hotkey_id id); diff --git a/libobs/obs-hotkey.c b/libobs/obs-hotkey.c index 0503b303390409..dfce58ba3247df 100644 --- a/libobs/obs-hotkey.c +++ b/libobs/obs-hotkey.c @@ -17,6 +17,7 @@ #include +#include "obs-hotkey-portal.h" #include "obs-internal.h" /* Since ids are just sequential size_t integers, we don't really need a @@ -181,6 +182,9 @@ static inline obs_hotkey_id obs_hotkey_register_internal(obs_hotkey_registerer_t context_add_hotkey(context, result); } + // add with xdg-desktop-portal + obs_hotkey_portal_register(hotkey); + hotkey_signal("hotkey_register", hotkey); return result; @@ -336,6 +340,8 @@ static obs_hotkey_pair_id register_hotkey_pair_internal(obs_hotkey_registerer_t obs_hotkey_pair_id id = pair->pair_id; + obs_hotkey_portal_register_pair(pair); + unlock(); return id; } @@ -866,6 +872,8 @@ static inline void unregister_hotkey(obs_hotkey_id id) bfree(hotkey->description); bfree(hotkey); + obs_hotkey_portal_unregister(id); + remove_bindings(id); } @@ -965,6 +973,8 @@ void obs_hotkeys_free(void) obs->hotkeys.translations[i] = NULL; } } + + obs_hotkey_portal_free(); } void obs_enum_hotkeys(obs_hotkey_enum_func func, void *data)