feat: globalshortcuts portal support
This commit is contained in:
parent
ce2f948c29
commit
217166c06b
5 changed files with 748 additions and 12 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
520
src/lib/platform/PortalGlobalShortcuts.cpp
Normal file
520
src/lib/platform/PortalGlobalShortcuts.cpp
Normal 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
|
||||
140
src/lib/platform/PortalGlobalShortcuts.h
Normal file
140
src/lib/platform/PortalGlobalShortcuts.h
Normal 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
|
||||
Loading…
Reference in a new issue