feat: globalshortcuts portal support

This commit is contained in:
KoljaFrahm 2026-07-06 17:37:35 +02:00 committed by Nick Bolton
parent ce2f948c29
commit 217166c06b
5 changed files with 748 additions and 12 deletions

View file

@ -18,6 +18,7 @@ if(UNIX AND NOT APPLE)
set(CMAKE_REQUIRED_LIBRARIES "${CMAKE_REQUIRED_LIBRARIES};${LIBPORTAL_LINK_LIBRARIES};")
check_symbol_exists(xdp_input_capture_session_get_restore_token "libportal/inputcapture.h" HAVE_IC_RESTORE)
check_symbol_exists(xdp_session_request_clipboard "libportal/portal-helpers.h;libportal/clipboard.h" HAVE_LP_CLIPBOARD)
check_symbol_exists(xdp_global_shortcut_new "libportal/globalshortcuts.h" HAVE_LP_SHORTCUTS)
cmake_pop_check_state()
endif()
@ -166,6 +167,12 @@ elseif(UNIX)
PortalClipboard.h
)
endif()
if(HAVE_LP_SHORTCUTS)
list(APPEND PLATFORM_SOURCES
PortalGlobalShortcuts.cpp
PortalGlobalShortcuts.h
)
endif()
endif()
endif()
endif()
@ -192,6 +199,9 @@ if(UNIX)
if (HAVE_LP_CLIPBOARD)
target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_CLIPBOARD)
endif()
if (HAVE_LP_SHORTCUTS)
target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_SHORTCUTS)
endif()
target_include_directories(platform PUBLIC ${LIBEI_INCLUDE_DIRS} ${LIBPORTAL_INCLUDE_DIRS})
target_link_libraries(platform
PUBLIC

View file

@ -59,8 +59,11 @@ EiScreen::EiScreen(bool isPrimary, IEventQueue *events, bool usePortal)
handleConnectedToEisEvent(e);
});
if (isPrimary) {
m_portalInputCapture = new PortalInputCapture(this, m_events);
// Portal input capture manages its own clipboard
m_portalInputCapture = new PortalInputCapture(this, m_events);
#ifdef HAVE_LIBPORTAL_SHORTCUTS
m_portalGlobalShortcuts = new PortalGlobalShortcuts(this, m_events);
#endif
} else {
m_events->addHandler(EventTypes::EISessionClosed, getEventTarget(), [this](const auto &) {
handlePortalSessionClosed();
@ -97,8 +100,11 @@ EiScreen::~EiScreen()
delete m_keyState;
delete m_clipboard;
delete m_portalRemoteDesktop;
#ifdef HAVE_LIBPORTAL_SHORTCUTS
delete m_portalGlobalShortcuts;
#endif
delete m_portalInputCapture;
delete m_portalRemoteDesktop;
}
void EiScreen::eiLogEvent(ei_log_priority priority, const char *message) const
@ -234,19 +240,47 @@ std::uint32_t EiScreen::registerHotKey(KeyID key, KeyModifierMask mask)
}
set->second.addItem(HotKeyItem(mask, id));
updatePortalGlobalShortcuts();
return id;
}
void EiScreen::unregisterHotKey(uint32_t id)
{
for (auto &[key, set] : m_hotkeys) {
(void)key;
if (set.removeById(id)) {
for (auto it = m_hotkeys.begin(); it != m_hotkeys.end(); ++it) {
if (it->second.removeById(id)) {
if (it->second.empty())
m_hotkeys.erase(it);
updatePortalGlobalShortcuts();
break;
}
}
}
void EiScreen::updatePortalGlobalShortcuts()
{
#ifdef HAVE_LIBPORTAL_SHORTCUTS
if (!m_portalGlobalShortcuts) {
return;
}
if (!m_activated) {
return;
}
std::vector<PortalGlobalShortcuts::HotKey> hotkeys;
for (const auto &[key, set] : m_hotkeys) {
(void)key;
const auto setHotKeys = set.getPortalHotKeys();
hotkeys.insert(hotkeys.end(), setHotKeys.begin(), setHotKeys.end());
}
m_portalGlobalShortcuts->setHotKeys(std::move(hotkeys));
#endif
}
void EiScreen::fakeInputBegin()
{
// FIXME -- not implemented
@ -359,16 +393,13 @@ void EiScreen::fakeKey(uint32_t keycode, bool isDown) const
void EiScreen::enable()
{
// Nothing really to be done here
// Portal-based clipboard gets notifications via events
// Socket-based clipboard is passive (no monitoring needed)
m_activated = true;
updatePortalGlobalShortcuts();
}
void EiScreen::disable()
{
// Nothing to do here
// Portal-based clipboard gets notifications via events
// Socket-based clipboard is passive (no monitoring needed)
m_activated = false;
}
void EiScreen::cancelIdleEmulationTimer() const
@ -703,6 +734,10 @@ ButtonID EiScreen::mapButtonFromEvdev(ei_event *event) const
bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask)
{
// Check if the keyid is registered as a hotkey
// This is the implementation if InputCapture is active. If InputCapture is not active,
// the hotkey is handled by the portal and we don't get here.
auto it = m_hotkeys.find(keyid);
if (it == m_hotkeys.end()) {
@ -717,7 +752,6 @@ bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask)
sendEvent(type, HotKeyInfo::alloc(id));
return true;
}
return false;
}
@ -1091,4 +1125,21 @@ std::uint32_t EiScreen::HotKeySet::findByMask(std::uint32_t mask) const
return 0;
}
#ifdef HAVE_LIBPORTAL_SHORTCUTS
const std::vector<PortalGlobalShortcuts::HotKey> EiScreen::HotKeySet::getPortalHotKeys() const
{
std::vector<PortalGlobalShortcuts::HotKey> portalHotKeys;
portalHotKeys.reserve(m_set.size());
for (const auto &item : m_set) {
portalHotKeys.push_back({
.id = item.id,
.key = m_id,
.mask = item.mask,
});
}
return portalHotKeys;
}
#endif
} // namespace deskflow

