diff --git a/examples/test_shortcut_capture/CMakeLists.txt b/examples/test_shortcut_capture/CMakeLists.txt index 9c690d3f6e..eb3b9918f3 100644 --- a/examples/test_shortcut_capture/CMakeLists.txt +++ b/examples/test_shortcut_capture/CMakeLists.txt @@ -12,7 +12,7 @@ qt_add_executable(${BIN_NAME} qt_generate_wayland_protocol_client_sources(${BIN_NAME} FILES - ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-v2.xml + ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-unstable-v3.xml ) target_link_libraries(${BIN_NAME} diff --git a/examples/test_shortcut_capture/main.cpp b/examples/test_shortcut_capture/main.cpp index 74711dc166..d02c99da27 100644 --- a/examples/test_shortcut_capture/main.cpp +++ b/examples/test_shortcut_capture/main.cpp @@ -1,11 +1,11 @@ // 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 -// Test application for treeland_shortcut_capture_v1 (treeland-shortcut-manager-v2, version 2). +// Test application for treeland_shortcut_capture_v3 (treeland-shortcut-manager-unstable-v3, version 1). // A window with a single button: click to request one-shot shortcut capture. // The captured key sequence or failure reason is shown below the button. -#include "qwayland-treeland-shortcut-manager-v2.h" +#include "qwayland-treeland-shortcut-manager-unstable-v3.h" #include @@ -16,17 +16,17 @@ #include #include -// Wraps the treeland_shortcut_capture_v1 Wayland object and emits Qt signals. +// Wraps the treeland_shortcut_capture_v3 Wayland object and emits Qt signals. class ShortcutCapture : public QObject - , public QtWayland::treeland_shortcut_capture_v1 + , public QtWayland::treeland_shortcut_capture_v3 { Q_OBJECT public: - explicit ShortcutCapture(struct ::treeland_shortcut_capture_v1 *capture, + explicit ShortcutCapture(struct ::treeland_shortcut_capture_v3 *capture, QObject *parent = nullptr) : QObject(parent) - , QtWayland::treeland_shortcut_capture_v1(capture) + , QtWayland::treeland_shortcut_capture_v3(capture) { } @@ -40,27 +40,27 @@ class ShortcutCapture void failed(uint32_t reason); protected: - void treeland_shortcut_capture_v1_captured(const QString &key) override + void treeland_shortcut_capture_v3_captured(const QString &key) override { emit captured(key); deleteLater(); } - void treeland_shortcut_capture_v1_failed(uint32_t reason) override + void treeland_shortcut_capture_v3_failed(uint32_t reason) override { emit failed(reason); deleteLater(); } }; -class ShortcutManagerV2 - : public QWaylandClientExtensionTemplate - , public QtWayland::treeland_shortcut_manager_v2 +class ShortcutManagerV3 + : public QWaylandClientExtensionTemplate + , public QtWayland::treeland_shortcut_manager_v3 { Q_OBJECT public: - explicit ShortcutManagerV2() - : QWaylandClientExtensionTemplate(2) + explicit ShortcutManagerV3() + : QWaylandClientExtensionTemplate(1) { } @@ -85,7 +85,7 @@ class ShortcutManagerV2 static QString cancelReasonText(uint32_t reason) { - using R = QtWayland::treeland_shortcut_capture_v1; + using R = QtWayland::treeland_shortcut_capture_v3; switch (static_cast(reason)) { case R::failed_reason_busy: return QStringLiteral("Another capture is already in progress (busy)"); @@ -105,7 +105,7 @@ int main(int argc, char *argv[]) qputenv("QT_QPA_PLATFORM", "wayland"); QApplication app(argc, argv); - ShortcutManagerV2 manager; + ShortcutManagerV3 manager; // Main window QWidget window; @@ -124,7 +124,7 @@ int main(int argc, char *argv[]) // Disable button until the protocol is available btn->setEnabled(false); - QObject::connect(&manager, &ShortcutManagerV2::activeChanged, btn, [&] { + QObject::connect(&manager, &ShortcutManagerV3::activeChanged, btn, [&] { btn->setEnabled(manager.isActive()); }); diff --git a/examples/test_shortcut_manager/CMakeLists.txt b/examples/test_shortcut_manager/CMakeLists.txt index 71ba7622c1..654d06145a 100644 --- a/examples/test_shortcut_manager/CMakeLists.txt +++ b/examples/test_shortcut_manager/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS WaylandClient) +find_package(Qt6 REQUIRED COMPONENTS Gui WaylandClient Widgets) find_package(TreelandProtocols REQUIRED) set(BIN_NAME test-shortcut-manager) @@ -9,12 +9,13 @@ qt_add_executable(${BIN_NAME} qt_generate_wayland_protocol_client_sources(${BIN_NAME} FILES - ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-v2.xml + ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-unstable-v3.xml ) target_link_libraries(${BIN_NAME} PRIVATE Qt6::Gui + Qt6::Widgets Qt6::WaylandClient ) diff --git a/examples/test_shortcut_manager/main.cpp b/examples/test_shortcut_manager/main.cpp index db61762e75..096a6b1642 100644 --- a/examples/test_shortcut_manager/main.cpp +++ b/examples/test_shortcut_manager/main.cpp @@ -1,136 +1,207 @@ // Copyright (C) 2025-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 -// This is a simple test application for the Treeland Shortcut Manager V2 protocol. -// It reads a JSON configuration file specifying a list of shortcut binding requests -// and sends them to the Treeland compositor via the Wayland protocol. -// The JSON file should contain an array of requests, each with the following format: -// [ -// { -// "req": "bind_key", -// "name": "shortcut1", -// "key": "Ctrl-A", -// "mode": 1, -// "action": 1 -// }, -// { ... }, -// ... -// ] - -#include -#include -#include +// Simple GUI test application for the Treeland Shortcut Manager V3 protocol. +// +// A window lists a set of hardcoded default shortcuts (the same compositor +// actions shipped under /usr/share/dsg/configs/org.deepin.dde.keybinding). +// Ticking a checkbox binds the shortcut with the compositor; unticking it +// unbinds the shortcut. There is no JSON input: the shortcuts are hardcoded +// below so the tool can be run without any external configuration. + +#include "qwayland-treeland-shortcut-manager-unstable-v3.h" + +#include +#include +#include +#include +#include +#include #include -#include -#include -#include -#include -#include -#include -#include - -#include "qwayland-treeland-shortcut-manager-v2.h" - -class ShortcutManagerV2 - : public QWaylandClientExtensionTemplate - , public QtWayland::treeland_shortcut_manager_v2 +#include + +#include +#include + +// Wraps treeland_shortcut_manager_v3 and re-emits its events as Qt signals. +class ShortcutManagerV3 + : public QWaylandClientExtensionTemplate + , public QtWayland::treeland_shortcut_manager_v3 { Q_OBJECT public: - explicit ShortcutManagerV2() - : QWaylandClientExtensionTemplate(1) + explicit ShortcutManagerV3() + : QWaylandClientExtensionTemplate(1) { + } + void bindShortcut(const QString &name, const QString &key, uint32_t flags, uint32_t action) + { + bind_key(name, key, flags, action); + } + + void unbindShortcut(const QString &name) + { + unbind(name); } - void treeland_shortcut_manager_v2_commit_success() override +Q_SIGNALS: + void activated(const QString &name, uint32_t flags); + void bindFailed(const QString &name, uint32_t error); + +protected: + void treeland_shortcut_manager_v3_activated(const QString &name, uint32_t flags) override { - qInfo() << "received commit success"; - emit commitStatusReceived(); + emit activated(name, flags); } - void treeland_shortcut_manager_v2_activated(const QString &name, uint repeat) override + void treeland_shortcut_manager_v3_bind_failure(const QString &name, uint32_t error) override { - qInfo() << "shortcut activated: " << name << " repeat: " << repeat; + emit bindFailed(name, error); } -signals: - void commitStatusReceived(); }; +namespace { + +using Action = QtWayland::treeland_shortcut_manager_v3::action; +using Flag = QtWayland::treeland_shortcut_manager_v3::keybind_flag; + +struct ShortcutDef +{ + const char *name; + const char *key; + Action action; + uint32_t flags; +}; + +// Hardcoded defaults mirroring the compositor actions shipped in +// /usr/share/dsg/configs/org.deepin.dde.keybinding (triggerType == 3). +// Action values here are already the V3 protocol enum values. +const ShortcutDef kShortcuts[] = { + { "show-desktop", "Meta+D", Action::action_show_desktop, Flag::keybind_flag_key_release }, + { "maximize", "Meta+Up", Action::action_maximize, Flag::keybind_flag_key_release }, + { "cancel-maximize", "Meta+Down", Action::action_cancel_maximize, Flag::keybind_flag_key_release }, + { "minimize", "Meta+N", Action::action_minimize, Flag::keybind_flag_key_release }, + { "close-window", "Alt+F4", Action::action_close_window, Flag::keybind_flag_key_release }, + { "move-window", "Alt+F7", Action::action_move_window, Flag::keybind_flag_key_release }, + { "resize-window", "Alt+F8", Action::action_resize_window, Flag::keybind_flag_key_release }, + { "show-window-menu", "Alt+Space", Action::action_show_window_menu, Flag::keybind_flag_key_release }, + { "toggle-multitaskview", "Meta+S", Action::action_toggle_multitask_view, Flag::keybind_flag_key_release }, + { "toggle-fpsdisplay", "Meta+F11", Action::action_toggle_fps_display, Flag::keybind_flag_key_release }, + { "workspace-1", "Meta+1", Action::action_workspace_1, Flag::keybind_flag_key_release }, + { "workspace-2", "Meta+2", Action::action_workspace_2, Flag::keybind_flag_key_release }, + { "workspace-3", "Meta+3", Action::action_workspace_3, Flag::keybind_flag_key_release }, + { "workspace-4", "Meta+4", Action::action_workspace_4, Flag::keybind_flag_key_release }, + { "prev-workspace", "Meta+Ctrl+Left", Action::action_prev_workspace, Flag::keybind_flag_key_release }, + { "next-workspace", "Meta+Ctrl+Right", Action::action_next_workspace, Flag::keybind_flag_key_release }, + { "tile-left", "Meta+Left", Action::action_tile_left, Flag::keybind_flag_key_release }, + { "tile-right", "Meta+Right", Action::action_tile_right, Flag::keybind_flag_key_release }, + { "taskswitch-next", "Alt+Tab", Action::action_taskswitch_next, Flag::keybind_flag_key_press | Flag::keybind_flag_repeat }, + { "taskswitch-prev", "Alt+Shift+Tab", Action::action_taskswitch_prev, Flag::keybind_flag_key_press | Flag::keybind_flag_repeat }, + { "zoom-in", "Meta+=", Action::action_zoom_in, Flag::keybind_flag_key_release }, + { "zoom-out", "Meta+-", Action::action_zoom_out, Flag::keybind_flag_key_release }, + { "zoom-reset", "Meta+0", Action::action_zoom_reset, Flag::keybind_flag_key_release }, +}; + +} // namespace int main(int argc, char *argv[]) { qputenv("QT_QPA_PLATFORM", "wayland"); - QGuiApplication app(argc, argv); - QCommandLineParser parser; - parser.setApplicationDescription("Test Shortcut Manager V2"); - parser.addHelpOption(); - parser.addOption({"config", "Path to the JSON configuration file", "file"}); - parser.process(app); - - QString configFilePath = parser.value("config"); - if (configFilePath.isEmpty()) { - qWarning() << "No configuration file specified. Use --config to specify the file."; - return -1; + QApplication app(argc, argv); + + ShortcutManagerV3 manager; + + QWidget window; + window.setWindowTitle(QStringLiteral("Shortcut Manager V3 Test")); + window.resize(520, 720); + + auto *layout = new QVBoxLayout(&window); + + auto *statusLabel = new QLabel(&window); + statusLabel->setWordWrap(true); + layout->addWidget(statusLabel); + + auto *log = new QPlainTextEdit(&window); + log->setReadOnly(true); + layout->addWidget(log, 1); + + // One checkbox per hardcoded shortcut, laid out in two columns. + // Checked == bound; all shortcuts start checked (bound) by default. + const int shortcutCount = static_cast(std::size(kShortcuts)); + auto *grid = new QGridLayout; + constexpr int columns = 2; + QList boxes; + for (int i = 0; i < shortcutCount; ++i) { + const ShortcutDef &def = kShortcuts[i]; + auto *box = new QCheckBox( + QStringLiteral("%1 (%2)").arg(QString::fromLatin1(def.name), QString::fromLatin1(def.key)), + &window); + box->setChecked(true); + grid->addWidget(box, i / columns, i % columns); + boxes.append(box); } - QJsonDocument requests; - { - QFile configFile(configFilePath); - if (!configFile.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open configuration file:" << configFilePath; - return -1; - } - QByteArray data = configFile.readAll(); - configFile.close(); - - QJsonParseError parseError; - requests = QJsonDocument::fromJson(data, &parseError); - if (parseError.error != QJsonParseError::NoError) { - qWarning() << "Failed to parse JSON configuration file:" << parseError.errorString(); - return -1; + layout->addLayout(grid); + + // (Re)apply every checkbox to the compositor. Used when the protocol + // becomes active so previously checked shortcuts are re-bound after a + // disconnect/reconnect of the extension. + const auto applyBindings = [&]() { + for (int i = 0; i < shortcutCount; ++i) { + const ShortcutDef &def = kShortcuts[i]; + if (boxes.at(i)->isChecked()) + manager.bindShortcut(QString::fromLatin1(def.name), + QString::fromLatin1(def.key), + def.flags, + def.action); + else + manager.unbindShortcut(QString::fromLatin1(def.name)); } - } - ShortcutManagerV2 manager; - manager.setParent(&app); - QObject::connect(&manager, &ShortcutManagerV2::activeChanged, &manager, [&] { - if (!manager.isActive()) { + }; + + QObject::connect(&manager, &ShortcutManagerV3::activeChanged, &window, [&]() { + const bool active = manager.isActive(); + statusLabel->setText(active + ? QStringLiteral("Protocol active — acquired, bindings applied.") + : QStringLiteral("Protocol inactive.")); + if (!active) return; - } manager.acquire(); - for (const QJsonValue &value : requests.array()) { - QJsonObject obj = value.toObject(); - QString req = obj.value("req").toString(); - QString name = obj.value("name").toString(); - if (req == "bind_key") { - QString key = obj.value("key").toString(); - uint mode = static_cast(obj.value("mode").toInt()); - uint action = static_cast(obj.value("action").toInt()); - manager.bind_key(name, key, mode, action); - } else if (req == "bind_swipe_gesture") { - uint finger = static_cast(obj.value("finger").toInt()); - uint direction = static_cast(obj.value("direction").toInt()); - uint action = static_cast(obj.value("action").toInt()); - manager.bind_swipe_gesture(name, finger, direction, action); - } else if (req == "bind_hold_gesture") { - uint finger = static_cast(obj.value("finger").toInt()); - uint action = static_cast(obj.value("action").toInt()); - manager.bind_hold_gesture(name, finger, action); - } else if (req == "unbind") { - manager.unbind(name); - } else if (req == "commit") { - QEventLoop loop; - QObject::connect(&manager, &ShortcutManagerV2::commitStatusReceived, &manager, [&loop](){ - loop.quit(); - }); - manager.commit(); - loop.exec(); - } else { - qWarning() << "Unknown request type:" << req; - } - } + applyBindings(); }); - app.exec(); - return 0; + + for (int i = 0; i < shortcutCount; ++i) { + const ShortcutDef &def = kShortcuts[i]; + QObject::connect(boxes.at(i), &QCheckBox::toggled, &window, [&, i](bool checked) { + if (checked) + manager.bindShortcut(QString::fromLatin1(def.name), + QString::fromLatin1(def.key), + def.flags, + def.action); + else + manager.unbindShortcut(QString::fromLatin1(def.name)); + log->appendPlainText(QStringLiteral("%1 %2") + .arg(checked ? QStringLiteral("bound") : QStringLiteral("unbound"), + QString::fromLatin1(def.name))); + }); + } + + QObject::connect(&manager, &ShortcutManagerV3::activated, &window, + [log](const QString &name, uint32_t flags) { + log->appendPlainText( + QStringLiteral("activated: %1 (flags=0x%2)") + .arg(name, QString::number(flags, 16))); + }); + + QObject::connect(&manager, &ShortcutManagerV3::bindFailed, &window, + [log](const QString &name, uint32_t error) { + log->appendPlainText( + QStringLiteral("bind failure: %1 (error=%2)") + .arg(name, QString::number(error))); + }); + + window.show(); + return app.exec(); } #include "main.moc" diff --git a/src/common/shellaction.cpp b/src/common/shellaction.cpp index a7bc605d19..9eac4821e9 100644 --- a/src/common/shellaction.cpp +++ b/src/common/shellaction.cpp @@ -11,6 +11,26 @@ #include "surface/surfacewrapper.h" #include "workspace/workspace.h" +static SurfaceWrapper::TileMode tileModeFor(ShellAction action) +{ + switch (action) { + case ShellAction::TileLeft: + return SurfaceWrapper::TileMode::Left; + case ShellAction::TileRight: + return SurfaceWrapper::TileMode::Right; + case ShellAction::TileTopLeft: + return SurfaceWrapper::TileMode::TopLeft; + case ShellAction::TileTopRight: + return SurfaceWrapper::TileMode::TopRight; + case ShellAction::TileBottomLeft: + return SurfaceWrapper::TileMode::BottomLeft; + case ShellAction::TileBottomRight: + return SurfaceWrapper::TileMode::BottomRight; + default: + return SurfaceWrapper::TileMode::None; + } +} + void ShellActionExecutor::execute(ShellAction action) { auto *helper = Helper::instance(); @@ -124,6 +144,13 @@ void ShellActionExecutor::execute(ShellAction action) } break; } + case ShellAction::Minimize: { + auto *surface = helper->activatedSurface(); + if (surface) { + surface->minimize(); + } + break; + } case ShellAction::MoveWindow: { auto *surface = helper->activatedSurface(); if (surface) { @@ -146,7 +173,11 @@ void ShellActionExecutor::execute(ShellAction action) break; } case ShellAction::TileLeft: - case ShellAction::TileRight: { + case ShellAction::TileRight: + case ShellAction::TileTopLeft: + case ShellAction::TileTopRight: + case ShellAction::TileBottomLeft: + case ShellAction::TileBottomRight: { auto *surface = helper->activatedSurface(); if (!surface) { break; @@ -155,9 +186,31 @@ void ShellActionExecutor::execute(ShellAction action) if (!output) { break; } - const auto mode = (action == ShellAction::TileLeft) ? SurfaceWrapper::TileMode::Left - : SurfaceWrapper::TileMode::Right; - surface->applyTileMode(mode, output); + surface->applyTileMode(tileModeFor(action), output); + break; + } + case ShellAction::MoveWindowToPrevWorkspace: { + auto *surface = helper->activatedSurface(); + if (!surface) { + break; + } + const int target = helper->workspace()->getLeftWorkspaceId(surface->workspaceId()); + if (target < 0) { + break; + } + helper->workspace()->moveSurfaceTo(surface, target); + break; + } + case ShellAction::MoveWindowToNextWorkspace: { + auto *surface = helper->activatedSurface(); + if (!surface) { + break; + } + const int target = helper->workspace()->getRightWorkspaceId(surface->workspaceId()); + if (target < 0) { + break; + } + helper->workspace()->moveSurfaceTo(surface, target); break; } } diff --git a/src/common/shellaction.h b/src/common/shellaction.h index ad126faadc..6a7a92e104 100644 --- a/src/common/shellaction.h +++ b/src/common/shellaction.h @@ -48,11 +48,18 @@ enum class ShellAction : uint32_t { // Window-level actions (shortcut-manager only); act on activatedSurface(). Maximize, CancelMaximize, + Minimize, MoveWindow, CloseWindow, ShowWindowMenu, TileLeft, TileRight, + TileTopLeft, + TileTopRight, + TileBottomLeft, + TileBottomRight, + MoveWindowToPrevWorkspace, + MoveWindowToNextWorkspace, }; // Stateless executor: every call re-reads Helper::instance(). Task switching, diff --git a/src/modules/shortcut/CMakeLists.txt b/src/modules/shortcut/CMakeLists.txt index a451ed359f..c489620d3f 100644 --- a/src/modules/shortcut/CMakeLists.txt +++ b/src/modules/shortcut/CMakeLists.txt @@ -1,8 +1,8 @@ find_package(TreelandProtocols REQUIRED) waylib_generate_qtwayland_server_protocol(libtreeland - PROTOCOL ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-v2.xml - BASENAME treeland-shortcut-manager-v2 + PROTOCOL ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-unstable-v3.xml + BASENAME treeland-shortcut-manager-unstable-v3 ) impl_treeland( diff --git a/src/modules/shortcut/shortcutcontroller.cpp b/src/modules/shortcut/shortcutcontroller.cpp index f94d31c969..7f2e6b48b8 100644 --- a/src/modules/shortcut/shortcutcontroller.cpp +++ b/src/modules/shortcut/shortcutcontroller.cpp @@ -3,7 +3,7 @@ #include "shortcutcontroller.h" -#include "qwayland-server-treeland-shortcut-manager-v2.h" +#include "qwayland-server-treeland-shortcut-manager-unstable-v3.h" #include "common/treelandlogging.h" #include "input/inputdevice.h" @@ -16,17 +16,17 @@ static_assert( static_cast(ShortcutController::KeyPress) == - QtWaylandServer::treeland_shortcut_manager_v2:: + QtWaylandServer::treeland_shortcut_manager_v3:: keybind_flag_key_press && static_cast(ShortcutController::KeyRelease) == - QtWaylandServer::treeland_shortcut_manager_v2:: + QtWaylandServer::treeland_shortcut_manager_v3:: keybind_flag_key_release && static_cast(ShortcutController::Repeat) == - QtWaylandServer::treeland_shortcut_manager_v2::keybind_flag_repeat, - "treeland-shortcut-manager-v2: protocol's keybind_flag disagree with " + QtWaylandServer::treeland_shortcut_manager_v3::keybind_flag_repeat, + "treeland-shortcut-manager-v3: protocol's keybind_flag disagree with " "Treeland in value"); -using BindError = QtWaylandServer::treeland_shortcut_manager_v2::bind_error; +using BindError = QtWaylandServer::treeland_shortcut_manager_v3::bind_error; ShortcutController::ShortcutController(QObject *parent) : QObject(parent) @@ -47,34 +47,36 @@ const char *ShortcutController::actionName(ShortcutAction action) uint ShortcutController::registerKey(const QString &name, const QString& key, ShortcutController::KeyFlags keybindFlags, ShortcutAction action) { - if (m_deleters.contains(name)) { - return BindError::bind_error_name_conflict; - } - - // For all-modifier bindings: Ctrl is a valid modifier but not a valid key. - auto keySeq = QKeySequence::fromString(key.endsWith("+Ctrl") ? key + "+Control" - : key, - QKeySequence::PortableText); - if (keySeq.count() != 1) { + const int combined = normalizedKeyCombinedFor(key); + if (combined == 0) { return BindError::bind_error_invalid_argument; } - auto keyComb = normalizeKeyCombination(keySeq[0]); + auto keyComb = QKeyCombination::fromCombined(combined); if (!isValidShortcutCombination(keyComb)) { return BindError::bind_error_invalid_argument; } - int combined = keyComb.toCombined(); if (keybindFlags & ~ShortcutController::KeyFlag::All) { return BindError::bind_error_invalid_argument; } auto &entry = m_keyMap[combined]; + // Per the v3 protocol, bind_key upserts when the same key sequence and + // action already exists (updating its flags) instead of failing. Re-binding + // the same name to the same trigger is therefore an in-place flag update, + // not a name_conflict; name_conflict only applies when the name is already + // used by a different key+action binding. if (entry.contains(action)) { const auto &[prevName, flags] = entry[action]; + if (name != prevName && m_deleters.contains(name)) { + return BindError::bind_error_name_conflict; + } m_deleters.remove(prevName); qCInfo(lcTlShortcut).noquote() << "Overriding existing key binding of" - << keySeq[0] << "for action" << actionName(action) + << keyComb << "for action" << actionName(action) << "by name" << prevName << "with new name" << name << "and flags" << keybindFlags; + } else if (m_deleters.contains(name)) { + return BindError::bind_error_name_conflict; } m_keyMap[combined][action] = std::make_pair(name, keybindFlags); m_actionCombinedMap[action] = combined; @@ -100,7 +102,7 @@ uint ShortcutController::registerSwipeGesture(const QString &name, uint finger, return BindError::bind_error_name_conflict; } - if (direction == SwipeGesture::Direction::Invalid) { + if (direction == SwipeGesture::Direction::Invalid || finger == 0) { return BindError::bind_error_invalid_argument; } @@ -164,6 +166,10 @@ uint ShortcutController::registerHoldGesture(const QString &name, uint finger, S return BindError::bind_error_name_conflict; } + if (finger == 0) { + return BindError::bind_error_invalid_argument; + } + auto gestureKey = std::make_pair(finger, SwipeGesture::Direction::Invalid); if (!m_gestures.contains(gestureKey)) { @@ -269,6 +275,16 @@ void ShortcutController::clear() m_actionCombinedMap.clear(); } +int ShortcutController::normalizedKeyCombinedFor(const QString &key) +{ + // For all-modifier bindings: Ctrl is a valid modifier but not a valid key. + const auto seq = QKeySequence::fromString(key.endsWith("+Ctrl") ? key + "+Control" : key, + QKeySequence::PortableText); + if (seq.count() != 1) + return 0; + return normalizeKeyCombination(seq[0]).toCombined(); +} + QKeyCombination ShortcutController::normalizeKeyCombination(QKeyCombination combination) { Qt::KeyboardModifiers mods = combination.keyboardModifiers(); Qt::Key key = combination.key(), nornalizedKey = Qt::Key_unknown; diff --git a/src/modules/shortcut/shortcutcontroller.h b/src/modules/shortcut/shortcutcontroller.h index 456d68d98d..e732bba613 100644 --- a/src/modules/shortcut/shortcutcontroller.h +++ b/src/modules/shortcut/shortcutcontroller.h @@ -4,6 +4,7 @@ #pragma once #include "input/gestures.h" +#include "modules/shortcut/qwayland-server-treeland-shortcut-manager-unstable-v3.h" #include #include @@ -15,36 +16,56 @@ class ShortcutController : public QObject { Q_OBJECT public: - // Values defined in treeland-shortcut-manager-v2 protocol + // Internal action vocabulary. Values are taken directly from the + // treeland-shortcut-manager-unstable-v3 protocol enum so they cannot + // drift; the PascalCase names keep the Qt-facing API readable. enum class ShortcutAction : uint32_t { - Notify = 1, - Workspace1 = 2, - Workspace2 = 3, - Workspace3 = 4, - Workspace4 = 5, - Workspace5 = 6, - Workspace6 = 7, - PrevWorkspace = 8, - NextWorkspace = 9, - ShowDesktop = 10, - Maximize = 11, - CancelMaximize = 12, - MoveWindow = 13, - CloseWindow = 14, - ShowWindowMenu = 15, - OpenMultiTaskView = 16, - CloseMultiTaskView = 17, - ToggleMultitaskView = 18, - ToggleFpsDisplay = 19, - Lockscreen = 20, - ShutdownMenu = 21, - Quit = 22, - TaskSwitchNext = 24, - TaskSwitchPrev = 25, - TaskSwitchSameAppNext = 26, - TaskSwitchSameAppPrev = 27, - TileLeft = 28, - TileRight = 29, + Notify = QtWaylandServer::treeland_shortcut_manager_v3::action_notify, + Workspace1 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_1, + Workspace2 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_2, + Workspace3 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_3, + Workspace4 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_4, + Workspace5 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_5, + Workspace6 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_6, + Workspace7 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_7, + Workspace8 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_8, + Workspace9 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_9, + Workspace10 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_10, + Workspace11 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_11, + Workspace12 = QtWaylandServer::treeland_shortcut_manager_v3::action_workspace_12, + PrevWorkspace = QtWaylandServer::treeland_shortcut_manager_v3::action_prev_workspace, + NextWorkspace = QtWaylandServer::treeland_shortcut_manager_v3::action_next_workspace, + Maximize = QtWaylandServer::treeland_shortcut_manager_v3::action_maximize, + CancelMaximize = QtWaylandServer::treeland_shortcut_manager_v3::action_cancel_maximize, + Minimize = QtWaylandServer::treeland_shortcut_manager_v3::action_minimize, + MoveWindow = QtWaylandServer::treeland_shortcut_manager_v3::action_move_window, + ResizeWindow = QtWaylandServer::treeland_shortcut_manager_v3::action_resize_window, + CloseWindow = QtWaylandServer::treeland_shortcut_manager_v3::action_close_window, + ShowWindowMenu = QtWaylandServer::treeland_shortcut_manager_v3::action_show_window_menu, + MoveWindowToPrevWorkspace = QtWaylandServer::treeland_shortcut_manager_v3::action_move_window_to_prev_workspace, + MoveWindowToNextWorkspace = QtWaylandServer::treeland_shortcut_manager_v3::action_move_window_to_next_workspace, + ShowDesktop = QtWaylandServer::treeland_shortcut_manager_v3::action_show_desktop, + OpenMultiTaskView = QtWaylandServer::treeland_shortcut_manager_v3::action_open_multitask_view, + CloseMultiTaskView = QtWaylandServer::treeland_shortcut_manager_v3::action_close_multitask_view, + ToggleMultitaskView = QtWaylandServer::treeland_shortcut_manager_v3::action_toggle_multitask_view, + TaskSwitchNext = QtWaylandServer::treeland_shortcut_manager_v3::action_taskswitch_next, + TaskSwitchPrev = QtWaylandServer::treeland_shortcut_manager_v3::action_taskswitch_prev, + TaskSwitchSameAppNext = QtWaylandServer::treeland_shortcut_manager_v3::action_taskswitch_sameapp_next, + TaskSwitchSameAppPrev = QtWaylandServer::treeland_shortcut_manager_v3::action_taskswitch_sameapp_prev, + TileLeft = QtWaylandServer::treeland_shortcut_manager_v3::action_tile_left, + TileRight = QtWaylandServer::treeland_shortcut_manager_v3::action_tile_right, + TileTop = QtWaylandServer::treeland_shortcut_manager_v3::action_tile_top, + TileBottom = QtWaylandServer::treeland_shortcut_manager_v3::action_tile_bottom, + TileTopLeft = QtWaylandServer::treeland_shortcut_manager_v3::action_tile_top_left, + TileTopRight = QtWaylandServer::treeland_shortcut_manager_v3::action_tile_top_right, + TileBottomLeft = QtWaylandServer::treeland_shortcut_manager_v3::action_tile_bottom_left, + TileBottomRight = QtWaylandServer::treeland_shortcut_manager_v3::action_tile_bottom_right, + ZoomIn = QtWaylandServer::treeland_shortcut_manager_v3::action_zoom_in, + ZoomOut = QtWaylandServer::treeland_shortcut_manager_v3::action_zoom_out, + ZoomReset = QtWaylandServer::treeland_shortcut_manager_v3::action_zoom_reset, + ToggleFpsDisplay = QtWaylandServer::treeland_shortcut_manager_v3::action_toggle_fps_display, + Lockscreen = QtWaylandServer::treeland_shortcut_manager_v3::action_lockscreen, + ShutdownMenu = QtWaylandServer::treeland_shortcut_manager_v3::action_shutdown_menu, }; Q_ENUM(ShortcutAction) static const char *actionName(ShortcutAction action); @@ -69,6 +90,10 @@ class ShortcutController : public QObject bool dispatchKeyEvent(const QKeyEvent *event); static QKeyCombination normalizeKeyCombination(QKeyCombination combination); static bool isValidShortcutCombination(QKeyCombination combination); + // Normalizes a portable-text key string to the (combined-key) identity + // registerKey() upserts on, or 0 when the string does not parse to exactly + // one combination. Single source of truth for the bind_key cache identity. + static int normalizedKeyCombinedFor(const QString &key); Qt::KeyboardModifiers modifierForAction(ShortcutAction action) const; Q_SIGNALS: diff --git a/src/modules/shortcut/shortcutmanager.cpp b/src/modules/shortcut/shortcutmanager.cpp index b82c1d48a3..108d614059 100644 --- a/src/modules/shortcut/shortcutmanager.cpp +++ b/src/modules/shortcut/shortcutmanager.cpp @@ -5,7 +5,7 @@ #include "common/treelandlogging.h" #include "input/gestures.h" -#include "qwayland-server-treeland-shortcut-manager-v2.h" +#include "qwayland-server-treeland-shortcut-manager-unstable-v3.h" #include "seat/helper.h" #include "seat/seatmanager.h" #include "session/session.h" @@ -19,15 +19,7 @@ #include #include - -#define SHORTCUT_REGISTRATION_SUCCESS 0 - -using ProtocolAction = QtWaylandServer::treeland_shortcut_manager_v2::action; -static_assert(static_cast(ProtocolAction::action_notify) == static_cast(ShortcutAction::Notify), - "treeland-shortcut-manager protocol action enum mismatch"); -static_assert(static_cast(ProtocolAction::action_taskswitch_sameapp_prev) - == static_cast(ShortcutAction::TaskSwitchSameAppPrev), - "treeland-shortcut-manager protocol action enum mismatch"); +#include struct KeyShortcut { ShortcutController::KeyFlags keybindFlags; @@ -55,16 +47,27 @@ class UserShortcuts { QList swipes; QList holds; - void append(const UserShortcuts &other) - { - keys.append(other.keys); - swipes.append(other.swipes); - holds.append(other.holds); - } }; +// Remove the shortcut identified by name from a UserShortcuts collection. +static void removeShortcutFromMap(UserShortcuts &shortcuts, const QString &name) +{ + shortcuts.keys.removeIf([&](const KeyShortcut &ks) { return ks.name == name; }); + shortcuts.swipes.removeIf([&](const SwipeShortcut &ss) { return ss.name == name; }); + shortcuts.holds.removeIf([&](const HoldShortcut &hs) { return hs.name == name; }); +} + +// Returns true when the action value is one of the defined ShortcutAction +// enum values. Uses QMetaEnum instead of a range check so it stays correct +// even if the enum gains holes or is reordered in the future. +static bool isValidAction(ShortcutAction action) +{ + const auto meta = QMetaEnum::fromType(); + return meta.valueToKey(static_cast(action)) != nullptr; +} + // Forward declaration -class ShortcutManagerV2Private; +class ShortcutManagerV3Private; // Returns true if the key itself is a pure modifier key that can never be a valid // shortcut on its own (Ctrl/Alt/Shift/Hyper/AltGr/lock keys). @@ -90,14 +93,14 @@ static bool isPureModifierKey(Qt::Key key) } } -class ShortcutCaptureV1 : public QtWaylandServer::treeland_shortcut_capture_v1 +class ShortcutCaptureV3 : public QtWaylandServer::treeland_shortcut_capture_v3 { public: - ShortcutCaptureV1(ShortcutManagerV2Private *manager, + ShortcutCaptureV3(ShortcutManagerV3Private *manager, wl_client *client, uint32_t id, int version) - : QtWaylandServer::treeland_shortcut_capture_v1(client, id, version) + : QtWaylandServer::treeland_shortcut_capture_v3(client, id, version) , m_manager(manager) , m_pending(true) { @@ -121,7 +124,7 @@ class ShortcutCaptureV1 : public QtWaylandServer::treeland_shortcut_capture_v1 } protected: - // Defined after ShortcutManagerV2Private + // Defined after ShortcutManagerV3Private void destroy(Resource *resource) override; // Called both when the client sends destroy and when the client disconnects. @@ -129,13 +132,13 @@ class ShortcutCaptureV1 : public QtWaylandServer::treeland_shortcut_capture_v1 void destroy_resource(Resource *resource) override; private: - ShortcutManagerV2Private *m_manager; + ShortcutManagerV3Private *m_manager; bool m_pending; }; static SwipeGesture::Direction toSwipeDirection(uint32_t direction) { - using Direction = QtWaylandServer::treeland_shortcut_manager_v2::direction; + using Direction = QtWaylandServer::treeland_shortcut_manager_v3::direction; switch (direction) { case Direction::direction_down: return SwipeGesture::Direction::Down; @@ -150,30 +153,32 @@ static SwipeGesture::Direction toSwipeDirection(uint32_t direction) } } -class ShortcutManagerV2Private : public QtWaylandServer::treeland_shortcut_manager_v2 +class ShortcutManagerV3Private : public QtWaylandServer::treeland_shortcut_manager_v3 { public: - explicit ShortcutManagerV2Private(ShortcutManagerV2 *_q); + explicit ShortcutManagerV3Private(); wl_global *global() const; - uint updateShortcuts(const UserShortcuts& shortcuts, QString &failName); - void sendActivated(WSocket *socket, const QString &name, ShortcutController::KeyFlags keyFlags); - void sendCommitSuccess(WSocket *socket); - void sendCommitFailure(WSocket *socket, const QString &name, uint error); - void sendInvalidCommit(WSocket *socket); + void sendBindFailure(WSocket *socket, const QString &name, uint error); + + // Register a single key shortcut. Returns 0 on success, non-zero + // bind_error code on failure. On success the shortcut is appended + // to m_shortcuts[socket]. + uint registerKey(WSocket *socket, const KeyShortcut &ks); + uint registerSwipe(WSocket *socket, const SwipeShortcut &ss); + uint registerHold(WSocket *socket, const HoldShortcut &hs); - ShortcutManagerV2 *q; ShortcutController *m_controller = nullptr; QMap ownerClients; QMap m_shortcuts; + // Binds queued while the client's session is not active. QMap m_pendingShortcuts; - QMap m_pendingCommittedShortcuts; QMap> m_pendingDeletes; - ShortcutCaptureV1 *m_pendingCapture = nullptr; + ShortcutCaptureV3 *m_pendingCapture = nullptr; // Seat whose keyboard focus was validated at capture start. // Events from other seats are ignored during capture. WSeat *m_pendingSeat = nullptr; @@ -201,7 +206,6 @@ class ShortcutManagerV2Private : public QtWaylandServer::treeland_shortcut_manag const QString &name, uint32_t finger, uint32_t action) override; - void commit(Resource *resource) override; void unbind(Resource *resource, const QString &name) override; void capture_next_shortcut(Resource *resource, struct ::wl_resource *surface, @@ -212,108 +216,88 @@ class ShortcutManagerV2Private : public QtWaylandServer::treeland_shortcut_manag WSocket *socketFromResource(Resource *resource); // Clears all capture-related state and returns the previously pending capture object. // The caller is responsible for sending a terminal event on the returned object. - ShortcutCaptureV1 *resetCaptureState(); + ShortcutCaptureV3 *resetCaptureState(); public: - void onCaptureDestroyed(ShortcutCaptureV1 *c); + void onCaptureDestroyed(ShortcutCaptureV3 *c); bool tryHandleCaptureEvent(WSeat *seat, QInputEvent *event); }; -ShortcutManagerV2Private::ShortcutManagerV2Private(ShortcutManagerV2 *_q) - : q(_q) +ShortcutManagerV3Private::ShortcutManagerV3Private() { } -wl_global *ShortcutManagerV2Private::global() const +wl_global *ShortcutManagerV3Private::global() const { return m_global; } -WSocket *ShortcutManagerV2Private::socketFromResource(Resource *resource) +WSocket *ShortcutManagerV3Private::socketFromResource(Resource *resource) { return WSocket::get(wl_resource_get_client(resource->handle))->rootSocket(); } -uint ShortcutManagerV2Private::updateShortcuts(const UserShortcuts& shortcuts, QString &failName) +uint ShortcutManagerV3Private::registerKey(WSocket *socket, const KeyShortcut &ks) { - uint status = SHORTCUT_REGISTRATION_SUCCESS; - QList names; - - const auto tryRegisterAll = [&]() { - for (const auto& [keybindFlags, name, key, action] : std::as_const(shortcuts.keys)) { - status = m_controller->registerKey(name, key, keybindFlags, action); - if (status) { - failName = name; - return; - } - names.append(name); - } - for (const auto& [name, finger, direction, action] : std::as_const(shortcuts.swipes)) { - status = m_controller->registerSwipeGesture(name, finger, direction, action); - if (status) { - failName = name; - return; - } - names.append(name); - } - for (const auto& [name, finger, action] : std::as_const(shortcuts.holds)) { - status = m_controller->registerHoldGesture(name, finger, action); - if (status) { - failName = name; - return; - } - names.append(name); - } - }; - - tryRegisterAll(); - if (status) { - for (const auto& name : std::as_const(names)) { - m_controller->unregisterShortcut(name); - } + if (!isValidAction(ks.action)) + return QtWaylandServer::treeland_shortcut_manager_v3::bind_error_invalid_argument; + uint status = m_controller->registerKey(ks.name, ks.key, ks.keybindFlags, ks.action); + if (!status) { + // The controller upserts on the (normalized key, action) identity: a + // successful bind can replace an existing binding registered under a + // different name. Drop any cached entry with the same identity before + // recording the new bind, or a later unbind or session restore would + // resurrect the replaced name. + const int combined = ShortcutController::normalizedKeyCombinedFor(ks.key); + auto &keys = m_shortcuts[socket].keys; + keys.removeIf([&](const KeyShortcut &existing) { + return existing.action == ks.action + && ShortcutController::normalizedKeyCombinedFor(existing.key) == combined; + }); + keys.append(ks); } return status; } -void ShortcutManagerV2Private::sendActivated(WSocket *socket, const QString &name, ShortcutController::KeyFlags keyFlags) +uint ShortcutManagerV3Private::registerSwipe(WSocket *socket, const SwipeShortcut &ss) { - Resource *resource = ownerClients.value(socket, nullptr); - if (!resource) - return; - - send_activated(resource->handle, name, keyFlags.toInt()); + if (!isValidAction(ss.action)) + return QtWaylandServer::treeland_shortcut_manager_v3::bind_error_invalid_argument; + uint status = m_controller->registerSwipeGesture(ss.name, ss.finger, ss.direction, ss.action); + if (!status) + m_shortcuts[socket].swipes.append(ss); + return status; } -void ShortcutManagerV2Private::sendCommitSuccess(WSocket *socket) +uint ShortcutManagerV3Private::registerHold(WSocket *socket, const HoldShortcut &hs) { - Resource *resource = ownerClients.value(socket, nullptr); - if (!resource) - return; - - send_commit_success(resource->handle); + if (!isValidAction(hs.action)) + return QtWaylandServer::treeland_shortcut_manager_v3::bind_error_invalid_argument; + uint status = m_controller->registerHoldGesture(hs.name, hs.finger, hs.action); + if (!status) + m_shortcuts[socket].holds.append(hs); + return status; } -void ShortcutManagerV2Private::sendCommitFailure(WSocket *socket, const QString &name, uint error) +void ShortcutManagerV3Private::sendActivated(WSocket *socket, const QString &name, ShortcutController::KeyFlags keyFlags) { Resource *resource = ownerClients.value(socket, nullptr); if (!resource) return; - send_commit_failure(resource->handle, name, error); + send_activated(resource->handle, name, keyFlags.toInt()); } -void ShortcutManagerV2Private::sendInvalidCommit(WSocket *socket) +void ShortcutManagerV3Private::sendBindFailure(WSocket *socket, const QString &name, uint error) { Resource *resource = ownerClients.value(socket, nullptr); if (!resource) return; - wl_resource_post_error(resource->handle, - error_invalid_commit, - "Commit sent before last commit is processed."); + send_bind_failure(resource->handle, name, error); } -void ShortcutManagerV2Private::destroy_resource(Resource *resource) +void ShortcutManagerV3Private::destroy_resource(Resource *resource) { for (auto it = ownerClients.begin(); it != ownerClients.end(); ) { if (it.value() == resource) { @@ -324,12 +308,12 @@ void ShortcutManagerV2Private::destroy_resource(Resource *resource) } } -void ShortcutManagerV2Private::destroy(Resource *resource) +void ShortcutManagerV3Private::destroy(Resource *resource) { wl_resource_destroy(resource->handle); } -void ShortcutManagerV2Private::acquire(Resource *resource) +void ShortcutManagerV3Private::acquire(Resource *resource) { WSocket *socket = socketFromResource(resource); if (ownerClients.contains(socket)) { @@ -342,14 +326,18 @@ void ShortcutManagerV2Private::acquire(Resource *resource) // remove stale shortcuts m_shortcuts.remove(socket); m_pendingShortcuts.remove(socket); - m_pendingCommittedShortcuts.remove(socket); m_pendingDeletes.remove(socket); + // TODO: clear() wipes the whole controller, including any bindings of + // other sessions. Today only the active session's bindings live in the + // controller (inactive sessions are held in m_pendingShortcuts), so this + // is correct. Revisit when multi-user handling is improved: clear + // per-socket state instead of the global controller. if (m_activeSessionSocket == socket) m_controller->clear(); ownerClients.insert(socket, resource); } -void ShortcutManagerV2Private::bind_key(Resource *resource, +void ShortcutManagerV3Private::bind_key(Resource *resource, const QString &name, const QString &key_sequence, uint32_t flags, @@ -363,15 +351,25 @@ void ShortcutManagerV2Private::bind_key(Resource *resource, return; } - m_pendingShortcuts[socket].keys.append(KeyShortcut{ + KeyShortcut ks{ .keybindFlags = ShortcutController::KeyFlags::fromInt(flags), .name = name, .key = key_sequence, .action = static_cast(action), - }); + }; + + if (socket == m_activeSessionSocket) { + // Active session: apply immediately. + uint status = registerKey(socket, ks); + if (status) + sendBindFailure(socket, name, status); + } else { + // Non-active session: defer until the session becomes active. + m_pendingShortcuts[socket].keys.append(ks); + } } -void ShortcutManagerV2Private::bind_swipe_gesture(Resource *resource, +void ShortcutManagerV3Private::bind_swipe_gesture(Resource *resource, const QString &name, uint32_t finger, uint32_t direction, @@ -385,15 +383,23 @@ void ShortcutManagerV2Private::bind_swipe_gesture(Resource *resource, return; } - m_pendingShortcuts[socket].swipes.append(SwipeShortcut{ + SwipeShortcut ss{ .name = name, .finger = finger, .direction = toSwipeDirection(direction), .action = static_cast(action), - }); + }; + + if (socket == m_activeSessionSocket) { + uint status = registerSwipe(socket, ss); + if (status) + sendBindFailure(socket, name, status); + } else { + m_pendingShortcuts[socket].swipes.append(ss); + } } -void ShortcutManagerV2Private::bind_hold_gesture(Resource *resource, +void ShortcutManagerV3Private::bind_hold_gesture(Resource *resource, const QString &name, uint32_t finger, uint32_t action) @@ -406,49 +412,22 @@ void ShortcutManagerV2Private::bind_hold_gesture(Resource *resource, return; } - m_pendingShortcuts[socket].holds.append(HoldShortcut{ + HoldShortcut hs{ .name = name, .finger = finger, .action = static_cast(action), - }); -} - -void ShortcutManagerV2Private::commit(Resource *resource) -{ - WSocket *socket = socketFromResource(resource); - if (ownerClients.value(socket, nullptr) != resource) { - wl_resource_post_error(resource->handle, - error_not_acquired, - "Client has not acquired the shortcut manager."); - return; - } - - if (!m_pendingShortcuts.contains(socket)) { - sendCommitSuccess(socket); - return; - } - - if (socket != m_activeSessionSocket) { - if (m_pendingCommittedShortcuts.contains(socket)) { - sendInvalidCommit(socket); - return; - } - m_pendingCommittedShortcuts[socket] = m_pendingShortcuts.take(socket); - return; - } + }; - const auto pendingShortcuts = m_pendingShortcuts.take(socket); - QString commitFailName; - uint status = updateShortcuts(pendingShortcuts, commitFailName); - if (!status) { - m_shortcuts[socket].append(pendingShortcuts); - sendCommitSuccess(socket); + if (socket == m_activeSessionSocket) { + uint status = registerHold(socket, hs); + if (status) + sendBindFailure(socket, name, status); } else { - sendCommitFailure(socket, commitFailName, status); + m_pendingShortcuts[socket].holds.append(hs); } } -void ShortcutManagerV2Private::unbind(Resource *resource, const QString &name) +void ShortcutManagerV3Private::unbind(Resource *resource, const QString &name) { WSocket *socket = socketFromResource(resource); if (ownerClients.value(socket, nullptr) != resource) { @@ -459,27 +438,31 @@ void ShortcutManagerV2Private::unbind(Resource *resource, const QString &name) } if (socket != m_activeSessionSocket) { + // Remove from pending binds so a bind-then-unbind on an inactive session + // doesn't re-register the shortcut when the session becomes active. + removeShortcutFromMap(m_pendingShortcuts[socket], name); m_pendingDeletes[socket].append(name); return; } m_controller->unregisterShortcut(name); + removeShortcutFromMap(m_shortcuts[socket], name); } -void ShortcutCaptureV1::destroy(Resource *resource) +void ShortcutCaptureV3::destroy(Resource *resource) { // destroy_resource() handles cleanup; just trigger it. wl_resource_destroy(resource->handle); } -void ShortcutCaptureV1::destroy_resource(Resource *) +void ShortcutCaptureV3::destroy_resource(Resource *) { if (m_pending) m_manager->onCaptureDestroyed(this); delete this; } -void ShortcutManagerV2Private::capture_next_shortcut(Resource *resource, +void ShortcutManagerV3Private::capture_next_shortcut(Resource *resource, struct ::wl_resource *surface, struct ::wl_resource *seat_resource, uint32_t capture) @@ -494,11 +477,11 @@ void ShortcutManagerV2Private::capture_next_shortcut(Resource *resource, // Create the capture resource. auto *captureObj = - new ShortcutCaptureV1(this, resource->client(), capture, resource->version()); + new ShortcutCaptureV3(this, resource->client(), capture, resource->version()); // Check if another capture is already in progress. if (m_pendingCapture || m_drainKey != Qt::Key_unknown) { - captureObj->sendFailed(ShortcutCaptureV1::failed_reason_busy); + captureObj->sendFailed(ShortcutCaptureV3::failed_reason_busy); return; } @@ -526,7 +509,7 @@ void ShortcutManagerV2Private::capture_next_shortcut(Resource *resource, // Validate surface focus / active state. auto *focusedSurface = requestedSeat ? requestedSeat->keyboardFocusSurface() : nullptr; if (!focusedSurface || focusedSurface != wSurface) { - captureObj->sendFailed(ShortcutCaptureV1::failed_reason_not_active); + captureObj->sendFailed(ShortcutCaptureV3::failed_reason_not_active); return; } @@ -535,13 +518,13 @@ void ShortcutManagerV2Private::capture_next_shortcut(Resource *resource, m_pendingSurface = wSurface; } -void ShortcutManagerV2Private::onCaptureDestroyed(ShortcutCaptureV1 *c) +void ShortcutManagerV3Private::onCaptureDestroyed(ShortcutCaptureV3 *c) { if (m_pendingCapture == c) resetCaptureState(); } -ShortcutCaptureV1 *ShortcutManagerV2Private::resetCaptureState() +ShortcutCaptureV3 *ShortcutManagerV3Private::resetCaptureState() { auto *c = m_pendingCapture; m_pendingCapture = nullptr; @@ -565,7 +548,7 @@ ShortcutCaptureV1 *ShortcutManagerV2Private::resetCaptureState() // while Win is held terminates capture on KeyPress). // all other keys — terminate on KeyPress via // ShortcutController::isValidShortcutCombination(). -bool ShortcutManagerV2Private::tryHandleCaptureEvent(WSeat *seat, QInputEvent *event) +bool ShortcutManagerV3Private::tryHandleCaptureEvent(WSeat *seat, QInputEvent *event) { // Drain the captured key's residual KeyRelease to avoid triggering // a newly bound shortcut with KeyRelease trigger semantics. @@ -589,7 +572,7 @@ bool ShortcutManagerV2Private::tryHandleCaptureEvent(WSeat *seat, QInputEvent *e // If the requesting surface lost focus, abort capture but do not consume the // current event, so input can continue to the newly focused target. if (!m_pendingSurface || m_pendingSeat->keyboardFocusSurface() != m_pendingSurface) { - resetCaptureState()->sendFailed(ShortcutCaptureV1::failed_reason_aborted); + resetCaptureState()->sendFailed(ShortcutCaptureV3::failed_reason_aborted); return false; } @@ -597,7 +580,7 @@ bool ShortcutManagerV2Private::tryHandleCaptureEvent(WSeat *seat, QInputEvent *e const auto type = event->type(); if (type == QEvent::MouseButtonPress || type == QEvent::MouseButtonRelease || type == QEvent::Wheel) { - resetCaptureState()->sendFailed(ShortcutCaptureV1::failed_reason_interrupted); + resetCaptureState()->sendFailed(ShortcutCaptureV3::failed_reason_interrupted); return true; } @@ -637,81 +620,80 @@ bool ShortcutManagerV2Private::tryHandleCaptureEvent(WSeat *seat, QInputEvent *e resetCaptureState()->sendCaptured(captured); } else { // Invalid combo: fail immediately. - resetCaptureState()->sendFailed(ShortcutCaptureV1::failed_reason_interrupted); + resetCaptureState()->sendFailed(ShortcutCaptureV3::failed_reason_interrupted); } } else { // KeyRelease, only needed for modifier-only paths. if (isPureModifierKey(key)) { // Pure modifier released without any regular key press. - resetCaptureState()->sendFailed(ShortcutCaptureV1::failed_reason_interrupted); + resetCaptureState()->sendFailed(ShortcutCaptureV3::failed_reason_interrupted); } else if (key == Qt::Key_Super_L || key == Qt::Key_Super_R || key == Qt::Key_Meta) { // Win/Super is only valid when pressed alone (no other modifiers held). if ((kevent->modifiers() & ~Qt::MetaModifier) == Qt::NoModifier) resetCaptureState()->sendCaptured(QStringLiteral("Meta")); else - resetCaptureState()->sendFailed(ShortcutCaptureV1::failed_reason_interrupted); + resetCaptureState()->sendFailed(ShortcutCaptureV3::failed_reason_interrupted); } else { // With press-trigger semantics, regular non-modifier release should not // normally reach here. Be defensive and fail-safe if it does. - resetCaptureState()->sendFailed(ShortcutCaptureV1::failed_reason_interrupted); + resetCaptureState()->sendFailed(ShortcutCaptureV3::failed_reason_interrupted); } } return true; } -ShortcutManagerV2::ShortcutManagerV2(QObject *parent) +ShortcutManagerV3::ShortcutManagerV3(QObject *parent) : QObject(parent) - , d(std::make_unique(this)) + , d(std::make_unique()) { d->m_controller = new ShortcutController(this); } -ShortcutManagerV2::~ShortcutManagerV2() = default; +ShortcutManagerV3::~ShortcutManagerV3() = default; -void ShortcutManagerV2::create(WServer *server) +void ShortcutManagerV3::create(WServer *server) { d->init(server->handle(), InterfaceVersion); } -void ShortcutManagerV2::destroy(WServer *server) +void ShortcutManagerV3::destroy(WServer *server) { Q_UNUSED(server); d->globalRemove(); Q_EMIT before_destroy(); } -wl_global *ShortcutManagerV2::global() const +wl_global *ShortcutManagerV3::global() const { return d->global(); } -bool ShortcutManagerV2::tryHandleCaptureEvent(WSeat *seat, QInputEvent *event) +bool ShortcutManagerV3::tryHandleCaptureEvent(WSeat *seat, QInputEvent *event) { return d->tryHandleCaptureEvent(seat, event); } -bool ShortcutManagerV2::isCaptureActive() +bool ShortcutManagerV3::isCaptureActive() const { return d->m_pendingCapture || d->m_drainKey != Qt::Key_unknown; } -QByteArrayView ShortcutManagerV2::interfaceName() const +QByteArrayView ShortcutManagerV3::interfaceName() const { - return "treeland_shortcut_manager_v2"; + return "treeland_shortcut_manager_v3"; } -ShortcutController* ShortcutManagerV2::controller() +ShortcutController* ShortcutManagerV3::controller() { return d->m_controller; } -void ShortcutManagerV2::sendActivated(const QString& name, ShortcutController::KeyFlags keyFlags) +void ShortcutManagerV3::sendActivated(const QString& name, ShortcutController::KeyFlags keyFlags) { d->sendActivated(d->m_activeSessionSocket, name, keyFlags); } -void ShortcutManagerV2::onSessionChanged() +void ShortcutManagerV3::onSessionChanged() { - QString commitFailName; auto session = Helper::instance()->sessionManager()->activeSession().lock(); if (!session) { return; @@ -725,32 +707,64 @@ void ShortcutManagerV2::onSessionChanged() d->m_controller->clear(); d->m_activeSessionSocket = socket; + // Re-register previously accepted shortcuts for this session. if (d->m_shortcuts.contains(socket)) { - uint status = d->updateShortcuts(d->m_shortcuts[socket], commitFailName); - if (status) { - qCWarning(lcTlShortcut) << "Failed to restore shortcuts" << commitFailName + const auto &shortcuts = d->m_shortcuts[socket]; + for (const auto &ks : std::as_const(shortcuts.keys)) { + uint status = d->m_controller->registerKey(ks.name, ks.key, ks.keybindFlags, ks.action); + if (status) { + qCWarning(lcTlShortcut) << "Failed to restore key shortcut" << ks.name << "by reason" << status << "for session" << session->id() << "for user" << session->username(); + } + } + for (const auto &ss : std::as_const(shortcuts.swipes)) { + uint status = d->m_controller->registerSwipeGesture(ss.name, ss.finger, ss.direction, ss.action); + if (status) { + qCWarning(lcTlShortcut) << "Failed to restore swipe shortcut" << ss.name + << "by reason" << status + << "for session" << session->id() + << "for user" << session->username(); + } + } + for (const auto &hs : std::as_const(shortcuts.holds)) { + uint status = d->m_controller->registerHoldGesture(hs.name, hs.finger, hs.action); + if (status) { + qCWarning(lcTlShortcut) << "Failed to restore hold shortcut" << hs.name + << "by reason" << status + << "for session" << session->id() + << "for user" << session->username(); + } } - return; } + // Apply deferred unbinds. if (d->m_pendingDeletes.contains(socket)) { const auto names = d->m_pendingDeletes.take(socket); for (const auto& name : std::as_const(names)) { d->m_controller->unregisterShortcut(name); + removeShortcutFromMap(d->m_shortcuts[socket], name); } } - if (d->m_pendingCommittedShortcuts.contains(socket)) { - const auto pendingShortcuts = d->m_pendingCommittedShortcuts.take(socket); - uint status = d->updateShortcuts(pendingShortcuts, commitFailName); - if (!status) { - d->m_shortcuts[socket].append(pendingShortcuts); - d->sendCommitSuccess(socket); - } else { - d->sendCommitFailure(socket, commitFailName, status); + // Apply deferred binds — each independently, send bind_failure for failures. + if (d->m_pendingShortcuts.contains(socket)) { + const auto pending = d->m_pendingShortcuts.take(socket); + for (const auto &ks : std::as_const(pending.keys)) { + uint status = d->registerKey(socket, ks); + if (status) + d->sendBindFailure(socket, ks.name, status); + } + for (const auto &ss : std::as_const(pending.swipes)) { + uint status = d->registerSwipe(socket, ss); + if (status) + d->sendBindFailure(socket, ss.name, status); + } + for (const auto &hs : std::as_const(pending.holds)) { + uint status = d->registerHold(socket, hs); + if (status) + d->sendBindFailure(socket, hs.name, status); } } } diff --git a/src/modules/shortcut/shortcutmanager.h b/src/modules/shortcut/shortcutmanager.h index 2b4d3567a3..fa69e6a6e2 100644 --- a/src/modules/shortcut/shortcutmanager.h +++ b/src/modules/shortcut/shortcutmanager.h @@ -11,7 +11,7 @@ #include #include -class ShortcutManagerV2Private; +class ShortcutManagerV3Private; WAYLIB_SERVER_BEGIN_NAMESPACE class WServer; @@ -21,23 +21,23 @@ WAYLIB_SERVER_END_NAMESPACE WAYLIB_SERVER_USE_NAMESPACE -class ShortcutManagerV2 +class ShortcutManagerV3 : public QObject , public WAYLIB_SERVER_NAMESPACE::WServerInterface { Q_OBJECT public: - explicit ShortcutManagerV2(QObject *parent = nullptr); - ~ShortcutManagerV2() override; + explicit ShortcutManagerV3(QObject *parent = nullptr); + ~ShortcutManagerV3() override; QByteArrayView interfaceName() const override; - static constexpr int InterfaceVersion = 2; + static constexpr int InterfaceVersion = 1; ShortcutController* controller(); void sendActivated(const QString& name, ShortcutController::KeyFlags keyFlags); bool tryHandleCaptureEvent(WAYLIB_SERVER_NAMESPACE::WSeat *seat, QInputEvent *event); - bool isCaptureActive(); + bool isCaptureActive() const; public Q_SLOTS: void onSessionChanged(); @@ -51,5 +51,5 @@ public Q_SLOTS: void before_destroy(); private: - std::unique_ptr d; + std::unique_ptr d; }; diff --git a/src/modules/shortcut/shortcutrunner.cpp b/src/modules/shortcut/shortcutrunner.cpp index 35b32608f1..d1bb08143c 100644 --- a/src/modules/shortcut/shortcutrunner.cpp +++ b/src/modules/shortcut/shortcutrunner.cpp @@ -9,6 +9,7 @@ #include "interfaces/multitaskviewinterface.h" #include "seat/helper.h" #include "shortcutcontroller.h" +#include "shortcutmanager.h" #include "treelandconfig.hpp" #include "workspace/workspace.h" #include "workspaceanimationcontroller.h" @@ -17,8 +18,8 @@ #include // Maps the shortcut-manager action enum (ShortcutAction) onto the shared -// ShellAction vocabulary. Notify, Quit and the task-switch stepping actions -// are producer-local and return std::nullopt. +// ShellAction vocabulary. Notify and the task-switch stepping actions are +// producer-local and return std::nullopt. static std::optional mapShortcutAction(ShortcutAction action) { switch (action) { @@ -34,6 +35,18 @@ static std::optional mapShortcutAction(ShortcutAction action) return ShellAction::SwitchWorkspace5; case ShortcutAction::Workspace6: return ShellAction::SwitchWorkspace6; + case ShortcutAction::Workspace7: + return ShellAction::SwitchWorkspace7; + case ShortcutAction::Workspace8: + return ShellAction::SwitchWorkspace8; + case ShortcutAction::Workspace9: + return ShellAction::SwitchWorkspace9; + case ShortcutAction::Workspace10: + return ShellAction::SwitchWorkspace10; + case ShortcutAction::Workspace11: + return ShellAction::SwitchWorkspace11; + case ShortcutAction::Workspace12: + return ShellAction::SwitchWorkspace12; case ShortcutAction::PrevWorkspace: return ShellAction::PreviousWorkspace; case ShortcutAction::NextWorkspace: @@ -56,6 +69,8 @@ static std::optional mapShortcutAction(ShortcutAction action) return ShellAction::Maximize; case ShortcutAction::CancelMaximize: return ShellAction::CancelMaximize; + case ShortcutAction::Minimize: + return ShellAction::Minimize; case ShortcutAction::MoveWindow: return ShellAction::MoveWindow; case ShortcutAction::CloseWindow: @@ -66,9 +81,38 @@ static std::optional mapShortcutAction(ShortcutAction action) return ShellAction::TileLeft; case ShortcutAction::TileRight: return ShellAction::TileRight; - default: + case ShortcutAction::TileTopLeft: + return ShellAction::TileTopLeft; + case ShortcutAction::TileTopRight: + return ShellAction::TileTopRight; + case ShortcutAction::TileBottomLeft: + return ShellAction::TileBottomLeft; + case ShortcutAction::TileBottomRight: + return ShellAction::TileBottomRight; + case ShortcutAction::ZoomIn: + return ShellAction::ZoomIn; + case ShortcutAction::ZoomOut: + return ShellAction::ZoomOut; + case ShortcutAction::ZoomReset: + return ShellAction::ZoomReset; + case ShortcutAction::MoveWindowToPrevWorkspace: + return ShellAction::MoveWindowToPrevWorkspace; + case ShortcutAction::MoveWindowToNextWorkspace: + return ShellAction::MoveWindowToNextWorkspace; + // Intentionally unsupported for now: producer-local notify, task-switch + // stepping, and actions whose execution plumbing has not landed yet. + // Kept as explicit cases so -Wswitch fires when the enum gains a member. + case ShortcutAction::Notify: + case ShortcutAction::ResizeWindow: + case ShortcutAction::TileTop: + case ShortcutAction::TileBottom: + case ShortcutAction::TaskSwitchNext: + case ShortcutAction::TaskSwitchPrev: + case ShortcutAction::TaskSwitchSameAppNext: + case ShortcutAction::TaskSwitchSameAppPrev: return std::nullopt; } + return std::nullopt; } ShortcutRunner::ShortcutRunner(QObject *parent) @@ -102,9 +146,6 @@ void ShortcutRunner::onActionTrigger(ShortcutAction action, const QString &name, case ShortcutAction::Notify: helper->m_shortcutManager->sendActivated(name, keyFlags); break; - case ShortcutAction::Quit: - Q_EMIT helper->requestQuit(); - break; case ShortcutAction::TaskSwitchNext: case ShortcutAction::TaskSwitchPrev: case ShortcutAction::TaskSwitchSameAppNext: diff --git a/src/modules/shortcut/shortcutrunner.h b/src/modules/shortcut/shortcutrunner.h index 8ca2c399b0..481f09f053 100644 --- a/src/modules/shortcut/shortcutrunner.h +++ b/src/modules/shortcut/shortcutrunner.h @@ -6,7 +6,7 @@ #include #include #include -#include "shortcutmanager.h" +#include "shortcutcontroller.h" class ShortcutRunner : public QObject { diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 1016c935d4..ac8b0a0c67 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -2421,11 +2421,11 @@ void Helper::init(Treeland::Treeland *treeland) m_wallpaperManager, &WallpaperManager::onWallpaperAdded); - m_shortcutManager = m_server->attach(); + m_shortcutManager = m_server->attach(); connect(m_treeland, &Treeland::Treeland::SessionChanged, m_shortcutManager, - &ShortcutManagerV2::onSessionChanged); + &ShortcutManagerV3::onSessionChanged); m_shortcutManager->onSessionChanged(); auto shortcutControl = m_shortcutManager->controller(); auto *shortcutRunner = new ShortcutRunner(shortcutControl); diff --git a/src/seat/helper.h b/src/seat/helper.h index 6ca9acb683..f1156ab0ec 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -6,7 +6,6 @@ #include #include "core/qmlengine.h" #include "modules/activation/activationmanagerinterfacev1.h" -#include "modules/shortcut/shortcutmanager.h" #include "modules/virtual-output/virtualoutputmanagerinterfacev1.h" #include "modules/wallpaper/wallpapermanagerinterfacev1.h" #include "modules/wallpaper/wallpapernotifierinterfacev1.h" @@ -120,7 +119,7 @@ class SettingManager; class SessionModel; class ShellHandler; class ShellActionExecutor; -class ShortcutManagerV2; +class ShortcutManagerV3; class ShortcutRunner; class SurfaceContainer; class SurfaceWrapper; @@ -487,7 +486,7 @@ private Q_SLOTS: WRelativePointerManagerV1 *m_relativePointerManager = nullptr; WPointerConstraintsV1 *m_pointerConstraintsV1 = nullptr; PointerConstraintsManager *m_pointerConstraintsManager = nullptr; - ShortcutManagerV2 *m_shortcutManager = nullptr; + ShortcutManagerV3 *m_shortcutManager = nullptr; PersonalizationManagerInterfaceV1 *m_personalizationInterfaceV1 = nullptr; AppearanceInterfaceV1 *m_appearanceInterfaceV1 = nullptr; AppearanceManagerInterfaceV1 *m_appearanceManagerInterfaceV1 = nullptr; diff --git a/tests/protocols/CMakeLists.txt b/tests/protocols/CMakeLists.txt index c930467273..3d994bf565 100644 --- a/tests/protocols/CMakeLists.txt +++ b/tests/protocols/CMakeLists.txt @@ -55,8 +55,8 @@ add_subdirectory(treeland-prelaunch-splash-v2) add_subdirectory(treeland-prelaunch-splash-desktop-v2) add_subdirectory(treeland-screensaver-v2) add_subdirectory(treeland-screensaver-desktop-v2) -add_subdirectory(treeland-shortcut-manager-v2) -add_subdirectory(treeland-shortcut-manager-desktop-v2) +add_subdirectory(treeland-shortcut-manager-v3) +add_subdirectory(treeland-shortcut-manager-desktop-v3) add_subdirectory(treeland-virtual-output-manager-v1) add_subdirectory(treeland-virtual-output-desktop-v1) add_subdirectory(treeland-wallpaper-color-v1) diff --git a/tests/protocols/INDEX.md b/tests/protocols/INDEX.md index 2def61c1a0..b568352961 100644 --- a/tests/protocols/INDEX.md +++ b/tests/protocols/INDEX.md @@ -41,7 +41,7 @@ Wayland 线上请求与事件;本文档规定发出请求后,测试必须观 | [remote-subsurface-unstable-v1](treeland-remote-subsurface-unstable-v1/README.md) | E / P | token 导出、真实 Remote `WSubsurface` 的父子关系、位置与层级;`parent_rejected` / `invalid_sibling` | 跨 client、循环 parent、映射生命周期与 sibling 间排序 | | [prelaunch-splash-v2](treeland-prelaunch-splash-v2/README.md) | I / E | splash 请求/关闭信号;生产 splash wrapper 创建、加入 workspace 与销毁 | | [screensaver-v1](treeland-screensaver-v1/README.md) | E / P | 真实 ext-idle 抑制生命周期 | -| [shortcut-manager-v2](treeland-shortcut-manager-v2/README.md) | E / P | 聚焦窗口捕获与快捷键激活 | +| [shortcut-manager-v3](treeland-shortcut-manager-v3/README.md) | E / P | 聚焦窗口捕获与快捷键激活 | | [virtual-output-manager-v1](treeland-virtual-output-manager-v1/README.md) | P / E | 虚拟输出资源校验;两个既有输出的镜像/恢复 | | [wallpaper-color-v1](treeland-wallpaper-color-v1/README.md) | I | 订阅、去重与颜色通知 | | [wallpaper-manager-unstable-v1](treeland-wallpaper-manager-unstable-v1/README.md) | I / P | 真实输出上的壁纸资源生命周期 | @@ -131,7 +131,7 @@ request stub 算作 request 覆盖;生成的 client-protocol 文件本身不 | remote-subsurface-unstable-v1 | 8 / 8 | token;真实 Remote `WSubsurface` 的父子关系、位置、Below 层级;`parent_rejected` / `invalid_sibling` | 跨 client、循环 parent、映射生命周期、sibling 间排序 | | prelaunch-splash-v2 | 3 / 3 | 创建、关闭;真实 splash wrapper 加入/离开 workspace | splash QML 最终可见性、纹理和像素 | | screensaver-v1 | 2 / 3 | 真实 ext-idle 被 inhibit/uninhibit 改变 | 显式 `destroy` request、实际锁屏 UI | -| shortcut-manager-v2 | 6 / 9 | `commit_success`、`captured`、`activated`;真实 virtual keyboard 输入链 | swipe、hold、`unbind`、`commit_failure` 的业务分支;物理键盘 | +| shortcut-manager-v3 | 6 / 9 | `bind_failure`、`captured`、`activated`;真实 virtual keyboard 输入链 | swipe、hold、`unbind` 的业务分支;物理键盘 | | virtual-output-manager-v1 | 5 / 5 | `outputs/error/virtual_output_list`;生产 copy output 创建与恢复 | 热插拔 successor、跨进程持久化、物理显示器内容一致性 | | wallpaper-color-v1 | 3 / 3 | `output_color` 订阅与去重 | 真实壁纸分析来源、壁纸应用后的 output 色彩变化 | | wallpaper-manager-unstable-v1 | 4 / 5 | `failed/changed`;`set_image_source` 与 wallpaper shell/output 关联 | `set_video_source`、实际映射/QML 接入、媒体解码失败、最终 output 像素 | diff --git a/tests/protocols/treeland-shortcut-manager-desktop-v2/CMakeLists.txt b/tests/protocols/treeland-shortcut-manager-desktop-v3/CMakeLists.txt similarity index 80% rename from tests/protocols/treeland-shortcut-manager-desktop-v2/CMakeLists.txt rename to tests/protocols/treeland-shortcut-manager-desktop-v3/CMakeLists.txt index c18d7e87d7..b522f401f3 100644 --- a/tests/protocols/treeland-shortcut-manager-desktop-v2/CMakeLists.txt +++ b/tests/protocols/treeland-shortcut-manager-desktop-v3/CMakeLists.txt @@ -1,9 +1,8 @@ - treeland_add_protocol_test( - NAME treeland_shortcut_manager_desktop_v2 - XML "${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-v2.xml" + NAME treeland_shortcut_manager_desktop_v3 + XML "${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-unstable-v3.xml" EXTRA_XMLS "${CMAKE_SOURCE_DIR}/3rdparty/wlroots/protocol/virtual-keyboard-unstable-v1.xml" EXTRA_LIBRARIES PkgConfig::XKBCOMMON SETUP "${CMAKE_CURRENT_SOURCE_DIR}/setup.cpp" - CLIENT "${CMAKE_CURRENT_SOURCE_DIR}/treeland-shortcut-manager-desktop-v2.c" + CLIENT "${CMAKE_CURRENT_SOURCE_DIR}/treeland-shortcut-manager-desktop-v3.c" ) diff --git a/tests/protocols/treeland-shortcut-manager-desktop-v2/setup.cpp b/tests/protocols/treeland-shortcut-manager-desktop-v3/setup.cpp similarity index 97% rename from tests/protocols/treeland-shortcut-manager-desktop-v2/setup.cpp rename to tests/protocols/treeland-shortcut-manager-desktop-v3/setup.cpp index 3e06bbfd27..bd7e1cbe15 100644 --- a/tests/protocols/treeland-shortcut-manager-desktop-v2/setup.cpp +++ b/tests/protocols/treeland-shortcut-manager-desktop-v3/setup.cpp @@ -5,7 +5,7 @@ #include "seat/helper.h" #include "seat/seatmanager.h" #include "surface/surfacewrapper.h" -#include "treeland-shortcut-manager-desktop-v2.h" +#include "treeland-shortcut-manager-desktop-v3.h" #include "workspace/workspace.h" #include diff --git a/tests/protocols/treeland-shortcut-manager-desktop-v2/treeland-shortcut-manager-desktop-v2.c b/tests/protocols/treeland-shortcut-manager-desktop-v3/treeland-shortcut-manager-desktop-v3.c similarity index 82% rename from tests/protocols/treeland-shortcut-manager-desktop-v2/treeland-shortcut-manager-desktop-v2.c rename to tests/protocols/treeland-shortcut-manager-desktop-v3/treeland-shortcut-manager-desktop-v3.c index fd8503b30c..170ce23eb2 100644 --- a/tests/protocols/treeland-shortcut-manager-desktop-v2/treeland-shortcut-manager-desktop-v2.c +++ b/tests/protocols/treeland-shortcut-manager-desktop-v3/treeland-shortcut-manager-desktop-v3.c @@ -1,9 +1,9 @@ // 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 "treeland-shortcut-manager-desktop-v2.h" +#include "treeland-shortcut-manager-desktop-v3.h" #include "server-bridge-api.h" #include "xdg-toplevel-client.h" -#include "treeland-shortcut-manager-v2-client-protocol.h" +#include "treeland-shortcut-manager-unstable-v3-client-protocol.h" #include "virtual-keyboard-unstable-v1-client-protocol.h" #include @@ -19,8 +19,7 @@ extern void shortcut_desktop_focus_window(void *data); struct shortcut_client { - int commit_success; - int commit_failure; + int bind_failure; int activated; char activated_name[64]; unsigned int activated_flags; @@ -101,7 +100,7 @@ static const struct wl_keyboard_listener seat_keyboard_listener = { }; static void manager_activated(void *data, - struct treeland_shortcut_manager_v2 *manager, + struct treeland_shortcut_manager_v3 *manager, const char *name, uint32_t flags) { @@ -112,30 +111,23 @@ static void manager_activated(void *data, strncpy(client->activated_name, name, sizeof(client->activated_name) - 1); } -static void manager_commit_success(void *data, struct treeland_shortcut_manager_v2 *manager) -{ - (void)manager; - ++((struct shortcut_client *)data)->commit_success; -} - -static void manager_commit_failure(void *data, - struct treeland_shortcut_manager_v2 *manager, - const char *name, - uint32_t error) +static void manager_bind_failure(void *data, + struct treeland_shortcut_manager_v3 *manager, + const char *name, + uint32_t error) { (void)manager; (void)name; (void)error; - ++((struct shortcut_client *)data)->commit_failure; + ++((struct shortcut_client *)data)->bind_failure; } -static const struct treeland_shortcut_manager_v2_listener manager_listener = { +static const struct treeland_shortcut_manager_v3_listener manager_listener = { .activated = manager_activated, - .commit_success = manager_commit_success, - .commit_failure = manager_commit_failure, + .bind_failure = manager_bind_failure, }; -static void capture_captured(void *data, struct treeland_shortcut_capture_v1 *capture, const char *key) +static void capture_captured(void *data, struct treeland_shortcut_capture_v3 *capture, const char *key) { (void)capture; struct shortcut_client *client = data; @@ -143,14 +135,14 @@ static void capture_captured(void *data, struct treeland_shortcut_capture_v1 *ca strncpy(client->captured_key, key, sizeof(client->captured_key) - 1); } -static void capture_failed(void *data, struct treeland_shortcut_capture_v1 *capture, uint32_t reason) +static void capture_failed(void *data, struct treeland_shortcut_capture_v3 *capture, uint32_t reason) { (void)capture; (void)reason; ++((struct shortcut_client *)data)->capture_failed; } -static const struct treeland_shortcut_capture_v1_listener capture_listener = { +static const struct treeland_shortcut_capture_v3_listener capture_listener = { .captured = capture_captured, .failed = capture_failed, }; @@ -204,8 +196,8 @@ int protocol_test_run(const char *socket_name) { struct client_connection connection; struct xdg_toplevel_client toplevel = { 0 }; - struct treeland_shortcut_manager_v2 *manager = NULL; - struct treeland_shortcut_capture_v1 *capture = NULL; + struct treeland_shortcut_manager_v3 *manager = NULL; + struct treeland_shortcut_capture_v3 *capture = NULL; struct zwp_virtual_keyboard_manager_v1 *virtual_keyboard_manager = NULL; struct zwp_virtual_keyboard_v1 *virtual_keyboard = NULL; struct wl_seat *seat = NULL; @@ -218,15 +210,15 @@ int protocol_test_run(const char *socket_name) if (!client_connect(&connection, socket_name)) return 1; - manager = client_bind(&connection, "treeland_shortcut_manager_v2", - &treeland_shortcut_manager_v2_interface, 2); + manager = client_bind(&connection, "treeland_shortcut_manager_v3", + &treeland_shortcut_manager_v3_interface, 1); seat = client_bind(&connection, "wl_seat", &wl_seat_interface, 1); virtual_keyboard_manager = client_bind(&connection, "zwp_virtual_keyboard_manager_v1", &zwp_virtual_keyboard_manager_v1_interface, 1); if (!manager || !seat || !virtual_keyboard_manager) goto failed; wl_seat_add_listener(seat, &seat_listener, &seat_events); - treeland_shortcut_manager_v2_add_listener(manager, &manager_listener, &client); + treeland_shortcut_manager_v3_add_listener(manager, &manager_listener, &client); stage = 1; if (!xdg_toplevel_client_create(&connection, &toplevel)) goto failed; @@ -257,11 +249,11 @@ int protocol_test_run(const char *socket_name) || keyboard_events.group) goto failed; - treeland_shortcut_manager_v2_acquire(manager); - capture = treeland_shortcut_manager_v2_capture_next_shortcut(manager, toplevel.surface, seat); + treeland_shortcut_manager_v3_acquire(manager); + capture = treeland_shortcut_manager_v3_capture_next_shortcut(manager, toplevel.surface, seat); if (!capture) goto failed; - treeland_shortcut_capture_v1_add_listener(capture, &capture_listener, &client); + treeland_shortcut_capture_v3_add_listener(capture, &capture_listener, &client); if (wl_display_roundtrip(connection.display) < 0 || client.capture_failed) goto failed; stage = 3; @@ -274,26 +266,25 @@ int protocol_test_run(const char *socket_name) if (wl_display_roundtrip(connection.display) < 0) goto failed; - treeland_shortcut_manager_v2_bind_key( + // v3: bind_key takes effect immediately — no commit needed. + treeland_shortcut_manager_v3_bind_key( manager, "desktop-shortcut", "F2", - TREELAND_SHORTCUT_MANAGER_V2_KEYBIND_FLAG_KEY_PRESS, - TREELAND_SHORTCUT_MANAGER_V2_ACTION_NOTIFY); - treeland_shortcut_manager_v2_commit(manager); - if (wl_display_roundtrip(connection.display) < 0 || client.commit_success != 1 - || client.commit_failure) + TREELAND_SHORTCUT_MANAGER_V3_KEYBIND_FLAG_KEY_PRESS, + TREELAND_SHORTCUT_MANAGER_V3_ACTION_NOTIFY); + if (wl_display_roundtrip(connection.display) < 0 || client.bind_failure) goto failed; stage = 5; send_key(virtual_keyboard, 60, WL_KEYBOARD_KEY_STATE_PRESSED); // KEY_F2 if (wl_display_roundtrip(connection.display) < 0 || client.activated != 1 || strcmp(client.activated_name, "desktop-shortcut") != 0 - || client.activated_flags != TREELAND_SHORTCUT_MANAGER_V2_KEYBIND_FLAG_KEY_PRESS) + || client.activated_flags != TREELAND_SHORTCUT_MANAGER_V3_KEYBIND_FLAG_KEY_PRESS) goto failed; stage = 6; - treeland_shortcut_capture_v1_destroy(capture); + treeland_shortcut_capture_v3_destroy(capture); zwp_virtual_keyboard_v1_destroy(virtual_keyboard); wl_keyboard_destroy(seat_keyboard); - treeland_shortcut_manager_v2_destroy(manager); + treeland_shortcut_manager_v3_destroy(manager); wl_seat_destroy(seat); xdg_toplevel_client_destroy(&toplevel); client_disconnect(&connection); @@ -302,15 +293,15 @@ int protocol_test_run(const char *socket_name) failed: fprintf(stderr, "shortcut desktop failure at stage %d: wrapper=%d workspace=%d visible=%d focus=%d " - "captured=%d key=%s failed=%d commit=(%d,%d) activated=%d name=%s flags=%u\n", + "captured=%d key=%s failed=%d bind_failure=%d activated=%d name=%s flags=%u\n", stage, state.wrapper_created, state.wrapper_in_workspace, state.wrapper_visible, state.keyboard_focused, client.captured, client.captured_key, client.capture_failed, - client.commit_success, client.commit_failure, client.activated, client.activated_name, + client.bind_failure, client.activated, client.activated_name, client.activated_flags); - if (capture) treeland_shortcut_capture_v1_destroy(capture); + if (capture) treeland_shortcut_capture_v3_destroy(capture); if (virtual_keyboard) zwp_virtual_keyboard_v1_destroy(virtual_keyboard); if (seat_keyboard) wl_keyboard_destroy(seat_keyboard); - if (manager) treeland_shortcut_manager_v2_destroy(manager); + if (manager) treeland_shortcut_manager_v3_destroy(manager); if (seat) wl_seat_destroy(seat); xdg_toplevel_client_destroy(&toplevel); client_disconnect(&connection); diff --git a/tests/protocols/treeland-shortcut-manager-desktop-v2/treeland-shortcut-manager-desktop-v2.h b/tests/protocols/treeland-shortcut-manager-desktop-v3/treeland-shortcut-manager-desktop-v3.h similarity index 100% rename from tests/protocols/treeland-shortcut-manager-desktop-v2/treeland-shortcut-manager-desktop-v2.h rename to tests/protocols/treeland-shortcut-manager-desktop-v3/treeland-shortcut-manager-desktop-v3.h diff --git a/tests/protocols/treeland-shortcut-manager-v2/README.md b/tests/protocols/treeland-shortcut-manager-v2/README.md deleted file mode 100644 index e74a321b3a..0000000000 --- a/tests/protocols/treeland-shortcut-manager-v2/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# `treeland-shortcut-manager-v2` 测试规范 - -## 范围 - -- 测试源码:`tests/protocols/treeland-shortcut-manager-v2/`、`tests/protocols/treeland-shortcut-manager-desktop-v2/` -- Fixture:协议 fixture;包含 mapped、聚焦 xdg-toplevel 和上游 `zwp_virtual_keyboard_v1` 的 desktop fixture。 -- 覆盖等级:desktop 用例为 **E**;基础 manager 用例为 **P**。 - -## 实际请求与预期结果 - -| 场景 | 客户端发送 | 生产业务逻辑与断言 | -| --- | --- | --- | -| 基础提交 | `acquire`、`bind_key`、`commit` | 活动 session 的 manager 发送 `commit_success` | -| 捕获快捷键 | 对已聚焦 mapped 窗口调用 `capture_next_shortcut`,虚拟键盘保持 Ctrl+Shift 并发送 evdev `KEY_C` press/release | 生产 capture filter 返回 `captured("Ctrl+Shift+C")` | -| 激活快捷键 | 注册 Ctrl+Shift+K 的 Notify action,虚拟键盘发送 evdev `KEY_K` press | `ShortcutRunner` 发送 `activated("desktop-shortcut", key-press)` | - -## 已证明的生产链路 - -客户端 map xdg-toplevel 后,服务端调用 `Helper::activateSurface()` 聚焦它,并逐项断言 -真实 wrapper 已加入 `Workspace`、可见,且 `WSeat::keyboardFocusSurface()` 正是该窗口 -的 `WSurface`。客户端通过上游 `zwp_virtual_keyboard_manager_v1` 为该 `wl_seat` 创建 -virtual keyboard,先发送 XKB keymap,再发送 Ctrl 与 Shift 的 depressed modifier mask。 - -接着客户端发送 `acquire()` 和 `capture_next_shortcut(toplevel.surface, seat)`,再发送 -evdev `KEY_C`(键码 46)按下。生产 `WInputMethodHelper` 将 virtual keyboard 附着到 -`WSeat`;键盘事件进入 `Helper::beforeDisposeEvent()`,由于 capture active,进入 -`ShortcutManagerV2::tryHandleCaptureEvent()`。该函数把真实 `QKeyEvent` 规范化为 -`Ctrl+Shift+C`,通过 capture resource 发送 `captured`;测试断言仅收到一次,随后 -发送 `KEY_C` release 验证 drain 路径不会产生额外结果。 - -然后客户端发送 `bind_key("desktop-shortcut", "Ctrl+Shift+K", KEY_PRESS, NOTIFY)` 和 -`commit()`,断言 `commit_success==1` 且没有 `commit_failure`。发送 evdev `KEY_K` -(键码 37)按下后,事件经过同一个 `WSeat → Helper::beforeDisposeEvent → -ShortcutController` 路径,`ShortcutRunner` 执行 `NOTIFY` 并由 manager 向 active session -客户端发送 `activated("desktop-shortcut", KEY_PRESS)`。测试逐项断言 name、flags 和 -计数均匹配。 - -## 未覆盖 - -这是虚拟输入的语义覆盖,不是物理键盘驱动覆盖。desktop 用例必须在本地通过后, -才能把上述结果标为该提交的执行证据。 - -当前基础 fixture 的客户端连接活动 session socket,因此只覆盖即时提交。inactive -session 的暂存与切换后应用,需要第二个 session socket 的专用 fixture。 diff --git a/tests/protocols/treeland-shortcut-manager-v2/CMakeLists.txt b/tests/protocols/treeland-shortcut-manager-v3/CMakeLists.txt similarity index 76% rename from tests/protocols/treeland-shortcut-manager-v2/CMakeLists.txt rename to tests/protocols/treeland-shortcut-manager-v3/CMakeLists.txt index 98d83623f5..c00e5f8752 100644 --- a/tests/protocols/treeland-shortcut-manager-v2/CMakeLists.txt +++ b/tests/protocols/treeland-shortcut-manager-v3/CMakeLists.txt @@ -1,7 +1,6 @@ - treeland_add_protocol_test( - NAME treeland_shortcut_manager_v2 - XML "${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-v2.xml" + NAME treeland_shortcut_manager_v3 + XML "${TREELAND_PROTOCOLS_DATA_DIR}/treeland-shortcut-manager-unstable-v3.xml" SETUP "${CMAKE_CURRENT_SOURCE_DIR}/setup.cpp" - CLIENT "${CMAKE_CURRENT_SOURCE_DIR}/treeland-shortcut-manager-v2.c" + CLIENT "${CMAKE_CURRENT_SOURCE_DIR}/treeland-shortcut-manager-v3.c" ) diff --git a/tests/protocols/treeland-shortcut-manager-v3/README.md b/tests/protocols/treeland-shortcut-manager-v3/README.md new file mode 100644 index 0000000000..b4a47ba9cb --- /dev/null +++ b/tests/protocols/treeland-shortcut-manager-v3/README.md @@ -0,0 +1,52 @@ +# `treeland-shortcut-manager-v3` 测试规范 + +## 范围 + +- 测试源码:`tests/protocols/treeland-shortcut-manager-v3/`、`tests/protocols/treeland-shortcut-manager-desktop-v3/` +- Fixture:协议 fixture;包含 mapped、聚焦 xdg-toplevel 和上游 `zwp_virtual_keyboard_v1` 的 desktop fixture。 +- 覆盖等级:desktop 用例为 **E**;基础 manager 用例为 **P**。 + +## 实际请求与预期结果 + +| 场景 | 客户端发送 | 生产业务逻辑与断言 | +| --- | --- | --- | +| 即时绑定 | `acquire`、`bind_key` | 活动 session 的 manager 立即注册绑定;成功时不发送事件,客户端通过 `wl_display.sync` 确认 | +| 名称冲突 | `bind_key`(重复名称、不同按键) | manager 发送 `bind_failure` 事件,error 为 `name_conflict` | +| 捕获快捷键 | 对已聚焦 mapped 窗口调用 `capture_next_shortcut`,先按下再释放 Ctrl 清空修饰键状态,随后虚拟键盘单独发送 evdev `KEY_F1` press/release | 生产 capture filter 将无修饰键的单键捕获为 `captured("F1")` | +| 激活快捷键 | 注册 F2 的 Notify action(即时生效),虚拟键盘发送 evdev `KEY_F2` press | `ShortcutRunner` 发送 `activated("desktop-shortcut", key-press)` | + +## v3 协议变更要点 + +1. **commit 机制移除**:`bind_key`、`bind_swipe_gesture`、`bind_hold_gesture` 立即生效,无需 `commit`。 +2. **bind_failure 事件**:绑定失败时逐条发送 `bind_failure` 事件,不影响同批次其他绑定。 +3. **action 枚举重编号**:`notify` 从 1→0,所有条目下移 1。 +4. **接口版本重置为 1**:客户端绑定版本为 1。 +5. **接口重命名**:`treeland_shortcut_manager_v3`、`treeland_shortcut_capture_v3`。 + +## 已证明的生产链路 + +客户端 map xdg-toplevel 后,服务端调用 `Helper::activateSurface()` 聚焦它,并逐项断言 +真实 wrapper 已加入 `Workspace`、可见,且 `WSeat::keyboardFocusSurface()` 正是该窗口 +的 `WSurface`。客户端通过上游 `zwp_virtual_keyboard_manager_v1` 为该 `wl_seat` 创建 +virtual keyboard,先发送 XKB keymap,再发送 Ctrl 与 Shift 的 depressed modifier mask。 + +接着客户端发送 `acquire()` 和 `capture_next_shortcut(toplevel.surface, seat)`,再发送 +evdev `KEY_F1`(键码 59)按下。生产 `WInputMethodHelper` 将 virtual keyboard 附着到 +`WSeat`;键盘事件进入 `Helper::beforeDisposeEvent()`,由于 capture active,进入 +`ShortcutManagerV3::tryHandleCaptureEvent()`。该函数把真实 `QKeyEvent` 规范化为 +`F1`,通过 capture resource 发送 `captured`;测试断言仅收到一次,随后 +发送 `KEY_F1` release 验证 drain 路径不会产生额外结果。 + +然后客户端发送 `bind_key("desktop-shortcut", "F2", KEY_PRESS, NOTIFY)`,由于 v3 立即 +生效,断言 `bind_failure==0`。发送 evdev `KEY_F2`(键码 60)按下后,事件经过同一个 +`WSeat → Helper::beforeDisposeEvent → ShortcutController` 路径,`ShortcutRunner` 执行 +`NOTIFY` 并由 manager 向 active session 客户端发送 `activated("desktop-shortcut", KEY_PRESS)`。 +测试逐项断言 name、flags 和计数均匹配。 + +## 未覆盖 + +这是虚拟输入的语义覆盖,不是物理键盘驱动覆盖。desktop 用例必须在本地通过后, +才能把上述结果标为该提交的执行证据。 + +当前基础 fixture 的客户端连接活动 session socket,因此只覆盖即时绑定。inactive +session 的暂存与切换后应用,需要第二个 session socket 的专用 fixture。 diff --git a/tests/protocols/treeland-shortcut-manager-v2/setup.cpp b/tests/protocols/treeland-shortcut-manager-v3/setup.cpp similarity index 72% rename from tests/protocols/treeland-shortcut-manager-v2/setup.cpp rename to tests/protocols/treeland-shortcut-manager-v3/setup.cpp index 81d982787c..171b1025d0 100644 --- a/tests/protocols/treeland-shortcut-manager-v2/setup.cpp +++ b/tests/protocols/treeland-shortcut-manager-v3/setup.cpp @@ -1,7 +1,7 @@ // 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 // -// Server-side fixture for the treeland-shortcut-manager-v2 protocol test. +// Server-side fixture for the treeland-shortcut-manager-v3 protocol test. #include "modules/shortcut/shortcutmanager.h" #include "server-bridge.h" @@ -12,5 +12,5 @@ WAYLIB_SERVER_USE_NAMESPACE void protocol_test_setup(Helper *helper) { - Q_ASSERT(find_server_interface(helper)); + Q_ASSERT(find_server_interface(helper)); } diff --git a/tests/protocols/treeland-shortcut-manager-v2/treeland-shortcut-manager-v2.c b/tests/protocols/treeland-shortcut-manager-v3/treeland-shortcut-manager-v3.c similarity index 65% rename from tests/protocols/treeland-shortcut-manager-v2/treeland-shortcut-manager-v2.c rename to tests/protocols/treeland-shortcut-manager-v3/treeland-shortcut-manager-v3.c index d0449bbbe5..c459f83eda 100644 --- a/tests/protocols/treeland-shortcut-manager-v2/treeland-shortcut-manager-v2.c +++ b/tests/protocols/treeland-shortcut-manager-v3/treeland-shortcut-manager-v3.c @@ -1,7 +1,7 @@ // 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 "treeland-shortcut-manager-v2.h" -#include "treeland-shortcut-manager-v2-client-protocol.h" +#include "treeland-shortcut-manager-v3.h" +#include "treeland-shortcut-manager-unstable-v3-client-protocol.h" #include #include @@ -67,32 +67,25 @@ int test_print_results(struct test_ctx *ctx) return failed == 0; } -static void commit_success(void *data, struct treeland_shortcut_manager_v2 *manager) -{ - (void)manager; - ((struct test_ctx *)data)->commit_success_received = 1; -} - -static void commit_failure(void *data, struct treeland_shortcut_manager_v2 *manager, - const char *name, uint32_t error) +static void bind_failure(void *data, struct treeland_shortcut_manager_v3 *manager, + const char *name, uint32_t error) { (void)manager; struct test_ctx *ctx = data; - ctx->commit_failure_received = 1; - ctx->commit_failure_error = error; + ctx->bind_failure_received = 1; + ctx->bind_failure_error = error; if (name) { - strncpy(ctx->commit_failure_name, name, sizeof(ctx->commit_failure_name) - 1); - ctx->commit_failure_name[sizeof(ctx->commit_failure_name) - 1] = '\0'; + strncpy(ctx->bind_failure_name, name, sizeof(ctx->bind_failure_name) - 1); + ctx->bind_failure_name[sizeof(ctx->bind_failure_name) - 1] = '\0'; } } -static const struct treeland_shortcut_manager_v2_listener manager_listener = { +static const struct treeland_shortcut_manager_v3_listener manager_listener = { .activated = NULL, - .commit_success = commit_success, - .commit_failure = commit_failure, + .bind_failure = bind_failure, }; -static void capture_captured(void *data, struct treeland_shortcut_capture_v1 *capture, +static void capture_captured(void *data, struct treeland_shortcut_capture_v3 *capture, const char *key) { (void)capture; @@ -104,7 +97,7 @@ static void capture_captured(void *data, struct treeland_shortcut_capture_v1 *ca } } -static void capture_failed(void *data, struct treeland_shortcut_capture_v1 *capture, +static void capture_failed(void *data, struct treeland_shortcut_capture_v3 *capture, uint32_t reason) { (void)capture; @@ -113,7 +106,7 @@ static void capture_failed(void *data, struct treeland_shortcut_capture_v1 *capt ctx->capture_failed_reason = reason; } -static const struct treeland_shortcut_capture_v1_listener capture_listener = { +static const struct treeland_shortcut_capture_v3_listener capture_listener = { .captured = capture_captured, .failed = capture_failed, }; @@ -124,11 +117,11 @@ static int connect_client(struct test_ctx *ctx, const char *socket_name) return 0; ctx->display = ctx->connection.display; ctx->compositor = client_bind(&ctx->connection, "wl_compositor", &wl_compositor_interface, 1); - ctx->manager = client_bind(&ctx->connection, "treeland_shortcut_manager_v2", - &treeland_shortcut_manager_v2_interface, 2); + ctx->manager = client_bind(&ctx->connection, "treeland_shortcut_manager_v3", + &treeland_shortcut_manager_v3_interface, 1); if (!ctx->manager) return 0; - treeland_shortcut_manager_v2_add_listener(ctx->manager, &manager_listener, ctx); + treeland_shortcut_manager_v3_add_listener(ctx->manager, &manager_listener, ctx); return 1; } @@ -136,42 +129,52 @@ static int bind_manager(struct test_ctx *ctx) { return ctx->manager != NULL; } static int acquire(struct test_ctx *ctx) { - treeland_shortcut_manager_v2_acquire(ctx->manager); + treeland_shortcut_manager_v3_acquire(ctx->manager); return 1; } -static int commit_empty(struct test_ctx *ctx) +static int bind_key(struct test_ctx *ctx) { - ctx->commit_success_received = 0; - treeland_shortcut_manager_v2_commit(ctx->manager); + ctx->bind_failure_received = 0; + treeland_shortcut_manager_v3_bind_key(ctx->manager, "test-shortcut", "Ctrl+Alt+T", + TREELAND_SHORTCUT_MANAGER_V3_KEYBIND_FLAG_KEY_PRESS + | TREELAND_SHORTCUT_MANAGER_V3_KEYBIND_FLAG_REPEAT, + TREELAND_SHORTCUT_MANAGER_V3_ACTION_NOTIFY); return 1; } -static int commit_success_received(struct test_ctx *ctx) { return ctx->commit_success_received; } +static int bind_key_succeeded(struct test_ctx *ctx) { return !ctx->bind_failure_received; } -static int bind_key_commit(struct test_ctx *ctx) +static int bind_key_duplicate(struct test_ctx *ctx) { - ctx->commit_success_received = 0; - treeland_shortcut_manager_v2_bind_key(ctx->manager, "test-shortcut", "Ctrl+Alt+T", - TREELAND_SHORTCUT_MANAGER_V2_KEYBIND_FLAG_KEY_PRESS - | TREELAND_SHORTCUT_MANAGER_V2_KEYBIND_FLAG_REPEAT, - TREELAND_SHORTCUT_MANAGER_V2_ACTION_NOTIFY); - treeland_shortcut_manager_v2_commit(ctx->manager); + ctx->bind_failure_received = 0; + treeland_shortcut_manager_v3_bind_key(ctx->manager, "test-shortcut", "Ctrl+Alt+T", + TREELAND_SHORTCUT_MANAGER_V3_KEYBIND_FLAG_KEY_PRESS, + TREELAND_SHORTCUT_MANAGER_V3_ACTION_NOTIFY); return 1; } -static int commit_success_after_bind(struct test_ctx *ctx) +static int bind_key_duplicate_succeeded(struct test_ctx *ctx) { - return ctx->commit_success_received; + // Upsert on same trigger: no bind_failure expected. + return !ctx->bind_failure_received; } -static int commit_again(struct test_ctx *ctx) +static int bind_key_name_conflict(struct test_ctx *ctx) { - ctx->commit_success_received = 0; - treeland_shortcut_manager_v2_commit(ctx->manager); + ctx->bind_failure_received = 0; + treeland_shortcut_manager_v3_bind_key(ctx->manager, "test-shortcut", "Ctrl+Alt+S", + TREELAND_SHORTCUT_MANAGER_V3_KEYBIND_FLAG_KEY_PRESS, + TREELAND_SHORTCUT_MANAGER_V3_ACTION_NOTIFY); return 1; } +static int bind_key_name_conflict_failed(struct test_ctx *ctx) +{ + return ctx->bind_failure_received + && ctx->bind_failure_error == TREELAND_SHORTCUT_MANAGER_V3_BIND_ERROR_NAME_CONFLICT; +} + static int capture_request(struct test_ctx *ctx) { ctx->capture_failed_received = 0; @@ -181,11 +184,11 @@ static int capture_request(struct test_ctx *ctx) ctx->test_surface = wl_compositor_create_surface(ctx->compositor); if (!ctx->test_surface) return 0; - ctx->capture = treeland_shortcut_manager_v2_capture_next_shortcut(ctx->manager, + ctx->capture = treeland_shortcut_manager_v3_capture_next_shortcut(ctx->manager, ctx->test_surface, NULL); if (!ctx->capture) return 0; - treeland_shortcut_capture_v1_add_listener(ctx->capture, &capture_listener, ctx); + treeland_shortcut_capture_v3_add_listener(ctx->capture, &capture_listener, ctx); return 1; } @@ -193,28 +196,28 @@ static int capture_failed_not_active(struct test_ctx *ctx) { return ctx->capture_failed_received - && ctx->capture_failed_reason == TREELAND_SHORTCUT_CAPTURE_V1_FAILED_REASON_NOT_ACTIVE + && ctx->capture_failed_reason == TREELAND_SHORTCUT_CAPTURE_V3_FAILED_REASON_NOT_ACTIVE && !ctx->capture_captured_received; } static const struct test_case cases[] = { { "manager.bind", bind_manager }, { "manager.acquire", acquire }, - { "manager.commit_empty", commit_empty }, - { "event.commit_success", commit_success_received }, - { "manager.bind_key_commit", bind_key_commit }, - { "event.commit_success_after_bind", commit_success_after_bind }, - { "manager.commit_again", commit_again }, - { "event.commit_success_again", commit_success_received }, + { "manager.bind_key", bind_key }, + { "event.no_bind_failure", bind_key_succeeded }, + { "manager.bind_key_upsert", bind_key_duplicate }, + { "event.no_bind_failure_upsert", bind_key_duplicate_succeeded }, + { "manager.bind_key_name_conflict", bind_key_name_conflict }, + { "event.bind_failure_name_conflict", bind_key_name_conflict_failed }, { "capture.request", capture_request }, { "event.capture_failed_not_active", capture_failed_not_active }, }; void test_cleanup(struct test_ctx *ctx) { - if (ctx->capture) treeland_shortcut_capture_v1_destroy(ctx->capture); + if (ctx->capture) treeland_shortcut_capture_v3_destroy(ctx->capture); if (ctx->test_surface) wl_surface_destroy(ctx->test_surface); - if (ctx->manager) treeland_shortcut_manager_v2_destroy(ctx->manager); + if (ctx->manager) treeland_shortcut_manager_v3_destroy(ctx->manager); client_disconnect(&ctx->connection); } @@ -223,7 +226,7 @@ int protocol_test_run(const char *socket_name) struct test_ctx ctx; test_init(&ctx); if (!connect_client(&ctx, socket_name)) { - fprintf(stderr, "failed to connect to or bind treeland_shortcut_manager_v2\n"); + fprintf(stderr, "failed to connect to or bind treeland_shortcut_manager_v3\n"); test_cleanup(&ctx); test_destroy(&ctx); return 1; diff --git a/tests/protocols/treeland-shortcut-manager-v2/treeland-shortcut-manager-v2.h b/tests/protocols/treeland-shortcut-manager-v3/treeland-shortcut-manager-v3.h similarity index 78% rename from tests/protocols/treeland-shortcut-manager-v2/treeland-shortcut-manager-v2.h rename to tests/protocols/treeland-shortcut-manager-v3/treeland-shortcut-manager-v3.h index c1ffb89a0a..1608903f19 100644 --- a/tests/protocols/treeland-shortcut-manager-v2/treeland-shortcut-manager-v2.h +++ b/tests/protocols/treeland-shortcut-manager-v3/treeland-shortcut-manager-v3.h @@ -1,7 +1,7 @@ // 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 -#ifndef TREELAND_SHORTCUT_MANAGER_V2_TEST_H -#define TREELAND_SHORTCUT_MANAGER_V2_TEST_H +#ifndef TREELAND_SHORTCUT_MANAGER_V3_TEST_H +#define TREELAND_SHORTCUT_MANAGER_V3_TEST_H #ifdef __cplusplus extern "C" { @@ -25,14 +25,13 @@ struct test_ctx { struct wl_compositor *compositor; - struct treeland_shortcut_manager_v2 *manager; - struct treeland_shortcut_capture_v1 *capture; + struct treeland_shortcut_manager_v3 *manager; + struct treeland_shortcut_capture_v3 *capture; struct wl_surface *test_surface; - int commit_success_received; - int commit_failure_received; - char commit_failure_name[64]; - uint32_t commit_failure_error; + int bind_failure_received; + char bind_failure_name[64]; + uint32_t bind_failure_error; int capture_captured_received; char capture_captured_key[64]; int capture_failed_received; diff --git a/tests/test_protocol_shortcut/main.cpp b/tests/test_protocol_shortcut/main.cpp index 3119cfef89..32587fd7c4 100644 --- a/tests/test_protocol_shortcut/main.cpp +++ b/tests/test_protocol_shortcut/main.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2024-2025 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-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 "modules/shortcut/shortcutmanager.h" @@ -29,12 +29,12 @@ private Q_SLOTS: void testCreate() { - QVERIFY(m_server->attach(m_server) != nullptr); + QVERIFY(m_server->attach(m_server) != nullptr); } void verifyShortcut() { - auto protocol = m_server->findChild(); + auto protocol = m_server->findChild(); QVERIFY(protocol != nullptr); }