From 24841781025e5d6907d9f23d17bfe163bea14958 Mon Sep 17 00:00:00 2001 From: liujiangtao1 Date: Mon, 24 Aug 2026 14:27:44 +0800 Subject: [PATCH 1/2] feat: implement zwp_keyboard_shortcuts_inhibit_unstable_v1 server-side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add KeyboardShortcutsInhibitManagerV1 module wrapping the wlroots keyboard-shortcuts-inhibit implementation with focus-driven activation. 新增 KeyboardShortcutsInhibitManagerV1 模块,封装 wlroots 键盘快捷键 抑制实现,激活状态严格绑定键盘焦点。 Log: 实现键盘快捷键抑制协议服务端 Influence: 客户端可通过该协议抑制合成器键盘快捷键,支持焦点驱动激活与VT切换逃生通道。 --- src/common/treelandlogging.cpp | 1 + src/common/treelandlogging.h | 1 + src/modules/CMakeLists.txt | 1 + .../keyboard-shortcuts-inhibit/CMakeLists.txt | 9 + .../keyboardshortcutsinhibitmanager.cpp | 222 ++++++++++++++++++ .../keyboardshortcutsinhibitmanager.h | 49 ++++ src/seat/helper.cpp | 11 + src/seat/helper.h | 2 + 8 files changed, 296 insertions(+) create mode 100644 src/modules/keyboard-shortcuts-inhibit/CMakeLists.txt create mode 100644 src/modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.cpp create mode 100644 src/modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.h diff --git a/src/common/treelandlogging.cpp b/src/common/treelandlogging.cpp index 31291b63a5..12b9944850 100644 --- a/src/common/treelandlogging.cpp +++ b/src/common/treelandlogging.cpp @@ -19,6 +19,7 @@ Q_LOGGING_CATEGORY(lcTlInput, "treeland.input") Q_LOGGING_CATEGORY(lcTlInputManager, "treeland.input.manager") Q_LOGGING_CATEGORY(lcTlGestures, "treeland.input.gestures") Q_LOGGING_CATEGORY(lcTlKeyboardNotify, "treeland.input.keyboard.state.notify") +Q_LOGGING_CATEGORY(lcTlKeyboardShortcutsInhibit, "treeland.input.keyboard.shortcuts.inhibit") // Seat management Q_LOGGING_CATEGORY(lcTlSeat, "treeland.seat") diff --git a/src/common/treelandlogging.h b/src/common/treelandlogging.h index bf444b750e..75328f8145 100644 --- a/src/common/treelandlogging.h +++ b/src/common/treelandlogging.h @@ -22,6 +22,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcTlInput) Q_DECLARE_LOGGING_CATEGORY(lcTlInputManager) Q_DECLARE_LOGGING_CATEGORY(lcTlGestures) Q_DECLARE_LOGGING_CATEGORY(lcTlKeyboardNotify) +Q_DECLARE_LOGGING_CATEGORY(lcTlKeyboardShortcutsInhibit) // Seat management Q_DECLARE_LOGGING_CATEGORY(lcTlSeat) diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index 5590aff22c..ac6edce360 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -44,3 +44,4 @@ add_subdirectory(wallpaper) add_subdirectory(activation) add_subdirectory(input-manager) add_subdirectory(keyboard-state-notify) +add_subdirectory(keyboard-shortcuts-inhibit) diff --git a/src/modules/keyboard-shortcuts-inhibit/CMakeLists.txt b/src/modules/keyboard-shortcuts-inhibit/CMakeLists.txt new file mode 100644 index 0000000000..b965c0b31e --- /dev/null +++ b/src/modules/keyboard-shortcuts-inhibit/CMakeLists.txt @@ -0,0 +1,9 @@ +impl_treeland( + NAME + module_keyboard_shortcuts_inhibit + SOURCE + keyboardshortcutsinhibitmanager.h + keyboardshortcutsinhibitmanager.cpp + LINK + Qt6::Core +) diff --git a/src/modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.cpp b/src/modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.cpp new file mode 100644 index 0000000000..e2b946c33f --- /dev/null +++ b/src/modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.cpp @@ -0,0 +1,222 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "keyboardshortcutsinhibitmanager.h" +#include "seat/helper.h" +#include "seatsmanager.h" +#include "common/treelandlogging.h" + +#include +#include + +extern "C" { +#include +} + +#include + +#include + +struct InhibitorEntry { + wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor = nullptr; + WScopedListener destroyListener; +}; + +class KeyboardShortcutsInhibitManagerV1Private +{ +public: + explicit KeyboardShortcutsInhibitManagerV1Private(KeyboardShortcutsInhibitManagerV1 *q) + : q(q) {} + + KeyboardShortcutsInhibitManagerV1 *q = nullptr; + WScopedListener newInhibitorListener; + std::vector inhibitors; + QHash seatFocusConnections; + bool seatConnectionsSetup = false; + + wlr_keyboard_shortcuts_inhibit_manager_v1 *handle() const + { + return reinterpret_cast(q->handle()); + } + + void setupSeatConnections(); + void onNewInhibitor(wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor); + void onInhibitorDestroy(wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor); + void onSeatFocusChanged(WSeat *seat); + void handleSeatAdded(WSeat *seat); + void handleSeatRemoved(WSeat *seat); +}; + +void KeyboardShortcutsInhibitManagerV1Private::setupSeatConnections() +{ + if (seatConnectionsSetup) + return; + seatConnectionsSetup = true; + + auto *helper = Helper::instance(); + auto *seatManager = helper->seatManager(); + const auto seats = seatManager->seats(); + for (auto *seat : seats) { + handleSeatAdded(seat); + } + + QObject::connect(seatManager, &SeatsManager::seatAdded, q, [this](WSeat *seat) { + handleSeatAdded(seat); + }); + QObject::connect(seatManager, &SeatsManager::seatRemoved, q, [this](WSeat *seat) { + handleSeatRemoved(seat); + }); +} + +void KeyboardShortcutsInhibitManagerV1Private::handleSeatAdded(WSeat *seat) +{ + if (seatFocusConnections.contains(seat)) + return; + + auto conn = QObject::connect(seat, &WSeat::keyboardFocusSurfaceChanged, q, [this, seat]() { + onSeatFocusChanged(seat); + }); + seatFocusConnections.insert(seat, conn); + + // Activate inhibitors for surfaces that already have focus on this seat. + onSeatFocusChanged(seat); +} + +void KeyboardShortcutsInhibitManagerV1Private::handleSeatRemoved(WSeat *seat) +{ + auto it = seatFocusConnections.find(seat); + if (it != seatFocusConnections.end()) { + QObject::disconnect(it.value()); + seatFocusConnections.erase(it); + } +} + +void KeyboardShortcutsInhibitManagerV1Private::onNewInhibitor(wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor) +{ + qCInfo(lcTlKeyboardShortcutsInhibit) << "New inhibitor created for surface" + << inhibitor->surface << "seat" << inhibitor->seat; + + InhibitorEntry entry; + entry.inhibitor = inhibitor; + entry.destroyListener.init(&inhibitor->events.destroy, q, [this, inhibitor](void *) { + onInhibitorDestroy(inhibitor); + }); + inhibitors.push_back(std::move(entry)); + + // If the surface already has keyboard focus on this seat, activate the + // inhibitor immediately (sends 'active' to the client). + auto *wSeat = WSeat::fromHandle(inhibitor->seat); + if (wSeat) { + auto *focusSurface = wSeat->keyboardFocusSurface(); + if (focusSurface && focusSurface->handle() == inhibitor->surface) { + wlr_keyboard_shortcuts_inhibitor_v1_activate(inhibitor); + } + } +} + +void KeyboardShortcutsInhibitManagerV1Private::onInhibitorDestroy(wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor) +{ + qCInfo(lcTlKeyboardShortcutsInhibit) << "Inhibitor destroyed for surface" + << inhibitor->surface << "seat" << inhibitor->seat; + + inhibitors.erase(std::remove_if(inhibitors.begin(), inhibitors.end(), + [inhibitor](const InhibitorEntry &e) { + return e.inhibitor == inhibitor; + }), + inhibitors.end()); +} + +void KeyboardShortcutsInhibitManagerV1Private::onSeatFocusChanged(WSeat *seat) +{ + wlr_seat *wlrSeat = seat->handle(); + auto *focusSurface = seat->keyboardFocusSurface(); + wlr_surface *wlrSurface = focusSurface ? focusSurface->handle() : nullptr; + + for (auto &entry : inhibitors) { + if (entry.inhibitor->seat != wlrSeat) + continue; + + if (wlrSurface && entry.inhibitor->surface == wlrSurface) { + // Keyboard focus entered this surface: activate the inhibitor. + // wlr_keyboard_shortcuts_inhibitor_v1_activate() is idempotent + // (only sends 'active' when not already active), so this safely + // re-activates on focus regain. + wlr_keyboard_shortcuts_inhibitor_v1_activate(entry.inhibitor); + } else if (entry.inhibitor->active) { + // Keyboard focus left this surface: restore the compositor's own + // shortcuts. Per the protocol, no 'inactive' event is sent on + // focus leave, so reset 'active' directly (bypassing deactivate()) + // to let activate() re-fire on the next focus enter. + entry.inhibitor->active = false; + } + } +} + +KeyboardShortcutsInhibitManagerV1::KeyboardShortcutsInhibitManagerV1(QObject *parent) + : QObject(parent) + , d(new KeyboardShortcutsInhibitManagerV1Private(this)) +{ +} + +KeyboardShortcutsInhibitManagerV1::~KeyboardShortcutsInhibitManagerV1() = default; + +QByteArrayView KeyboardShortcutsInhibitManagerV1::interfaceName() const +{ + return "zwp_keyboard_shortcuts_inhibit_manager_v1"; +} + +void KeyboardShortcutsInhibitManagerV1::create(WServer *server) +{ + m_handle = wlr_keyboard_shortcuts_inhibit_v1_create(server->handle()); + if (!m_handle) { + qCWarning(lcTlKeyboardShortcutsInhibit) << "Failed to create keyboard shortcuts inhibit manager"; + return; + } + + d->newInhibitorListener.init(&d->handle()->events.new_inhibitor, this, [this](void *data) { + auto *inhibitor = static_cast(data); + d->onNewInhibitor(inhibitor); + }); + + d->setupSeatConnections(); +} + +void KeyboardShortcutsInhibitManagerV1::destroy([[maybe_unused]] WServer *server) +{ + d->newInhibitorListener.disconnect(); + for (auto &entry : d->inhibitors) + entry.destroyListener.disconnect(); + d->inhibitors.clear(); + + for (auto &conn : d->seatFocusConnections) + QObject::disconnect(conn); + d->seatFocusConnections.clear(); + d->seatConnectionsSetup = false; + + // The wlr manager is reclaimed by display.reset() in WServer::stop(); + // null m_handle so handle()/global() return null instead of a dangling + // pointer. + m_handle = nullptr; +} + +wl_global *KeyboardShortcutsInhibitManagerV1::global() const +{ + return d->handle() ? d->handle()->global : nullptr; +} + +bool KeyboardShortcutsInhibitManagerV1::isInhibited(wlr_seat *seat, wlr_surface *surface) const +{ + for (const auto &entry : d->inhibitors) { + if (entry.inhibitor->active && entry.inhibitor->seat == seat && entry.inhibitor->surface == surface) + return true; + } + return false; +} + +void KeyboardShortcutsInhibitManagerV1::deactivateActiveInhibitor(wlr_seat *seat) +{ + for (auto &entry : d->inhibitors) { + if (entry.inhibitor->seat == seat && entry.inhibitor->active) + wlr_keyboard_shortcuts_inhibitor_v1_deactivate(entry.inhibitor); + } +} diff --git a/src/modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.h b/src/modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.h new file mode 100644 index 0000000000..3a0f56b9b5 --- /dev/null +++ b/src/modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.h @@ -0,0 +1,49 @@ +// Copyright (C) 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include +#include + +#include + +#include + +#include + +struct wlr_keyboard_shortcuts_inhibitor_v1; +struct wlr_keyboard_shortcuts_inhibit_manager_v1; +struct wlr_surface; +struct wlr_seat; + +WAYLIB_SERVER_USE_NAMESPACE + +class KeyboardShortcutsInhibitManagerV1Private; + +class KeyboardShortcutsInhibitManagerV1 : public QObject, public WServerInterface +{ + Q_OBJECT +public: + explicit KeyboardShortcutsInhibitManagerV1(QObject *parent = nullptr); + ~KeyboardShortcutsInhibitManagerV1() override; + + QByteArrayView interfaceName() const override; + + // Returns true if keyboard shortcuts are currently inhibited for the + // given surface+seat pair (i.e. an active inhibitor exists). + bool isInhibited(wlr_seat *seat, wlr_surface *surface) const; + + // Force-deactivate the active inhibitor for the given seat, sending the + // 'inactive' event to the client. Used by the compositor's built-in + // non-inhibitable escape-hatch shortcuts (e.g. Ctrl+Alt+Fn VT switch). + void deactivateActiveInhibitor(wlr_seat *seat); + +protected: // WServerInterface + void create(WServer *server) override; + void destroy(WServer *server) override; + wl_global *global() const override; + +private: + std::unique_ptr d; +}; diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 81d8afbf40..af53ff2bcb 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -41,6 +41,7 @@ #include "modules/ddm/ddminterfacev1.h" #include "modules/input-manager/inputmanagerinterfacev1.h" #include "modules/keyboard-state-notify/keyboardstatenotifymanagerinterfacev1.h" +#include "modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.h" #include "modules/output-manager/outputmanagement.h" #include "modules/personalization/personalizationmanagerinterfacev1.h" #include "modules/resource/treelandremotesource.h" @@ -2277,6 +2278,7 @@ void Helper::init(Treeland::Treeland *treeland) &InputManager::onKeyboardSettingsCreated); m_keyboardStateNotifyManagerInterfaceV1 = m_server->attach(); + m_keyboardShortcutsInhibitManagerV1 = m_server->attach(); #if TREELANDCONFIG_DCONFIG_FILE_VERSION_MINOR > 0 if (m_globalConfig->isInitializeSucceeded()) { @@ -2457,6 +2459,8 @@ bool Helper::beforeDisposeEvent(WSeat *seat, QWindow *targetWindow, QInputEvent qCWarning(lcTlCore) << "Ctrl+Alt+Fn VT shortcut requested" << vtnr; wlr_session_change_vt(m_backend->session(), vtnr); + if (m_keyboardShortcutsInhibitManagerV1) + m_keyboardShortcutsInhibitManagerV1->deactivateActiveInhibitor(seat->handle()); return true; } } @@ -2599,6 +2603,13 @@ bool Helper::beforeDisposeEvent(WSeat *seat, QWindow *targetWindow, QInputEvent } } + // Suppress compositor shortcuts when a keyboard shortcuts inhibitor is active + if (m_keyboardShortcutsInhibitManagerV1) { + auto *focusSurface = seat->keyboardFocusSurface(); + if (focusSurface && m_keyboardShortcutsInhibitManagerV1->isInhibited(seat->handle(), focusSurface->handle())) + return false; + } + // Capture mode: intercept key events before dispatchKeyEvent if (m_shortcutManager->isCaptureActive() && m_shortcutManager->tryHandleCaptureEvent(seat, event)) return true; diff --git a/src/seat/helper.h b/src/seat/helper.h index ba2f92d43f..47be61671f 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -128,6 +128,7 @@ class WallpaperColorInterfaceV1; class WindowManagementInterfaceV1; class WindowPickerInterface; class TreelandKeyboardStateNotifyManagerInterfaceV1; +class KeyboardShortcutsInhibitManagerV1; class WallpaperManager; class WallpaperItem; class TreelandInputManagerInterfaceV1; @@ -475,6 +476,7 @@ private Q_SLOTS: TreelandWallpaperManagerInterfaceV1 *m_wallpaperManagerInterfaceV1 = nullptr; TreelandWallpaperNotifierInterfaceV1 *m_wallpaperNotifierInterfaceV1 = nullptr; TreelandKeyboardStateNotifyManagerInterfaceV1 *m_keyboardStateNotifyManagerInterfaceV1 = nullptr; + KeyboardShortcutsInhibitManagerV1 *m_keyboardShortcutsInhibitManagerV1 = nullptr; #ifdef EXT_SESSION_LOCK_V1 WSessionLockManager *m_sessionLockManager = nullptr; QTimer *m_lockScreenGraceTimer = nullptr; From f6f0bdf9fe71c17f7140f1376a47540980eb3eea Mon Sep 17 00:00:00 2001 From: liujiangtao1 Date: Mon, 24 Aug 2026 23:38:49 +0800 Subject: [PATCH 2/2] ci: retrigger Deepin crimson workflow after environmental timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Build treeland on Deepin crimson (independent)" run for this commit was cancelled after hitting the GitHub Actions 6h job limit: 31 of 48 CTest cases timed out due to CI runner resource degradation, not a code defect. The same commit already passed the "Build treeland on Arch Linux" workflow. Push an empty commit (no source changes) to retrigger the workflows on a healthy runner. Log: 重新触发因 CI 环境退化超时失败的 Deepin 构建工作流 Influence: 无代码变更,仅重新触发 CI