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};")
|
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_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_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()
|
cmake_pop_check_state()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
@ -166,6 +167,12 @@ elseif(UNIX)
|
||||||
PortalClipboard.h
|
PortalClipboard.h
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
if(HAVE_LP_SHORTCUTS)
|
||||||
|
list(APPEND PLATFORM_SOURCES
|
||||||
|
PortalGlobalShortcuts.cpp
|
||||||
|
PortalGlobalShortcuts.h
|
||||||
|
)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -192,6 +199,9 @@ if(UNIX)
|
||||||
if (HAVE_LP_CLIPBOARD)
|
if (HAVE_LP_CLIPBOARD)
|
||||||
target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_CLIPBOARD)
|
target_compile_definitions(platform PUBLIC HAVE_LIBPORTAL_CLIPBOARD)
|
||||||
endif()
|
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_include_directories(platform PUBLIC ${LIBEI_INCLUDE_DIRS} ${LIBPORTAL_INCLUDE_DIRS})
|
||||||
target_link_libraries(platform
|
target_link_libraries(platform
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,11 @@ EiScreen::EiScreen(bool isPrimary, IEventQueue *events, bool usePortal)
|
||||||
handleConnectedToEisEvent(e);
|
handleConnectedToEisEvent(e);
|
||||||
});
|
});
|
||||||
if (isPrimary) {
|
if (isPrimary) {
|
||||||
m_portalInputCapture = new PortalInputCapture(this, m_events);
|
|
||||||
// Portal input capture manages its own clipboard
|
// 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 {
|
} else {
|
||||||
m_events->addHandler(EventTypes::EISessionClosed, getEventTarget(), [this](const auto &) {
|
m_events->addHandler(EventTypes::EISessionClosed, getEventTarget(), [this](const auto &) {
|
||||||
handlePortalSessionClosed();
|
handlePortalSessionClosed();
|
||||||
|
|
@ -97,8 +100,11 @@ EiScreen::~EiScreen()
|
||||||
delete m_keyState;
|
delete m_keyState;
|
||||||
delete m_clipboard;
|
delete m_clipboard;
|
||||||
|
|
||||||
delete m_portalRemoteDesktop;
|
#ifdef HAVE_LIBPORTAL_SHORTCUTS
|
||||||
|
delete m_portalGlobalShortcuts;
|
||||||
|
#endif
|
||||||
delete m_portalInputCapture;
|
delete m_portalInputCapture;
|
||||||
|
delete m_portalRemoteDesktop;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::eiLogEvent(ei_log_priority priority, const char *message) const
|
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));
|
set->second.addItem(HotKeyItem(mask, id));
|
||||||
|
|
||||||
|
updatePortalGlobalShortcuts();
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::unregisterHotKey(uint32_t id)
|
void EiScreen::unregisterHotKey(uint32_t id)
|
||||||
{
|
{
|
||||||
for (auto &[key, set] : m_hotkeys) {
|
for (auto it = m_hotkeys.begin(); it != m_hotkeys.end(); ++it) {
|
||||||
(void)key;
|
if (it->second.removeById(id)) {
|
||||||
if (set.removeById(id)) {
|
if (it->second.empty())
|
||||||
|
m_hotkeys.erase(it);
|
||||||
|
updatePortalGlobalShortcuts();
|
||||||
break;
|
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()
|
void EiScreen::fakeInputBegin()
|
||||||
{
|
{
|
||||||
// FIXME -- not implemented
|
// FIXME -- not implemented
|
||||||
|
|
@ -359,16 +393,13 @@ void EiScreen::fakeKey(uint32_t keycode, bool isDown) const
|
||||||
|
|
||||||
void EiScreen::enable()
|
void EiScreen::enable()
|
||||||
{
|
{
|
||||||
// Nothing really to be done here
|
m_activated = true;
|
||||||
// Portal-based clipboard gets notifications via events
|
updatePortalGlobalShortcuts();
|
||||||
// Socket-based clipboard is passive (no monitoring needed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::disable()
|
void EiScreen::disable()
|
||||||
{
|
{
|
||||||
// Nothing to do here
|
m_activated = false;
|
||||||
// Portal-based clipboard gets notifications via events
|
|
||||||
// Socket-based clipboard is passive (no monitoring needed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::cancelIdleEmulationTimer() const
|
void EiScreen::cancelIdleEmulationTimer() const
|
||||||
|
|
@ -703,6 +734,10 @@ ButtonID EiScreen::mapButtonFromEvdev(ei_event *event) const
|
||||||
|
|
||||||
bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask)
|
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);
|
auto it = m_hotkeys.find(keyid);
|
||||||
|
|
||||||
if (it == m_hotkeys.end()) {
|
if (it == m_hotkeys.end()) {
|
||||||
|
|
@ -717,7 +752,6 @@ bool EiScreen::onHotkey(KeyID keyid, bool isPressed, KeyModifierMask mask)
|
||||||
sendEvent(type, HotKeyInfo::alloc(id));
|
sendEvent(type, HotKeyInfo::alloc(id));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1091,4 +1125,21 @@ std::uint32_t EiScreen::HotKeySet::findByMask(std::uint32_t mask) const
|
||||||
return 0;
|
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
|
} // namespace deskflow
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
|
|
||||||
#include "deskflow/IScreen.h"
|
#include "deskflow/IScreen.h"
|
||||||
#include "deskflow/PlatformScreen.h"
|
#include "deskflow/PlatformScreen.h"
|
||||||
|
#ifdef HAVE_LIBPORTAL_SHORTCUTS
|
||||||
|
#include "platform/PortalGlobalShortcuts.h"
|
||||||
|
#endif
|
||||||
#include "platform/XDGPowerManager.h"
|
#include "platform/XDGPowerManager.h"
|
||||||
|
|
||||||
#include <bitset>
|
#include <bitset>
|
||||||
|
|
@ -30,6 +33,7 @@ namespace deskflow {
|
||||||
class EiKeyState;
|
class EiKeyState;
|
||||||
class PortalRemoteDesktop;
|
class PortalRemoteDesktop;
|
||||||
class PortalInputCapture;
|
class PortalInputCapture;
|
||||||
|
class PortalGlobalShortcuts;
|
||||||
class EiClipboard;
|
class EiClipboard;
|
||||||
|
|
||||||
using ClipboardInfo = IScreen::ClipboardInfo;
|
using ClipboardInfo = IScreen::ClipboardInfo;
|
||||||
|
|
@ -128,6 +132,7 @@ private:
|
||||||
void ensureEmulating() const;
|
void ensureEmulating() const;
|
||||||
void stopEmulating() const;
|
void stopEmulating() const;
|
||||||
void cancelIdleEmulationTimer() const;
|
void cancelIdleEmulationTimer() const;
|
||||||
|
void updatePortalGlobalShortcuts();
|
||||||
|
|
||||||
static void handleEiLogEvent(ei *ei, const ei_log_priority priority, const char *message, ei_log_context *)
|
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;
|
PortalRemoteDesktop *m_portalRemoteDesktop = nullptr;
|
||||||
PortalInputCapture *m_portalInputCapture = nullptr;
|
PortalInputCapture *m_portalInputCapture = nullptr;
|
||||||
|
PortalGlobalShortcuts *m_portalGlobalShortcuts = nullptr;
|
||||||
|
|
||||||
|
bool m_activated = false;
|
||||||
|
|
||||||
struct HotKeyItem
|
struct HotKeyItem
|
||||||
{
|
{
|
||||||
|
|
@ -218,6 +226,13 @@ private:
|
||||||
bool removeById(std::uint32_t id);
|
bool removeById(std::uint32_t id);
|
||||||
void addItem(HotKeyItem item);
|
void addItem(HotKeyItem item);
|
||||||
std::uint32_t findByMask(std::uint32_t mask) const;
|
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:
|
private:
|
||||||
KeyID m_id = 0;
|
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