Launchers: handle Alt+X/P/G in eventFilter, not QAction shortcuts

Alt+P (and the other launcher keys) collided with Kate's own
Alt+<letter> bindings; registered as ApplicationShortcut QActions this
makes Qt report an ambiguous shortcut and fire neither. Go nuclear: drop
the QAction shortcuts entirely and intercept Alt+X/P/G in eventFilter
(accept ShortcutOverride, act on KeyPress), scoped to our window — the
same ambiguity-proof technique used for the Acme keys. QActions kept for
objectName/label; onRadialActivated now resolves all three launcher ids
directly so the radial escape-hatch still reaches them.
This commit is contained in:
Levi Neely 2026-10-08 10:42:28 +02:00
parent 07af27e82c
commit 0e1c6943ac
2 changed files with 66 additions and 36 deletions

View File

@ -78,9 +78,17 @@ Therefore the palette is replaced, not extended, and the replacement is built on
### M3 — KTextEditor plugin + M-x + action registry — DONE
- `src/plugin/ollieplugin.{h,cpp}` + `ollieplugin.json`: a loadable
`KTextEditor::Plugin` (`olliepalette.so`) built with `kcoreaddons_add_plugin`.
- `OllieView` (one per MainWindow) registers the **M-x** action, `Alt+X`, a
single modifier with the keys adjacent on a compact layout; `ApplicationShortcut`
context so it is always live.
- `OllieView` (one per MainWindow) provides the **M-x** launcher on `Alt+X`.
LAUNCHER KEYS ARE NOT QAction SHORTCUTS: `Alt+X` (M-x), `Alt+P` (Go to File)
and `Alt+G` (Go to Symbol) are intercepted in `eventFilter()` (accept the
`ShortcutOverride`, act on the `KeyPress`), scoped to this plugin's window.
Registering them as `ApplicationShortcut` QActions clashed with Kate's own
`Alt+<letter>` bindings and Qt refused to fire either ("ambiguous shortcut").
The event-filter route cannot be ambiguous because nothing is registered —
the same technique already used for the Acme line-editing keys. The QActions
still exist (objectName/label) for command-palette harvest and as radial
escape-hatch slices (`ollie_command_palette` / `ollie_goto_file` /
`ollie_goto_symbol`, handled directly in `onRadialActivated`).
- On trigger it aggregates every enabled, visible `QAction` from the window's
`guiFactory()->clients()` action collections, keyed by stable `objectName`,
and feeds them to `PaletteWidget`. The action's current shortcut is shown in
@ -237,7 +245,7 @@ Therefore the palette is replaced, not extended, and the replacement is built on
whole path (orderless: "palette model" matches `src/palette/palettemodel.cpp`)
and highlights matched characters along it; a base-name hit still ranks first
because `/` is a word boundary and the length penalty favours shorter paths.
The root is `currentProjectRoot()` (explicit folder when set, else derived).
The root is `currentProjectRoot()` (Kate's loaded project, else derived).
Activation opens the file via `MainWindow::openUrl` + `activateView`. Also
reachable from the M-x palette (`ollie:switch:file`) and, by objectName
`ollie_goto_file`, from a radial. (2 path-ranking tests in

View File

@ -221,43 +221,30 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
QStringLiteral("olliepalette/frecency.json")));
m_frecency.load();
// The one keyboard door: Alt+X (M-x). A single modifier, no chord, and the
// keys sit together on a compact layout.
auto *openAction = new QAction(i18n("Command Palette (M-x)"), this);
// The three launcher "doors". Their shortcuts are NOT registered as
// QAction/ApplicationShortcut: Kate (and other plugins) already claim many
// Alt+<letter> combos, and a registered duplicate makes Qt refuse to fire
// either, reporting an "ambiguous shortcut". We therefore register NO
// shortcut here and instead intercept the key in eventFilter() (the same
// technique used for the Acme line-editing keys), which cannot be
// 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->setShortcut(QKeySequence(Qt::ALT | Qt::Key_X));
openAction->setShortcutContext(Qt::ApplicationShortcut);
connect(openAction, &QAction::triggered, this, &OllieView::showCommandPalette);
// Attach the shortcut to the window so it is always live.
auto *gotoFileAction = new QAction(i18n("Go to File (project) [Alt+P]"), this);
gotoFileAction->setObjectName(QStringLiteral("ollie_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"));
connect(gotoSymbolAction, &QAction::triggered, this, &OllieView::showSymbolSwitcher);
// Keep them discoverable as window actions (no shortcut attached).
if (QWidget *w = m_mainWindow->window()) {
w->addAction(openAction);
}
// Project "Go to File" switcher: Alt+P (single modifier; the Sublime/VSCode
// Ctrl+P idiom adapted to one-handed use). Lists the project's files —
// "open folder == open project" — scoped to the active document's git/root
// directory.
auto *gotoFileAction = new QAction(i18n("Go to File (project)"), this);
gotoFileAction->setObjectName(QStringLiteral("ollie_goto_file"));
gotoFileAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_P));
gotoFileAction->setShortcutContext(Qt::ApplicationShortcut);
connect(gotoFileAction, &QAction::triggered, this, &OllieView::showFileSwitcher);
if (QWidget *w = m_mainWindow->window()) {
w->addAction(gotoFileAction);
}
// Project "Go to Symbol" switcher: Alt+G (single modifier; "Go to symbol").
// Alt+R is reserved for the radial key trigger, so the symbol switcher uses
// Alt+G instead. Lists ctags symbols across the project's files, scoped to
// the same root as the file switcher. Activation opens the file and jumps to
// the symbol's line.
auto *gotoSymbolAction = new QAction(i18n("Go to Symbol (project)"), this);
gotoSymbolAction->setObjectName(QStringLiteral("ollie_goto_symbol"));
gotoSymbolAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_G));
gotoSymbolAction->setShortcutContext(Qt::ApplicationShortcut);
connect(gotoSymbolAction, &QAction::triggered, this, &OllieView::showSymbolSwitcher);
if (QWidget *w = m_mainWindow->window()) {
w->addAction(gotoSymbolAction);
}
@ -879,6 +866,33 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
return true; // consume the keypress
}
}
// Launcher doors: Alt+X (M-x), Alt+P (Go to File), Alt+G (Go to Symbol).
// Handled here rather than via QAction shortcuts so they can never clash
// with Kate's own Alt+<letter> bindings (which produced "ambiguous
// shortcut" and swallowed the key). Scope to this plugin's window so we
// do not hijack the combo in other Kate main windows.
const bool altOnly =
(ke->modifiers() & Qt::KeyboardModifierMask) == Qt::AltModifier;
if (altOnly
&& (ke->key() == Qt::Key_X || ke->key() == Qt::Key_P || ke->key() == Qt::Key_G)) {
auto *w = qobject_cast<QWidget *>(obj);
QWidget *ourWindow = m_mainWindow->window();
const bool ours = w && ourWindow && ourWindow->isAncestorOf(w);
if (ours) {
if (event->type() == QEvent::ShortcutOverride) {
event->accept(); // suppress any registered (ambiguous) shortcut
return true;
}
switch (ke->key()) {
case Qt::Key_X: showCommandPalette(); break;
case Qt::Key_P: showFileSwitcher(); break;
case Qt::Key_G: showSymbolSwitcher(); break;
default: break;
}
return true; // consume the keypress
}
}
}
// Bracket-pair selection: double-clicking next to a bracket selects the
@ -1045,11 +1059,19 @@ void OllieView::showRadialAt(int defIndex, const QPoint &globalPos)
void OllieView::onRadialActivated(const QString &actionId)
{
// The palette is reachable from inside a radial as a special slice.
// 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();
}