From d227d6161b410f85953b70b093c3f3f845b9502f Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 7 Oct 2026 16:26:03 +0200 Subject: [PATCH] Palette: guarantee distinguishable labels, never drop distinct actions Dedup is now strictly by QAction pointer identity (functionally lossless: the same action object listed once). Distinct actions are never collapsed. When two distinct actions would show the same label, enrich it through a fallback chain until every visible row is unique: component name -> objectName -> shortcut -> numeric suffix. Ids remain unique so activation always hits the intended action. --- docs/PLAN.md | 12 ++++---- src/plugin/ollieplugin.cpp | 58 ++++++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index 81f25e0..927fffa 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -74,11 +74,13 @@ Therefore the palette is replaced, not extended, and the replacement is built on - Installs to `kf6/ktexteditor`; metadata verified valid via `KPluginMetaData`. - Not yet wired: frecency (the model supports a bonus, but the plugin currently passes 0 for every action — usage tracking is deferred). -- Duplicate handling: actions are deduplicated by pointer identity (the same - action reachable through several GUI clients is listed once). Distinct actions - that merely share a visible label (e.g. two plugins each offering "Reload") - are kept but disambiguated with their component name, and every entry gets a - unique id so activation always triggers the right action. +- Duplicate handling: actions are deduplicated only by pointer identity (the + exact same action object reachable through several GUI clients is listed + once — functionally lossless). Distinct actions are never dropped. When two + distinct actions would render with the same label, the label is enriched + through a fallback chain until every visible row is unique: component name → + objectName → shortcut → numeric suffix. Each entry also gets a unique id so + activation always triggers the intended action. ### M4 — Radial caret menu (mouse door) — NEXT - Mouse-triggered radial menu positioned at the caret diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index c88f506..0bdb3e9 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -114,21 +114,63 @@ QList OllieView::collectActions() const } } - // Two distinct actions can share a visible label (e.g. different plugins - // each offering "Reload"). Those are not duplicates, so keep them, but - // disambiguate by provenance so the user can tell them apart. + // Distinct actions can share a visible label (e.g. different plugins each + // offering "Reload"). These are NOT duplicates — never drop them. Instead + // guarantee every visible row is distinguishable by enriching colliding + // labels through a fallback chain of discriminators, and give each entry a + // unique id so activation always triggers the intended action. QHash labelCounts; for (const ActionEntry &e : entries) { ++labelCounts[e.label]; } - // Assign unique, reasonably stable ids and disambiguate colliding labels. + // First pass: for any shared label, append the strongest discriminator + // available (component, else objectName, else shortcut). + for (ActionEntry &e : entries) { + if (labelCounts.value(e.label) <= 1) { + continue; + } + const QString objName = e.action ? e.action->objectName() : QString(); + QString disc; + if (!e.component.isEmpty()) { + disc = e.component; + } else if (!objName.isEmpty()) { + disc = objName; + } else if (!e.shortcut.isEmpty()) { + disc = e.shortcut; + } + if (!disc.isEmpty()) { + e.label = e.label + QStringLiteral(" — ") + disc; + } + } + + // Second pass: if any labels are STILL identical (same label and same + // discriminator), append objectName/shortcut, then a numeric suffix, so no + // two rows ever look the same. + QHash stillColliding; + for (const ActionEntry &e : entries) { + ++stillColliding[e.label]; + } + QHash dupSeq; + for (ActionEntry &e : entries) { + if (stillColliding.value(e.label) <= 1) { + continue; + } + const QString objName = e.action ? e.action->objectName() : QString(); + if (!objName.isEmpty() && !e.label.contains(objName)) { + e.label += QStringLiteral(" [") + objName + QLatin1Char(']'); + } else if (!e.shortcut.isEmpty() && !e.label.contains(e.shortcut)) { + e.label += QStringLiteral(" (") + e.shortcut + QLatin1Char(')'); + } else { + // Last resort: number the indistinguishable rows 1..n. + const int n = ++dupSeq[e.label]; + e.label += QStringLiteral(" (") + QString::number(n) + QLatin1Char(')'); + } + } + + // Assign unique, reasonably stable ids (independent of the display label). QHash idSeq; for (ActionEntry &e : entries) { - if (labelCounts.value(e.label) > 1 && !e.component.isEmpty()) { - e.label = e.label + QStringLiteral(" — ") + e.component; - } - const QString objName = e.action ? e.action->objectName() : QString(); QString base = e.component; if (!base.isEmpty()) {