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()
|
||||
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()
|
||||
|
||||
# The core deft plugin: command palette (M-x), project file/symbol switchers,
|
||||
# and the Acme line-editing keys + bracket/quote selection. (radial, sam, and
|
||||
# plumb are now their own plugins; the util features here will split further.)
|
||||
kcoreaddons_add_plugin(deft
|
||||
SOURCES ${OLLIE_PLUGIN_SOURCES}
|
||||
SOURCES ollieplugin.cpp ollieplugin.h
|
||||
INSTALL_NAMESPACE "kf6/ktexteditor"
|
||||
)
|
||||
|
||||
|
|
@ -69,7 +22,6 @@ target_include_directories(deft PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
|
|||
|
||||
target_link_libraries(deft PRIVATE
|
||||
palette
|
||||
radial
|
||||
olliecommands_lib
|
||||
project_lib
|
||||
KF6::TextEditor
|
||||
|
|
@ -78,12 +30,3 @@ target_link_libraries(deft PRIVATE
|
|||
KF6::I18n
|
||||
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 "frecencystore.h"
|
||||
#include "radialmenu.h"
|
||||
#include "radialconfig.h"
|
||||
#include "olliecommands.h"
|
||||
#include "projectindex.h"
|
||||
#include "symbolindex.h"
|
||||
|
|
@ -26,7 +24,6 @@
|
|||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QCursor>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
|
|
@ -37,35 +34,14 @@
|
|||
#include <QMouseEvent>
|
||||
#include <QSet>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QWidget>
|
||||
#include <QWindow>
|
||||
|
||||
namespace deft
|
||||
{
|
||||
|
||||
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.
|
||||
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
|
||||
// their objectName/label (command-palette harvest, radial escape-hatch).
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
// Keep them discoverable as window actions (no shortcut attached).
|
||||
|
|
@ -344,11 +320,9 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
|||
[this]() { m_indexer.invalidateAll(); });
|
||||
}
|
||||
|
||||
setupRadials();
|
||||
|
||||
// One application-wide filter serves both the mouse-button radial gesture
|
||||
// and the Acme-style left+middle chord, catching presses no matter which
|
||||
// internal child of the view actually receives them.
|
||||
// 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
|
||||
// which internal child of the view actually receives them.
|
||||
qApp->installEventFilter(this);
|
||||
}
|
||||
|
||||
|
|
@ -357,7 +331,6 @@ OllieView::~OllieView()
|
|||
delete m_palette;
|
||||
delete m_filePalette;
|
||||
delete m_symbolPalette;
|
||||
delete m_radial;
|
||||
}
|
||||
|
||||
QList<OllieView::ActionEntry> OllieView::collectActions() const
|
||||
|
|
@ -850,45 +823,6 @@ QAction *OllieView::findActionByName(const QString &nameOrText) const
|
|||
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)
|
||||
{
|
||||
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
|
||||
// paste. Button 1 (left) held while selecting is the anchor:
|
||||
// left + middle tap = Cut, left + right tap = Paste.
|
||||
// Both use edit_cut / edit_paste, which use the standard KDE clipboard
|
||||
// (Klipper-backed). The right-button chord is free because the radial
|
||||
// gesture is already suppressed while the left button is held. Middle-button
|
||||
// events in the view are consumed on BOTH press and release to kill the
|
||||
// middle-click primary paste (it fires on release, not just press).
|
||||
// (Klipper-backed). Middle-button events in the view are consumed on BOTH
|
||||
// press and release to kill the middle-click primary paste (it fires on
|
||||
// release, not just press).
|
||||
if (event->type() == QEvent::MouseButtonRelease) {
|
||||
auto *mev = static_cast<QMouseEvent *>(event);
|
||||
if (mev->button() == Qt::LeftButton) {
|
||||
|
|
@ -1132,156 +1040,32 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
|
|||
paste->trigger();
|
||||
}
|
||||
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 (m_leftDownInView) {
|
||||
// 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"))) {
|
||||
cut->trigger();
|
||||
}
|
||||
m_swallowMiddleRelease = true;
|
||||
return true;
|
||||
}
|
||||
// A plain middle press (no chord): always suppress the X11 primary
|
||||
// paste that fires on release. If the middle button is bound to a
|
||||
// radial, fall through to the radial dispatch below; otherwise
|
||||
// consume the press here too.
|
||||
// A plain middle press (no chord): suppress the X11 primary paste
|
||||
// that fires on release, and consume the press.
|
||||
m_swallowMiddleRelease = true;
|
||||
if (!m_buttonToRadial.contains(static_cast<int>(Qt::MiddleButton))) {
|
||||
return true;
|
||||
}
|
||||
// else: do not return — let the radial-trigger dispatch handle it.
|
||||
}
|
||||
if (mev->button() == Qt::LeftButton && inView) {
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
//
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
#include "kateprojectbridge.h"
|
||||
#include "projectindexer.h"
|
||||
#include "palettemodel.h" // PaletteItem
|
||||
#include "waylandcursorwarp.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
|
@ -38,9 +37,7 @@ namespace deft
|
|||
{
|
||||
|
||||
class PaletteWidget;
|
||||
class RadialMenu;
|
||||
class OllieCommands;
|
||||
struct RadialDef;
|
||||
|
||||
class OlliePlugin : public KTextEditor::Plugin
|
||||
{
|
||||
|
|
@ -92,9 +89,6 @@ private:
|
|||
// small margin), anchored near the top.
|
||||
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
|
||||
// unique id and to disambiguate labels shared by several commands.
|
||||
struct ActionEntry {
|
||||
|
|
@ -109,16 +103,10 @@ private:
|
|||
// window's GUI factory clients, deduplicated by pointer identity.
|
||||
QList<ActionEntry> collectActions() const;
|
||||
|
||||
// Find a Kate action by its raw objectName across all GUI clients. Used to
|
||||
// resolve radial-menu slice ids (which reference Kate action names).
|
||||
// Find a Kate action by its raw objectName across all GUI clients. Used by
|
||||
// the Acme mouse chords to invoke edit_cut / edit_paste.
|
||||
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,
|
||||
// so there is nothing for Qt to report as an ambiguous shortcut).
|
||||
void acmeBeginningOfLine(); // C-a
|
||||
|
|
@ -142,10 +130,6 @@ private:
|
|||
QString m_fileSwitcherRoot; // project root for the open switcher
|
||||
PaletteWidget *m_symbolPalette = nullptr; // project "Go to 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_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
|
||||
|
|
@ -153,7 +137,6 @@ 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 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
|
||||
radialmodel.cpp
|
||||
radialmodel.h
|
||||
|
|
@ -18,3 +24,81 @@ if(Qt6Test_FOUND)
|
|||
target_link_libraries(test_radialconfig PRIVATE radial Qt6::Test Qt6::Widgets)
|
||||
add_test(NAME radialconfig COMMAND test_radialconfig)
|
||||
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
|
||||
// escape hatch — not the top ring of in-place actions — because they pull
|
||||
// attention away from the caret. Resolved directly by onRadialActivated.
|
||||
leaf(QStringLiteral("File…"), QStringLiteral("ollie_goto_file")),
|
||||
leaf(QStringLiteral("Sym…"), QStringLiteral("ollie_goto_symbol")),
|
||||
leaf(QStringLiteral("File…"), QStringLiteral("deft_goto_file")),
|
||||
leaf(QStringLiteral("Sym…"), QStringLiteral("deft_goto_symbol")),
|
||||
// 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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
QVERIFY(ids.contains(QStringLiteral("ollie_goto_file")));
|
||||
QVERIFY(ids.contains(QStringLiteral("ollie_goto_symbol")));
|
||||
QVERIFY(ids.contains(QStringLiteral("ollie_command_palette")));
|
||||
QVERIFY(ids.contains(QStringLiteral("deft_goto_file")));
|
||||
QVERIFY(ids.contains(QStringLiteral("deft_goto_symbol")));
|
||||
QVERIFY(ids.contains(QStringLiteral("deft_command_palette")));
|
||||
}
|
||||
|
||||
void loadFileReadsValidConfig()
|
||||
|
|
|
|||
Loading…
Reference in New Issue