From 5bd55801e98f2e3037670c1b4457c541be76eae7 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 27 Sep 2026 03:05:54 +1300 Subject: [PATCH 1/2] 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); } From 47e5a8072b81e3c7e328c5864acdf32a683109da Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 27 Sep 2026 03:15:07 +1300 Subject: [PATCH 2/2] Shortcut bar: reopen at the cursor, wrap to a width the user sets After customising, the bar now opens at the cursor like every other time, instead of where the customising window was. A size grip in the bar's corner sets its width. The tiles are laid out in rows that wrap at that width, so a bar with many commands and pinned elements becomes a block rather than one long strip; the height follows the rows, and the width is saved. Until the user sets one, the tiles stay on one row, as before. The grip is driven by hand: QSizeGrip asks the window manager to resize, and on X11 a popup is not managed, so nothing would happen. The hint line wraps rather than being cut off at narrow widths. The customising window remembers the size it was left at. Discussion #1033. Co-Authored-By: Claude Opus 5.5 --- .../ElementsCollection/elementpickerpopup.cpp | 139 +++++++++++++++++- .../ElementsCollection/elementpickerpopup.h | 13 ++ sources/shortcutbarsettings.cpp | 28 ++++ sources/shortcutbarsettings.h | 5 + 4 files changed, 178 insertions(+), 7 deletions(-) diff --git a/sources/ElementsCollection/elementpickerpopup.cpp b/sources/ElementsCollection/elementpickerpopup.cpp index a2d383a7b..2cfc1aaa4 100644 --- a/sources/ElementsCollection/elementpickerpopup.cpp +++ b/sources/ElementsCollection/elementpickerpopup.cpp @@ -27,11 +27,14 @@ #include #include #include +#include #include +#include #include #include #include #include +#include #include #include #include @@ -39,6 +42,8 @@ #include #include +#include + #include "../qetapp.h" #include "../shortcutmanager.h" #include "elementslocation.h" @@ -219,11 +224,22 @@ ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source, m_hint = new QLabel(tr("Entrée pour insérer · Échap pour fermer"), this); m_hint->setEnabled(false); + //Wrapped rather than cut off when the bar is made narrow + m_hint->setWordWrap(true); + + //The bar's width is the user's: dragging the grip wraps the tiles + //onto more rows, and the width is kept for next time + m_grip = new QSizeGrip(this); + m_grip->setToolTip(tr("Glisser pour changer la largeur de la barre")); + m_grip->installEventFilter(this); + auto *hint_row = new QHBoxLayout(); + hint_row->addWidget(m_hint, 1); + hint_row->addWidget(m_grip, 0, Qt::AlignBottom | Qt::AlignRight); layout->addWidget(m_commands); layout->addWidget(m_search); layout->addWidget(m_view); - layout->addWidget(m_hint); + layout->addLayout(hint_row); layout->addWidget(m_editor); //Search as you type, after a short idle: shorter than the dock's @@ -303,6 +319,16 @@ void ElementPickerPopup::show(const QPoint &global_pos) void ElementPickerPopup::keepOnScreen(const QPoint &global_pos) { adjustSize(); + //The sizes the user gave the customising window and the bar + if (m_customising) { + const QSize size = ShortcutBarSettings::editorSize(); + if (size.isValid()) { + resize(size.expandedTo(minimumSizeHint())); + } + } else if (m_bar_mode && ShortcutBarSettings::barWidth() > 0) { + resize(qMax(ShortcutBarSettings::barWidth(), minimumSizeHint().width()), + height()); + } QPoint pos = global_pos; if (QScreen *screen = QGuiApplication::screenAt(global_pos)) { @@ -335,6 +361,11 @@ void ElementPickerPopup::setCommands(const QStringList &ids) delete item->widget(); delete item; } + qDeleteAll(m_tiles); + m_tiles.clear(); + m_tile_rows = new QVBoxLayout(); + m_tile_rows->setSpacing(2); + m_commands_layout->addLayout(m_tile_rows); for (const QString &id : ids) { @@ -354,7 +385,7 @@ void ElementPickerPopup::setCommands(const QStringList &ids) hide(); emit elementChosen(location); }); - m_commands_layout->addWidget(button); + m_tiles << button; continue; } @@ -383,7 +414,7 @@ void ElementPickerPopup::setCommands(const QStringList &ids) hide(); action->trigger(); }); - m_commands_layout->addWidget(button); + m_tiles << button; } m_commands_layout->addStretch(); if (m_bar_mode) @@ -394,12 +425,103 @@ void ElementPickerPopup::setCommands(const QStringList &ids) customise->setToolTip(tr("Personnaliser la barre…")); customise->setFocusPolicy(Qt::NoFocus); connect(customise, &QToolButton::clicked, this, &ElementPickerPopup::startCustomising); - m_commands_layout->addWidget(customise); + m_commands_layout->addWidget(customise, 0, Qt::AlignTop); } + layoutTiles(ShortcutBarSettings::barWidth()); + m_grip->setVisible(m_bar_mode); //An empty bar still shows, so it can be customised back m_commands->setVisible(m_bar_mode || !ids.isEmpty()); } +/** + @brief ElementPickerPopup::layoutTiles + Put the bar's tiles in rows no wider than the bar at @a bar_width, in + their order, as many per row as fit. A @a bar_width of 0 or less, the + width the user never set, keeps them on one row. +*/ +void ElementPickerPopup::layoutTiles(int bar_width) +{ + if (!m_tile_rows) { + return; + } + //Emptying a row layout leaves its buttons alone; they go into the + //new rows below + while (QLayoutItem *item = m_tile_rows->takeAt(0)) { + delete item->layout(); + } + + //Room left beside the tiles: the popup's margins and the "…" button + const int limit = bar_width > 0 + ? bar_width - 12 - 30 + : std::numeric_limits::max(); + QHBoxLayout *row = nullptr; + int used = 0; + for (QToolButton *tile : std::as_const(m_tiles)) + { + const int w = tile->sizeHint().width() + m_tile_rows->spacing(); + if (!row || (used + w > limit && used > 0)) { + row = new QHBoxLayout(); + row->setSpacing(2); + row->addStretch(); + m_tile_rows->addLayout(row); + used = 0; + } + //Before the stretch, which stays last so the row is left-aligned + row->insertWidget(row->count() - 1, tile); + used += w; + } +} + +/** + @brief ElementPickerPopup::eventFilter + Drive the size grip by hand. QSizeGrip asks the window manager to do the + resize, and a popup is a window the window manager does not manage, so + on X11 nothing would happen. Only the width follows the mouse: the tiles + re-flow into rows as it changes, the height follows the rows, and the + width is saved when the grip is let go. +*/ +bool ElementPickerPopup::eventFilter(QObject *watched, QEvent *event) +{ + if (watched != m_grip) { + return QFrame::eventFilter(watched, event); + } + switch (event->type()) + { + case QEvent::MouseButtonPress: { + auto *me = static_cast(event); + if (me->button() != Qt::LeftButton) { + break; + } + m_grip_active = true; + m_grip_press_x = me->globalPosition().toPoint().x(); + m_grip_press_width = width(); + return true; + } + case QEvent::MouseMove: { + if (!m_grip_active) { + break; + } + auto *me = static_cast(event); + const int w = qMax(minimumWidth(), m_grip_press_width + + me->globalPosition().toPoint().x() - m_grip_press_x); + layoutTiles(w); + layout()->activate(); + resize(w, sizeHint().height()); + return true; + } + case QEvent::MouseButtonRelease: + if (!m_grip_active) { + break; + } + m_grip_active = false; + ShortcutBarSettings::setBarWidth(width()); + return true; + default: + break; + } + return QFrame::eventFilter(watched, event); +} + /** @brief ElementPickerPopup::barItem @return a list item for @a id, a command or a pinned element: icon only, @@ -451,6 +573,7 @@ void ElementPickerPopup::startCustomising() fillCustomising(ShortcutBarSettings::ids(m_context)); m_commands->hide(); setPickerVisible(false); + m_grip->hide(); //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(); @@ -516,7 +639,8 @@ void ElementPickerPopup::runSymbolSearch() /** @brief ElementPickerPopup::finishCustomising Leave the edit, saving the bar row when @a save, and show the bar again - where it was so the result can be seen and used straight away. + at the cursor so the result can be seen and used straight away. The + window's size is kept either way. */ void ElementPickerPopup::finishCustomising(bool save) { @@ -529,14 +653,15 @@ void ElementPickerPopup::finishCustomising(bool save) ShortcutBarSettings::setIds(m_context, ids); } - const QPoint where = pos(); + ShortcutBarSettings::setEditorSize(size()); m_customising = false; hide(); m_editor->hide(); setPickerVisible(true); setWindowFlags(Qt::Popup); setWindowTitle(QString()); - popUpShortcutBar(where, m_context); + //At the cursor, like any other opening of the bar + popUpShortcutBar(QCursor::pos(), m_context); } /** diff --git a/sources/ElementsCollection/elementpickerpopup.h b/sources/ElementsCollection/elementpickerpopup.h index fdbd4af95..9983661f1 100644 --- a/sources/ElementsCollection/elementpickerpopup.h +++ b/sources/ElementsCollection/elementpickerpopup.h @@ -32,6 +32,9 @@ class QAction; class QHBoxLayout; class QListWidget; class QListWidgetItem; +class QSizeGrip; +class QToolButton; +class QVBoxLayout; /** @brief A cursor-anchored element picker. @@ -65,6 +68,7 @@ class ElementPickerPopup : public QFrame protected: void keyPressEvent(QKeyEvent *event) override; void closeEvent(QCloseEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; private: void runSearch(); @@ -72,6 +76,7 @@ class ElementPickerPopup : public QFrame void showPalette(); void show(const QPoint &global_pos); void setCommands(const QStringList &ids); + void layoutTiles(int bar_width); QAction *commandAction(const QString &id) const; QListWidgetItem *barItem(const QString &id, bool icon_only) const; void startCustomising(); @@ -90,6 +95,14 @@ class ElementPickerPopup : public QFrame QLabel *m_hint = nullptr; QWidget *m_commands = nullptr; QHBoxLayout *m_commands_layout = nullptr; + /// The bar's tiles, commands and elements, in order + QList m_tiles; + QVBoxLayout *m_tile_rows = nullptr; + QSizeGrip *m_grip = nullptr; + /// The size grip is being dragged, from this x at this width + bool m_grip_active = false; + int m_grip_press_x = 0; + int m_grip_press_width = 0; /// Opened as the shortcut bar, as opposed to the plain picker bool m_bar_mode = false; ShortcutBarSettings::Context m_context = ShortcutBarSettings::Canvas; diff --git a/sources/shortcutbarsettings.cpp b/sources/shortcutbarsettings.cpp index 3a3c1c9ee..97e66416d 100644 --- a/sources/shortcutbarsettings.cpp +++ b/sources/shortcutbarsettings.cpp @@ -160,3 +160,31 @@ bool ShortcutBarSettings::hasElements(Context context) const QStringList list = ids(context); return std::any_of(list.cbegin(), list.cend(), &ShortcutBarSettings::isElement); } + +/** + @return the width the user gave the bar with its size grip, or 0 when + they never did: the tiles then stay on one row +*/ +int ShortcutBarSettings::barWidth() +{ + return QSettings().value(QStringLiteral("diagrameditor/shortcut_bar/width"), 0).toInt(); +} + +void ShortcutBarSettings::setBarWidth(int width) +{ + QSettings().setValue(QStringLiteral("diagrameditor/shortcut_bar/width"), width); +} + +/** + @return the size the user left the customising window at, or an invalid + size when it was never opened +*/ +QSize ShortcutBarSettings::editorSize() +{ + return QSettings().value(QStringLiteral("diagrameditor/shortcut_bar/editor_size")).toSize(); +} + +void ShortcutBarSettings::setEditorSize(const QSize &size) +{ + QSettings().setValue(QStringLiteral("diagrameditor/shortcut_bar/editor_size"), size); +} diff --git a/sources/shortcutbarsettings.h b/sources/shortcutbarsettings.h index b655aca76..a56d845ab 100644 --- a/sources/shortcutbarsettings.h +++ b/sources/shortcutbarsettings.h @@ -21,6 +21,7 @@ #include #include #include +#include /** @brief The commands shown on the diagram editor's shortcut bar. @@ -52,6 +53,10 @@ class ShortcutBarSettings static QStringList availableIds(); static bool isElement(const QString &id); static bool hasElements(Context context); + static int barWidth(); + static void setBarWidth(int width); + static QSize editorSize(); + static void setEditorSize(const QSize &size); }; #endif // SHORTCUTBARSETTINGS_H