Radial: Wayland pointer warp via zwp_pointer_constraints_v1
QCursor::setPos is a no-op on Wayland, so cursor-follows-focus did
nothing there. Add WaylandCursorWarp (src/plugin/waylandcursorwarp.{h,
cpp}): on Wayland, lock the pointer to the top-level window's wl_surface,
set_cursor_position_hint to the target pane centre (surface-local), commit,
then release — the compositor warps to the hint on unlock (the mechanism
plan9port/acme use). Panes are children of one window = one surface, so
it is a warp within that surface. Handles come from QWaylandApplication +
QPlatformNativeInterface 'surface'. X11 keeps QCursor::setPos.
Build: pointer-constraints client glue generated via ECM and compiled as
a C static lib (kcoreaddons_add_plugin silently drops .c sources); enable
C language in project(); link Qt6::GuiPrivate + wayland-client. All
guarded + a stub when the Wayland bits are absent. 13 ctest suites pass.
This commit is contained in:
parent
ea7d84868d
commit
eeee28a94a
|
|
@ -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)
|
||||
|
|
|
|||
15
docs/PLAN.md
15
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QWidget>
|
||||
#include <QWindow>
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include "kateprojectbridge.h"
|
||||
#include "projectindexer.h"
|
||||
#include "palettemodel.h" // PaletteItem
|
||||
#include "waylandcursorwarp.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include "waylandcursorwarp.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QTimer>
|
||||
#include <QWindow>
|
||||
#include <qpa/qplatformnativeinterface.h>
|
||||
|
||||
#include <QtGui/qguiapplication_platform.h> // QNativeInterface::QWaylandApplication
|
||||
|
||||
#include <wayland-client.h>
|
||||
#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<WaylandCursorWarp *>(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<QNativeInterface::QWaylandApplication>();
|
||||
}
|
||||
} // 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<uint32_t>(version, zwp_pointer_constraints_v1_interface.version);
|
||||
m_constraints = static_cast<zwp_pointer_constraints_v1 *>(
|
||||
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<wl_surface *>(
|
||||
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
|
||||
|
|
@ -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 <QObject>
|
||||
#include <QPointF>
|
||||
|
||||
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
|
||||
|
|
@ -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 <QGuiApplication>
|
||||
|
||||
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
|
||||
Loading…
Reference in New Issue