View file

@ -9,6 +9,9 @@
#include "deskflow/IScreen.h"
#include "deskflow/PlatformScreen.h"
#ifdef HAVE_LIBPORTAL_SHORTCUTS
#include "platform/PortalGlobalShortcuts.h"
#endif
#include "platform/XDGPowerManager.h"
#include <bitset>
@ -30,6 +33,7 @@ namespace deskflow {
class EiKeyState;
class PortalRemoteDesktop;
class PortalInputCapture;
class PortalGlobalShortcuts;
class EiClipboard;
using ClipboardInfo = IScreen::ClipboardInfo;
@ -128,6 +132,7 @@ private:
void ensureEmulating() const;
void stopEmulating() const;
void cancelIdleEmulationTimer() const;
void updatePortalGlobalShortcuts();
static void handleEiLogEvent(ei *ei, const ei_log_priority priority, const char *message, ei_log_context *)
{
@ -192,6 +197,9 @@ private:
PortalRemoteDesktop *m_portalRemoteDesktop = nullptr;
PortalInputCapture *m_portalInputCapture = nullptr;
PortalGlobalShortcuts *m_portalGlobalShortcuts = nullptr;
bool m_activated = false;
struct HotKeyItem
{
@ -218,6 +226,13 @@ private:
bool removeById(std::uint32_t id);
void addItem(HotKeyItem item);
std::uint32_t findByMask(std::uint32_t mask) const;
bool empty() const
{
return m_set.empty();
}
#ifdef HAVE_LIBPORTAL_SHORTCUTS
const std::vector<PortalGlobalShortcuts::HotKey> getPortalHotKeys() const;
#endif
private:
KeyID m_id = 0;

View file

@ -0,0 +1,520 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "platform/PortalGlobalShortcuts.h"
#include "base/Log.h"
#include "base/TMethodJob.h"
#include "platform/EiScreen.h"
#include <inttypes.h>
#include <algorithm> // std::find_if, std::ranges::all_of
#include <cctype> // std::isalnum
#include <cstdio> // snprintf
namespace deskflow {
PortalGlobalShortcuts::PortalGlobalShortcuts(EiScreen *screen, IEventQueue *events)
: m_screen{screen},
m_events{events},
m_portal{xdp_portal_new()}
{
m_glibMainLoop = g_main_loop_new(nullptr, true);
auto tMethodJob = new TMethodJob<PortalGlobalShortcuts>(this, &PortalGlobalShortcuts::glibThread);
m_glibThread = new Thread(tMethodJob);
}
PortalGlobalShortcuts::~PortalGlobalShortcuts()
{
{
std::scoped_lock lock{m_bindingsMutex};
if (m_pendingIdleSource != 0) {
g_source_remove(m_pendingIdleSource);
m_pendingIdleSource = 0;
}
}
if (m_cancellable) {
g_cancellable_cancel(m_cancellable);
}
if (m_glibMainLoop && g_main_loop_is_running(m_glibMainLoop)) {
g_main_loop_quit(m_glibMainLoop);
}
if (m_glibThread) {
m_glibThread->cancel();
m_glibThread->wait();
delete m_glibThread;
m_glibThread = nullptr;
}
if (m_glibMainLoop) {
g_main_loop_unref(m_glibMainLoop);
m_glibMainLoop = nullptr;
}
closeSession();
if (m_cancellable) {
g_object_unref(m_cancellable);
m_cancellable = nullptr;
}
if (m_portal) {
g_object_unref(m_portal);
m_portal = nullptr;
}
}
void PortalGlobalShortcuts::setHotKeys(std::vector<HotKey> hotkeys)
{
std::vector<Binding> bindings;
bindings.reserve(hotkeys.size());
for (const auto &hotkey : hotkeys) {
bindings.push_back(makeBinding(hotkey));
}
std::scoped_lock lock{m_bindingsMutex};
m_bindings = std::move(bindings);
if (m_pendingIdleSource != 0) {
// Already scheduled — it will pick up the bindings we just stored via
// bindingsSnapshot() once it runs, so nothing further to do here.
return;
}
m_pendingIdleSource = g_idle_add(
[](gpointer data) -> gboolean {
auto *self = static_cast<PortalGlobalShortcuts *>(data);
{
std::scoped_lock lock{self->m_bindingsMutex};
self->m_pendingIdleSource = 0;
}
if (self->m_session) {
self->closeSession();
}
return self->createSession();
},
this
);
}
PortalGlobalShortcuts::Binding PortalGlobalShortcuts::makeBinding(const HotKey &hotkey)
{
Binding binding;
binding.id = hotkey.id;
binding.key = hotkey.key;
binding.mask = hotkey.mask;
binding.shortcutId = makeShortcutId(hotkey.key, hotkey.mask);
const std::string trigger = makePreferredTrigger(hotkey.key, hotkey.mask);
// This is not the prettiest way, but we currently have no direct access to the Hotkey object
// of lib/gui/Hotkey.cpp, so we cannot use Hotkey::text()
binding.description = trigger.empty() ? "Deskflow hotkey #" + std::to_string(hotkey.id) : "Hotkey " + trigger;
binding.preferredTrigger = trigger;
return binding;
}
std::vector<PortalGlobalShortcuts::Binding> PortalGlobalShortcuts::bindingsSnapshot() const
{
std::scoped_lock lock{m_bindingsMutex};
return m_bindings;
}
std::uint32_t PortalGlobalShortcuts::hotKeyIdForShortcutId(const char *shortcutId) const
{
std::scoped_lock lock{m_bindingsMutex};
const auto it = std::find_if(m_bindings.begin(), m_bindings.end(), [shortcutId](const Binding &binding) {
return binding.shortcutId == shortcutId;
});
if (it == m_bindings.end()) {
return 0;
}
return it->id;
}
gboolean PortalGlobalShortcuts::createSession()
{
// Supersede any still-pending CreateSession/BindShortcuts from an earlier attempt.
if (m_cancellable) {
g_cancellable_cancel(m_cancellable);
g_object_unref(m_cancellable);
m_cancellable = nullptr;
}
m_cancellable = g_cancellable_new();
LOG_DEBUG("creating global shortcuts session");
xdp_portal_create_global_shortcuts_session(
m_portal, m_cancellable,
[](GObject *obj, GAsyncResult *res, gpointer data) {
static_cast<PortalGlobalShortcuts *>(data)->createSessionDone(obj, res);
},
this
);
return G_SOURCE_REMOVE;
}
void PortalGlobalShortcuts::closeSession()
{
if (!m_session) {
LOG_DEBUG("no session to close");
return;
}
if (m_cancellable) {
g_cancellable_cancel(m_cancellable);
g_object_unref(m_cancellable);
m_cancellable = nullptr;
}
using enum Signal;
if (XdpSession *parentSession = xdp_global_shortcuts_session_get_session(m_session)) {
if (m_signals.at(SessionClosed)) {
g_signal_handler_disconnect(parentSession, m_signals.at(SessionClosed));
m_signals.at(SessionClosed) = 0;
}
}
if (m_signals.at(Activated)) {
g_signal_handler_disconnect(m_session, m_signals.at(Activated));
m_signals.at(Activated) = 0;
}
if (m_signals.at(Deactivated)) {
g_signal_handler_disconnect(m_session, m_signals.at(Deactivated));
m_signals.at(Deactivated) = 0;
}
if (m_signals.at(ShortcutsChanged)) {
g_signal_handler_disconnect(m_session, m_signals.at(ShortcutsChanged));
m_signals.at(ShortcutsChanged) = 0;
}
xdp_global_shortcuts_session_close(m_session);
g_object_unref(m_session);
m_session = nullptr;
}
void PortalGlobalShortcuts::setupSession(XdpGlobalShortcutsSession *session)
{
auto parentSession = xdp_global_shortcuts_session_get_session(session);
using enum Signal;
m_signals.at(SessionClosed) = g_signal_connect(G_OBJECT(parentSession), "closed", G_CALLBACK(sessionClosed), this);
m_signals.at(Activated) = g_signal_connect(G_OBJECT(session), "activated", G_CALLBACK(activated), this);
m_signals.at(Deactivated) = g_signal_connect(G_OBJECT(session), "deactivated", G_CALLBACK(deactivated), this);
m_signals.at(ShortcutsChanged) =
g_signal_connect(G_OBJECT(session), "shortcuts-changed", G_CALLBACK(shortcutsChanged), this);
}
void PortalGlobalShortcuts::bindShortcuts()
{
const auto bindings = bindingsSnapshot();
LOG_DEBUG("binding %zu global shortcuts", bindings.size());
g_autoptr(GPtrArray) shortcuts = nullptr;
shortcuts = g_ptr_array_new_with_free_func((GDestroyNotify)xdp_global_shortcut_free);
for (const auto &binding : bindings) {
g_ptr_array_add(
shortcuts, xdp_global_shortcut_new(
binding.shortcutId.c_str(), binding.description.c_str(),
binding.preferredTrigger.empty() ? nullptr : binding.preferredTrigger.c_str()
)
);
}
xdp_global_shortcuts_session_bind_shortcuts(
m_session, shortcuts,
nullptr, // parent window
m_cancellable,
[](GObject *object, GAsyncResult *res, gpointer data) {
static_cast<PortalGlobalShortcuts *>(data)->bindShortcutsDone(object, res);
},
this
);
}
void PortalGlobalShortcuts::createSessionDone(GObject *object, GAsyncResult *res)
{
g_autoptr(GError) error = nullptr;
auto session = xdp_portal_create_global_shortcuts_session_finish(XDP_PORTAL(object), res, &error);
if (!session) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
LOG_DEBUG("global shortcuts session creation was superseded");
return;
}
LOG_WARN("failed to create global shortcuts session, hotkeys will not be forwarded: %s", error->message);
return;
}
LOG_DEBUG("portal global shortcuts session created");
m_session = session;
setupSession(session);
bindShortcuts();
}
void PortalGlobalShortcuts::bindShortcutsDone(GObject *object, GAsyncResult *res)
{
g_autoptr(GError) error = nullptr;
g_autoptr(GPtrArray) shortcuts = nullptr;
if (XDP_GLOBAL_SHORTCUTS_SESSION(object) != m_session) {
return;
}
shortcuts = xdp_global_shortcuts_session_bind_shortcuts_finish(m_session, res, &error);
if (!shortcuts) {
if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
LOG_DEBUG("global shortcuts binding was superseded");
return;
}
LOG_ERR("failed to bind global shortcuts: %s", error->message);
return;
}
LOG_DEBUG("bound %u global shortcuts", shortcuts->len);
for (guint i = 0; i < shortcuts->len; i++) {
XdpGlobalShortcutAssigned *shortcut;
shortcut = static_cast<XdpGlobalShortcutAssigned *>(g_ptr_array_index(shortcuts, i));
LOG_DEBUG(
"global shortcut %s assigned to %s", xdp_global_shortcut_assigned_get_shortcut_id(shortcut),
xdp_global_shortcut_assigned_get_trigger_description(shortcut)
);
}
// EiScreen can't be notified about the change, because triggerDescription is only a
// human-readable string and not a preferred trigger string
}
void PortalGlobalShortcuts::handleSessionClosed(XdpSession *)
{
LOG_WARN("portal global shortcuts session was closed by the system, will attempt to recreate it");
closeSession();
createSession();
}
void PortalGlobalShortcuts::handleActivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *
)
{
if (session != m_session) {
return;
}
const auto hotKeyId = hotKeyIdForShortcutId(shortcutId);
if (hotKeyId == 0) {
LOG_WARN("unknown global shortcut activated: %s", shortcutId);
return;
}
LOG_DEBUG("global shortcut activated: %s timestamp=%" PRIu64, shortcutId, timestamp);
m_events->addEvent(Event(
EventTypes::PrimaryScreenHotkeyDown, m_screen->getEventTarget(), IPrimaryScreen::HotKeyInfo::alloc(hotKeyId)
));
}
void PortalGlobalShortcuts::handleDeactivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *
)
{
if (session != m_session) {
return;
}
const auto hotKeyId = hotKeyIdForShortcutId(shortcutId);
if (hotKeyId == 0) {
LOG_WARN("unknown global shortcut deactivated: %s", shortcutId);
return;
}
LOG_DEBUG("global shortcut deactivated: %s timestamp=%" PRIu64, shortcutId, timestamp);
m_events->addEvent(
Event(EventTypes::PrimaryScreenHotkeyUp, m_screen->getEventTarget(), IPrimaryScreen::HotKeyInfo::alloc(hotKeyId))
);
}
void PortalGlobalShortcuts::handleShortcutsChanged(XdpGlobalShortcutsSession *session, GPtrArray *shortcuts)
{
if (session != m_session) {
return;
}
if (shortcuts == nullptr) {
LOG_WARN("global shortcuts changed without shortcuts data");
return;
}
LOG_DEBUG("global shortcuts changed");
for (guint i = 0; i < shortcuts->len; i++) {
XdpGlobalShortcutAssigned *shortcut;
const char *shortcutId;
const char *triggerDescription;
shortcut = static_cast<XdpGlobalShortcutAssigned *>(g_ptr_array_index(shortcuts, i));
shortcutId = xdp_global_shortcut_assigned_get_shortcut_id(shortcut);
triggerDescription = xdp_global_shortcut_assigned_get_trigger_description(shortcut);
LOG_DEBUG("global shortcut %s changed to %s", shortcutId, triggerDescription);
// EiScreen can't be notified about the change, because triggerDescription is only a
// human-readable string and not a preferred trigger string
}
}
void PortalGlobalShortcuts::glibThread(const void *)
{
auto context = g_main_loop_get_context(m_glibMainLoop);
LOG_DEBUG("global shortcuts glib thread running");
while (g_main_loop_is_running(m_glibMainLoop)) {
Thread::testCancel();
g_main_context_iteration(context, true);
}
LOG_DEBUG("global shortcuts glib thread shutting down");
}
// see https://specifications.freedesktop.org/shortcuts/latest/ on how to construct
// a preferred trigger string for a key combination
bool PortalGlobalShortcuts::isValidPreferredTriggerIdentifier(const std::string &name)
{
if (name.empty()) {
return false;
}
return std::ranges::all_of(name, [](unsigned char c) { return std::isalnum(c) || c == '_'; });
}
std::optional<xkb_keysym_t> PortalGlobalShortcuts::deskflowKeyToXkbKeysym(KeyID key)
{
if (key == kKeyNone) {
return std::nullopt;
}
// KeyTypes.h explicitly excludes
// 0xE000..0xE0FF from the "X11 keysym - 0x1000" rule.
if (key >= 0xE000 && key <= 0xE0FF) {
return std::nullopt;
}
// Deskflow control/function keys. KeyTypes.h says these
// are equal to the corresponding X11 keysym - 0x1000.
if (key >= 0xE100 && key <= 0xEFFF) {
return static_cast<xkb_keysym_t>(key + 0x1000);
}
// Remaining values are normal UTF-32/Unicode codepoints.
if (key < 0xE000) {
const auto keysym = xkb_utf32_to_keysym(key);
if (keysym == XKB_KEY_NoSymbol) {
return std::nullopt;
}
return keysym;
}
return std::nullopt;
}
std::string PortalGlobalShortcuts::keyNameForPreferredTrigger(KeyID key)
{
const auto keysym = deskflowKeyToXkbKeysym(key);
if (!keysym) {
return {};
}
char name[128] = {};
const int len = xkb_keysym_get_name(*keysym, name, sizeof(name));
if (len <= 0 || static_cast<std::size_t>(len) >= sizeof(name)) {
return {};
}
std::string result{name};
if (!isValidPreferredTriggerIdentifier(result)) {
return {};
}
return result;
}
std::string PortalGlobalShortcuts::makePreferredTrigger(KeyID key, KeyModifierMask mask)
{
constexpr KeyModifierMask supportedMask = KeyModifierShift | KeyModifierControl | KeyModifierAlt | KeyModifierMeta |
KeyModifierSuper | KeyModifierAltGr | KeyModifierLevel5Lock |
KeyModifierCapsLock | KeyModifierNumLock | KeyModifierScrollLock;
if ((mask & ~supportedMask) != 0) {
return {};
}
const auto keyName = keyNameForPreferredTrigger(key);
if (keyName.empty()) {
return {};
}
std::vector<std::string> parts;
if (mask & KeyModifierShift) {
parts.emplace_back("SHIFT");
}
if (mask & KeyModifierControl) {
parts.emplace_back("CTRL");
}
if (mask & KeyModifierAlt) {
parts.emplace_back("ALT");
}
if (mask & (KeyModifierMeta | KeyModifierSuper)) {
parts.emplace_back("LOGO");
}
if (mask & KeyModifierAltGr) {
parts.emplace_back("LEVEL3");
}
if (mask & KeyModifierLevel5Lock) {
parts.emplace_back("LEVEL5");
}
if (mask & KeyModifierCapsLock) {
parts.emplace_back("CAPS");
}
if (mask & KeyModifierNumLock) {
parts.emplace_back("NUM");
}
if (mask & KeyModifierScrollLock) {
parts.emplace_back("SCROLL");
}
parts.emplace_back(keyName);
std::string trigger;
for (std::size_t i = 0; i < parts.size(); ++i) {
if (i != 0) {
trigger += "+";
}
trigger += parts[i];
}
return trigger;
}
std::string PortalGlobalShortcuts::makeShortcutId(KeyID key, KeyModifierMask mask)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "deskflow-hotkey-%08x-%08x", mask, key);
return buffer;
}
} // namespace deskflow

