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); }