From 3a61513f29d2cd97bad57a78229919d6046a7a45 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 7 Oct 2026 18:34:51 +1300 Subject: [PATCH 1/2] Let the user choose what each folio editor toolbar holds Configure QElectroTech > Toolbar contents: pick a toolbar, drag or add commands from the list on the left, remove them with Delete, reorder, add separators, and add, rename or delete toolbars of one's own. Reset to defaults gives a toolbar its original contents back. Each toolbar is stored as a list of command ids in QSettings (diagrameditor/toolbars/), the same pattern the shortcut bar uses. A toolbar the user never changed stores nothing and is built exactly as before; an id no command carries any more is skipped. The four buttons that are widgets rather than commands (handle size, text grid, background colour, conductor colour) get ids of their own and can each be on one toolbar at a time. Stored scripts can go on any toolbar. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 4 + sources/diagramtoolbarsettings.cpp | 352 +++++++++++++++ sources/diagramtoolbarsettings.h | 79 ++++ sources/qetapp.cpp | 2 + sources/qetdiagrameditor.cpp | 114 +++-- sources/qetdiagrameditor.h | 5 + .../configpage/toolbarcommandsconfigpage.cpp | 423 ++++++++++++++++++ .../ui/configpage/toolbarcommandsconfigpage.h | 78 ++++ tests/qttest/CMakeLists.txt | 20 + tests/qttest/tst_diagramtoolbarsettings.cpp | 245 ++++++++++ 10 files changed, 1283 insertions(+), 39 deletions(-) create mode 100644 sources/diagramtoolbarsettings.cpp create mode 100644 sources/diagramtoolbarsettings.h create mode 100644 sources/ui/configpage/toolbarcommandsconfigpage.cpp create mode 100644 sources/ui/configpage/toolbarcommandsconfigpage.h create mode 100644 tests/qttest/tst_diagramtoolbarsettings.cpp 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" From 0fef6b19cea3be48e4a4a353b87166fe39855817 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 7 Oct 2026 19:31:42 +1300 Subject: [PATCH 2/2] Add a Customise window: toolbars, shortcut bar, keyboard, gestures Configuration > Personnaliser... (also at the bottom of a toolbar's right-click menu) opens one window with a tab for each page that sets how the user works: Barres d'outils, Contenu des barres, Barre de raccourcis, Raccourcis and Gestes de la souris, like SolidWorks' Tools > Customize. The tabs are the same pages the configuration dialog shows, which keeps them. OK applies every tab, Cancel none. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 2 + sources/qetapp.cpp | 32 ++++++++++ sources/qetapp.h | 1 + sources/qetmainwindow.cpp | 22 +++++++ sources/qetmainwindow.h | 2 + sources/ui/customizedialog.cpp | 77 +++++++++++++++++++++++ sources/ui/customizedialog.h | 51 +++++++++++++++ tests/qttest/CMakeLists.txt | 13 ++++ tests/qttest/tst_customizedialog.cpp | 93 ++++++++++++++++++++++++++++ 9 files changed, 293 insertions(+) create mode 100644 sources/ui/customizedialog.cpp create mode 100644 sources/ui/customizedialog.h create mode 100644 tests/qttest/tst_customizedialog.cpp diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index c259b6d46..6f5cbd9b6 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -862,6 +862,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/customizedialog.cpp + ${QET_DIR}/sources/ui/customizedialog.h ${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.cpp ${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.h ${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.cpp diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index 6d7f03356..62390bab9 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -45,6 +45,8 @@ #include "ui/configpage/shortcutbarconfigpage.h" #include "ui/configpage/toolbarsconfigpage.h" #include "ui/configpage/toolbarcommandsconfigpage.h" +#include "ui/customizedialog.h" +#include #include "machine_info.h" #include "TerminalStrip/ui/terminalstripeditorwindow.h" #include "qetversion.h" @@ -2258,6 +2260,36 @@ void QETApp::configureQET() #endif } +/** + @brief QETApp::customizeQET + Open the Customise window: the toolbar, shortcut bar, keyboard and + gesture pages of the configuration dialog, as tabs of one window. + @param tab : the tab to show first +*/ +void QETApp::customizeQET(int tab) +{ + QWidget *parent_widget = qApp->activeWindow(); + + CustomizeDialog dialog; + //Same reason as the configuration dialog (#527) + dialog.setWindowModality(Qt::ApplicationModal); + dialog.addPage(new ToolbarsConfigPage()); + dialog.addPage(new ToolbarCommandsConfigPage()); + dialog.addPage(new ShortcutBarConfigPage()); + dialog.addPage(new ShortcutsConfigPage()); + dialog.addPage(new GesturesConfigPage()); + if (auto *tabs = dialog.findChild(QStringLiteral("customizeTabs"))) { + tabs->setCurrentIndex(tab); + } + QET::trackDialogGeometry(&dialog); + + if (parent_widget) { + dialog.setParent(parent_widget, dialog.windowFlags()); + } + dialog.exec(); + dialog.setParent(nullptr, dialog.windowFlags()); +} + /** @brief QETApp::exportConfiguration Save the settings of QElectroTech to a file the user chooses, to keep diff --git a/sources/qetapp.h b/sources/qetapp.h index cd58aa671..234db5fa5 100644 --- a/sources/qetapp.h +++ b/sources/qetapp.h @@ -294,6 +294,7 @@ class QETApp : public QObject void openTitleBlockTemplate(const QString &); void openTitleBlockTemplateFiles(const QStringList &); void configureQET(); + void customizeQET(int tab = 0); /// Save the settings to a file (discussion #610) void exportConfiguration(); /// Replace the settings with a saved file, then close QElectroTech diff --git a/sources/qetmainwindow.cpp b/sources/qetmainwindow.cpp index 3ee58586a..0ff5def85 100644 --- a/sources/qetmainwindow.cpp +++ b/sources/qetmainwindow.cpp @@ -93,6 +93,11 @@ void QETMainWindow::initCommonActions() } }); + customize_action_ = new QAction(QET::Icons::ConfigureToolbars, tr("&Personnaliser..."), this); + ShortcutManager::instance().registerAction(customize_action_, "mainwindow.customize", tr("Général"), QKeySequence()); + customize_action_ -> setStatusTip(tr("Barres d'outils, barre de raccourcis, clavier et gestes de la souris, dans une seule fenêtre", "status bar tip")); + connect(customize_action_, &QAction::triggered, qet_app, [qet_app]() { qet_app->customizeQET(); }); + export_config_action_ = new QAction(QET::Icons::DocumentExport, tr("Enregistrer la configuration sous..."), this); export_config_action_ -> setStatusTip(tr("Enregistre les réglages de QElectroTech dans un fichier", "status bar tip")); connect(export_config_action_, &QAction::triggered, qet_app, &QETApp::exportConfiguration); @@ -190,6 +195,7 @@ void QETMainWindow::initCommonMenus() settings_menu_ = new QMenu(tr("&Configuration", "window menu"), this); settings_menu_ -> addAction(fullscreen_action_); settings_menu_ -> addAction(configure_action_); + settings_menu_ -> addAction(customize_action_); settings_menu_ -> addSeparator(); settings_menu_ -> addAction(export_config_action_); settings_menu_ -> addAction(import_config_action_); @@ -274,6 +280,22 @@ void QETMainWindow::updateFullScreenAction() ShortcutManager::instance().registerAction(fullscreen_action_, "mainwindow.fullscreen", tr("Général"), Qt::CTRL | Qt::SHIFT | Qt::Key_F); } +/** + @brief QETMainWindow::createPopupMenu + The menu shown on a right-click on a toolbar or a dock title, and the + Configuration > Afficher submenu: Qt's list of toolbars and docks, + then Personnaliser..., as in most applications with toolbars. +*/ +QMenu *QETMainWindow::createPopupMenu() +{ + QMenu *menu = QMainWindow::createPopupMenu(); + if (menu) { + menu -> addSeparator(); + menu -> addAction(customize_action_); + } + return menu; +} + /** Check whether a sub menu dedicated to docks and toolbars can be inserted on top of the settings menu. diff --git a/sources/qetmainwindow.h b/sources/qetmainwindow.h index fe76fb7e0..38aac40e3 100644 --- a/sources/qetmainwindow.h +++ b/sources/qetmainwindow.h @@ -43,6 +43,7 @@ class QETMainWindow : public QMainWindow { protected: void activateMenuBar(); bool event(QEvent *) override; + QMenu *createPopupMenu() override; void dragEnterEvent(QDragEnterEvent *e) override; void dropEvent(QDropEvent *e) override; virtual void firstActivation(QEvent *); @@ -56,6 +57,7 @@ class QETMainWindow : public QMainWindow { // attributes protected: QAction *configure_action_; ///< Launch the QElectroTech configuration dialog + QAction *customize_action_; ///< Launch the Customise window (toolbars, keys, gestures) QAction *export_config_action_; ///< Save the settings to a file QAction *import_config_action_; ///< Replace the settings with a saved file QAction *fullscreen_action_; ///< Toggle full screen diff --git a/sources/ui/customizedialog.cpp b/sources/ui/customizedialog.cpp new file mode 100644 index 000000000..03a0bca97 --- /dev/null +++ b/sources/ui/customizedialog.cpp @@ -0,0 +1,77 @@ +/* + 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 "customizedialog.h" + +#include "configpage/configpage.h" + +#include +#include +#include +#include + +CustomizeDialog::CustomizeDialog(QWidget *parent) : + QDialog(parent) +{ + setWindowTitle(tr("Personnaliser", "window title")); + + m_tabs = new QTabWidget(this); + m_tabs->setObjectName(QStringLiteral("customizeTabs")); + m_tabs->setDocumentMode(true); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, &QDialogButtonBox::accepted, this, [this]() { + applyConf(); + accept(); + }); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + auto *layout = new QVBoxLayout(this); + layout->addWidget(m_tabs); + layout->addWidget(buttons); + + resize(1000, 700); +} + +/** + @brief CustomizeDialog::addPage + Add @a page as a tab, named after its title. The dialog takes it. +*/ +void CustomizeDialog::addPage(ConfigPage *page) +{ + if (!page || m_pages.contains(page)) { + return; + } + m_pages << page; + m_tabs->addTab(page, page->icon(), page->title()); +} + +QList CustomizeDialog::pages() const +{ + return m_pages; +} + +/** + @brief CustomizeDialog::applyConf + Apply every page, in tab order. +*/ +void CustomizeDialog::applyConf() +{ + for (ConfigPage *page : std::as_const(m_pages)) { + page->applyConf(); + } +} diff --git a/sources/ui/customizedialog.h b/sources/ui/customizedialog.h new file mode 100644 index 000000000..a764c1530 --- /dev/null +++ b/sources/ui/customizedialog.h @@ -0,0 +1,51 @@ +/* + 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 CUSTOMIZEDIALOG_H +#define CUSTOMIZEDIALOG_H + +#include +#include + +class ConfigPage; +class QTabWidget; + +/** + @brief The CustomizeDialog class + Everything about how the user works in one window, one tab per + configuration page: toolbars, what they hold, the shortcut bar, the + keyboard and the mouse gestures, like SolidWorks' Tools > Customize. + The pages are the ones of the configuration dialog, which still shows + them too. OK applies every page, Cancel none. +*/ +class CustomizeDialog : public QDialog +{ + Q_OBJECT + + public: + explicit CustomizeDialog(QWidget *parent = nullptr); + + void addPage(ConfigPage *page); + QList pages() const; + void applyConf(); + + private: + QTabWidget *m_tabs; + QList m_pages; +}; + +#endif // CUSTOMIZEDIALOG_H diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 2f2e6b048..502887209 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -341,6 +341,19 @@ set_tests_properties(tst_gesturesettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM target_include_directories(tst_gesturesettings PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_gesturesettings PRIVATE Qt::Test Qt::Widgets Qt::Svg Qt::Xml pugixml::pugixml) +# CustomizeDialog: the configuration pages as tabs of one window; OK +# applies every page once, Cancel none. +add_executable( + tst_customizedialog + tst_customizedialog.cpp + ${QET_DIR}/sources/ui/customizedialog.cpp + ${QET_DIR}/sources/ui/customizedialog.h + ${QET_DIR}/sources/ui/configpage/configpage.h) +add_test(NAME tst_customizedialog COMMAND tst_customizedialog) +set_tests_properties(tst_customizedialog PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +target_include_directories(tst_customizedialog PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_customizedialog PRIVATE Qt::Test Qt::Widgets) + # 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_customizedialog.cpp b/tests/qttest/tst_customizedialog.cpp new file mode 100644 index 000000000..7187bc282 --- /dev/null +++ b/tests/qttest/tst_customizedialog.cpp @@ -0,0 +1,93 @@ +/* + 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 "ui/customizedialog.h" +#include "ui/configpage/configpage.h" + +#include +#include +#include +#include +#include + +/// A configuration page that counts how often it was applied +class CountingPage : public ConfigPage +{ + public: + CountingPage(const QString &title) : ConfigPage(nullptr), m_title(title) {} + void applyConf() override { ++applied; } + QString title() const override { return m_title; } + QIcon icon() const override { return QIcon(); } + int applied = 0; + private: + QString m_title; +}; + +/** + CustomizeDialog: one tab per page in the order added, OK applies every + page once, Cancel applies none. +*/ +class tst_customizedialog : public QObject +{ + Q_OBJECT + +private slots: + void tabsInOrder() + { + CustomizeDialog dialog; + auto *a = new CountingPage(QStringLiteral("Barres d'outils")); + auto *b = new CountingPage(QStringLiteral("Clavier")); + dialog.addPage(a); + dialog.addPage(b); + dialog.addPage(a); // twice: ignored + dialog.addPage(nullptr); + auto *tabs = dialog.findChild(QStringLiteral("customizeTabs")); + QVERIFY(tabs); + QCOMPARE(tabs->count(), 2); + QCOMPARE(tabs->tabText(0), QStringLiteral("Barres d'outils")); + QCOMPARE(tabs->tabText(1), QStringLiteral("Clavier")); + QCOMPARE(tabs->widget(1), b); + } + + void okAppliesEveryPage() + { + CustomizeDialog dialog; + auto *a = new CountingPage(QStringLiteral("a")); + auto *b = new CountingPage(QStringLiteral("b")); + dialog.addPage(a); + dialog.addPage(b); + dialog.show(); + dialog.findChild()->button(QDialogButtonBox::Ok)->click(); + QCOMPARE(a->applied, 1); + QCOMPARE(b->applied, 1); + QCOMPARE(dialog.result(), int(QDialog::Accepted)); + } + + void cancelAppliesNothing() + { + CustomizeDialog dialog; + auto *a = new CountingPage(QStringLiteral("a")); + dialog.addPage(a); + dialog.show(); + dialog.findChild()->button(QDialogButtonBox::Cancel)->click(); + QCOMPARE(a->applied, 0); + QCOMPARE(dialog.result(), int(QDialog::Rejected)); + } +}; + +QTEST_MAIN(tst_customizedialog) +#include "tst_customizedialog.moc"