radial: extract into standalone [deft:util] radial plugin (deft_radial.so)
Stage 3a of the util split. radial becomes its own KTextEditor plugin:
new src/radial/radialplugin.{h,cpp} (RadialPlugin/RadialView) + radialplugin.json
("[deft:util] radial"), built as deft_radial.so with its own event filter
(mouse-button gesture + context-menu suppression + pane-focus pointer warp).
The optional Wayland pointer-warp glue (waylandcursorwarp.* + the ECM
wayland-scanner protocol lib) moves from src/plugin into src/radial, since the
warp is only used to follow focus after a radial pane action.
Door object names renamed ollie_* -> deft_* (deft_command_palette /
deft_goto_file / deft_goto_symbol) so the radial resolves the sibling plugins'
launcher actions via the GUI factory; built-in radial config + test updated.
The core deft plugin drops radial code, members, the radial branches of its
event filter, and the Wayland glue; it keeps palette + switch + acme keys.
This commit is contained in:
parent
cd72aa043b
commit
3bf6f9425f
|
|
@ -10,58 +10,11 @@ if(NOT KF6TextEditor_FOUND)
|
||||||
return()
|
return()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# --- optional Wayland pointer-warp support -------------------------------
|
# The core deft plugin: command palette (M-x), project file/symbol switchers,
|
||||||
# Warping the pointer on Wayland needs the pointer-constraints protocol glue
|
# and the Acme line-editing keys + bracket/quote selection. (radial, sam, and
|
||||||
# (generated with wayland-scanner via ECM) plus Qt's Wayland native interface.
|
# plumb are now their own plugins; the util features here will split further.)
|
||||||
# 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(deft
|
kcoreaddons_add_plugin(deft
|
||||||
SOURCES ${OLLIE_PLUGIN_SOURCES}
|
SOURCES ollieplugin.cpp ollieplugin.h
|
||||||
INSTALL_NAMESPACE "kf6/ktexteditor"
|
INSTALL_NAMESPACE "kf6/ktexteditor"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -69,7 +22,6 @@ target_include_directories(deft PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
|
||||||
target_link_libraries(deft PRIVATE
|
target_link_libraries(deft PRIVATE
|
||||||
palette
|
palette
|
||||||
radial
|
|
||||||
olliecommands_lib
|
olliecommands_lib
|
||||||
project_lib
|
project_lib
|
||||||
KF6::TextEditor
|
KF6::TextEditor
|
||||||
|
|
@ -78,12 +30,3 @@ target_link_libraries(deft PRIVATE
|
||||||
KF6::I18n
|
KF6::I18n
|
||||||
Qt6::Widgets
|
Qt6::Widgets
|
||||||
)
|
)
|
||||||
|
|
||||||
if(OLLIE_WAYLAND)
|
|
||||||
target_compile_definitions(deft PRIVATE OLLIE_WAYLAND=1)
|
|
||||||
target_include_directories(deft PRIVATE ${WAYLAND_CLIENT_INCLUDE_DIRS})
|
|
||||||
target_link_libraries(deft PRIVATE
|
|
||||||
ollie_wayland_proto
|
|
||||||
Qt6::GuiPrivate
|
|
||||||
${WAYLAND_CLIENT_LIBRARIES})
|
|
||||||
endif()
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@
|
||||||
|
|
||||||
#include "palettewidget.h"
|
#include "palettewidget.h"
|
||||||
#include "frecencystore.h"
|
#include "frecencystore.h"
|
||||||
#include "radialmenu.h"
|
|
||||||
#include "radialconfig.h"
|
|
||||||
#include "olliecommands.h"
|
#include "olliecommands.h"
|
||||||
#include "projectindex.h"
|
#include "projectindex.h"
|
||||||
#include "symbolindex.h"
|
#include "symbolindex.h"
|
||||||
|
|
@ -26,7 +24,6 @@
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCursor>
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
|
|
@ -37,35 +34,14 @@
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTimer>
|
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QWindow>
|
|
||||||
|
|
||||||
namespace deft
|
namespace deft
|
||||||
{
|
{
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// True for the Kate actions that change which split pane is active: moving
|
|
||||||
// focus between panes, creating a split (Kate activates the new pane), or
|
|
||||||
// closing a pane (Kate activates a neighbour). After one of these fires we warp
|
|
||||||
// the mouse pointer onto the now-active pane so the cursor follows the focus
|
|
||||||
// (mouse-first UX).
|
|
||||||
bool isPaneFocusAction(const QString &id)
|
|
||||||
{
|
|
||||||
return id == QLatin1String("go_left_split_view")
|
|
||||||
|| id == QLatin1String("go_right_split_view")
|
|
||||||
|| id == QLatin1String("go_upward_split_view")
|
|
||||||
|| id == QLatin1String("go_downward_split_view")
|
|
||||||
|| id == QLatin1String("go_next_split_view")
|
|
||||||
|| id == QLatin1String("go_prev_split_view")
|
|
||||||
|| id == QLatin1String("view_split_vert")
|
|
||||||
|| id == QLatin1String("view_split_horiz")
|
|
||||||
|| id == QLatin1String("view_close_current_space")
|
|
||||||
|| id == QLatin1String("view_close_others");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bracket pairs understood by the double-click selection feature.
|
// Bracket pairs understood by the double-click selection feature.
|
||||||
QChar matchingBracket(QChar c, bool &isOpener)
|
QChar matchingBracket(QChar c, bool &isOpener)
|
||||||
{
|
{
|
||||||
|
|
@ -302,15 +278,15 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
||||||
// ambiguous because nothing is registered. The QActions are kept only for
|
// ambiguous because nothing is registered. The QActions are kept only for
|
||||||
// their objectName/label (command-palette harvest, radial escape-hatch).
|
// their objectName/label (command-palette harvest, radial escape-hatch).
|
||||||
auto *openAction = new QAction(i18n("Command Palette (M-x) [Alt+X]"), this);
|
auto *openAction = new QAction(i18n("Command Palette (M-x) [Alt+X]"), this);
|
||||||
openAction->setObjectName(QStringLiteral("ollie_command_palette"));
|
openAction->setObjectName(QStringLiteral("deft_command_palette"));
|
||||||
connect(openAction, &QAction::triggered, this, &OllieView::showCommandPalette);
|
connect(openAction, &QAction::triggered, this, &OllieView::showCommandPalette);
|
||||||
|
|
||||||
auto *gotoFileAction = new QAction(i18n("Go to File (project) [Alt+P]"), this);
|
auto *gotoFileAction = new QAction(i18n("Go to File (project) [Alt+P]"), this);
|
||||||
gotoFileAction->setObjectName(QStringLiteral("ollie_goto_file"));
|
gotoFileAction->setObjectName(QStringLiteral("deft_goto_file"));
|
||||||
connect(gotoFileAction, &QAction::triggered, this, &OllieView::showFileSwitcher);
|
connect(gotoFileAction, &QAction::triggered, this, &OllieView::showFileSwitcher);
|
||||||
|
|
||||||
auto *gotoSymbolAction = new QAction(i18n("Go to Symbol (project) [Alt+G]"), this);
|
auto *gotoSymbolAction = new QAction(i18n("Go to Symbol (project) [Alt+G]"), this);
|
||||||
gotoSymbolAction->setObjectName(QStringLiteral("ollie_goto_symbol"));
|
gotoSymbolAction->setObjectName(QStringLiteral("deft_goto_symbol"));
|
||||||
connect(gotoSymbolAction, &QAction::triggered, this, &OllieView::showSymbolSwitcher);
|
connect(gotoSymbolAction, &QAction::triggered, this, &OllieView::showSymbolSwitcher);
|
||||||
|
|
||||||
// Keep them discoverable as window actions (no shortcut attached).
|
// Keep them discoverable as window actions (no shortcut attached).
|
||||||
|
|
@ -344,11 +320,9 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
||||||
[this]() { m_indexer.invalidateAll(); });
|
[this]() { m_indexer.invalidateAll(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
setupRadials();
|
// Application-wide filter for the Acme line-editing keys, the launcher
|
||||||
|
// doors (Alt+X/P/G), and the Acme mouse chords, catching events no matter
|
||||||
// One application-wide filter serves both the mouse-button radial gesture
|
// which internal child of the view actually receives them.
|
||||||
// and the Acme-style left+middle chord, catching presses no matter which
|
|
||||||
// internal child of the view actually receives them.
|
|
||||||
qApp->installEventFilter(this);
|
qApp->installEventFilter(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,7 +331,6 @@ OllieView::~OllieView()
|
||||||
delete m_palette;
|
delete m_palette;
|
||||||
delete m_filePalette;
|
delete m_filePalette;
|
||||||
delete m_symbolPalette;
|
delete m_symbolPalette;
|
||||||
delete m_radial;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<OllieView::ActionEntry> OllieView::collectActions() const
|
QList<OllieView::ActionEntry> OllieView::collectActions() const
|
||||||
|
|
@ -850,45 +823,6 @@ QAction *OllieView::findActionByName(const QString &nameOrText) const
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OllieView::setupRadials()
|
|
||||||
{
|
|
||||||
// Prefer the user's config (~/.config/olliepalette/radials.json); fall back
|
|
||||||
// to the built-in set when it is absent, empty, or invalid.
|
|
||||||
m_radialDefs = RadialConfig::load();
|
|
||||||
|
|
||||||
QWidget *w = m_mainWindow->window();
|
|
||||||
|
|
||||||
for (int i = 0; i < m_radialDefs.size(); ++i) {
|
|
||||||
const RadialDef &def = m_radialDefs.at(i);
|
|
||||||
|
|
||||||
// Key trigger (weapon-wheel style).
|
|
||||||
if (!def.trigger.isEmpty()) {
|
|
||||||
auto *trigger = new QAction(this);
|
|
||||||
trigger->setObjectName(QStringLiteral("ollie_radial_") + def.name);
|
|
||||||
trigger->setText(i18n("Radial Menu: %1", def.name));
|
|
||||||
trigger->setShortcut(QKeySequence(def.trigger));
|
|
||||||
trigger->setShortcutContext(Qt::ApplicationShortcut);
|
|
||||||
connect(trigger, &QAction::triggered, this, [this, i]() { showRadial(i); });
|
|
||||||
if (w) {
|
|
||||||
w->addAction(trigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mouse-button gesture (the natural radial trigger).
|
|
||||||
if (!def.mouseButton.isEmpty()) {
|
|
||||||
Qt::MouseButton btn = Qt::NoButton;
|
|
||||||
if (def.mouseButton == QLatin1String("RightButton")) {
|
|
||||||
btn = Qt::RightButton;
|
|
||||||
} else if (def.mouseButton == QLatin1String("MiddleButton")) {
|
|
||||||
btn = Qt::MiddleButton;
|
|
||||||
}
|
|
||||||
if (btn != Qt::NoButton) {
|
|
||||||
m_buttonToRadial.insert(static_cast<int>(btn), i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OllieView::selectBracketPairAt(KTextEditor::View *view, const QPoint &globalPos)
|
bool OllieView::selectBracketPairAt(KTextEditor::View *view, const QPoint &globalPos)
|
||||||
{
|
{
|
||||||
if (!view) {
|
if (!view) {
|
||||||
|
|
@ -1074,39 +1008,13 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// While a drag gesture is active, drive the radial directly from the raw
|
|
||||||
// events: the opening press leaves an implicit grab on the editor view, so
|
|
||||||
// the live move/release events are delivered there, and this application
|
|
||||||
// filter is the one place that reliably sees them.
|
|
||||||
if (m_gestureActive) {
|
|
||||||
switch (event->type()) {
|
|
||||||
case QEvent::MouseMove:
|
|
||||||
if (m_radial) {
|
|
||||||
m_radial->driveHoverGlobal(
|
|
||||||
static_cast<QMouseEvent *>(event)->globalPosition().toPoint());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
case QEvent::MouseButtonRelease:
|
|
||||||
m_gestureActive = false;
|
|
||||||
m_swallowMiddleRelease = false; // the gesture consumed this release
|
|
||||||
if (m_radial) {
|
|
||||||
m_radial->driveReleaseGlobal(
|
|
||||||
static_cast<QMouseEvent *>(event)->globalPosition().toPoint());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acme-style left-button chords, plus suppression of X11 primary-selection
|
// Acme-style left-button chords, plus suppression of X11 primary-selection
|
||||||
// paste. Button 1 (left) held while selecting is the anchor:
|
// paste. Button 1 (left) held while selecting is the anchor:
|
||||||
// left + middle tap = Cut, left + right tap = Paste.
|
// left + middle tap = Cut, left + right tap = Paste.
|
||||||
// Both use edit_cut / edit_paste, which use the standard KDE clipboard
|
// Both use edit_cut / edit_paste, which use the standard KDE clipboard
|
||||||
// (Klipper-backed). The right-button chord is free because the radial
|
// (Klipper-backed). Middle-button events in the view are consumed on BOTH
|
||||||
// gesture is already suppressed while the left button is held. Middle-button
|
// press and release to kill the middle-click primary paste (it fires on
|
||||||
// events in the view are consumed on BOTH press and release to kill the
|
// release, not just press).
|
||||||
// middle-click primary paste (it fires on release, not just press).
|
|
||||||
if (event->type() == QEvent::MouseButtonRelease) {
|
if (event->type() == QEvent::MouseButtonRelease) {
|
||||||
auto *mev = static_cast<QMouseEvent *>(event);
|
auto *mev = static_cast<QMouseEvent *>(event);
|
||||||
if (mev->button() == Qt::LeftButton) {
|
if (mev->button() == Qt::LeftButton) {
|
||||||
|
|
@ -1132,156 +1040,32 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
|
||||||
paste->trigger();
|
paste->trigger();
|
||||||
}
|
}
|
||||||
m_swallowRightRelease = true;
|
m_swallowRightRelease = true;
|
||||||
return true; // consume: chord, not a radial / context menu
|
return true; // consume: chord, not a context menu
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mev->button() == Qt::MiddleButton && inView) {
|
if (mev->button() == Qt::MiddleButton && inView) {
|
||||||
if (m_leftDownInView) {
|
if (m_leftDownInView) {
|
||||||
// left + middle = Cut the selection (Acme chord). Consume both
|
// left + middle = Cut the selection (Acme chord). Consume both
|
||||||
// press and release; never a radial.
|
// press and release.
|
||||||
if (QAction *cut = findActionByName(QStringLiteral("edit_cut"))) {
|
if (QAction *cut = findActionByName(QStringLiteral("edit_cut"))) {
|
||||||
cut->trigger();
|
cut->trigger();
|
||||||
}
|
}
|
||||||
m_swallowMiddleRelease = true;
|
m_swallowMiddleRelease = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// A plain middle press (no chord): always suppress the X11 primary
|
// A plain middle press (no chord): suppress the X11 primary paste
|
||||||
// paste that fires on release. If the middle button is bound to a
|
// that fires on release, and consume the press.
|
||||||
// radial, fall through to the radial dispatch below; otherwise
|
|
||||||
// consume the press here too.
|
|
||||||
m_swallowMiddleRelease = true;
|
m_swallowMiddleRelease = true;
|
||||||
if (!m_buttonToRadial.contains(static_cast<int>(Qt::MiddleButton))) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// else: do not return — let the radial-trigger dispatch handle it.
|
|
||||||
}
|
|
||||||
if (mev->button() == Qt::LeftButton && inView) {
|
if (mev->button() == Qt::LeftButton && inView) {
|
||||||
m_leftDownInView = true; // anchor set; selection proceeds normally
|
m_leftDownInView = true; // anchor set; selection proceeds normally
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppress the context menu that would otherwise follow an RMB gesture.
|
|
||||||
if (event->type() == QEvent::ContextMenu
|
|
||||||
&& m_buttonToRadial.contains(static_cast<int>(Qt::RightButton))) {
|
|
||||||
KTextEditor::View *view = m_mainWindow->activeView();
|
|
||||||
auto *widget = qobject_cast<QWidget *>(obj);
|
|
||||||
if (view && widget && view->isAncestorOf(widget)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return QObject::eventFilter(obj, event);
|
return QObject::eventFilter(obj, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event->type() != QEvent::MouseButtonPress) {
|
|
||||||
return QObject::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
auto *me = static_cast<QMouseEvent *>(event);
|
|
||||||
const int button = static_cast<int>(me->button());
|
|
||||||
if (!m_buttonToRadial.contains(button)) {
|
|
||||||
return QObject::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only hijack presses that land inside the active view's editing area.
|
|
||||||
KTextEditor::View *view = m_mainWindow->activeView();
|
|
||||||
auto *widget = qobject_cast<QWidget *>(obj);
|
|
||||||
if (!view || !widget || !view->isAncestorOf(widget)) {
|
|
||||||
return QObject::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress the radial when the left button is already held: the user is
|
|
||||||
// mid-selection (or setting up a left+middle chord), not starting a clean
|
|
||||||
// radial gesture. buttons() reports the other buttons down at press time.
|
|
||||||
if (m_leftDownInView || (me->buttons() & Qt::LeftButton)) {
|
|
||||||
return QObject::eventFilter(obj, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop the radial at the press position and begin the drag gesture. The
|
|
||||||
// press is consumed so the context menu does not appear and the caret is
|
|
||||||
// not moved (an intentional, configured trade-off).
|
|
||||||
showRadialAt(m_buttonToRadial.value(button), me->globalPosition().toPoint());
|
|
||||||
m_gestureActive = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OllieView::showRadial(int defIndex)
|
|
||||||
{
|
|
||||||
QPoint global;
|
|
||||||
if (KTextEditor::View *view = m_mainWindow->activeView()) {
|
|
||||||
global = view->mapToGlobal(view->cursorPositionCoordinates());
|
|
||||||
} else if (QWidget *w = m_mainWindow->window()) {
|
|
||||||
global = w->mapToGlobal(w->rect().center());
|
|
||||||
}
|
|
||||||
showRadialAt(defIndex, global);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OllieView::showRadialAt(int defIndex, const QPoint &globalPos)
|
|
||||||
{
|
|
||||||
if (defIndex < 0 || defIndex >= m_radialDefs.size()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!m_radial) {
|
|
||||||
m_radial = new RadialMenu(m_mainWindow->window());
|
|
||||||
connect(m_radial, &RadialMenu::activatedId, this, &OllieView::onRadialActivated);
|
|
||||||
}
|
|
||||||
m_radial->popup(m_radialDefs.at(defIndex).root, globalPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OllieView::onRadialActivated(const QString &actionId)
|
|
||||||
{
|
|
||||||
// The launcher doors are reachable from inside a radial as special slices.
|
|
||||||
if (actionId == QLatin1String("ollie_command_palette")) {
|
|
||||||
showCommandPalette();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (actionId == QLatin1String("ollie_goto_file")) {
|
|
||||||
showFileSwitcher();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (actionId == QLatin1String("ollie_goto_symbol")) {
|
|
||||||
showSymbolSwitcher();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (QAction *a = findActionByName(actionId)) {
|
|
||||||
a->trigger();
|
|
||||||
// If this changed the active pane (focus move, or a split that creates
|
|
||||||
// and activates a new pane), warp the pointer onto it so the mouse
|
|
||||||
// follows the focus. Deferred by ~a frame so Kate has finished
|
|
||||||
// activating — and, for a freshly created split, laying out — the pane.
|
|
||||||
if (isPaneFocusAction(actionId)) {
|
|
||||||
QTimer::singleShot(16, this, [this]() { warpCursorToActiveView(); });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OllieView::warpCursorToActiveView()
|
|
||||||
{
|
|
||||||
KTextEditor::View *view = m_mainWindow->activeView();
|
|
||||||
if (!view) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Acme-style line-editing keys
|
// Acme-style line-editing keys
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
#include "kateprojectbridge.h"
|
#include "kateprojectbridge.h"
|
||||||
#include "projectindexer.h"
|
#include "projectindexer.h"
|
||||||
#include "palettemodel.h" // PaletteItem
|
#include "palettemodel.h" // PaletteItem
|
||||||
#include "waylandcursorwarp.h"
|
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
|
@ -38,9 +37,7 @@ namespace deft
|
||||||
{
|
{
|
||||||
|
|
||||||
class PaletteWidget;
|
class PaletteWidget;
|
||||||
class RadialMenu;
|
|
||||||
class OllieCommands;
|
class OllieCommands;
|
||||||
struct RadialDef;
|
|
||||||
|
|
||||||
class OlliePlugin : public KTextEditor::Plugin
|
class OlliePlugin : public KTextEditor::Plugin
|
||||||
{
|
{
|
||||||
|
|
@ -92,9 +89,6 @@ private:
|
||||||
// small margin), anchored near the top.
|
// small margin), anchored near the top.
|
||||||
void positionPalette(PaletteWidget *palette);
|
void positionPalette(PaletteWidget *palette);
|
||||||
|
|
||||||
// Move the OS pointer onto the center of the active view — used after a
|
|
||||||
// radial pane-focus action so the mouse follows the keyboard focus.
|
|
||||||
void warpCursorToActiveView();
|
|
||||||
// One harvested, runnable action with the provenance needed to build a
|
// One harvested, runnable action with the provenance needed to build a
|
||||||
// unique id and to disambiguate labels shared by several commands.
|
// unique id and to disambiguate labels shared by several commands.
|
||||||
struct ActionEntry {
|
struct ActionEntry {
|
||||||
|
|
@ -109,16 +103,10 @@ private:
|
||||||
// window's GUI factory clients, deduplicated by pointer identity.
|
// window's GUI factory clients, deduplicated by pointer identity.
|
||||||
QList<ActionEntry> collectActions() const;
|
QList<ActionEntry> collectActions() const;
|
||||||
|
|
||||||
// Find a Kate action by its raw objectName across all GUI clients. Used to
|
// Find a Kate action by its raw objectName across all GUI clients. Used by
|
||||||
// resolve radial-menu slice ids (which reference Kate action names).
|
// the Acme mouse chords to invoke edit_cut / edit_paste.
|
||||||
QAction *findActionByName(const QString &objectName) const;
|
QAction *findActionByName(const QString &objectName) const;
|
||||||
|
|
||||||
// Build the configured radials and bind each to its trigger key.
|
|
||||||
void setupRadials();
|
|
||||||
void showRadial(int defIndex); // centre on the caret
|
|
||||||
void showRadialAt(int defIndex, const QPoint &globalPos); // centre on a point
|
|
||||||
void onRadialActivated(const QString &actionId);
|
|
||||||
|
|
||||||
// Acme-style line-editing keys, handled in the event filter (no QActions,
|
// Acme-style line-editing keys, handled in the event filter (no QActions,
|
||||||
// so there is nothing for Qt to report as an ambiguous shortcut).
|
// so there is nothing for Qt to report as an ambiguous shortcut).
|
||||||
void acmeBeginningOfLine(); // C-a
|
void acmeBeginningOfLine(); // C-a
|
||||||
|
|
@ -142,10 +130,6 @@ private:
|
||||||
QString m_fileSwitcherRoot; // project root for the open switcher
|
QString m_fileSwitcherRoot; // project root for the open switcher
|
||||||
PaletteWidget *m_symbolPalette = nullptr; // project "Go to Symbol" switcher
|
PaletteWidget *m_symbolPalette = nullptr; // project "Go to Symbol" switcher
|
||||||
QString m_symbolSwitcherRoot; // project root for the symbol switcher
|
QString m_symbolSwitcherRoot; // project root for the symbol switcher
|
||||||
RadialMenu *m_radial = nullptr;
|
|
||||||
QList<RadialDef> m_radialDefs;
|
|
||||||
QHash<int, int> m_buttonToRadial; // Qt::MouseButton (int) -> def index
|
|
||||||
bool m_gestureActive = false; // an RMB drag gesture is in progress
|
|
||||||
bool m_leftDownInView = false; // LMB held inside the view (Acme chord anchor)
|
bool m_leftDownInView = false; // LMB held inside the view (Acme chord anchor)
|
||||||
bool m_swallowMiddleRelease = false; // swallow the release paired with a consumed middle press
|
bool m_swallowMiddleRelease = false; // swallow the release paired with a consumed middle press
|
||||||
bool m_swallowRightRelease = false; // swallow the release paired with a left+right paste chord
|
bool m_swallowRightRelease = false; // swallow the release paired with a left+right paste chord
|
||||||
|
|
@ -153,7 +137,6 @@ private:
|
||||||
FrecencyStore m_frecency; // persisted usage feeding the palette ranking
|
FrecencyStore m_frecency; // persisted usage feeding the palette ranking
|
||||||
KateProjectBridge m_projectBridge; // read-only view of Kate's project plugin
|
KateProjectBridge m_projectBridge; // read-only view of Kate's project plugin
|
||||||
ProjectIndexer m_indexer; // async, cached file/symbol indexing
|
ProjectIndexer m_indexer; // async, cached file/symbol indexing
|
||||||
WaylandCursorWarp m_cursorWarp; // pointer warp on Wayland (no-op on X11)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace deft
|
} // namespace deft
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
# radial — the radial caret menu.
|
||||||
|
#
|
||||||
|
# `radial` (the static lib) is the pure model + menu widget + config loader,
|
||||||
|
# unit tested headless. `deft_radial` is the Kate plugin ([deft:util] radial);
|
||||||
|
# it also carries the optional Wayland pointer-warp glue used to follow the
|
||||||
|
# keyboard focus between split panes after a pane-focus radial action.
|
||||||
add_library(radial STATIC
|
add_library(radial STATIC
|
||||||
radialmodel.cpp
|
radialmodel.cpp
|
||||||
radialmodel.h
|
radialmodel.h
|
||||||
|
|
@ -18,3 +24,81 @@ if(Qt6Test_FOUND)
|
||||||
target_link_libraries(test_radialconfig PRIVATE radial Qt6::Test Qt6::Widgets)
|
target_link_libraries(test_radialconfig PRIVATE radial Qt6::Test Qt6::Widgets)
|
||||||
add_test(NAME radialconfig COMMAND test_radialconfig)
|
add_test(NAME radialconfig COMMAND test_radialconfig)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- [deft:util] radial — the Kate plugin ----------------------------------
|
||||||
|
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(DEFT_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(RADIAL_PLUGIN_SOURCES radialplugin.cpp radialplugin.h waylandcursorwarp.h)
|
||||||
|
set(DEFT_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(DEFT_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(deft_wayland_proto STATIC ${DEFT_WAYLAND_PROTO_SRCS})
|
||||||
|
target_include_directories(deft_wayland_proto PUBLIC
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}" ${WAYLAND_CLIENT_INCLUDE_DIRS})
|
||||||
|
set_target_properties(deft_wayland_proto PROPERTIES
|
||||||
|
POSITION_INDEPENDENT_CODE ON
|
||||||
|
LINKER_LANGUAGE C
|
||||||
|
AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
|
||||||
|
list(APPEND RADIAL_PLUGIN_SOURCES waylandcursorwarp.cpp)
|
||||||
|
set(DEFT_WAYLAND ON)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT DEFT_WAYLAND)
|
||||||
|
message(STATUS "Wayland pointer-warp support disabled (missing scanner / "
|
||||||
|
"wayland-client / Qt6 GuiPrivate / protocol xml)")
|
||||||
|
list(APPEND RADIAL_PLUGIN_SOURCES waylandcursorwarp_stub.cpp)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
kcoreaddons_add_plugin(deft_radial
|
||||||
|
SOURCES ${RADIAL_PLUGIN_SOURCES}
|
||||||
|
INSTALL_NAMESPACE "kf6/ktexteditor"
|
||||||
|
)
|
||||||
|
target_include_directories(deft_radial PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
target_link_libraries(deft_radial PRIVATE
|
||||||
|
radial
|
||||||
|
KF6::TextEditor
|
||||||
|
KF6::XmlGui
|
||||||
|
KF6::CoreAddons
|
||||||
|
KF6::I18n
|
||||||
|
Qt6::Widgets
|
||||||
|
)
|
||||||
|
|
||||||
|
if(DEFT_WAYLAND)
|
||||||
|
target_compile_definitions(deft_radial PRIVATE OLLIE_WAYLAND=1)
|
||||||
|
target_include_directories(deft_radial PRIVATE ${WAYLAND_CLIENT_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(deft_radial PRIVATE
|
||||||
|
deft_wayland_proto
|
||||||
|
Qt6::GuiPrivate
|
||||||
|
${WAYLAND_CLIENT_LIBRARIES})
|
||||||
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -151,10 +151,10 @@ QList<RadialDef> RadialConfig::builtinDefault()
|
||||||
// Panel-opening switchers live in the "More" branch next to the palette
|
// Panel-opening switchers live in the "More" branch next to the palette
|
||||||
// escape hatch — not the top ring of in-place actions — because they pull
|
// escape hatch — not the top ring of in-place actions — because they pull
|
||||||
// attention away from the caret. Resolved directly by onRadialActivated.
|
// attention away from the caret. Resolved directly by onRadialActivated.
|
||||||
leaf(QStringLiteral("File…"), QStringLiteral("ollie_goto_file")),
|
leaf(QStringLiteral("File…"), QStringLiteral("deft_goto_file")),
|
||||||
leaf(QStringLiteral("Sym…"), QStringLiteral("ollie_goto_symbol")),
|
leaf(QStringLiteral("Sym…"), QStringLiteral("deft_goto_symbol")),
|
||||||
// Opens the full command palette (handled specially by the plugin).
|
// Opens the full command palette (handled specially by the plugin).
|
||||||
leaf(QStringLiteral("M-x"), QStringLiteral("ollie_command_palette")),
|
leaf(QStringLiteral("M-x"), QStringLiteral("deft_command_palette")),
|
||||||
};
|
};
|
||||||
|
|
||||||
edit.root.children = {
|
edit.root.children = {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,311 @@
|
||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
#include "radialplugin.h"
|
||||||
|
|
||||||
|
#include "radialconfig.h"
|
||||||
|
#include "radialmenu.h"
|
||||||
|
|
||||||
|
#include <KTextEditor/MainWindow>
|
||||||
|
#include <KTextEditor/View>
|
||||||
|
|
||||||
|
#include <KActionCollection>
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <KPluginFactory>
|
||||||
|
#include <KXMLGUIClient>
|
||||||
|
#include <KXMLGUIFactory>
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QCursor>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QKeySequence>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
|
namespace deft
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Kate pane-focus / split actions. After one of these the active pane changes,
|
||||||
|
// so the pointer is warped to follow the keyboard focus.
|
||||||
|
bool isPaneFocusAction(const QString &id)
|
||||||
|
{
|
||||||
|
return id == QLatin1String("go_left_split_view")
|
||||||
|
|| id == QLatin1String("go_right_split_view")
|
||||||
|
|| id == QLatin1String("go_upward_split_view")
|
||||||
|
|| id == QLatin1String("go_downward_split_view")
|
||||||
|
|| id == QLatin1String("go_next_split_view")
|
||||||
|
|| id == QLatin1String("go_prev_split_view")
|
||||||
|
|| id == QLatin1String("view_split_vert")
|
||||||
|
|| id == QLatin1String("view_split_horiz")
|
||||||
|
|| id == QLatin1String("view_close_current_space")
|
||||||
|
|| id == QLatin1String("view_close_others");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// RadialPlugin
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
RadialPlugin::RadialPlugin(QObject *parent, const QVariantList &args)
|
||||||
|
: KTextEditor::Plugin(parent)
|
||||||
|
{
|
||||||
|
Q_UNUSED(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
RadialPlugin::~RadialPlugin() = default;
|
||||||
|
|
||||||
|
QObject *RadialPlugin::createView(KTextEditor::MainWindow *mainWindow)
|
||||||
|
{
|
||||||
|
return new RadialView(this, mainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// RadialView
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
RadialView::RadialView(RadialPlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
||||||
|
: QObject(plugin)
|
||||||
|
, m_mainWindow(mainWindow)
|
||||||
|
{
|
||||||
|
setupRadials();
|
||||||
|
|
||||||
|
// Application-wide filter: the opening mouse press leaves an implicit grab
|
||||||
|
// on the editor view, so the live move/release events are delivered there,
|
||||||
|
// and this filter is the one place that reliably sees them.
|
||||||
|
qApp->installEventFilter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
RadialView::~RadialView()
|
||||||
|
{
|
||||||
|
delete m_radial;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadialView::setupRadials()
|
||||||
|
{
|
||||||
|
// Prefer the user's config (~/.config/olliepalette/radials.json); fall back
|
||||||
|
// to the built-in set when it is absent, empty, or invalid.
|
||||||
|
m_radialDefs = RadialConfig::load();
|
||||||
|
|
||||||
|
QWidget *w = m_mainWindow->window();
|
||||||
|
|
||||||
|
for (int i = 0; i < m_radialDefs.size(); ++i) {
|
||||||
|
const RadialDef &def = m_radialDefs.at(i);
|
||||||
|
|
||||||
|
// Key trigger (weapon-wheel style).
|
||||||
|
if (!def.trigger.isEmpty()) {
|
||||||
|
auto *trigger = new QAction(this);
|
||||||
|
trigger->setObjectName(QStringLiteral("deft_radial_") + def.name);
|
||||||
|
trigger->setText(i18n("Radial Menu: %1", def.name));
|
||||||
|
trigger->setShortcut(QKeySequence(def.trigger));
|
||||||
|
trigger->setShortcutContext(Qt::ApplicationShortcut);
|
||||||
|
connect(trigger, &QAction::triggered, this, [this, i]() { showRadial(i); });
|
||||||
|
if (w) {
|
||||||
|
w->addAction(trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse-button gesture (the natural radial trigger).
|
||||||
|
if (!def.mouseButton.isEmpty()) {
|
||||||
|
Qt::MouseButton btn = Qt::NoButton;
|
||||||
|
if (def.mouseButton == QLatin1String("RightButton")) {
|
||||||
|
btn = Qt::RightButton;
|
||||||
|
} else if (def.mouseButton == QLatin1String("MiddleButton")) {
|
||||||
|
btn = Qt::MiddleButton;
|
||||||
|
}
|
||||||
|
if (btn != Qt::NoButton) {
|
||||||
|
m_buttonToRadial.insert(static_cast<int>(btn), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadialView::showRadial(int defIndex)
|
||||||
|
{
|
||||||
|
QPoint global;
|
||||||
|
if (KTextEditor::View *view = m_mainWindow->activeView()) {
|
||||||
|
global = view->mapToGlobal(view->cursorPositionCoordinates());
|
||||||
|
} else if (QWidget *w = m_mainWindow->window()) {
|
||||||
|
global = w->mapToGlobal(w->rect().center());
|
||||||
|
}
|
||||||
|
showRadialAt(defIndex, global);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadialView::showRadialAt(int defIndex, const QPoint &globalPos)
|
||||||
|
{
|
||||||
|
if (defIndex < 0 || defIndex >= m_radialDefs.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!m_radial) {
|
||||||
|
m_radial = new RadialMenu(m_mainWindow->window());
|
||||||
|
connect(m_radial, &RadialMenu::activatedId, this, &RadialView::onRadialActivated);
|
||||||
|
}
|
||||||
|
m_radial->popup(m_radialDefs.at(defIndex).root, globalPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadialView::onRadialActivated(const QString &actionId)
|
||||||
|
{
|
||||||
|
// Launcher doors (palette / switchers) are reachable from inside a radial as
|
||||||
|
// special slices. Sibling deft plugins register these actions on the window;
|
||||||
|
// trigger them by objectName so no cross-plugin link is needed.
|
||||||
|
if (QAction *a = findActionByName(actionId)) {
|
||||||
|
a->trigger();
|
||||||
|
// If this changed the active pane (focus move, or a split that creates
|
||||||
|
// and activates a new pane), warp the pointer onto it so the mouse
|
||||||
|
// follows the focus. Deferred by ~a frame so Kate has finished
|
||||||
|
// activating — and, for a freshly created split, laying out — the pane.
|
||||||
|
if (isPaneFocusAction(actionId)) {
|
||||||
|
QTimer::singleShot(16, this, [this]() { warpCursorToActiveView(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QAction *RadialView::findActionByName(const QString &nameOrText) const
|
||||||
|
{
|
||||||
|
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
|
||||||
|
if (!factory || nameOrText.isEmpty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const QList<KXMLGUIClient *> clients = factory->clients();
|
||||||
|
|
||||||
|
// First pass: exact objectName match (fast, unambiguous).
|
||||||
|
for (KXMLGUIClient *client : clients) {
|
||||||
|
if (KActionCollection *coll = client->actionCollection()) {
|
||||||
|
if (QAction *a = coll->action(nameOrText)) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: match by visible text, case-insensitively. Keeps radial slices
|
||||||
|
// working when a Kate action's objectName differs from our config id across
|
||||||
|
// versions (we can reference it by its menu label instead).
|
||||||
|
const QString wanted = nameOrText.trimmed();
|
||||||
|
for (KXMLGUIClient *client : clients) {
|
||||||
|
KActionCollection *coll = client->actionCollection();
|
||||||
|
if (!coll) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QList<QAction *> actions = coll->actions();
|
||||||
|
for (QAction *a : actions) {
|
||||||
|
if (!a) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString text = a->text().remove(QLatin1Char('&')).trimmed();
|
||||||
|
if (text.compare(wanted, Qt::CaseInsensitive) == 0) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RadialView::warpCursorToActiveView()
|
||||||
|
{
|
||||||
|
KTextEditor::View *view = m_mainWindow->activeView();
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RadialView::eventFilter(QObject *obj, QEvent *event)
|
||||||
|
{
|
||||||
|
// While a drag gesture is active, drive the radial directly from the raw
|
||||||
|
// events: the opening press leaves an implicit grab on the editor view, so
|
||||||
|
// the live move/release events are delivered there, and this application
|
||||||
|
// filter is the one place that reliably sees them.
|
||||||
|
if (m_gestureActive) {
|
||||||
|
switch (event->type()) {
|
||||||
|
case QEvent::MouseMove:
|
||||||
|
if (m_radial) {
|
||||||
|
m_radial->driveHoverGlobal(
|
||||||
|
static_cast<QMouseEvent *>(event)->globalPosition().toPoint());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case QEvent::MouseButtonRelease:
|
||||||
|
m_gestureActive = false;
|
||||||
|
if (m_radial) {
|
||||||
|
m_radial->driveReleaseGlobal(
|
||||||
|
static_cast<QMouseEvent *>(event)->globalPosition().toPoint());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress the context menu that would otherwise follow an RMB gesture.
|
||||||
|
if (event->type() == QEvent::ContextMenu
|
||||||
|
&& m_buttonToRadial.contains(static_cast<int>(Qt::RightButton))) {
|
||||||
|
KTextEditor::View *view = m_mainWindow->activeView();
|
||||||
|
auto *widget = qobject_cast<QWidget *>(obj);
|
||||||
|
if (view && widget && view->isAncestorOf(widget)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return QObject::eventFilter(obj, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type() != QEvent::MouseButtonPress) {
|
||||||
|
return QObject::eventFilter(obj, event);
|
||||||
|
}
|
||||||
|
auto *me = static_cast<QMouseEvent *>(event);
|
||||||
|
const int button = static_cast<int>(me->button());
|
||||||
|
if (!m_buttonToRadial.contains(button)) {
|
||||||
|
return QObject::eventFilter(obj, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only hijack presses that land inside the active view's editing area.
|
||||||
|
KTextEditor::View *view = m_mainWindow->activeView();
|
||||||
|
auto *widget = qobject_cast<QWidget *>(obj);
|
||||||
|
if (!view || !widget || !view->isAncestorOf(widget)) {
|
||||||
|
return QObject::eventFilter(obj, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress the radial when the left button is already held: the user is
|
||||||
|
// mid-selection (or setting up an acme chord), not starting a clean radial
|
||||||
|
// gesture. buttons() reports the other buttons down at press time.
|
||||||
|
if (me->buttons() & Qt::LeftButton) {
|
||||||
|
return QObject::eventFilter(obj, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the radial at the press position and begin the drag gesture. The
|
||||||
|
// press is consumed so the context menu does not appear and the caret is
|
||||||
|
// not moved (an intentional, configured trade-off).
|
||||||
|
showRadialAt(m_buttonToRadial.value(button), me->globalPosition().toPoint());
|
||||||
|
m_gestureActive = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace deft
|
||||||
|
|
||||||
|
// K_PLUGIN_FACTORY generates the factory and the moc-level registration. The
|
||||||
|
// matching .moc include is required in this translation unit.
|
||||||
|
K_PLUGIN_FACTORY_WITH_JSON(RadialPluginFactory, "radialplugin.json",
|
||||||
|
registerPlugin<deft::RadialPlugin>();)
|
||||||
|
|
||||||
|
#include "radialplugin.moc"
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
*
|
||||||
|
* [deft:util] radial — the radial caret menu plugin for Kate.
|
||||||
|
*
|
||||||
|
* RadialPlugin is the KTextEditor::Plugin entry point. For every MainWindow it
|
||||||
|
* creates a RadialView, which builds the configured radial menus (each bound to
|
||||||
|
* a key and/or a mouse button) and pops the right one at the caret or pointer.
|
||||||
|
* A radial slice triggers a Kate action by objectName; sibling deft plugins
|
||||||
|
* (palette, switch) expose their launcher actions on the window, so a radial can
|
||||||
|
* open them too. On a pane-focus action the pointer follows the keyboard focus.
|
||||||
|
*/
|
||||||
|
#ifndef DEFT_RADIALPLUGIN_H
|
||||||
|
#define DEFT_RADIALPLUGIN_H
|
||||||
|
|
||||||
|
#include <KTextEditor/Plugin>
|
||||||
|
|
||||||
|
#include "radialconfig.h" // RadialDef
|
||||||
|
#include "waylandcursorwarp.h"
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
class QAction;
|
||||||
|
|
||||||
|
namespace KTextEditor
|
||||||
|
{
|
||||||
|
class MainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace deft
|
||||||
|
{
|
||||||
|
|
||||||
|
class RadialMenu;
|
||||||
|
|
||||||
|
class RadialPlugin : public KTextEditor::Plugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit RadialPlugin(QObject *parent, const QVariantList &args = {});
|
||||||
|
~RadialPlugin() override;
|
||||||
|
|
||||||
|
QObject *createView(KTextEditor::MainWindow *mainWindow) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RadialView : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
RadialView(RadialPlugin *plugin, KTextEditor::MainWindow *mainWindow);
|
||||||
|
~RadialView() override;
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void onRadialActivated(const QString &actionId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Build the configured radials and bind each to its trigger key / button.
|
||||||
|
void setupRadials();
|
||||||
|
void showRadial(int defIndex); // centre on the caret
|
||||||
|
void showRadialAt(int defIndex, const QPoint &globalPos); // centre on a point
|
||||||
|
|
||||||
|
// Find a Kate action by its raw objectName (then visible text) across all
|
||||||
|
// GUI clients. Resolves radial-slice ids, including the launcher actions
|
||||||
|
// that sibling deft plugins register on the window.
|
||||||
|
QAction *findActionByName(const QString &objectName) const;
|
||||||
|
|
||||||
|
// Move the OS pointer onto the center of the active view — used after a
|
||||||
|
// radial pane-focus action so the mouse follows the keyboard focus.
|
||||||
|
void warpCursorToActiveView();
|
||||||
|
|
||||||
|
// The application event filter: catches the configured mouse-button gesture
|
||||||
|
// inside the active view and drives the radial, and suppresses the context
|
||||||
|
// menu that would otherwise follow a right-button gesture.
|
||||||
|
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||||
|
|
||||||
|
KTextEditor::MainWindow *m_mainWindow;
|
||||||
|
RadialMenu *m_radial = nullptr;
|
||||||
|
QList<RadialDef> m_radialDefs;
|
||||||
|
QHash<int, int> m_buttonToRadial; // Qt::MouseButton (int) -> def index
|
||||||
|
bool m_gestureActive = false; // an RMB drag gesture is in progress
|
||||||
|
WaylandCursorWarp m_cursorWarp; // pointer warp on Wayland (no-op on X11)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace deft
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "Levi Neely"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Description": "A radial caret menu: pop a configurable action wheel at the caret (key) or pointer (mouse button), select by moving and releasing.",
|
||||||
|
"Icon": "application-menu",
|
||||||
|
"License": "LGPL-2.0-or-later",
|
||||||
|
"Name": "[deft:util] radial",
|
||||||
|
"Version": "0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -168,9 +168,9 @@ private Q_SLOTS:
|
||||||
ids << c.actionId;
|
ids << c.actionId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QVERIFY(ids.contains(QStringLiteral("ollie_goto_file")));
|
QVERIFY(ids.contains(QStringLiteral("deft_goto_file")));
|
||||||
QVERIFY(ids.contains(QStringLiteral("ollie_goto_symbol")));
|
QVERIFY(ids.contains(QStringLiteral("deft_goto_symbol")));
|
||||||
QVERIFY(ids.contains(QStringLiteral("ollie_command_palette")));
|
QVERIFY(ids.contains(QStringLiteral("deft_command_palette")));
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadFileReadsValidConfig()
|
void loadFileReadsValidConfig()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue