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.
This commit is contained in:
Levi Neely 2026-10-07 16:26:03 +02:00
parent 485d395d4d
commit d227d6161b
2 changed files with 57 additions and 13 deletions

View File

@ -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`. - Installs to `kf6/ktexteditor`; metadata verified valid via `KPluginMetaData`.
- Not yet wired: frecency (the model supports a bonus, but the plugin currently - Not yet wired: frecency (the model supports a bonus, but the plugin currently
passes 0 for every action — usage tracking is deferred). passes 0 for every action — usage tracking is deferred).
- Duplicate handling: actions are deduplicated by pointer identity (the same - Duplicate handling: actions are deduplicated only by pointer identity (the
action reachable through several GUI clients is listed once). Distinct actions exact same action object reachable through several GUI clients is listed
that merely share a visible label (e.g. two plugins each offering "Reload") once — functionally lossless). Distinct actions are never dropped. When two
are kept but disambiguated with their component name, and every entry gets a distinct actions would render with the same label, the label is enriched
unique id so activation always triggers the right action. 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 ### M4 — Radial caret menu (mouse door) — NEXT
- Mouse-triggered radial menu positioned at the caret - Mouse-triggered radial menu positioned at the caret

View File

@ -114,21 +114,63 @@ QList<OllieView::ActionEntry> OllieView::collectActions() const
} }
} }
// Two distinct actions can share a visible label (e.g. different plugins // Distinct actions can share a visible label (e.g. different plugins each
// each offering "Reload"). Those are not duplicates, so keep them, but // offering "Reload"). These are NOT duplicates — never drop them. Instead
// disambiguate by provenance so the user can tell them apart. // 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<QString, int> labelCounts; QHash<QString, int> labelCounts;
for (const ActionEntry &e : entries) { for (const ActionEntry &e : entries) {
++labelCounts[e.label]; ++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<QString, int> stillColliding;
for (const ActionEntry &e : entries) {
++stillColliding[e.label];
}
QHash<QString, int> 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<QString, int> idSeq; QHash<QString, int> idSeq;
for (ActionEntry &e : entries) { 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(); const QString objName = e.action ? e.action->objectName() : QString();
QString base = e.component; QString base = e.component;
if (!base.isEmpty()) { if (!base.isEmpty()) {