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:
parent
485d395d4d
commit
d227d6161b
12
docs/PLAN.md
12
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`.
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue