diff --git a/CMakeLists.txt b/CMakeLists.txt index 1720153..69b3e3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.21) -project(kate-custom VERSION 0.1.0 LANGUAGES CXX) +project(kate-custom VERSION 0.1.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/docs/PLAN.md b/docs/PLAN.md index 73e3888..e83f924 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -143,6 +143,21 @@ Therefore the palette is replaced, not extended, and the replacement is built on all **left+something**, so a middle press that is part of a chord (left held) still cuts and never opens the radial. The X11 middle-click primary paste is suppressed on release either way. +- Cursor-follows-focus: after a radial `go_*_split_view` slice (the window + radial's "Focus" sub-branch) the mouse pointer warps onto the newly active + pane. Kate's split panes are child widgets of one top-level window (one + `wl_surface`), so this is a warp *within* that surface — pure geometry + (`view->mapTo(topLevel, view->rect().center())`). On **X11** it uses + `QCursor::setPos`. On **Wayland** (which forbids that) it uses the + `zwp_pointer_constraints_v1` protocol: lock the pointer to the window surface, + `set_cursor_position_hint`, commit, then release — KWin warps to the hint on + unlock (the mechanism plan9port/acme use). Implemented in + `src/plugin/waylandcursorwarp.{h,cpp}`; the pointer-constraints client glue is + generated by ECM (`ecm_add_wayland_client_protocol`) and compiled as a small + C static lib (`kcoreaddons_add_plugin` drops `.c` sources). Everything is + guarded: no Wayland global / surface → `warp()` returns false and the caller + falls back. The feature compiles out entirely if wayland-scanner / + wayland-client / Qt6 GuiPrivate are absent (a stub provides the symbols). - User-file radial config — DONE. `RadialConfig::load()` reads `$XDG_CONFIG_HOME/olliepalette/radials.json` when present and non-empty, otherwise returns `builtinDefault()`; a present-but-broken file also falls diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt index cc9035f..48bdf81 100644 --- a/src/plugin/CMakeLists.txt +++ b/src/plugin/CMakeLists.txt @@ -10,11 +10,63 @@ if(NOT KF6TextEditor_FOUND) return() endif() +# --- optional Wayland pointer-warp support ------------------------------- +# Warping the pointer on Wayland needs the pointer-constraints protocol glue +# (generated with wayland-scanner via ECM) plus Qt's Wayland native interface. +# If any piece is missing we build without it; WaylandCursorWarp::warp() then +# simply returns false and the X11 path (QCursor::setPos) is used instead. +set(OLLIE_WAYLAND OFF) +find_package(PkgConfig QUIET) +find_package(WaylandScanner QUIET) +find_package(Qt6 QUIET COMPONENTS GuiPrivate) +if(PkgConfig_FOUND) + pkg_check_modules(WAYLAND_CLIENT QUIET wayland-client) + pkg_check_modules(WAYLAND_PROTOCOLS QUIET wayland-protocols) +endif() + +set(OLLIE_PLUGIN_SOURCES ollieplugin.cpp ollieplugin.h waylandcursorwarp.h) +set(OLLIE_WAYLAND_PROTO_SRCS "") + +if(WaylandScanner_FOUND AND WAYLAND_CLIENT_FOUND AND Qt6GuiPrivate_FOUND) + if(WAYLAND_PROTOCOLS_FOUND) + pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) + endif() + set(PC_XML + "${WAYLAND_PROTOCOLS_DIR}/unstable/pointer-constraints/pointer-constraints-unstable-v1.xml") + if(EXISTS "${PC_XML}") + # ECM wires the scanner output (basename-client-protocol.h + .c) into the + # given source-list variable with correct generated-source handling. + ecm_add_wayland_client_protocol(OLLIE_WAYLAND_PROTO_SRCS + PROTOCOL "${PC_XML}" + BASENAME pointer-constraints-unstable-v1) + # kcoreaddons_add_plugin only compiles C++ sources — a generated .c in + # its SOURCES is silently dropped. Build the C glue as its own static + # lib and link it in. + add_library(ollie_wayland_proto STATIC ${OLLIE_WAYLAND_PROTO_SRCS}) + target_include_directories(ollie_wayland_proto PUBLIC + "${CMAKE_CURRENT_BINARY_DIR}" ${WAYLAND_CLIENT_INCLUDE_DIRS}) + set_target_properties(ollie_wayland_proto PROPERTIES + POSITION_INDEPENDENT_CODE ON + LINKER_LANGUAGE C + AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) + list(APPEND OLLIE_PLUGIN_SOURCES waylandcursorwarp.cpp) + set(OLLIE_WAYLAND ON) + endif() +endif() + +if(NOT OLLIE_WAYLAND) + message(STATUS "Wayland pointer-warp support disabled (missing scanner / " + "wayland-client / Qt6 GuiPrivate / protocol xml)") + list(APPEND OLLIE_PLUGIN_SOURCES waylandcursorwarp_stub.cpp) +endif() + kcoreaddons_add_plugin(olliepalette - SOURCES ollieplugin.cpp ollieplugin.h + SOURCES ${OLLIE_PLUGIN_SOURCES} INSTALL_NAMESPACE "kf6/ktexteditor" ) +target_include_directories(olliepalette PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + target_link_libraries(olliepalette PRIVATE palette radial @@ -26,3 +78,12 @@ target_link_libraries(olliepalette PRIVATE KF6::I18n Qt6::Widgets ) + +if(OLLIE_WAYLAND) + target_compile_definitions(olliepalette PRIVATE OLLIE_WAYLAND=1) + target_include_directories(olliepalette PRIVATE ${WAYLAND_CLIENT_INCLUDE_DIRS}) + target_link_libraries(olliepalette PRIVATE + ollie_wayland_proto + Qt6::GuiPrivate + ${WAYLAND_CLIENT_LIBRARIES}) +endif() diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index b54c331..1696279 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -39,6 +39,7 @@ #include #include #include +#include namespace katecustom { @@ -1166,9 +1167,28 @@ void OllieView::warpCursorToActiveView() if (!view) { return; } - // KTextEditor::View is a QWidget; center the pointer in its editing area. - const QPoint center = view->mapToGlobal(view->rect().center()); - QCursor::setPos(center); + // Target: the centre of the new active view, in the top-level window's + // coordinates. Kate's split panes are child widgets of one top-level window + // (one wl_surface), so this is the pane centre within that single surface. + QWidget *top = view->window(); + if (!top) { + return; + } + const QPoint centerInView = view->rect().center(); + const QPoint centerInTop = view->mapTo(top, centerInView); + + // Wayland forbids QCursor::setPos; use the pointer-constraints warp against + // the top-level surface. On X11 (or if the Wayland path is unavailable), + // fall back to the direct warp, which works there. + if (WaylandCursorWarp::isWaylandSession()) { + if (QWindow *win = top->windowHandle()) { + if (m_cursorWarp.warp(win, QPointF(centerInTop))) { + return; + } + } + return; // Wayland but warp unavailable — nothing more we can do. + } + QCursor::setPos(top->mapToGlobal(centerInTop)); } // --------------------------------------------------------------------------- diff --git a/src/plugin/ollieplugin.h b/src/plugin/ollieplugin.h index 6e65d90..ed364bc 100644 --- a/src/plugin/ollieplugin.h +++ b/src/plugin/ollieplugin.h @@ -17,6 +17,7 @@ #include "kateprojectbridge.h" #include "projectindexer.h" #include "palettemodel.h" // PaletteItem +#include "waylandcursorwarp.h" #include #include @@ -151,6 +152,7 @@ private: FrecencyStore m_frecency; // persisted usage feeding the palette ranking KateProjectBridge m_projectBridge; // read-only view of Kate's project plugin ProjectIndexer m_indexer; // async, cached file/symbol indexing + WaylandCursorWarp m_cursorWarp; // pointer warp on Wayland (no-op on X11) }; } // namespace katecustom diff --git a/src/plugin/waylandcursorwarp.cpp b/src/plugin/waylandcursorwarp.cpp new file mode 100644 index 0000000..5352f8b --- /dev/null +++ b/src/plugin/waylandcursorwarp.cpp @@ -0,0 +1,156 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#include "waylandcursorwarp.h" + +#include +#include +#include +#include + +#include // QNativeInterface::QWaylandApplication + +#include +#include "wayland-pointer-constraints-unstable-v1-client-protocol.h" + +namespace katecustom +{ + +namespace +{ +// --- registry listener trampoline --------------------------------------- +void registryGlobal(void *data, wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) +{ + static_cast(data)->onGlobal(registry, name, interface, version); +} +void registryGlobalRemove(void *, wl_registry *, uint32_t) {} + +const wl_registry_listener kRegistryListener = { + registryGlobal, + registryGlobalRemove, +}; + +// Pull a wl_* handle out of Qt's Wayland application native interface. +QNativeInterface::QWaylandApplication *waylandApp() +{ + return qGuiApp->nativeInterface(); +} +} // namespace + +WaylandCursorWarp::WaylandCursorWarp(QObject *parent) + : QObject(parent) +{ +} + +WaylandCursorWarp::~WaylandCursorWarp() +{ + if (m_constraints) { + zwp_pointer_constraints_v1_destroy(m_constraints); + m_constraints = nullptr; + } +} + +bool WaylandCursorWarp::isWaylandSession() +{ + return QGuiApplication::platformName().startsWith(QLatin1String("wayland")); +} + +void WaylandCursorWarp::onGlobal(wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) +{ + if (qstrcmp(interface, zwp_pointer_constraints_v1_interface.name) == 0) { + // Bind at most the version we were built against. + const uint32_t bindVersion = + qMin(version, zwp_pointer_constraints_v1_interface.version); + m_constraints = static_cast( + wl_registry_bind(registry, name, &zwp_pointer_constraints_v1_interface, + bindVersion)); + } +} + +bool WaylandCursorWarp::ensureConstraints() +{ + if (m_constraints) { + return true; + } + if (m_triedBind) { + return false; // already looked; the compositor does not offer it + } + m_triedBind = true; + + auto *app = waylandApp(); + if (!app) { + return false; + } + wl_display *display = app->display(); + if (!display) { + return false; + } + wl_registry *registry = wl_display_get_registry(display); + if (!registry) { + return false; + } + wl_registry_add_listener(registry, &kRegistryListener, this); + // Round-trip so the global advertisement is processed and onGlobal() runs. + wl_display_roundtrip(display); + wl_registry_destroy(registry); + return m_constraints != nullptr; +} + +bool WaylandCursorWarp::warp(QWindow *window, const QPointF &surfaceLocalPos) +{ + if (!window || !isWaylandSession()) { + return false; + } + if (!ensureConstraints()) { + return false; + } + + auto *app = waylandApp(); + if (!app) { + return false; + } + wl_pointer *pointer = app->pointer(); + wl_display *display = app->display(); + if (!pointer || !display) { + return false; + } + + QPlatformNativeInterface *pni = QGuiApplication::platformNativeInterface(); + if (!pni) { + return false; + } + auto *surface = static_cast( + pni->nativeResourceForWindow(QByteArrayLiteral("surface"), window)); + if (!surface) { + return false; + } + + // Lock the pointer to the surface it is already over, hint the target + // position (surface-local), commit so the double-buffered hint is pending, + // then release the lock — the compositor warps to the hint on unlock. + zwp_locked_pointer_v1 *lock = zwp_pointer_constraints_v1_lock_pointer( + m_constraints, surface, pointer, nullptr, + ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT); + if (!lock) { + return false; + } + zwp_locked_pointer_v1_set_cursor_position_hint( + lock, wl_fixed_from_double(surfaceLocalPos.x()), + wl_fixed_from_double(surfaceLocalPos.y())); + wl_surface_commit(surface); + wl_display_flush(display); + + // Release on the next event-loop turn: the lock must have been applied by + // the compositor (pointer is over the surface) before we unlock, otherwise + // the position hint is discarded. Destroying later keeps the sequence + // compatible with compositors that apply the hint strictly on unlock. + QTimer::singleShot(0, this, [lock, display]() { + zwp_locked_pointer_v1_destroy(lock); + wl_display_flush(display); + }); + return true; +} + +} // namespace katecustom diff --git a/src/plugin/waylandcursorwarp.h b/src/plugin/waylandcursorwarp.h new file mode 100644 index 0000000..d7bd950 --- /dev/null +++ b/src/plugin/waylandcursorwarp.h @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + * + * WaylandCursorWarp — warp the pointer on Wayland via the pointer-constraints + * protocol. + * + * Wayland forbids the unconditional pointer warp that QCursor::setPos() uses on + * X11, so that call is a silent no-op under a native Wayland session. The + * sanctioned way to reposition the cursor is zwp_pointer_constraints_v1: lock + * the pointer to the surface it is already over, set a surface-local position + * hint, then release the lock — the compositor warps the pointer to the hint on + * unlock (this is exactly how plan9port/acme moves the cursor on Wayland). + * + * Kate's split panes are child widgets of a single top-level window, i.e. one + * wl_surface, so moving the pointer between panes is a warp *within* that + * surface — a plain geometry problem. The pointer is already over the window + * after a mouse-driven radial gesture, which is the precondition the protocol + * requires for the lock to activate. + * + * All handles come from public Qt native interfaces (QWaylandApplication) plus + * the QPA QPlatformNativeInterface "surface" resource. Everything is guarded: + * if the compositor lacks the global, or the surface/pointer is unavailable, + * warp() returns false and the caller falls back (or no-ops). + */ +#ifndef KATECUSTOM_WAYLANDCURSORWARP_H +#define KATECUSTOM_WAYLANDCURSORWARP_H + +#include +#include + +class QWindow; + +struct wl_registry; +struct zwp_pointer_constraints_v1; + +namespace katecustom +{ + +class WaylandCursorWarp : public QObject +{ + Q_OBJECT +public: + explicit WaylandCursorWarp(QObject *parent = nullptr); + ~WaylandCursorWarp() override; + + /*! True if the running session is Wayland (QGuiApplication platform). */ + static bool isWaylandSession(); + + /*! + * Warp the pointer to \a surfaceLocalPos (coordinates relative to the top + * left of \a window's surface, in logical pixels). Returns true if the + * warp was issued, false if the Wayland machinery was unavailable (caller + * should fall back to QCursor::setPos on X11, or give up on Wayland). + */ + bool warp(QWindow *window, const QPointF &surfaceLocalPos); + + // Registry callback (public so the C trampoline can reach it). + void onGlobal(wl_registry *registry, uint32_t name, const char *interface, + uint32_t version); + +private: + bool ensureConstraints(); + + zwp_pointer_constraints_v1 *m_constraints = nullptr; + bool m_triedBind = false; +}; + +} // namespace katecustom + +#endif diff --git a/src/plugin/waylandcursorwarp_stub.cpp b/src/plugin/waylandcursorwarp_stub.cpp new file mode 100644 index 0000000..ca307f3 --- /dev/null +++ b/src/plugin/waylandcursorwarp_stub.cpp @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + * + * Stub WaylandCursorWarp for builds without Wayland support (no scanner / + * wayland-client / Qt GuiPrivate / protocol xml). warp() always fails, so the + * caller falls back to QCursor::setPos (X11) or no-ops. isWaylandSession() is + * still answered from the Qt platform name. + */ +#include "waylandcursorwarp.h" + +#include + +namespace katecustom +{ + +WaylandCursorWarp::WaylandCursorWarp(QObject *parent) + : QObject(parent) +{ +} + +WaylandCursorWarp::~WaylandCursorWarp() = default; + +bool WaylandCursorWarp::isWaylandSession() +{ + return QGuiApplication::platformName().startsWith(QLatin1String("wayland")); +} + +void WaylandCursorWarp::onGlobal(wl_registry *, uint32_t, const char *, uint32_t) +{ +} + +bool WaylandCursorWarp::ensureConstraints() +{ + return false; +} + +bool WaylandCursorWarp::warp(QWindow *, const QPointF &) +{ + return false; // no Wayland support compiled in +} + +} // namespace katecustom