From 6d42556d9f74c577ea5aad8b34c7179d6f66a296 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 26 Sep 2026 13:11:22 +1200 Subject: [PATCH] Customise the shortcut bar in place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-click the shortcut bar, or click the "…" button at its end, and it turns into a small window holding two lists: the bar's commands, left to right, and every other command. Drag a command onto the bar, off it, or to another place on it; a double click moves it to the other list. Terminé saves and shows the bar again where it was, with the result; Annuler, Esc or closing the window leaves it as it was. "Valeurs par défaut" puts back the defaults for this context. A Qt::Popup closes on a press outside it and holds the mouse grab, so the bar is re-shown as a Qt::Tool window for the time of the edit. Only QSettings is written, through ShortcutBarSettings, never the project's undo stack. The popup now looks the commands up itself (popUpShortcutBar(pos, context)) instead of being handed actions, since it has to rebuild them after an edit. ShortcutBarSettings::availableIds() lists what can go on the bar, shared with the configuration page. An empty bar still shows its "…" button, so it can be filled again. Discussion #1033. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG (cherry picked from commit 4145d4bec1d403126b2a6587105aea47092db8a2) --- .../ElementsCollection/elementpickerpopup.cpp | 284 +++++++++++++++++- .../ElementsCollection/elementpickerpopup.h | 24 +- sources/qetdiagrameditor.cpp | 9 +- sources/shortcutbarsettings.cpp | 20 ++ sources/shortcutbarsettings.h | 1 + .../ui/configpage/shortcutbarconfigpage.cpp | 13 +- 6 files changed, 323 insertions(+), 28 deletions(-) diff --git a/sources/ElementsCollection/elementpickerpopup.cpp b/sources/ElementsCollection/elementpickerpopup.cpp index d3c9e9a39..870c3894d 100644 --- a/sources/ElementsCollection/elementpickerpopup.cpp +++ b/sources/ElementsCollection/elementpickerpopup.cpp @@ -22,7 +22,11 @@ #include #include #include +#include +#include +#include #include +#include #include #include #include @@ -36,6 +40,7 @@ #include #include "../qetapp.h" +#include "../shortcutmanager.h" #include "elementslocation.h" //The palette is read from disk each time the picker opens. It is meant as @@ -67,6 +72,90 @@ ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source, m_commands_layout->setContentsMargins(0, 0, 0, 0); m_commands_layout->setSpacing(2); m_commands->hide(); + //SolidWorks: right-click the bar to customise it + m_commands->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_commands, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) { + QMenu menu; + menu.addAction(tr("Personnaliser la barre…"), this, &ElementPickerPopup::startCustomising); + menu.exec(m_commands->mapToGlobal(pos)); + }); + + //Customising, shown instead of everything else. Two lists that + //commands are dragged between: the bar, left to right, and the + //commands that are not on it. + m_editor = new QWidget(this); + 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); + help->setWordWrap(true); + m_edit_row = new QListWidget(m_editor); + m_edit_row->setFlow(QListView::LeftToRight); + m_edit_row->setWrapping(false); + m_edit_row->setIconSize(QSize(24, 24)); + m_edit_row->setFixedHeight(44); + m_edit_available = new QListWidget(m_editor); + m_edit_available->setIconSize(QSize(20, 20)); + m_edit_available->setMinimumHeight(220); + for (QListWidget *list : {m_edit_row, m_edit_available}) { + list->setDragDropMode(QAbstractItemView::DragDrop); + list->setDefaultDropAction(Qt::MoveAction); + list->setSelectionMode(QAbstractItemView::SingleSelection); + } + //A drop copies the item as it was in the other list, so a command + //dragged onto the bar would keep its text and one dragged off would + //have none. Set the text for the list it has landed in. + //Deferred: a drop inserts the row first and copies the item's data + //into it afterwards. + auto relabel = [this](QListWidget *list, bool icon_only) { + return [this, list, icon_only]() { + QTimer::singleShot(0, this, [list, icon_only]() { + for (int i = 0 ; i < list->count() ; ++i) { + QListWidgetItem *item = list->item(i); + item->setText(icon_only && !item->icon().isNull() + ? QString() : item->toolTip()); + } + }); + }; + }; + connect(m_edit_row->model(), &QAbstractItemModel::rowsInserted, + this, relabel(m_edit_row, true)); + connect(m_edit_available->model(), &QAbstractItemModel::rowsInserted, + this, relabel(m_edit_available, false)); + + 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)); + }); + 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)); + }); + auto *defaults = new QPushButton(tr("Valeurs par défaut"), m_editor); + auto *cancel = new QPushButton(tr("Annuler"), m_editor); + auto *done = new QPushButton(tr("Terminé"), m_editor); + done->setDefault(true); + connect(defaults, &QPushButton::clicked, this, [this]() { + fillCustomising(ShortcutBarSettings::defaultIds(m_context)); + }); + connect(cancel, &QPushButton::clicked, this, [this]() { finishCustomising(false); }); + connect(done, &QPushButton::clicked, this, [this]() { finishCustomising(true); }); + auto *editor_buttons = new QHBoxLayout(); + editor_buttons->addWidget(defaults); + editor_buttons->addStretch(); + editor_buttons->addWidget(cancel); + editor_buttons->addWidget(done); + 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); + editor_layout->addLayout(editor_buttons); + m_editor->hide(); m_search = new QLineEdit(this); m_search->setPlaceholderText(tr("Rechercher un élément…")); @@ -87,6 +176,7 @@ ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source, layout->addWidget(m_search); layout->addWidget(m_view); layout->addWidget(m_hint); + layout->addWidget(m_editor); //Search as you type, after a short idle: shorter than the dock's //500 ms, since the popup is opened and closed in a few seconds, but @@ -108,10 +198,51 @@ ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source, focused and any previous query cleared. @param global_pos */ -void ElementPickerPopup::popUpAt(const QPoint &global_pos, - const QList &commands) +void ElementPickerPopup::popUpAt(const QPoint &global_pos) { + m_bar_mode = false; + setCommands({}); + show(global_pos); +} + +/** + @brief ElementPickerPopup::popUpShortcutBar + Show the picker at @a global_pos with the shortcut bar's commands for + @a context above it. + @param global_pos + @param context +*/ +void ElementPickerPopup::popUpShortcutBar(const QPoint &global_pos, + ShortcutBarSettings::Context context) +{ + 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); + show(global_pos); +} + +/** + @brief ElementPickerPopup::commandAction + @return the action registered under @a id by the window this popup + belongs to, or nullptr +*/ +QAction *ElementPickerPopup::commandAction(const QString &id) const +{ + return ShortcutManager::instance().action(id, parentWidget()); +} + +/** + @brief ElementPickerPopup::show + Reset the search and show at @a global_pos, kept on screen. +*/ +void ElementPickerPopup::show(const QPoint &global_pos) +{ m_search->clear(); m_model->clear(); showPalette(); @@ -130,7 +261,7 @@ void ElementPickerPopup::popUpAt(const QPoint &global_pos, } move(pos); - show(); + QFrame::show(); m_search->setFocus(); } @@ -178,7 +309,138 @@ void ElementPickerPopup::setCommands(const QList &commands) m_commands_layout->addWidget(button); } m_commands_layout->addStretch(); - m_commands->setVisible(!commands.isEmpty()); + if (m_bar_mode) + { + auto *customise = new QToolButton(m_commands); + customise->setAutoRaise(true); + customise->setText(QStringLiteral("…")); + customise->setToolTip(tr("Personnaliser la barre…")); + customise->setFocusPolicy(Qt::NoFocus); + connect(customise, &QToolButton::clicked, this, &ElementPickerPopup::startCustomising); + m_commands_layout->addWidget(customise); + } + //An empty bar still shows, so it can be customised back + m_commands->setVisible(m_bar_mode || !commands.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. +*/ +QListWidgetItem *ElementPickerPopup::commandItem(const QString &id, bool icon_only) const +{ + QAction *action = commandAction(id); + const QString text = action ? action->text().remove(QLatin1Char('&')) : id; + auto *item = new QListWidgetItem(); + item->setData(Qt::UserRole, id); + item->setToolTip(text); + if (action && !action->icon().isNull()) { + item->setIcon(action->icon()); + } + if (!icon_only || item->icon().isNull()) { + item->setText(text); + } + return item; +} + +/** + @brief ElementPickerPopup::startCustomising + Turn the bar into a small window holding the two lists. A Qt::Popup + closes as soon as the mouse is pressed outside it and keeps the mouse + grab during a drag, so the window is re-shown as a tool window for the + time of the edit. +*/ +void ElementPickerPopup::startCustomising() +{ + const QPoint where = pos(); + m_customising = true; + hide(); + setWindowFlags(Qt::Tool); + setWindowTitle(tr("Personnaliser la barre de raccourcis : %1") + .arg(ShortcutBarSettings::title(m_context))); + + fillCustomising(ShortcutBarSettings::ids(m_context)); + m_commands->hide(); + setPickerVisible(false); + m_editor->show(); + + adjustSize(); + move(where); + QFrame::show(); + activateWindow(); +} + +/** + @brief ElementPickerPopup::fillCustomising + Put @a ids on the bar row, and every other available command in the + list below it. +*/ +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)); + } + for (const QString &id : ShortcutBarSettings::availableIds()) { + if (!ids.contains(id)) { + m_edit_available->addItem(commandItem(id, false)); + } + } + m_edit_available->sortItems(); +} + +/** + @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. +*/ +void ElementPickerPopup::finishCustomising(bool save) +{ + if (save) + { + QStringList ids; + for (int i = 0 ; i < m_edit_row->count() ; ++i) { + ids << m_edit_row->item(i)->data(Qt::UserRole).toString(); + } + ShortcutBarSettings::setIds(m_context, ids); + } + + const QPoint where = pos(); + m_customising = false; + hide(); + m_editor->hide(); + setPickerVisible(true); + setWindowFlags(Qt::Popup); + setWindowTitle(QString()); + popUpShortcutBar(where, m_context); +} + +/** + @brief ElementPickerPopup::closeEvent + Closing the customising window from its title bar cancels the edit, + like the Annuler button, rather than leaving the bar half-edited. +*/ +void ElementPickerPopup::closeEvent(QCloseEvent *event) +{ + if (m_customising) { + event->ignore(); + finishCustomising(false); + return; + } + QFrame::closeEvent(event); +} + +/** + @brief ElementPickerPopup::setPickerVisible + Show or hide the search field, the results and the hint line. +*/ +void ElementPickerPopup::setPickerVisible(bool visible) +{ + m_search->setVisible(visible); + m_view->setVisible(visible); + m_hint->setVisible(visible); } /** @@ -259,17 +521,25 @@ void ElementPickerPopup::keyPressEvent(QKeyEvent *event) switch (event->key()) { case Qt::Key_Escape: - hide(); + if (m_customising) { + finishCustomising(false); + } else { + hide(); + } return; case Qt::Key_Return: case Qt::Key_Enter: - chooseCurrent(); + if (m_customising) { + finishCustomising(true); + } else { + chooseCurrent(); + } return; case Qt::Key_Down: case Qt::Key_Up: case Qt::Key_PageDown: case Qt::Key_PageUp: - if (m_model->rowCount()) { + if (!m_customising && m_model->rowCount()) { //Forwarded rather than focus-switched, so typing carries //on going to the search field. QCoreApplication::sendEvent(m_view, event); diff --git a/sources/ElementsCollection/elementpickerpopup.h b/sources/ElementsCollection/elementpickerpopup.h index ae3200a83..b57fa32a1 100644 --- a/sources/ElementsCollection/elementpickerpopup.h +++ b/sources/ElementsCollection/elementpickerpopup.h @@ -19,6 +19,7 @@ #define ELEMENTPICKERPOPUP_H #include "elementslocation.h" +#include "../shortcutbarsettings.h" #include @@ -29,6 +30,8 @@ class QStandardItemModel; class QLabel; class QAction; class QHBoxLayout; +class QListWidget; +class QListWidgetItem; /** @brief A cursor-anchored element picker. @@ -51,8 +54,9 @@ class ElementPickerPopup : public QFrame explicit ElementPickerPopup(ElementsCollectionWidget *source, QWidget *parent = nullptr); - void popUpAt(const QPoint &global_pos, - const QList &commands = {}); + void popUpAt(const QPoint &global_pos); + void popUpShortcutBar(const QPoint &global_pos, + ShortcutBarSettings::Context context); signals: /// Emitted when the user picks an element; the popup has closed @@ -60,12 +64,20 @@ class ElementPickerPopup : public QFrame protected: void keyPressEvent(QKeyEvent *event) override; + void closeEvent(QCloseEvent *event) override; private: void runSearch(); void chooseCurrent(); void showPalette(); + void show(const QPoint &global_pos); void setCommands(const QList &commands); + QAction *commandAction(const QString &id) const; + QListWidgetItem *commandItem(const QString &id, bool icon_only) const; + void startCustomising(); + void fillCustomising(const QStringList &ids); + void finishCustomising(bool save); + void setPickerVisible(bool visible); int loadPaletteDir(const QString &dir_path, const QString &prefix, int depth); @@ -76,6 +88,14 @@ class ElementPickerPopup : public QFrame QLabel *m_hint = nullptr; QWidget *m_commands = nullptr; QHBoxLayout *m_commands_layout = nullptr; + /// Opened as the shortcut bar, as opposed to the plain picker + bool m_bar_mode = false; + ShortcutBarSettings::Context m_context = ShortcutBarSettings::Canvas; + /// Customising the bar in place + bool m_customising = false; + QWidget *m_editor = nullptr; + QListWidget *m_edit_row = nullptr; + QListWidget *m_edit_available = nullptr; bool m_palette_mode = true; }; diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 08571a337..e5f5f81cb 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -3168,14 +3168,7 @@ void QETDiagramEditor::showShortcutBar() : ShortcutBarSettings::Selection; } - QList commands; - for (const QString &id : ShortcutBarSettings::ids(context)) { - if (QAction *action = ShortcutManager::instance().action(id, this)) { - commands << action; - } - } - - elementPicker()->popUpAt(QCursor::pos(), commands); + elementPicker()->popUpShortcutBar(QCursor::pos(), context); } /** diff --git a/sources/shortcutbarsettings.cpp b/sources/shortcutbarsettings.cpp index 95072076a..fa66300a3 100644 --- a/sources/shortcutbarsettings.cpp +++ b/sources/shortcutbarsettings.cpp @@ -17,6 +17,8 @@ */ #include "shortcutbarsettings.h" +#include "shortcutmanager.h" + #include #include @@ -120,3 +122,21 @@ void ShortcutBarSettings::setIds(Context context, const QStringList &ids) settings.setValue(settingsKey(context), ids); } } + +/** + @return every command that can go on the bar: those the diagram editor + registered with ShortcutManager, except the one that opens the bar. +*/ +QStringList ShortcutBarSettings::availableIds() +{ + QStringList ids; + for (const ShortcutManager::ShortcutInfo &info : + ShortcutManager::instance().allShortcuts()) + { + if (info.id.startsWith(QLatin1String("diagrameditor.")) + && info.id != QLatin1String("diagrameditor.show_shortcut_bar")) { + ids << info.id; + } + } + return ids; +} diff --git a/sources/shortcutbarsettings.h b/sources/shortcutbarsettings.h index 753698e75..8e83b9232 100644 --- a/sources/shortcutbarsettings.h +++ b/sources/shortcutbarsettings.h @@ -45,6 +45,7 @@ class ShortcutBarSettings static QStringList ids(Context context); static QStringList defaultIds(Context context); static void setIds(Context context, const QStringList &ids); + static QStringList availableIds(); }; #endif // SHORTCUTBARSETTINGS_H diff --git a/sources/ui/configpage/shortcutbarconfigpage.cpp b/sources/ui/configpage/shortcutbarconfigpage.cpp index d805aaa2d..2dee5dccb 100644 --- a/sources/ui/configpage/shortcutbarconfigpage.cpp +++ b/sources/ui/configpage/shortcutbarconfigpage.cpp @@ -29,16 +29,6 @@ #include #include -namespace { - /// The bar can hold any diagram editor command, except the one that - /// opens it. -bool offerable(const QString &id) -{ - return id.startsWith(QLatin1String("diagrameditor.")) - && id != QLatin1String("diagrameditor.show_shortcut_bar"); -} -} - /** @brief ShortcutBarConfigPage::ShortcutBarConfigPage @param parent @@ -46,9 +36,10 @@ bool offerable(const QString &id) ShortcutBarConfigPage::ShortcutBarConfigPage(QWidget *parent) : ConfigPage(parent) { + const QStringList available = ShortcutBarSettings::availableIds(); for (const ShortcutManager::ShortcutInfo &info : ShortcutManager::instance().allShortcuts()) { - if (offerable(info.id)) { + if (available.contains(info.id)) { m_descriptions.insert(info.id, info.description); } }