View file

@ -0,0 +1,140 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2026 Deskflow Developers
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "base/IEventQueue.h"
#include "mt/Thread.h"
#include <glib.h>
#include <libportal/globalshortcuts.h>
#include <optional>
#include <xkbcommon/xkbcommon.h>
#include <map>
#include <mutex>
#include <string>
#include <vector>
#include "deskflow/KeyTypes.h"
namespace deskflow {
class EiScreen;
class PortalGlobalShortcuts
{
public:
PortalGlobalShortcuts(EiScreen *screen, IEventQueue *events);
~PortalGlobalShortcuts();
struct HotKey
{
std::uint32_t id = 0;
KeyID key = kKeyNone;
KeyModifierMask mask = 0;
};
void setHotKeys(std::vector<HotKey> hotkeys);
private:
void glibThread(const void *);
void setupSession(XdpGlobalShortcutsSession *session);
void createSessionDone(GObject *object, GAsyncResult *res);
void bindShortcutsDone(GObject *object, GAsyncResult *res);
void handleSessionClosed(XdpSession *session);
void handleActivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *options
);
void handleDeactivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *options
);
void handleShortcutsChanged(XdpGlobalShortcutsSession *session, GPtrArray *shortcuts);
/// g_signal_connect callback wrapper
static void sessionClosed(XdpSession *session, const gpointer data)
{
static_cast<PortalGlobalShortcuts *>(data)->handleSessionClosed(session);
}
static void activated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *options,
const gpointer data
)
{
static_cast<PortalGlobalShortcuts *>(data)->handleActivated(session, shortcutId, timestamp, options);
}
static void deactivated(
XdpGlobalShortcutsSession *session, const char *shortcutId, const guint64 timestamp, const GVariant *options,
const gpointer data
)
{
static_cast<PortalGlobalShortcuts *>(data)->handleDeactivated(session, shortcutId, timestamp, options);
}
static void shortcutsChanged(XdpGlobalShortcutsSession *session, GPtrArray *shortcuts, const gpointer data)
{
static_cast<PortalGlobalShortcuts *>(data)->handleShortcutsChanged(session, shortcuts);
}
enum class Signal : uint8_t
{
SessionClosed,
Activated,
Deactivated,
ShortcutsChanged,
};
struct Binding
{
std::uint32_t id = 0;
KeyID key = kKeyNone;
KeyModifierMask mask = 0;
std::string shortcutId;
std::string description;
std::string preferredTrigger;
};
static std::optional<xkb_keysym_t> deskflowKeyToXkbKeysym(KeyID key);
static std::string keyNameForPreferredTrigger(KeyID key);
static std::string makePreferredTrigger(KeyID key, KeyModifierMask mask);
static bool isValidPreferredTriggerIdentifier(const std::string &name);
gboolean createSession();
void closeSession();
void bindShortcuts();
std::uint32_t hotKeyIdForShortcutId(const char *shortcutId) const;
static Binding makeBinding(const HotKey &hotkey);
std::vector<Binding> bindingsSnapshot() const;
static std::string makeShortcutId(KeyID key, KeyModifierMask mask);
EiScreen *m_screen = nullptr;
IEventQueue *m_events = nullptr;
Thread *m_glibThread = nullptr;
GMainLoop *m_glibMainLoop = nullptr;
XdpPortal *m_portal = nullptr;
XdpGlobalShortcutsSession *m_session = nullptr;
GCancellable *m_cancellable = nullptr;
std::map<Signal, gulong> m_signals = {
{Signal::SessionClosed, 0},
{Signal::Activated, 0},
{Signal::Deactivated, 0},
{Signal::ShortcutsChanged, 0},
};
mutable std::mutex m_bindingsMutex;
guint m_pendingIdleSource = 0; // guarded by m_bindingsMutex
// Snapshot derived from EiScreen::m_hotkeys. This is portal-facing state
std::vector<Binding> m_bindings;
};
} // namespace deskflow