M3: dedup palette actions by identity, disambiguate shared labels
The same QAction is often reachable through multiple GUI clients; those are true duplicates and are now collapsed by pointer identity. Distinct actions that merely share a visible label (common across plugins) are kept but disambiguated by appending the owning component's display name, and every entry is assigned a unique, stable id so activating a row always triggers the intended action rather than whichever one won a hash key.
This commit is contained in:
parent
91a0e2d260
commit
485d395d4d
|
|
@ -74,6 +74,11 @@ 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
|
||||||
|
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.
|
||||||
|
|
||||||
### 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
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@
|
||||||
#include <KPluginFactory>
|
#include <KPluginFactory>
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <QHash>
|
||||||
#include <QKeySequence>
|
#include <QKeySequence>
|
||||||
|
#include <QSet>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
namespace katecustom
|
namespace katecustom
|
||||||
|
|
@ -64,24 +66,35 @@ OllieView::~OllieView()
|
||||||
delete m_palette;
|
delete m_palette;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OllieView::collectActions(QHash<QString, QPointer<QAction>> &out) const
|
QList<OllieView::ActionEntry> OllieView::collectActions() const
|
||||||
{
|
{
|
||||||
|
QList<ActionEntry> entries;
|
||||||
|
|
||||||
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
|
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
|
||||||
if (!factory) {
|
if (!factory) {
|
||||||
return;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dedup by pointer identity: the same QAction is frequently reachable
|
||||||
|
// through several GUI clients. Those are true duplicates and must appear
|
||||||
|
// only once.
|
||||||
|
QSet<QAction *> seen;
|
||||||
|
|
||||||
const QList<KXMLGUIClient *> clients = factory->clients();
|
const QList<KXMLGUIClient *> clients = factory->clients();
|
||||||
for (KXMLGUIClient *client : clients) {
|
for (KXMLGUIClient *client : clients) {
|
||||||
KActionCollection *coll = client->actionCollection();
|
KActionCollection *coll = client->actionCollection();
|
||||||
if (!coll) {
|
if (!coll) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const QString component = coll->componentDisplayName();
|
||||||
const QList<QAction *> actions = coll->actions();
|
const QList<QAction *> actions = coll->actions();
|
||||||
for (QAction *action : actions) {
|
for (QAction *action : actions) {
|
||||||
if (!action || action->isSeparator()) {
|
if (!action || action->isSeparator()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (seen.contains(action)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Only offer things the user could actually invoke now.
|
// Only offer things the user could actually invoke now.
|
||||||
if (!action->isEnabled() || !action->isVisible()) {
|
if (!action->isEnabled() || !action->isVisible()) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -90,16 +103,45 @@ void OllieView::collectActions(QHash<QString, QPointer<QAction>> &out) const
|
||||||
if (label.isEmpty()) {
|
if (label.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Prefer the stable objectName as id; fall back to the label.
|
seen.insert(action);
|
||||||
QString id = action->objectName();
|
|
||||||
if (id.isEmpty()) {
|
ActionEntry e;
|
||||||
id = label;
|
e.action = action;
|
||||||
}
|
e.label = label;
|
||||||
if (!out.contains(id)) {
|
e.component = component;
|
||||||
out.insert(id, action);
|
e.shortcut = action->shortcut().toString(QKeySequence::NativeText);
|
||||||
}
|
entries.push_back(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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;
|
||||||
|
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()) {
|
||||||
|
base += QLatin1Char('/');
|
||||||
|
}
|
||||||
|
base += objName.isEmpty() ? e.label : objName;
|
||||||
|
|
||||||
|
// Guarantee uniqueness even if base still collides.
|
||||||
|
const int n = idSeq[base]++;
|
||||||
|
e.id = (n == 0) ? base : base + QStringLiteral("#") + QString::number(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OllieView::showCommandPalette()
|
void OllieView::showCommandPalette()
|
||||||
|
|
@ -109,22 +151,18 @@ void OllieView::showCommandPalette()
|
||||||
connect(m_palette, &PaletteWidget::activatedId, this, &OllieView::runAction);
|
connect(m_palette, &PaletteWidget::activatedId, this, &OllieView::runAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_actionsById.clear();
|
const QList<ActionEntry> entries = collectActions();
|
||||||
collectActions(m_actionsById);
|
|
||||||
|
|
||||||
|
m_actionsById.clear();
|
||||||
QList<PaletteItem> items;
|
QList<PaletteItem> items;
|
||||||
items.reserve(m_actionsById.size());
|
items.reserve(entries.size());
|
||||||
for (auto it = m_actionsById.cbegin(); it != m_actionsById.cend(); ++it) {
|
for (const ActionEntry &e : entries) {
|
||||||
QAction *action = it.value();
|
m_actionsById.insert(e.id, e.action);
|
||||||
if (!action) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
PaletteItem item;
|
PaletteItem item;
|
||||||
item.id = it.key();
|
item.id = e.id;
|
||||||
item.label = action->text().remove(QLatin1Char('&')).trimmed();
|
item.label = e.label;
|
||||||
// Show the current shortcut as the group column hint, when present.
|
item.group = e.shortcut; // shortcut shown as the trailing hint
|
||||||
const QString sc = action->shortcut().toString(QKeySequence::NativeText);
|
|
||||||
item.group = sc;
|
|
||||||
items.push_back(item);
|
items.push_back(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,19 @@ private Q_SLOTS:
|
||||||
void runAction(const QString &actionId);
|
void runAction(const QString &actionId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// One harvested, runnable action with the provenance needed to build a
|
||||||
|
// unique id and to disambiguate labels shared by several commands.
|
||||||
|
struct ActionEntry {
|
||||||
|
QPointer<QAction> action;
|
||||||
|
QString label; // visible text, ampersands stripped
|
||||||
|
QString component; // owning collection's display name (provenance)
|
||||||
|
QString shortcut; // current shortcut, native text
|
||||||
|
QString id; // unique, stable key used on activation
|
||||||
|
};
|
||||||
|
|
||||||
// Collect every unique, enabled, visible QAction reachable from the
|
// Collect every unique, enabled, visible QAction reachable from the
|
||||||
// window's GUI factory clients. Keyed by a stable id for lookup on accept.
|
// window's GUI factory clients, deduplicated by pointer identity.
|
||||||
void collectActions(QHash<QString, QPointer<QAction>> &out) const;
|
QList<ActionEntry> collectActions() const;
|
||||||
|
|
||||||
KTextEditor::MainWindow *m_mainWindow;
|
KTextEditor::MainWindow *m_mainWindow;
|
||||||
PaletteWidget *m_palette = nullptr;
|
PaletteWidget *m_palette = nullptr;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue