From 5bd55801e98f2e3037670c1b4457c541be76eae7 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 27 Sep 2026 03:05:54 +1300 Subject: [PATCH] Pin elements to the shortcut bar The bar's customising window gets an element search next to the command list. Type part of a name, then drag a hit onto the bar, double-click it, or press Enter to pin the best one. A pinned element shows as its icon among the commands, and clicking it places the element, as the picker does. Pinned elements are saved in the same list as the commands, by collection path (common://, custom://, company://), so the row keeps the user's order. Elements embedded in a project are not offered: their path names the project as loaded now. Elements are offered for the empty-folio bar only; with something selected the bar is for acting on it. Once any element is pinned there, the palette folder grid under the bar is hidden, and comes back while typing a search. Dragging a pinned element off the bar onto the commands, or a double click, removes it. The Preferences page shows pinned elements by name and icon, and removing one there drops it instead of listing it as a command. The customising window is now kept on screen, since it is taller than before. Discussion #1033. Co-Authored-By: Claude Opus 5.5 --- .../ElementsCollection/elementpickerpopup.cpp | 226 +++++++++++++++--- .../ElementsCollection/elementpickerpopup.h | 10 +- sources/shortcutbarsettings.cpp | 20 ++ sources/shortcutbarsettings.h | 6 + .../ui/configpage/shortcutbarconfigpage.cpp | 32 ++- 5 files changed, 246 insertions(+), 48 deletions(-) diff --git a/sources/ElementsCollection/elementpickerpopup.cpp b/sources/ElementsCollection/elementpickerpopup.cpp index 870c3894d..a2d383a7b 100644 --- a/sources/ElementsCollection/elementpickerpopup.cpp +++ b/sources/ElementsCollection/elementpickerpopup.cpp @@ -87,9 +87,9 @@ ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source, auto *editor_layout = new QVBoxLayout(m_editor); editor_layout->setContentsMargins(0, 0, 0, 0); auto *help = new QLabel( - tr("Glissez les commandes dans la barre, hors de la barre, ou " - "d'une place à l'autre. Un double-clic fait passer une commande " - "d'une liste à l'autre."), m_editor); + tr("Glissez les commandes et les éléments dans la barre, hors de la " + "barre, ou d'une place à l'autre. Un double-clic fait passer une " + "commande ou un élément d'une liste à l'autre."), m_editor); help->setWordWrap(true); m_edit_row = new QListWidget(m_editor); m_edit_row->setFlow(QListView::LeftToRight); @@ -124,16 +124,59 @@ ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source, this, relabel(m_edit_row, true)); connect(m_edit_available->model(), &QAbstractItemModel::rowsInserted, this, relabel(m_edit_available, false)); + //An element dragged off the bar onto the commands is removed from + //the bar, not listed as a command + connect(m_edit_available->model(), &QAbstractItemModel::rowsInserted, this, [this]() { + QTimer::singleShot(0, this, [this]() { + for (int i = m_edit_available->count() - 1 ; i >= 0 ; --i) { + if (ShortcutBarSettings::isElement( + m_edit_available->item(i)->data(Qt::UserRole).toString())) { + delete m_edit_available->takeItem(i); + } + } + }); + }); connect(m_edit_row, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) { const QString id = item->data(Qt::UserRole).toString(); delete item; - m_edit_available->addItem(commandItem(id, false)); + if (!ShortcutBarSettings::isElement(id)) { + m_edit_available->addItem(barItem(id, false)); + } }); connect(m_edit_available, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) { const QString id = item->data(Qt::UserRole).toString(); delete item; - m_edit_row->addItem(commandItem(id, true)); + m_edit_row->addItem(barItem(id, true)); + }); + + //Elements to pin: the same ranked search as the picker, dragged + //onto the bar. Copied, not moved, so a result can be pinned and + //still be seen in the list. + m_edit_symbols_box = new QWidget(m_editor); + auto *symbols_layout = new QVBoxLayout(m_edit_symbols_box); + symbols_layout->setContentsMargins(0, 0, 0, 0); + m_edit_symbols_search = new QLineEdit(m_edit_symbols_box); + m_edit_symbols_search->setPlaceholderText(tr("Rechercher un élément…")); + m_edit_symbols_search->setClearButtonEnabled(true); + m_edit_symbols = new QListWidget(m_edit_symbols_box); + m_edit_symbols->setIconSize(QSize(32, 32)); + m_edit_symbols->setMinimumHeight(220); + m_edit_symbols->setDragDropMode(QAbstractItemView::DragOnly); + m_edit_symbols->setDefaultDropAction(Qt::CopyAction); + m_edit_symbols->setSelectionMode(QAbstractItemView::SingleSelection); + symbols_layout->addWidget(new QLabel(tr("Éléments :"), m_edit_symbols_box)); + symbols_layout->addWidget(m_edit_symbols_search); + symbols_layout->addWidget(m_edit_symbols); + + auto *symbols_timer = new QTimer(this); + symbols_timer->setSingleShot(true); + symbols_timer->setInterval(300); + connect(m_edit_symbols_search, &QLineEdit::textChanged, this, + [symbols_timer]() { symbols_timer->start(); }); + connect(symbols_timer, &QTimer::timeout, this, &ElementPickerPopup::runSymbolSearch); + connect(m_edit_symbols, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) { + m_edit_row->addItem(barItem(item->data(Qt::UserRole).toString(), true)); }); auto *defaults = new QPushButton(tr("Valeurs par défaut"), m_editor); auto *cancel = new QPushButton(tr("Annuler"), m_editor); @@ -152,8 +195,13 @@ ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source, editor_layout->addWidget(help); editor_layout->addWidget(new QLabel(tr("Dans la barre :"), m_editor)); editor_layout->addWidget(m_edit_row); - editor_layout->addWidget(new QLabel(tr("Autres commandes :"), m_editor)); - editor_layout->addWidget(m_edit_available); + auto *lists = new QHBoxLayout(); + auto *commands_column = new QVBoxLayout(); + commands_column->addWidget(new QLabel(tr("Autres commandes :"), m_editor)); + commands_column->addWidget(m_edit_available); + lists->addLayout(commands_column); + lists->addWidget(m_edit_symbols_box); + editor_layout->addLayout(lists); editor_layout->addLayout(editor_buttons); m_editor->hide(); @@ -217,13 +265,7 @@ void ElementPickerPopup::popUpShortcutBar(const QPoint &global_pos, { m_bar_mode = true; m_context = context; - QList commands; - for (const QString &id : ShortcutBarSettings::ids(context)) { - if (QAction *action = commandAction(id)) { - commands << action; - } - } - setCommands(commands); + setCommands(ShortcutBarSettings::ids(context)); show(global_pos); } @@ -247,11 +289,22 @@ void ElementPickerPopup::show(const QPoint &global_pos) m_model->clear(); showPalette(); + keepOnScreen(global_pos); + QFrame::show(); + m_search->setFocus(); +} + +/** + @brief ElementPickerPopup::keepOnScreen + Fit the popup to its contents and move it to @a global_pos, kept fully on + the screen that point is on: opening at the cursor near a right or bottom + edge would otherwise push it off. +*/ +void ElementPickerPopup::keepOnScreen(const QPoint &global_pos) +{ adjustSize(); QPoint pos = global_pos; - //Keep it fully on the screen the cursor is on: opening at the cursor - //near a right or bottom edge would otherwise push it off. if (QScreen *screen = QGuiApplication::screenAt(global_pos)) { const QRect avail = screen->availableGeometry(); pos.setX(qBound(avail.left(), @@ -261,30 +314,54 @@ void ElementPickerPopup::show(const QPoint &global_pos) } move(pos); - QFrame::show(); - m_search->setFocus(); } /** @brief ElementPickerPopup::setCommands - Show @a commands as a row of buttons above the search field, or hide the - row when there are none. A disabled command keeps its place, greyed out, - so the row looks the same every time for a given selection. + Show @a ids as a row of buttons above the search field, or hide the row + when there are none. A disabled command keeps its place, greyed out, so + the row looks the same every time for a given selection. An id no live + action carries, or a pinned element that no longer exists, is skipped. Clicking a button closes the picker first, then triggers the action: a command such as "add a line" starts a mode on the folio, which needs the - focus the popup holds. - @param commands + focus the popup holds. A pinned element is placed, as if chosen from the + picker. + @param ids : command ids and pinned elements' collection paths */ -void ElementPickerPopup::setCommands(const QList &commands) +void ElementPickerPopup::setCommands(const QStringList &ids) { while (QLayoutItem *item = m_commands_layout->takeAt(0)) { delete item->widget(); delete item; } - for (QAction *action : commands) + for (const QString &id : ids) { + if (ShortcutBarSettings::isElement(id)) + { + const ElementsLocation location(id); + if (!location.exist()) { + continue; + } + auto *button = new QToolButton(m_commands); + button->setAutoRaise(true); + button->setIconSize(QSize(24, 24)); + button->setIcon(location.icon()); + button->setToolTip(location.name()); + button->setFocusPolicy(Qt::NoFocus); + connect(button, &QToolButton::clicked, this, [this, location]() { + hide(); + emit elementChosen(location); + }); + m_commands_layout->addWidget(button); + continue; + } + + QAction *action = commandAction(id); + if (!action) { + continue; + } auto *button = new QToolButton(m_commands); button->setAutoRaise(true); button->setIconSize(QSize(24, 24)); @@ -320,23 +397,34 @@ void ElementPickerPopup::setCommands(const QList &commands) m_commands_layout->addWidget(customise); } //An empty bar still shows, so it can be customised back - m_commands->setVisible(m_bar_mode || !commands.isEmpty()); + m_commands->setVisible(m_bar_mode || !ids.isEmpty()); } /** - @brief ElementPickerPopup::commandItem - @return a list item for command @a id: icon only, for the bar row, or - icon and text, for the list of other commands. The id is in UserRole. + @brief ElementPickerPopup::barItem + @return a list item for @a id, a command or a pinned element: icon only, + for the bar row, or icon and text, for the other lists. The id is in + UserRole. */ -QListWidgetItem *ElementPickerPopup::commandItem(const QString &id, bool icon_only) const +QListWidgetItem *ElementPickerPopup::barItem(const QString &id, bool icon_only) const { - QAction *action = commandAction(id); - const QString text = action ? action->text().remove(QLatin1Char('&')) : id; + QString text = id; + QIcon icon; + if (ShortcutBarSettings::isElement(id)) { + const ElementsLocation location(id); + if (location.exist()) { + text = location.name(); + icon = location.icon(); + } + } else if (QAction *action = commandAction(id)) { + text = action->text().remove(QLatin1Char('&')); + icon = action->icon(); + } auto *item = new QListWidgetItem(); item->setData(Qt::UserRole, id); item->setToolTip(text); - if (action && !action->icon().isNull()) { - item->setIcon(action->icon()); + if (!icon.isNull()) { + item->setIcon(icon); } if (!icon_only || item->icon().isNull()) { item->setText(text); @@ -363,10 +451,14 @@ void ElementPickerPopup::startCustomising() fillCustomising(ShortcutBarSettings::ids(m_context)); m_commands->hide(); setPickerVisible(false); + //Elements are pinned to the empty-folio bar only: with something + //selected, the bar is for acting on it, not for adding more + m_edit_symbols_search->clear(); + m_edit_symbols->clear(); + m_edit_symbols_box->setVisible(m_context == ShortcutBarSettings::Canvas); m_editor->show(); - adjustSize(); - move(where); + keepOnScreen(where); QFrame::show(); activateWindow(); } @@ -381,16 +473,46 @@ void ElementPickerPopup::fillCustomising(const QStringList &ids) m_edit_row->clear(); m_edit_available->clear(); for (const QString &id : ids) { - m_edit_row->addItem(commandItem(id, true)); + m_edit_row->addItem(barItem(id, true)); } for (const QString &id : ShortcutBarSettings::availableIds()) { if (!ids.contains(id)) { - m_edit_available->addItem(commandItem(id, false)); + m_edit_available->addItem(barItem(id, false)); } } m_edit_available->sortItems(); } +/** + @brief ElementPickerPopup::runSymbolSearch + Fill the customising window's element list with the ranked hits for its + search field. Elements embedded in a project are left out: their path + names the project as it is loaded now, so it would not find them again + in a later session. +*/ +void ElementPickerPopup::runSymbolSearch() +{ + m_edit_symbols->clear(); + if (!m_source) { + return; + } + const QVector hits = + m_source->rankedSearch(m_edit_symbols_search->text()); + for (const ElementSearchHit &hit : hits) + { + if (ElementsLocation(hit.path).isProject()) { + continue; + } + auto *item = new QListWidgetItem(hit.icon, hit.name); + item->setData(Qt::UserRole, hit.path); + item->setToolTip(QStringLiteral("%1\n%2").arg(hit.name, hit.folder)); + m_edit_symbols->addItem(item); + if (m_edit_symbols->count() >= max_palette_entries) { + break; + } + } +} + /** @brief ElementPickerPopup::finishCustomising Leave the edit, saving the bar row when @a save, and show the bar again @@ -454,10 +576,18 @@ void ElementPickerPopup::runSearch() } if (m_search->text().isEmpty()) { + const bool was_hidden = m_view->isHidden(); showPalette(); + if (was_hidden != m_view->isHidden()) { + keepOnScreen(pos()); + } return; } + if (m_view->isHidden()) { + m_view->show(); + keepOnScreen(pos()); + } m_palette_mode = false; m_view->setViewMode(QListView::ListMode); m_view->setGridSize(QSize()); @@ -529,7 +659,15 @@ void ElementPickerPopup::keyPressEvent(QKeyEvent *event) return; case Qt::Key_Return: case Qt::Key_Enter: - if (m_customising) { + if (m_customising && m_edit_symbols_search->hasFocus()) { + //Enter in the element search pins the best hit rather + //than closing the window. Searched now, not on the + //timer, so a quick Enter after typing gets this text. + runSymbolSearch(); + if (QListWidgetItem *item = m_edit_symbols->item(0)) { + m_edit_row->addItem(barItem(item->data(Qt::UserRole).toString(), true)); + } + } else if (m_customising) { finishCustomising(true); } else { chooseCurrent(); @@ -572,6 +710,16 @@ void ElementPickerPopup::showPalette() m_palette_mode = true; m_model->clear(); + //Elements pinned to the bar replace the folder grid: the bar already + //shows the user's shortlist, and a second one below it is noise. + //The search still works; the list comes back as soon as one types. + if (m_bar_mode && ShortcutBarSettings::hasElements(m_context)) { + m_view->hide(); + m_hint->setText(tr("Tapez pour rechercher un élément · Échap pour fermer")); + return; + } + m_view->show(); + m_view->setViewMode(QListView::IconMode); m_view->setIconSize(QSize(48, 48)); m_view->setGridSize(QSize(92, 84)); diff --git a/sources/ElementsCollection/elementpickerpopup.h b/sources/ElementsCollection/elementpickerpopup.h index b57fa32a1..fdbd4af95 100644 --- a/sources/ElementsCollection/elementpickerpopup.h +++ b/sources/ElementsCollection/elementpickerpopup.h @@ -71,13 +71,15 @@ class ElementPickerPopup : public QFrame void chooseCurrent(); void showPalette(); void show(const QPoint &global_pos); - void setCommands(const QList &commands); + void setCommands(const QStringList &ids); QAction *commandAction(const QString &id) const; - QListWidgetItem *commandItem(const QString &id, bool icon_only) const; + QListWidgetItem *barItem(const QString &id, bool icon_only) const; void startCustomising(); void fillCustomising(const QStringList &ids); void finishCustomising(bool save); + void runSymbolSearch(); void setPickerVisible(bool visible); + void keepOnScreen(const QPoint &global_pos); int loadPaletteDir(const QString &dir_path, const QString &prefix, int depth); @@ -96,6 +98,10 @@ class ElementPickerPopup : public QFrame QWidget *m_editor = nullptr; QListWidget *m_edit_row = nullptr; QListWidget *m_edit_available = nullptr; + /// Element search, for pinning elements to the bar + QWidget *m_edit_symbols_box = nullptr; + QLineEdit *m_edit_symbols_search = nullptr; + QListWidget *m_edit_symbols = nullptr; bool m_palette_mode = true; }; diff --git a/sources/shortcutbarsettings.cpp b/sources/shortcutbarsettings.cpp index fa66300a3..3a3c1c9ee 100644 --- a/sources/shortcutbarsettings.cpp +++ b/sources/shortcutbarsettings.cpp @@ -22,6 +22,8 @@ #include #include +#include + namespace { QString settingsKey(ShortcutBarSettings::Context context) { @@ -140,3 +142,21 @@ QStringList ShortcutBarSettings::availableIds() } return ids; } + +/** + @return true if @a id is a pinned element's collection path rather than + a command id. Command ids never contain "://". +*/ +bool ShortcutBarSettings::isElement(const QString &id) +{ + return id.contains(QLatin1String("://")); +} + +/** + @return true if the row for @a context holds at least one element +*/ +bool ShortcutBarSettings::hasElements(Context context) +{ + const QStringList list = ids(context); + return std::any_of(list.cbegin(), list.cend(), &ShortcutBarSettings::isElement); +} diff --git a/sources/shortcutbarsettings.h b/sources/shortcutbarsettings.h index 8e83b9232..b655aca76 100644 --- a/sources/shortcutbarsettings.h +++ b/sources/shortcutbarsettings.h @@ -30,6 +30,10 @@ so any registered command can go on it and the bar needs no command list of its own. Stored in QSettings, one key per context; a context the user never changed uses the defaults below. + + A row can also hold elements the user pinned, by collection path + ("common://…", "custom://…", "company://…"), mixed in with the commands. + Clicking one places that element. */ class ShortcutBarSettings { @@ -46,6 +50,8 @@ class ShortcutBarSettings static QStringList defaultIds(Context context); static void setIds(Context context, const QStringList &ids); static QStringList availableIds(); + static bool isElement(const QString &id); + static bool hasElements(Context context); }; #endif // SHORTCUTBARSETTINGS_H diff --git a/sources/ui/configpage/shortcutbarconfigpage.cpp b/sources/ui/configpage/shortcutbarconfigpage.cpp index 2dee5dccb..d8d3f510d 100644 --- a/sources/ui/configpage/shortcutbarconfigpage.cpp +++ b/sources/ui/configpage/shortcutbarconfigpage.cpp @@ -17,6 +17,7 @@ */ #include "shortcutbarconfigpage.h" +#include "../../ElementsCollection/elementslocation.h" #include "../../qeticons.h" #include "../../shortcutmanager.h" @@ -180,7 +181,14 @@ void ShortcutBarConfigPage::addSelected() void ShortcutBarConfigPage::removeSelected() { for (QListWidgetItem *item : m_chosen->selectedItems()) { - m_available->addItem(m_chosen->takeItem(m_chosen->row(item))); + QListWidgetItem *taken = m_chosen->takeItem(m_chosen->row(item)); + //A removed element is not a command to list; it is pinned + //again from the bar's own customising window + if (ShortcutBarSettings::isElement(taken->data(Qt::UserRole).toString())) { + delete taken; + } else { + m_available->addItem(taken); + } } } @@ -208,15 +216,25 @@ void ShortcutBarConfigPage::resetContext() /** @brief ShortcutBarConfigPage::appendItem - Add @a id to @a list with the command's text and icon. An id no live - action carries (a command from a build without it) is still listed, by - its id, so saving does not silently drop it. + Add @a id to @a list with the command's text and icon, or a pinned + element's name and icon. An id no live action carries (a command from a + build without it) is still listed, by its id, so saving does not + silently drop it. */ void ShortcutBarConfigPage::appendItem(QListWidget *list, const QString &id) { - QAction *action = ShortcutManager::instance().action(id, nullptr); - const QString text = m_descriptions.value(id, id); - auto *item = new QListWidgetItem(action ? action->icon() : QIcon(), text); + QString text = m_descriptions.value(id, id); + QIcon icon; + if (ShortcutBarSettings::isElement(id)) { + const ElementsLocation location(id); + if (location.exist()) { + text = location.name(); + icon = location.icon(); + } + } else if (QAction *action = ShortcutManager::instance().action(id, nullptr)) { + icon = action->icon(); + } + auto *item = new QListWidgetItem(icon, text); item->setData(Qt::UserRole, id); list->addItem(item); }