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`.
- 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

View File

@ -114,21 +114,63 @@ QList<OllieView::ActionEntry> 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<QString, int> labelCounts;
for (const ActionEntry &e : entries) {
++labelCounts[e.label];
}
// Assign unique, reasonably stable ids and disambiguate colliding labels.
QHash<QString, int> idSeq;
// 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 && !e.component.isEmpty()) {
e.label = e.label + QStringLiteral(" — ") + e.component;
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;
for (ActionEntry &e : entries) {
const QString objName = e.action ? e.action->objectName() : QString();
QString base = e.component;
if (!base.isEmpty()) {