diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 334d7e715..7a6e99362 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -301,6 +301,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/shortcutmanager.h
${QET_DIR}/sources/shortcutbarsettings.cpp
${QET_DIR}/sources/shortcutbarsettings.h
+ ${QET_DIR}/sources/diagramtoolbarsettings.cpp
+ ${QET_DIR}/sources/diagramtoolbarsettings.h
${QET_DIR}/sources/toolbarsettings.cpp
${QET_DIR}/sources/toolbarsettings.h
${QET_DIR}/sources/diagramcontexttoolbar.cpp
@@ -858,6 +860,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.h
${QET_DIR}/sources/ui/configpage/shortcutbarconfigpage.cpp
${QET_DIR}/sources/ui/configpage/shortcutbarconfigpage.h
+ ${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.cpp
+ ${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.h
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.h
diff --git a/sources/diagramtoolbarsettings.cpp b/sources/diagramtoolbarsettings.cpp
new file mode 100644
index 000000000..df95b01ff
--- /dev/null
+++ b/sources/diagramtoolbarsettings.cpp
@@ -0,0 +1,352 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "diagramtoolbarsettings.h"
+
+#include "shortcutmanager.h"
+
+#include
+#include
+#include
+#include
+
+namespace {
+ const QString CONTENTS = QStringLiteral("diagrameditor/toolbars/");
+ const QString CUSTOM_NAMES = QStringLiteral("diagrameditor/custom_toolbars/names");
+ const QString CUSTOM_TITLES = QStringLiteral("diagrameditor/custom_toolbars/titles");
+ //Set on a window by markWindow()
+ const char *const MARKED = "qetDiagramToolbars";
+}
+
+/**
+ @return every toolbar, built-in first, then the user's own in the order
+ they were added
+*/
+QList DiagramToolbarSettings::toolbars()
+{
+ QList list;
+ for (const QString &name : builtInNames()) {
+ list << Toolbar{name, builtInTitle(name), false};
+ }
+ return list << customToolbars();
+}
+
+/**
+ @return the object names of the toolbars the diagram editor always has.
+ The scripts toolbar is not one: it is filled from the scripts folder.
+*/
+QStringList DiagramToolbarSettings::builtInNames()
+{
+ return {QStringLiteral("toolbar"),
+ QStringLiteral("display"),
+ QStringLiteral("diagram"),
+ QStringLiteral("adding"),
+ QStringLiteral("diagram_depth_toolbar")};
+}
+
+QString DiagramToolbarSettings::builtInTitle(const QString &name)
+{
+ if (name == QLatin1String("toolbar"))
+ return QCoreApplication::translate("QETDiagramEditor", "Outils");
+ if (name == QLatin1String("display"))
+ return QCoreApplication::translate("QETDiagramEditor", "Affichage");
+ if (name == QLatin1String("diagram"))
+ return QCoreApplication::translate("QETDiagramEditor", "Schéma");
+ if (name == QLatin1String("adding"))
+ return QCoreApplication::translate("QETDiagramEditor", "Ajouter");
+ if (name == QLatin1String("diagram_depth_toolbar"))
+ return QCoreApplication::translate("QETDiagramEditor", "Profondeur", "toolbar title");
+ return QString();
+}
+
+/**
+ @return the toolbars the user added, in the order they were added
+*/
+QList DiagramToolbarSettings::customToolbars()
+{
+ QSettings settings;
+ const QStringList names = settings.value(CUSTOM_NAMES).toStringList();
+ const QStringList titles = settings.value(CUSTOM_TITLES).toStringList();
+ QList list;
+ for (int i = 0 ; i < names.size() ; ++i) {
+ list << Toolbar{names.at(i), titles.value(i, names.at(i)), true};
+ }
+ return list;
+}
+
+/**
+ @brief DiagramToolbarSettings::setCustomToolbars
+ Store the user's own toolbars. The contents of one that is no longer
+ in @a toolbars are removed with it.
+*/
+void DiagramToolbarSettings::setCustomToolbars(const QList &toolbars)
+{
+ QStringList names, titles;
+ for (const Toolbar &toolbar : toolbars) {
+ names << toolbar.name;
+ titles << toolbar.title;
+ }
+ for (const Toolbar &old : customToolbars()) {
+ if (!names.contains(old.name)) {
+ removeIds(old.name);
+ }
+ }
+ QSettings settings;
+ if (names.isEmpty()) {
+ settings.remove(CUSTOM_NAMES);
+ settings.remove(CUSTOM_TITLES);
+ } else {
+ settings.setValue(CUSTOM_NAMES, names);
+ settings.setValue(CUSTOM_TITLES, titles);
+ }
+}
+
+/**
+ @return an object name for a new toolbar that none of @a existing has
+ and none had before (stored positions stay with the toolbar they
+ belonged to)
+*/
+QString DiagramToolbarSettings::newCustomName(const QList &existing)
+{
+ QStringList taken;
+ for (const Toolbar &toolbar : existing) taken << toolbar.name;
+ for (const Toolbar &toolbar : customToolbars()) taken << toolbar.name;
+ for (int i = 1 ; ; ++i) {
+ const QString name = QStringLiteral("custom_%1").arg(i);
+ if (!taken.contains(name)) return name;
+ }
+}
+
+/**
+ @return the ids toolbar @a name holds: the user's list if they saved
+ one, the defaults otherwise. A saved empty list stays empty.
+*/
+QStringList DiagramToolbarSettings::ids(const QString &name)
+{
+ QSettings settings;
+ const QString key = CONTENTS + name;
+ if (!settings.contains(key)) {
+ return defaultIds(name);
+ }
+ return settings.value(key).toStringList();
+}
+
+/**
+ @return what toolbar @a name holds for a new user: the toolbars as
+ they were built before they could be changed. A user's own toolbar
+ starts empty.
+*/
+QStringList DiagramToolbarSettings::defaultIds(const QString &name)
+{
+ const QString sep = separatorId();
+ if (name == QLatin1String("toolbar")) {
+ return {QStringLiteral("diagrameditor.new_file"),
+ QStringLiteral("diagrameditor.open_file"),
+ QStringLiteral("diagrameditor.save_file"),
+ QStringLiteral("diagrameditor.save_file_as"),
+ QStringLiteral("diagrameditor.close_file"),
+ QStringLiteral("diagrameditor.print"),
+ QStringLiteral("diagrameditor.export_to_pdf"),
+ sep,
+ QStringLiteral("diagrameditor.undo"),
+ QStringLiteral("diagrameditor.redo"),
+ sep,
+ QStringLiteral("diagrameditor.cut"),
+ QStringLiteral("diagrameditor.copy"),
+ QStringLiteral("diagrameditor.paste"),
+ QStringLiteral("diagrameditor.duplicate"),
+ sep,
+ QStringLiteral("diagrameditor.delete_selection"),
+ QStringLiteral("diagrameditor.rotate_selection")};
+ }
+ if (name == QLatin1String("display")) {
+ return {QStringLiteral("diagrameditor.mode_selection"),
+ QStringLiteral("diagrameditor.mode_visualise"),
+ sep,
+ QStringLiteral("widget:handler_size"),
+ sep,
+ QStringLiteral("diagrameditor.draw_grid"),
+ QStringLiteral("widget:text_grid"),
+ QStringLiteral("diagrameditor.draw_guides"),
+ QStringLiteral("widget:background_color"),
+ sep,
+ QStringLiteral("diagrameditor.zoom_content"),
+ QStringLiteral("diagrameditor.zoom_fit"),
+ QStringLiteral("diagrameditor.zoom_reset")};
+ }
+ if (name == QLatin1String("diagram")) {
+ return {QStringLiteral("diagrameditor.edit_diagram_properties"),
+ QStringLiteral("diagrameditor.conductor_reset"),
+ QStringLiteral("diagrameditor.auto_conductor"),
+ QStringLiteral("diagrameditor.auto_break_conductor"),
+ QStringLiteral("widget:conductor_color")};
+ }
+ if (name == QLatin1String("adding")) {
+ //add_pdf is skipped when built without QtPdf, as before
+ return {QStringLiteral("diagrameditor.add_text"),
+ QStringLiteral("diagrameditor.add_image"),
+ QStringLiteral("diagrameditor.add_pdf"),
+ QStringLiteral("diagrameditor.add_line"),
+ QStringLiteral("diagrameditor.add_rectangle"),
+ QStringLiteral("diagrameditor.add_ellipse"),
+ QStringLiteral("diagrameditor.add_arc"),
+ QStringLiteral("diagrameditor.add_polyline"),
+ QStringLiteral("diagrameditor.add_path"),
+ QStringLiteral("diagrameditor.add_fillet"),
+ QStringLiteral("diagrameditor.add_terminal_strip")};
+ }
+ if (name == QLatin1String("diagram_depth_toolbar")) {
+ return {QStringLiteral("depth.forward"),
+ QStringLiteral("depth.raise"),
+ QStringLiteral("depth.lower"),
+ QStringLiteral("depth.backward")};
+ }
+ return {};
+}
+
+/**
+ @brief DiagramToolbarSettings::setIds
+ Save @a ids for toolbar @a name. Saving the defaults removes the key, so
+ a later change of defaults still reaches this user.
+*/
+void DiagramToolbarSettings::setIds(const QString &name, const QStringList &ids)
+{
+ if (ids == defaultIds(name)) {
+ removeIds(name);
+ } else {
+ QSettings().setValue(CONTENTS + name, ids);
+ }
+}
+
+void DiagramToolbarSettings::removeIds(const QString &name)
+{
+ QSettings().remove(CONTENTS + name);
+}
+
+QString DiagramToolbarSettings::separatorId()
+{
+ return QStringLiteral("separator");
+}
+
+/**
+ @return the toolbar buttons that are widgets, not commands. Each can be
+ on one toolbar at a time.
+*/
+QStringList DiagramToolbarSettings::widgetIds()
+{
+ return {QStringLiteral("widget:handler_size"),
+ QStringLiteral("widget:text_grid"),
+ QStringLiteral("widget:background_color"),
+ QStringLiteral("widget:conductor_color")};
+}
+
+bool DiagramToolbarSettings::isWidget(const QString &id)
+{
+ return id.startsWith(QLatin1String("widget:"));
+}
+
+QString DiagramToolbarSettings::widgetTitle(const QString &id)
+{
+ if (id == QLatin1String("widget:handler_size"))
+ return QCoreApplication::translate("DiagramToolbarSettings", "Taille des poignées");
+ if (id == QLatin1String("widget:text_grid"))
+ return QCoreApplication::translate("DiagramToolbarSettings", "Grille des textes");
+ if (id == QLatin1String("widget:background_color"))
+ return QCoreApplication::translate("DiagramToolbarSettings", "Couleur de fond du folio");
+ if (id == QLatin1String("widget:conductor_color"))
+ return QCoreApplication::translate("DiagramToolbarSettings", "Couleur des conducteurs");
+ return id;
+}
+
+/**
+ @return every command that can go on a toolbar: the diagram editor's,
+ its stored scripts', and the depth commands it shares with the element
+ editor
+*/
+QStringList DiagramToolbarSettings::availableCommandIds()
+{
+ QStringList ids;
+ for (const ShortcutManager::ShortcutInfo &info :
+ ShortcutManager::instance().allShortcuts())
+ {
+ if (info.id.startsWith(QLatin1String("diagrameditor."))
+ || info.id.startsWith(QLatin1String("depth."))) {
+ ids << info.id;
+ }
+ }
+ return ids;
+}
+
+/**
+ @brief DiagramToolbarSettings::fill
+ Empty @a toolbar and put the commands of @a ids on it, in order.
+ @a resolve gives the window's action for an id, a widget id included;
+ an id it has no action for is skipped. A separator never starts or
+ ends the toolbar and never follows another: those are left out, as
+ Qt would otherwise draw them.
+*/
+void DiagramToolbarSettings::fill(QToolBar *toolbar, const QStringList &ids,
+ const std::function &resolve)
+{
+ //Separators belong to the toolbar that made them; commands do not
+ const QList old_actions = toolbar->actions();
+ toolbar->clear();
+ for (QAction *action : old_actions) {
+ if (action->isSeparator() && action->parent() == toolbar) delete action;
+ }
+ bool pending_separator = false;
+ for (const QString &id : ids)
+ {
+ if (id == separatorId()) {
+ pending_separator = !toolbar->actions().isEmpty();
+ continue;
+ }
+ QAction *action = resolve(id);
+ if (!action) {
+ continue;
+ }
+ if (pending_separator) {
+ toolbar->addSeparator();
+ pending_separator = false;
+ }
+ toolbar->addAction(action);
+ }
+}
+
+/**
+ @brief DiagramToolbarSettings::markWindow
+ Mark @a window as one whose toolbars applyToAll() rebuilds. It must
+ have a rebuildToolBars() slot or invokable.
+*/
+void DiagramToolbarSettings::markWindow(QObject *window)
+{
+ window->setProperty(MARKED, true);
+}
+
+/**
+ @brief DiagramToolbarSettings::applyToAll
+ Rebuild the toolbars of every open window marked by markWindow(),
+ after the settings change.
+*/
+void DiagramToolbarSettings::applyToAll()
+{
+ for (QWidget *widget : QApplication::topLevelWidgets()) {
+ if (widget->property(MARKED).toBool()) {
+ QMetaObject::invokeMethod(widget, "rebuildToolBars");
+ }
+ }
+}
diff --git a/sources/diagramtoolbarsettings.h b/sources/diagramtoolbarsettings.h
new file mode 100644
index 000000000..d240821c9
--- /dev/null
+++ b/sources/diagramtoolbarsettings.h
@@ -0,0 +1,79 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef DIAGRAMTOOLBARSETTINGS_H
+#define DIAGRAMTOOLBARSETTINGS_H
+
+#include
+#include
+#include
+
+#include
+
+class QAction;
+class QToolBar;
+
+/**
+ @brief What each of the diagram editor's toolbars holds.
+
+ A toolbar is a list of ids, in order: ShortcutManager command ids, the
+ separator id, and the ids of the few toolbar buttons that are widgets
+ rather than commands (the colour buttons, the text grid, the handle
+ size). Stored in QSettings, one key per toolbar, by the toolbar's
+ object name; a toolbar the user never changed uses the defaults below,
+ which are the toolbars as they were before they could be changed.
+
+ The user can also add toolbars of their own. They start empty.
+
+ An id no command carries any more is skipped, so a list saved by
+ another version still opens.
+*/
+class DiagramToolbarSettings
+{
+ public:
+ struct Toolbar {
+ QString name; ///< object name, used for storage and by QMainWindow::saveState()
+ QString title; ///< shown to the user
+ bool custom = false;
+ };
+
+ static QList toolbars();
+ static QStringList builtInNames();
+ static QString builtInTitle(const QString &name);
+ static QList customToolbars();
+ static void setCustomToolbars(const QList &toolbars);
+ static QString newCustomName(const QList &existing);
+
+ static QStringList ids(const QString &name);
+ static QStringList defaultIds(const QString &name);
+ static void setIds(const QString &name, const QStringList &ids);
+ static void removeIds(const QString &name);
+
+ static QString separatorId();
+ static QStringList widgetIds();
+ static bool isWidget(const QString &id);
+ static QString widgetTitle(const QString &id);
+ static QStringList availableCommandIds();
+
+ static void fill(QToolBar *toolbar, const QStringList &ids,
+ const std::function &resolve);
+
+ static void markWindow(QObject *window);
+ static void applyToAll();
+};
+
+#endif // DIAGRAMTOOLBARSETTINGS_H
diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp
index ccd4981c7..d8efc99d1 100644
--- a/sources/qetapp.cpp
+++ b/sources/qetapp.cpp
@@ -43,6 +43,7 @@
#include "ui/configpage/shortcutsconfigpage.h"
#include "ui/configpage/shortcutbarconfigpage.h"
#include "ui/configpage/toolbarsconfigpage.h"
+#include "ui/configpage/toolbarcommandsconfigpage.h"
#include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h"
@@ -2227,6 +2228,7 @@ void QETApp::configureQET()
cd.addPage(new ShortcutsConfigPage());
cd.addPage(new ShortcutBarConfigPage());
cd.addPage(new ToolbarsConfigPage());
+ cd.addPage(new ToolbarCommandsConfigPage());
#ifdef QET_SPACEMOUSE_SUPPORT
cd.addPage(new SpaceMouseConfigPage());
#endif
diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp
index 6df5fbd25..5e72f57c2 100644
--- a/sources/qetdiagrameditor.cpp
+++ b/sources/qetdiagrameditor.cpp
@@ -27,9 +27,11 @@
#endif
#include
#include
+#include
#include "ElementsCollection/elementscollectionwidget.h"
#include "ElementsCollection/elementpickerpopup.h"
#include "shortcutbarsettings.h"
+#include "diagramtoolbarsettings.h"
#include "qetgraphicsitem/conductor.h"
#include "itemgroups.h"
#include "commandsearchpopup.h"
@@ -1316,50 +1318,27 @@ void QETDiagramEditor::setUpToolBar()
diagram_tool_bar = new QToolBar(tr("Schéma"), this);
diagram_tool_bar -> setObjectName("diagram");
- main_tool_bar -> addActions(m_file_actions_group.actions());
- main_tool_bar -> addAction(m_print);
- main_tool_bar -> addAction(m_export_to_pdf);
- main_tool_bar -> addSeparator();
- main_tool_bar -> addAction(undo);
- main_tool_bar -> addAction(redo);
- main_tool_bar -> addSeparator();
- main_tool_bar -> addAction(m_cut);
- main_tool_bar -> addAction(m_copy);
- main_tool_bar -> addAction(m_paste);
- main_tool_bar -> addAction(m_duplicate);
- main_tool_bar -> addSeparator();
- main_tool_bar -> addAction(m_delete_selection);
- main_tool_bar -> addAction(m_rotate_selection);
-
- // Modes selection / visualisation et zoom
- view_tool_bar -> addAction(m_mode_selection);
- view_tool_bar -> addAction(m_mode_visualise);
- view_tool_bar -> addSeparator();
- view_tool_bar -> addWidget(new DiagramEditorHandlerSizeWidget(this));
- view_tool_bar -> addSeparator();
- view_tool_bar -> addAction(m_draw_grid);
- view_tool_bar -> addWidget(m_text_grid_button);
- view_tool_bar -> addAction(m_draw_guides);
- view_tool_bar -> addWidget(m_background_color_button);
- view_tool_bar -> addSeparator();
- view_tool_bar -> addActions(m_zoom_action_toolBar);
-
- diagram_tool_bar -> addAction (m_edit_diagram_properties);
- diagram_tool_bar -> addAction (m_conductor_reset);
- diagram_tool_bar -> addAction (m_auto_conductor);
- diagram_tool_bar -> addAction (m_auto_break_conductor);
- //Sits with the conductor actions it works alongside: it colours
- //the selected conductors and sets the colour of the next one drawn.
- m_conductor_color_button = new ConductorColorToolButton(this, this);
- diagram_tool_bar -> addWidget (m_conductor_color_button);
-
m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this);
m_add_item_tool_bar->setObjectName("adding");
- m_add_item_tool_bar->addActions(m_add_item_actions_group.actions());
m_depth_tool_bar = new QToolBar(tr("Profondeur", "toolbar title"));
m_depth_tool_bar->setObjectName("diagram_depth_toolbar");
- m_depth_tool_bar->addActions(m_depth_action_group->actions());
+
+ //The toolbar buttons that are widgets, not commands. Each is held by
+ //an action of this window, so it survives being taken off a toolbar.
+ auto add_widget = [this](const QString &id, QWidget *widget) {
+ auto *action = new QWidgetAction(this);
+ action->setDefaultWidget(widget);
+ action->setText(DiagramToolbarSettings::widgetTitle(id));
+ m_toolbar_widgets.insert(id, action);
+ };
+ add_widget(QStringLiteral("widget:handler_size"), new DiagramEditorHandlerSizeWidget(this));
+ add_widget(QStringLiteral("widget:text_grid"), m_text_grid_button);
+ add_widget(QStringLiteral("widget:background_color"), m_background_color_button);
+ //Sits with the conductor actions it works alongside: it colours
+ //the selected conductors and sets the colour of the next one drawn.
+ m_conductor_color_button = new ConductorColorToolButton(this, this);
+ add_widget(QStringLiteral("widget:conductor_color"), m_conductor_color_button);
addToolBar(Qt::TopToolBarArea, main_tool_bar);
addToolBar(Qt::TopToolBarArea, view_tool_bar);
@@ -1367,6 +1346,9 @@ void QETDiagramEditor::setUpToolBar()
addToolBar(Qt::TopToolBarArea, m_add_item_tool_bar);
addToolBar(Qt::TopToolBarArea, m_depth_tool_bar);
+ DiagramToolbarSettings::markWindow(this);
+ rebuildToolBars();
+
m_scripts_tool_bar = new QToolBar(tr("Scripts", "toolbar title"), this);
m_scripts_tool_bar->setObjectName("scripts");
addToolBar(Qt::TopToolBarArea, m_scripts_tool_bar);
@@ -1376,6 +1358,59 @@ void QETDiagramEditor::setUpToolBar()
#endif
}
+/**
+ @brief QETDiagramEditor::rebuildToolBars
+ Fill every toolbar from DiagramToolbarSettings, adding the user's own
+ toolbars that this window does not have yet and removing those that
+ were deleted. Called when the window is built, when the settings
+ change, and when the stored scripts change (a script can be on any
+ toolbar).
+*/
+void QETDiagramEditor::rebuildToolBars()
+{
+ const QList custom = DiagramToolbarSettings::customToolbars();
+ QStringList custom_names;
+ for (const DiagramToolbarSettings::Toolbar &toolbar : custom) {
+ custom_names << toolbar.name;
+ }
+ for (QToolBar *toolbar : m_custom_tool_bars) {
+ if (!custom_names.contains(toolbar->objectName())) {
+ removeToolBar(toolbar);
+ toolbar->deleteLater();
+ }
+ }
+ m_custom_tool_bars.erase(std::remove_if(m_custom_tool_bars.begin(), m_custom_tool_bars.end(),
+ [&custom_names](QToolBar *toolbar) { return !custom_names.contains(toolbar->objectName()); }),
+ m_custom_tool_bars.end());
+
+ for (const DiagramToolbarSettings::Toolbar &toolbar : custom)
+ {
+ auto existing = std::find_if(m_custom_tool_bars.cbegin(), m_custom_tool_bars.cend(),
+ [&toolbar](QToolBar *t) { return t->objectName() == toolbar.name; });
+ if (existing != m_custom_tool_bars.cend()) {
+ (*existing)->setWindowTitle(toolbar.title);
+ continue;
+ }
+ auto *new_tool_bar = new QToolBar(toolbar.title, this);
+ new_tool_bar->setObjectName(toolbar.name);
+ new_tool_bar->setMovable(!ToolbarSettings::locked());
+ addToolBar(Qt::TopToolBarArea, new_tool_bar);
+ m_custom_tool_bars << new_tool_bar;
+ }
+
+ auto resolve = [this](const QString &id) -> QAction * {
+ if (DiagramToolbarSettings::isWidget(id)) {
+ return m_toolbar_widgets.value(id);
+ }
+ return ShortcutManager::instance().action(id, this);
+ };
+ const QList built_in {main_tool_bar, view_tool_bar, diagram_tool_bar,
+ m_add_item_tool_bar, m_depth_tool_bar};
+ for (QToolBar *toolbar : built_in + m_custom_tool_bars) {
+ DiagramToolbarSettings::fill(toolbar, DiagramToolbarSettings::ids(toolbar->objectName()), resolve);
+ }
+}
+
/**
@brief QETDiagramEditor::setUpMenu
*/
@@ -4039,6 +4074,7 @@ void QETDiagramEditor::rebuildScriptActions()
if (has_scripts != m_had_scripts) m_scripts_tool_bar->setVisible(has_scripts);
m_had_scripts = has_scripts;
+ rebuildToolBars();
updateScriptActions();
}
diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h
index 500a90e91..0cc42c171 100644
--- a/sources/qetdiagrameditor.h
+++ b/sources/qetdiagrameditor.h
@@ -140,6 +140,7 @@ class QETDiagramEditor : public QETMainWindow
void rememberPlacedElement(const ElementsLocation &location);
void showElementPicker();
void showShortcutBar();
+ void rebuildToolBars();
bool repeatLastCommand();
void generateTerminalBlock();
void setWindowedMode();
@@ -328,6 +329,10 @@ class QETDiagramEditor : public QETMainWindow
*m_add_item_tool_bar = nullptr,
*m_depth_tool_bar = nullptr,
*m_scripts_tool_bar = nullptr; ///< One button per stored script
+ /// Toolbars the user added (DiagramToolbarSettings)
+ QList m_custom_tool_bars;
+ /// The toolbar buttons that are widgets, by DiagramToolbarSettings id
+ QHash m_toolbar_widgets;
#ifdef QET_HAS_SCRIPTING
QMenu *m_scripts_menu = nullptr;
QList m_script_actions; ///< One per stored script, rebuilt when the folder changes
diff --git a/sources/ui/configpage/toolbarcommandsconfigpage.cpp b/sources/ui/configpage/toolbarcommandsconfigpage.cpp
new file mode 100644
index 000000000..30bb36e3c
--- /dev/null
+++ b/sources/ui/configpage/toolbarcommandsconfigpage.cpp
@@ -0,0 +1,423 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "toolbarcommandsconfigpage.h"
+
+#include "../../qeticons.h"
+#include "../../shortcutmanager.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+ToolbarCommandsConfigPage::ToolbarCommandsConfigPage(QWidget *parent) :
+ ConfigPage(parent)
+{
+ const QStringList available = DiagramToolbarSettings::availableCommandIds();
+ for (const ShortcutManager::ShortcutInfo &info :
+ ShortcutManager::instance().allShortcuts()) {
+ if (available.contains(info.id)) {
+ m_descriptions.insert(info.id, info.description);
+ }
+ }
+ m_custom = DiagramToolbarSettings::customToolbars();
+ for (const DiagramToolbarSettings::Toolbar &toolbar : DiagramToolbarSettings::toolbars()) {
+ m_pending.insert(toolbar.name, DiagramToolbarSettings::ids(toolbar.name));
+ }
+
+ auto *explanation = new QLabel(
+ tr("Choisissez les commandes de chaque barre d'outils de l'éditeur "
+ "de schémas. Glissez une commande de la liste de gauche vers la "
+ "barre, ou double-cliquez dessus ; Suppr la retire."), this);
+ explanation->setWordWrap(true);
+
+ m_toolbar = new QComboBox(this);
+ m_toolbar->setObjectName(QStringLiteral("toolbarCombo"));
+ auto *new_toolbar = new QPushButton(tr("Nouvelle barre…"), this);
+ new_toolbar->setObjectName(QStringLiteral("newToolbarButton"));
+ m_rename = new QPushButton(tr("Renommer…"), this);
+ m_delete = new QPushButton(tr("Supprimer la barre"), this);
+ m_delete->setObjectName(QStringLiteral("deleteToolbarButton"));
+ auto *reset = new QPushButton(tr("Valeurs par défaut"), this);
+ reset->setObjectName(QStringLiteral("resetButton"));
+
+ m_filter = new QLineEdit(this);
+ m_filter->setPlaceholderText(tr("Rechercher une commande"));
+ m_filter->setClearButtonEnabled(true);
+
+ m_available = new QListWidget(this);
+ m_available->setObjectName(QStringLiteral("availableList"));
+ m_available->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_available->setSortingEnabled(true);
+ m_available->setDragDropMode(QAbstractItemView::DragOnly);
+ m_available->setDefaultDropAction(Qt::MoveAction);
+ m_chosen = new QListWidget(this);
+ m_chosen->setObjectName(QStringLiteral("chosenList"));
+ m_chosen->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ //Takes commands dragged from the list on the left, and reorders
+ m_chosen->setDragDropMode(QAbstractItemView::DragDrop);
+ m_chosen->setDefaultDropAction(Qt::MoveAction);
+
+ auto *add = new QPushButton(tr("Ajouter →"), this);
+ auto *separator = new QPushButton(tr("Séparateur"), this);
+ separator->setObjectName(QStringLiteral("separatorButton"));
+ auto *remove = new QPushButton(tr("← Retirer"), this);
+ auto *up = new QPushButton(tr("Monter"), this);
+ auto *down = new QPushButton(tr("Descendre"), this);
+
+ auto *buttons = new QVBoxLayout();
+ buttons->addStretch();
+ buttons->addWidget(add);
+ buttons->addWidget(separator);
+ buttons->addWidget(remove);
+ buttons->addSpacing(12);
+ buttons->addWidget(up);
+ buttons->addWidget(down);
+ buttons->addStretch();
+
+ auto *grid = new QGridLayout();
+ grid->addWidget(new QLabel(tr("Commandes disponibles"), this), 0, 0);
+ grid->addWidget(new QLabel(tr("Dans la barre, dans l'ordre"), this), 0, 2);
+ grid->addWidget(m_filter, 1, 0);
+ grid->addWidget(m_available, 2, 0);
+ grid->addLayout(buttons, 2, 1);
+ grid->addWidget(m_chosen, 1, 2, 2, 1);
+
+ auto *toolbar_row = new QHBoxLayout();
+ toolbar_row->addWidget(new QLabel(tr("Barre d'outils :"), this));
+ toolbar_row->addWidget(m_toolbar, 1);
+ toolbar_row->addWidget(new_toolbar);
+ toolbar_row->addWidget(m_rename);
+ toolbar_row->addWidget(m_delete);
+ toolbar_row->addWidget(reset);
+
+ auto *layout = new QVBoxLayout(this);
+ layout->addWidget(explanation);
+ layout->addLayout(toolbar_row);
+ layout->addLayout(grid);
+
+ connect(m_toolbar, qOverload(&QComboBox::currentIndexChanged), this, [this]() {
+ storeToolbar();
+ showToolbar();
+ });
+ connect(m_filter, &QLineEdit::textChanged, this, &ToolbarCommandsConfigPage::fillAvailable);
+ connect(add, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::addSelected);
+ connect(separator, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::addSeparator);
+ connect(remove, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::removeSelected);
+ connect(up, &QPushButton::clicked, this, [this]() { moveSelected(-1); });
+ connect(down, &QPushButton::clicked, this, [this]() { moveSelected(1); });
+ connect(reset, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::resetToolbar);
+ connect(new_toolbar, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::newToolbar);
+ connect(m_rename, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::renameToolbar);
+ connect(m_delete, &QPushButton::clicked, this, &ToolbarCommandsConfigPage::deleteToolbar);
+ connect(m_available, &QListWidget::itemDoubleClicked, this, &ToolbarCommandsConfigPage::addSelected);
+ connect(m_chosen, &QListWidget::itemDoubleClicked, this, &ToolbarCommandsConfigPage::removeSelected);
+ //A command dropped from the left is no longer available. Later,
+ //not now: a dropped row is inserted before its data is set.
+ connect(m_chosen->model(), &QAbstractItemModel::rowsInserted, this, [this]() {
+ if (m_chosen->property("filling").toBool()) return;
+ QTimer::singleShot(0, this, [this]() {
+ storeToolbar();
+ fillAvailable();
+ });
+ });
+
+ auto *delete_key = new QAction(m_chosen);
+ delete_key->setShortcut(QKeySequence::Delete);
+ delete_key->setShortcutContext(Qt::WidgetShortcut);
+ m_chosen->addAction(delete_key);
+ connect(delete_key, &QAction::triggered, this, &ToolbarCommandsConfigPage::removeSelected);
+
+ fillToolbarCombo(DiagramToolbarSettings::builtInNames().constFirst());
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::applyConf
+ Save the user's own toolbars and every toolbar's contents, then
+ rebuild the toolbars of the open diagram editors.
+*/
+void ToolbarCommandsConfigPage::applyConf()
+{
+ storeToolbar();
+ DiagramToolbarSettings::setCustomToolbars(m_custom);
+ for (const DiagramToolbarSettings::Toolbar &toolbar : DiagramToolbarSettings::toolbars()) {
+ DiagramToolbarSettings::setIds(toolbar.name, m_pending.value(toolbar.name));
+ }
+ DiagramToolbarSettings::applyToAll();
+}
+
+QString ToolbarCommandsConfigPage::title() const
+{
+ return tr("Contenu des barres", "configuration page title: what each toolbar holds");
+}
+
+QIcon ToolbarCommandsConfigPage::icon() const
+{
+ return QET::Icons::ConfigureToolbars;
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::fillToolbarCombo
+ List the built-in toolbars and the user's own, and show @a current.
+*/
+void ToolbarCommandsConfigPage::fillToolbarCombo(const QString ¤t)
+{
+ const QSignalBlocker blocker(m_toolbar);
+ m_toolbar->clear();
+ for (const QString &name : DiagramToolbarSettings::builtInNames()) {
+ m_toolbar->addItem(DiagramToolbarSettings::builtInTitle(name), name);
+ }
+ for (const DiagramToolbarSettings::Toolbar &toolbar : std::as_const(m_custom)) {
+ m_toolbar->addItem(toolbar.title, toolbar.name);
+ }
+ const int index = m_toolbar->findData(current);
+ m_toolbar->setCurrentIndex(index < 0 ? 0 : index);
+ showToolbar();
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::showToolbar
+ Fill both lists for the toolbar chosen in the combo box.
+*/
+void ToolbarCommandsConfigPage::showToolbar()
+{
+ m_shown = m_toolbar->currentData().toString();
+ const bool custom = !DiagramToolbarSettings::builtInNames().contains(m_shown);
+ m_rename->setEnabled(custom);
+ m_delete->setEnabled(custom);
+
+ m_chosen->setProperty("filling", true);
+ m_chosen->clear();
+ for (const QString &id : m_pending.value(m_shown)) {
+ m_chosen->addItem(makeItem(id));
+ }
+ m_chosen->setProperty("filling", false);
+ fillAvailable();
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::storeToolbar
+ Keep the shown toolbar's list, in the order on screen.
+*/
+void ToolbarCommandsConfigPage::storeToolbar()
+{
+ if (m_shown.isEmpty()) {
+ return;
+ }
+ QStringList ids;
+ for (int i = 0 ; i < m_chosen->count() ; ++i) {
+ ids << m_chosen->item(i)->data(Qt::UserRole).toString();
+ }
+ m_pending.insert(m_shown, ids);
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::fillAvailable
+ List what can still go on the shown toolbar and matches the search: a
+ command not already on it, and a widget button on no toolbar at all
+ (a widget can only be in one place).
+*/
+void ToolbarCommandsConfigPage::fillAvailable()
+{
+ QStringList on_shown;
+ for (int i = 0 ; i < m_chosen->count() ; ++i) {
+ on_shown << m_chosen->item(i)->data(Qt::UserRole).toString();
+ }
+ QStringList on_any = on_shown;
+ for (auto it = m_pending.cbegin() ; it != m_pending.cend() ; ++it) {
+ if (it.key() != m_shown) on_any << it.value();
+ }
+
+ QStringList ids;
+ for (auto it = m_descriptions.cbegin() ; it != m_descriptions.cend() ; ++it) {
+ if (!on_shown.contains(it.key())) ids << it.key();
+ }
+ for (const QString &id : DiagramToolbarSettings::widgetIds()) {
+ if (!on_any.contains(id)) ids << id;
+ }
+
+ m_available->clear();
+ const QString filter = m_filter->text().trimmed();
+ for (const QString &id : std::as_const(ids)) {
+ QListWidgetItem *item = makeItem(id);
+ if (filter.isEmpty() || item->text().contains(filter, Qt::CaseInsensitive)) {
+ m_available->addItem(item);
+ } else {
+ delete item;
+ }
+ }
+}
+
+void ToolbarCommandsConfigPage::addSelected()
+{
+ const QList selected = m_available->selectedItems();
+ if (selected.isEmpty()) {
+ return;
+ }
+ m_chosen->setProperty("filling", true);
+ for (QListWidgetItem *item : selected) {
+ m_chosen->addItem(m_available->takeItem(m_available->row(item)));
+ }
+ m_chosen->setProperty("filling", false);
+ storeToolbar();
+ fillAvailable();
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::addSeparator
+ Insert a separator after the selected row, or at the end.
+*/
+void ToolbarCommandsConfigPage::addSeparator()
+{
+ const int row = m_chosen->currentRow();
+ m_chosen->setProperty("filling", true);
+ m_chosen->insertItem(row < 0 ? m_chosen->count() : row + 1,
+ makeItem(DiagramToolbarSettings::separatorId()));
+ m_chosen->setProperty("filling", false);
+ storeToolbar();
+}
+
+void ToolbarCommandsConfigPage::removeSelected()
+{
+ for (QListWidgetItem *item : m_chosen->selectedItems()) {
+ delete m_chosen->takeItem(m_chosen->row(item));
+ }
+ storeToolbar();
+ fillAvailable();
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::moveSelected
+ Move the selected command @a step rows, keeping it selected.
+*/
+void ToolbarCommandsConfigPage::moveSelected(int step)
+{
+ const int row = m_chosen->currentRow();
+ const int target = row + step;
+ if (row < 0 || target < 0 || target >= m_chosen->count()) {
+ return;
+ }
+ m_chosen->setProperty("filling", true);
+ QListWidgetItem *item = m_chosen->takeItem(row);
+ m_chosen->insertItem(target, item);
+ m_chosen->setProperty("filling", false);
+ m_chosen->setCurrentRow(target);
+ storeToolbar();
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::resetToolbar
+ Give the shown toolbar its defaults back; the user's own toolbar is
+ emptied.
+*/
+void ToolbarCommandsConfigPage::resetToolbar()
+{
+ m_pending.insert(m_shown, DiagramToolbarSettings::defaultIds(m_shown));
+ //A widget the defaults put here leaves the toolbar it was moved to
+ for (const QString &id : DiagramToolbarSettings::defaultIds(m_shown)) {
+ if (!DiagramToolbarSettings::isWidget(id)) continue;
+ for (auto it = m_pending.begin() ; it != m_pending.end() ; ++it) {
+ if (it.key() != m_shown) it.value().removeAll(id);
+ }
+ }
+ showToolbar();
+}
+
+void ToolbarCommandsConfigPage::newToolbar()
+{
+ bool ok = false;
+ const QString title = QInputDialog::getText(this, tr("Nouvelle barre d'outils"),
+ tr("Nom de la barre d'outils :"),
+ QLineEdit::Normal, QString(), &ok).trimmed();
+ if (!ok || title.isEmpty()) {
+ return;
+ }
+ storeToolbar();
+ DiagramToolbarSettings::Toolbar toolbar;
+ toolbar.name = DiagramToolbarSettings::newCustomName(m_custom);
+ toolbar.title = title;
+ toolbar.custom = true;
+ m_custom << toolbar;
+ m_pending.insert(toolbar.name, QStringList());
+ fillToolbarCombo(toolbar.name);
+}
+
+void ToolbarCommandsConfigPage::renameToolbar()
+{
+ for (DiagramToolbarSettings::Toolbar &toolbar : m_custom) {
+ if (toolbar.name != m_shown) continue;
+ bool ok = false;
+ const QString title = QInputDialog::getText(this, tr("Renommer la barre d'outils"),
+ tr("Nom de la barre d'outils :"),
+ QLineEdit::Normal, toolbar.title, &ok).trimmed();
+ if (ok && !title.isEmpty()) {
+ storeToolbar();
+ toolbar.title = title;
+ fillToolbarCombo(m_shown);
+ }
+ return;
+ }
+}
+
+void ToolbarCommandsConfigPage::deleteToolbar()
+{
+ const QString name = m_shown;
+ for (int i = 0 ; i < m_custom.size() ; ++i) {
+ if (m_custom.at(i).name == name) {
+ m_custom.removeAt(i);
+ m_pending.remove(name);
+ m_shown.clear();
+ fillToolbarCombo(DiagramToolbarSettings::builtInNames().constFirst());
+ return;
+ }
+ }
+}
+
+/**
+ @brief ToolbarCommandsConfigPage::makeItem
+ @return a list item for @a id with the command's text and icon. An id
+ no live action carries (a command from a build without it, or a script
+ since removed) is still listed, by its id, so saving does not silently
+ drop it.
+*/
+QListWidgetItem *ToolbarCommandsConfigPage::makeItem(const QString &id) const
+{
+ QString text = m_descriptions.value(id, id);
+ QString tip;
+ QIcon icon;
+ if (id == DiagramToolbarSettings::separatorId()) {
+ text = tr("─── Séparateur ───");
+ } else if (DiagramToolbarSettings::isWidget(id)) {
+ text = DiagramToolbarSettings::widgetTitle(id);
+ } else if (QAction *action = ShortcutManager::instance().action(id, nullptr)) {
+ icon = action->icon();
+ tip = action->statusTip();
+ }
+ auto *item = new QListWidgetItem(icon, text);
+ item->setData(Qt::UserRole, id);
+ //Tells apart commands with the same name ("Ajouter une ligne")
+ item->setToolTip(tip);
+ return item;
+}
diff --git a/sources/ui/configpage/toolbarcommandsconfigpage.h b/sources/ui/configpage/toolbarcommandsconfigpage.h
new file mode 100644
index 000000000..286978cc4
--- /dev/null
+++ b/sources/ui/configpage/toolbarcommandsconfigpage.h
@@ -0,0 +1,78 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef TOOLBARCOMMANDSCONFIGPAGE_H
+#define TOOLBARCOMMANDSCONFIGPAGE_H
+
+#include "configpage.h"
+#include "../../diagramtoolbarsettings.h"
+
+#include
+
+class QComboBox;
+class QLineEdit;
+class QListWidget;
+class QListWidgetItem;
+class QPushButton;
+
+/**
+ @brief The ToolbarCommandsConfigPage class
+ Choose what each of the diagram editor's toolbars holds, and add,
+ rename or delete toolbars of one's own. Commands are dragged or added
+ from the list on the left. Changes are kept per toolbar while the
+ dialog is open, then saved and applied to the open windows by
+ applyConf().
+*/
+class ToolbarCommandsConfigPage : public ConfigPage
+{
+ Q_OBJECT
+
+ public:
+ explicit ToolbarCommandsConfigPage(QWidget *parent = nullptr);
+
+ void applyConf() override;
+ QString title() const override;
+ QIcon icon() const override;
+
+ private:
+ void fillToolbarCombo(const QString ¤t);
+ void showToolbar();
+ void storeToolbar();
+ void fillAvailable();
+ void addSelected();
+ void addSeparator();
+ void removeSelected();
+ void moveSelected(int step);
+ void resetToolbar();
+ void newToolbar();
+ void renameToolbar();
+ void deleteToolbar();
+ QListWidgetItem *makeItem(const QString &id) const;
+
+ QComboBox *m_toolbar;
+ QLineEdit *m_filter;
+ QListWidget *m_available;
+ QListWidget *m_chosen;
+ QPushButton *m_rename;
+ QPushButton *m_delete;
+ QString m_shown;
+ QList m_custom;
+ QHash m_pending;
+ QHash m_descriptions;
+};
+
+#endif // TOOLBARCOMMANDSCONFIGPAGE_H
diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt
index 1a9ad8487..e37a26c43 100644
--- a/tests/qttest/CMakeLists.txt
+++ b/tests/qttest/CMakeLists.txt
@@ -298,6 +298,26 @@ set_tests_properties(tst_toolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM
target_include_directories(tst_toolbarsettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_toolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
+# DiagramToolbarSettings and its configuration page: unchanged toolbars
+# store nothing and build as before, unknown ids and stray separators are
+# skipped, the user's own toolbars are kept, a widget is in one place only.
+add_executable(
+ tst_diagramtoolbarsettings
+ tst_diagramtoolbarsettings.cpp
+ ${QET_DIR}/sources/diagramtoolbarsettings.cpp
+ ${QET_DIR}/sources/diagramtoolbarsettings.h
+ ${QET_DIR}/sources/shortcutmanager.cpp
+ ${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.cpp
+ ${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.h
+ ${QET_DIR}/sources/ui/configpage/configpage.h
+ ${QET_DIR}/sources/qeticons.cpp
+ ${QET_DIR}/sources/qetpalette.cpp
+ ${QET_DIR}/sources/qetstyle.cpp)
+add_test(NAME tst_diagramtoolbarsettings COMMAND tst_diagramtoolbarsettings)
+set_tests_properties(tst_diagramtoolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
+target_include_directories(tst_diagramtoolbarsettings PRIVATE ${QET_DIR}/sources)
+target_link_libraries(tst_diagramtoolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
+
# scriptheader.h is header-only: the // ==QETScript== block that turns a
# stored script into a button; and ShortcutManager::unregisterAction(), which
# takes a deleted script's command out of the lists again.
diff --git a/tests/qttest/tst_diagramtoolbarsettings.cpp b/tests/qttest/tst_diagramtoolbarsettings.cpp
new file mode 100644
index 000000000..9c77cc8d9
--- /dev/null
+++ b/tests/qttest/tst_diagramtoolbarsettings.cpp
@@ -0,0 +1,245 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "diagramtoolbarsettings.h"
+#include "shortcutmanager.h"
+#include "ui/configpage/toolbarcommandsconfigpage.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ A top-level window standing in for the diagram editor: counts the
+ calls applyToAll() makes.
+*/
+class RebuildCounter : public QWidget
+{
+ Q_OBJECT
+ public:
+ int rebuilds = 0;
+ public slots:
+ void rebuildToolBars() { ++rebuilds; }
+};
+
+/**
+ DiagramToolbarSettings and its configuration page: an unchanged
+ toolbar stores nothing and is built as before, unknown ids and stray
+ separators are skipped, the user's own toolbars are kept and removed
+ with their contents, and the page saves what it shows.
+*/
+class tst_diagramtoolbarsettings : public QObject
+{
+ Q_OBJECT
+
+ QObject m_owner;
+ QHash m_actions;
+
+ /// The action for @a id, made on first use, as the editor's resolver
+ QAction *resolve(const QString &id)
+ {
+ if (id.startsWith(QLatin1String("missing."))) return nullptr;
+ if (!m_actions.contains(id)) {
+ auto *action = new QAction(id, &m_owner);
+ m_actions.insert(id, action);
+ }
+ return m_actions.value(id);
+ }
+
+ static QStringList shape(QToolBar *toolbar)
+ {
+ QStringList list;
+ for (QAction *action : toolbar->actions()) {
+ list << (action->isSeparator() ? QStringLiteral("|") : action->text());
+ }
+ return list;
+ }
+
+ static void clearSettings()
+ {
+ QSettings settings;
+ settings.remove(QStringLiteral("diagrameditor/toolbars"));
+ settings.remove(QStringLiteral("diagrameditor/custom_toolbars"));
+ }
+
+private slots:
+ void initTestCase()
+ {
+ QStandardPaths::setTestModeEnabled(true);
+ clearSettings();
+ //A few of the diagram editor's commands, as it registers them
+ for (const QString &id : {QStringLiteral("diagrameditor.copy"),
+ QStringLiteral("diagrameditor.paste"),
+ QStringLiteral("diagrameditor.zoom_fit"),
+ QStringLiteral("depth.raise"),
+ QStringLiteral("elementeditor.copy")}) {
+ ShortcutManager::instance().registerAction(
+ new QAction(id, &m_owner), id, QStringLiteral("test"), QKeySequence());
+ }
+ }
+
+ void init() { clearSettings(); }
+
+ void defaultsStoreNothing()
+ {
+ for (const QString &name : DiagramToolbarSettings::builtInNames()) {
+ QVERIFY(!DiagramToolbarSettings::defaultIds(name).isEmpty());
+ QCOMPARE(DiagramToolbarSettings::ids(name), DiagramToolbarSettings::defaultIds(name));
+ DiagramToolbarSettings::setIds(name, DiagramToolbarSettings::defaultIds(name));
+ }
+ QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/toolbar")));
+
+ DiagramToolbarSettings::setIds(QStringLiteral("toolbar"), {QStringLiteral("diagrameditor.copy")});
+ QCOMPARE(DiagramToolbarSettings::ids(QStringLiteral("toolbar")),
+ QStringList{QStringLiteral("diagrameditor.copy")});
+ //Saving an empty toolbar keeps it empty, not back to the defaults
+ DiagramToolbarSettings::setIds(QStringLiteral("toolbar"), {});
+ QVERIFY(DiagramToolbarSettings::ids(QStringLiteral("toolbar")).isEmpty());
+ }
+
+ /// The defaults are built exactly as the toolbars were hand-built
+ /// before: the main toolbar's three separators in place
+ void defaultsBuildAsBefore()
+ {
+ QToolBar toolbar;
+ DiagramToolbarSettings::fill(&toolbar, DiagramToolbarSettings::defaultIds(QStringLiteral("toolbar")),
+ [this](const QString &id) { return resolve(id); });
+ QCOMPARE(toolbar.actions().size(), 18);
+ QCOMPARE(shape(&toolbar).mid(6, 2),
+ (QStringList{QStringLiteral("diagrameditor.export_to_pdf"), QStringLiteral("|")}));
+ QCOMPARE(shape(&toolbar).count(QStringLiteral("|")), 3);
+ }
+
+ void fillSkipsUnknownAndStraySeparators()
+ {
+ const QString sep = DiagramToolbarSettings::separatorId();
+ QToolBar toolbar;
+ toolbar.addAction(QStringLiteral("left over"));
+ DiagramToolbarSettings::fill(&toolbar,
+ {sep, QStringLiteral("a"), sep, sep, QStringLiteral("missing.b"),
+ QStringLiteral("c"), sep, QStringLiteral("missing.d"), sep},
+ [this](const QString &id) { return resolve(id); });
+ QCOMPARE(shape(&toolbar), (QStringList{QStringLiteral("a"), QStringLiteral("|"), QStringLiteral("c")}));
+
+ DiagramToolbarSettings::fill(&toolbar, {}, [this](const QString &id) { return resolve(id); });
+ QVERIFY(toolbar.actions().isEmpty());
+ }
+
+ void customToolbarsKeptAndRemoved()
+ {
+ using T = DiagramToolbarSettings::Toolbar;
+ const QString first = DiagramToolbarSettings::newCustomName({});
+ QCOMPARE(first, QStringLiteral("custom_1"));
+ DiagramToolbarSettings::setCustomToolbars({T{first, QStringLiteral("Mine"), true},
+ T{QStringLiteral("custom_2"), QStringLiteral("Other"), true}});
+ DiagramToolbarSettings::setIds(first, {QStringLiteral("diagrameditor.copy")});
+ QCOMPARE(DiagramToolbarSettings::customToolbars().size(), 2);
+ QCOMPARE(DiagramToolbarSettings::customToolbars().first().title, QStringLiteral("Mine"));
+ QCOMPARE(DiagramToolbarSettings::toolbars().size(),
+ DiagramToolbarSettings::builtInNames().size() + 2);
+ QCOMPARE(DiagramToolbarSettings::newCustomName({}), QStringLiteral("custom_3"));
+ //A new toolbar starts empty
+ QVERIFY(DiagramToolbarSettings::ids(QStringLiteral("custom_2")).isEmpty());
+
+ DiagramToolbarSettings::setCustomToolbars({T{QStringLiteral("custom_2"), QStringLiteral("Other"), true}});
+ QCOMPARE(DiagramToolbarSettings::customToolbars().size(), 1);
+ QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/") + first));
+
+ DiagramToolbarSettings::setCustomToolbars({});
+ QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/custom_toolbars/names")));
+ }
+
+ void availableCommandsAreTheDiagramEditors()
+ {
+ const QStringList ids = DiagramToolbarSettings::availableCommandIds();
+ QVERIFY(ids.contains(QStringLiteral("diagrameditor.copy")));
+ QVERIFY(ids.contains(QStringLiteral("depth.raise")));
+ QVERIFY(!ids.contains(QStringLiteral("elementeditor.copy")));
+ }
+
+ void pageSavesAndRebuilds()
+ {
+ RebuildCounter marked, unmarked;
+ DiagramToolbarSettings::markWindow(&marked);
+
+ ToolbarCommandsConfigPage page;
+ auto *combo = page.findChild(QStringLiteral("toolbarCombo"));
+ auto *available = page.findChild(QStringLiteral("availableList"));
+ auto *chosen = page.findChild(QStringLiteral("chosenList"));
+ QVERIFY(combo && available && chosen);
+ QCOMPARE(combo->count(), DiagramToolbarSettings::builtInNames().size());
+ QCOMPARE(chosen->count(), DiagramToolbarSettings::defaultIds(QStringLiteral("toolbar")).size());
+
+ //OK without a change stores nothing
+ page.applyConf();
+ QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/toolbar")));
+ QCOMPARE(marked.rebuilds, 1);
+ QCOMPARE(unmarked.rebuilds, 0);
+
+ //Remove everything but the first command, add a separator
+ while (chosen->count() > 1) delete chosen->takeItem(1);
+ chosen->setCurrentRow(0);
+ page.findChild(QStringLiteral("separatorButton"))->click();
+ page.applyConf();
+ QCOMPARE(DiagramToolbarSettings::ids(QStringLiteral("toolbar")),
+ (QStringList{QStringLiteral("diagrameditor.new_file"), DiagramToolbarSettings::separatorId()}));
+ QCOMPARE(marked.rebuilds, 2);
+ }
+
+ /// A widget button can be in one place only: once moved to another
+ /// toolbar it is no longer offered, and resetting its own toolbar
+ /// takes it back
+ void widgetInOnePlace()
+ {
+ ToolbarCommandsConfigPage page;
+ auto *combo = page.findChild(QStringLiteral("toolbarCombo"));
+ auto *available = page.findChild(QStringLiteral("availableList"));
+ auto *chosen = page.findChild(QStringLiteral("chosenList"));
+ auto offered = [available](const QString &id) {
+ for (int i = 0 ; i < available->count() ; ++i)
+ if (available->item(i)->data(Qt::UserRole).toString() == id) return true;
+ return false;
+ };
+ const QString colour = QStringLiteral("widget:conductor_color");
+
+ //On "diagram" by default, so not offered for "toolbar"
+ QVERIFY(!offered(colour));
+ //Taken off "diagram" (switching toolbars keeps the change)
+ combo->setCurrentIndex(combo->findData(QStringLiteral("diagram")));
+ for (int i = chosen->count() - 1 ; i >= 0 ; --i) {
+ if (chosen->item(i)->data(Qt::UserRole).toString() == colour) delete chosen->takeItem(i);
+ }
+ combo->setCurrentIndex(combo->findData(QStringLiteral("toolbar")));
+ QVERIFY(offered(colour));
+
+ //Reset "diagram": the colour button goes back there
+ combo->setCurrentIndex(combo->findData(QStringLiteral("diagram")));
+ page.findChild(QStringLiteral("resetButton"))->click();
+ QCOMPARE(chosen->count(), DiagramToolbarSettings::defaultIds(QStringLiteral("diagram")).size());
+ combo->setCurrentIndex(combo->findData(QStringLiteral("toolbar")));
+ QVERIFY(!offered(colour));
+ }
+};
+
+QTEST_MAIN(tst_diagramtoolbarsettings)
+#include "tst_diagramtoolbarsettings.moc"