diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index a3966c5a0..41380463c 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -754,6 +754,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/diagrampropertieseditordockwidget.h
${QET_DIR}/sources/ui/backupdialog.cpp
${QET_DIR}/sources/ui/backupdialog.h
+ ${QET_DIR}/sources/ui/backuprestoredialog.cpp
+ ${QET_DIR}/sources/ui/backuprestoredialog.h
${QET_DIR}/sources/ui/dialogwaiting.cpp
${QET_DIR}/sources/ui/dialogwaiting.h
${QET_DIR}/sources/ui/duplicateoffsetdialog.cpp
@@ -867,6 +869,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/utils/conductorcreator.cpp
${QET_DIR}/sources/utils/conductorcreator.h
+ ${QET_DIR}/sources/utils/colordialogdoubleclick.cpp
+ ${QET_DIR}/sources/utils/colordialogdoubleclick.h
${QET_DIR}/sources/utils/macosxopenevent.cpp
${QET_DIR}/sources/utils/macosxopenevent.h
${QET_DIR}/sources/utils/qetsettings.cpp
diff --git a/lang/qet_en.ts b/lang/qet_en.ts
index e76b18c19..4368b8d93 100644
--- a/lang/qet_en.ts
+++ b/lang/qet_en.ts
@@ -1936,7 +1936,7 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
<Shift> to move
-<Shift> to move
+Hold Shift and drag to move this text on its own
diff --git a/lang/qet_fr.ts b/lang/qet_fr.ts
index 6a0a85ece..01e15a8c4 100644
--- a/lang/qet_fr.ts
+++ b/lang/qet_fr.ts
@@ -1935,7 +1935,7 @@ Remarque: Ces options n'autorisent ou bloquent l'auto numérotation, s
<Shift> to move
-<Shift> pour déplacer
+Maintenir Maj et faire glisser pour déplacer ce texte seul
diff --git a/sources/ElementsCollection/elementcollectionitem.cpp b/sources/ElementsCollection/elementcollectionitem.cpp
index 3e63c8c8c..ac63291b6 100644
--- a/sources/ElementsCollection/elementcollectionitem.cpp
+++ b/sources/ElementsCollection/elementcollectionitem.cpp
@@ -18,6 +18,9 @@
#include "elementcollectionitem.h"
+#include
+#include
+
/**
@brief ElementCollectionItem::ElementCollectionItem
Constructor
@@ -247,6 +250,73 @@ QList ElementCollectionItem::items() const
return list;
}
+/**
+ @brief onGuiThread
+ @return true when called from the thread the application lives in
+*/
+static bool onGuiThread()
+{
+ return QThread::currentThread() == QCoreApplication::instance()->thread();
+}
+
+/**
+ @brief ElementCollectionItem::setData
+ ElementsCollectionModel::loadCollections() runs setUpData() for every
+ item on worker threads (QtConcurrent::map), while the items are
+ already in the model. QStandardItem::setData() updates the model and
+ emits its dataChanged() signal, which is not safe from a worker
+ thread. So a value set from a worker thread is kept on the item, and
+ the model applies it on the GUI thread with applyDeferredData() once
+ every item is set up. setText(), setFlags(), setToolTip() and setIcon()
+ all end up here.
+ @param value
+ @param role
+*/
+void ElementCollectionItem::setData(const QVariant &value, int role)
+{
+ if (role == Qt::EditRole)
+ role = Qt::DisplayRole;
+
+ if (onGuiThread())
+ QStandardItem::setData(value, role);
+ else
+ m_deferred_data.insert(role, value);
+}
+
+/**
+ @brief ElementCollectionItem::data
+ On a worker thread, a value set by setData() but not applied yet is
+ returned, so setUpData() reads back what it has just set
+ (localName() tests text() for example).
+ @param role
+ @return
+*/
+QVariant ElementCollectionItem::data(int role) const
+{
+ if (role == Qt::EditRole)
+ role = Qt::DisplayRole;
+
+ if (!onGuiThread())
+ {
+ const auto it = m_deferred_data.constFind(role);
+ if (it != m_deferred_data.constEnd())
+ return it.value();
+ }
+ return QStandardItem::data(role);
+}
+
+/**
+ @brief ElementCollectionItem::applyDeferredData
+ Apply the values set from a worker thread, see setData().
+ Must be called on the GUI thread, after the worker is done.
+*/
+void ElementCollectionItem::applyDeferredData()
+{
+ for (auto it = m_deferred_data.constBegin() ; it != m_deferred_data.constEnd() ; ++it)
+ QStandardItem::setData(it.value(), it.key());
+ m_deferred_data.clear();
+}
+
void setUpData(ElementCollectionItem *eci) {
eci->setUpData();
}
diff --git a/sources/ElementsCollection/elementcollectionitem.h b/sources/ElementsCollection/elementcollectionitem.h
index ad12477c1..057215662 100644
--- a/sources/ElementsCollection/elementcollectionitem.h
+++ b/sources/ElementsCollection/elementcollectionitem.h
@@ -18,6 +18,7 @@
#ifndef ELEMENTCOLLECTIONITEM2_H
#define ELEMENTCOLLECTIONITEM2_H
+#include
#include
/**
@@ -56,6 +57,16 @@ class ElementCollectionItem : public QStandardItem
QList elementsChild() const;
QList directoriesChild() const;
QList items() const;
+
+ QVariant data(int role = Qt::UserRole + 1) const override;
+ void setData(const QVariant &value, int role = Qt::UserRole + 1) override;
+ void applyDeferredData();
+
+ private:
+ /// Values set by setUpData() while it runs on a worker thread,
+ /// see setData(). Only ever touched by that one worker thread,
+ /// until applyDeferredData() empties it on the GUI thread.
+ QHash m_deferred_data;
};
void setUpData(ElementCollectionItem *eci);
diff --git a/sources/ElementsCollection/elementscollectionmodel.cpp b/sources/ElementsCollection/elementscollectionmodel.cpp
index 00ef0d2eb..440f10417 100644
--- a/sources/ElementsCollection/elementscollectionmodel.cpp
+++ b/sources/ElementsCollection/elementscollectionmodel.cpp
@@ -312,8 +312,15 @@ void ElementsCollectionModel::loadCollections(bool common_collection,
this, &ElementsCollectionModel::loadingProgressValueChanged);
connect(watcher, &QFutureWatcher::progressRangeChanged,
this, &ElementsCollectionModel::loadingProgressRangeChanged);
- connect(watcher, &QFutureWatcher::finished,
- this, &ElementsCollectionModel::loadingFinished);
+ //setUpData() ran on worker threads, which only kept the values on
+ //the items (ElementCollectionItem::setData()): apply them here, on
+ //the GUI thread, before anyone is told the loading is finished.
+ connect(watcher, &QFutureWatcher::finished, this, [this]()
+ {
+ for (ElementCollectionItem *eci : std::as_const(m_items_list_to_setUp))
+ eci->applyDeferredData();
+ emit loadingFinished();
+ });
connect(watcher, &QFutureWatcher::finished, watcher, &QFutureWatcher::deleteLater);
diff --git a/sources/ElementsCollection/elementscollectionwidget.cpp b/sources/ElementsCollection/elementscollectionwidget.cpp
index 8183faaaa..0e9725ad6 100644
--- a/sources/ElementsCollection/elementscollectionwidget.cpp
+++ b/sources/ElementsCollection/elementscollectionwidget.cpp
@@ -172,6 +172,18 @@ void ElementsCollectionWidget::setCurrentLocation(
m_model->indexFromLocation(location));
}
+/**
+ @brief ElementsCollectionWidget::addTemplate
+ Show a newly saved template in the templates tab, which is otherwise
+ only read when the collections are loaded or reloaded.
+ @param location : the saved .qetmak file
+*/
+void ElementsCollectionWidget::addTemplate(const ElementsLocation &location)
+{
+ if (m_macros_model && location.exist())
+ m_macros_model->addLocation(location);
+}
+
void ElementsCollectionWidget::leaveEvent(QEvent *event)
{
if (QETDiagramEditor *qde = QETApp::diagramEditorAncestorOf(this))
diff --git a/sources/ElementsCollection/elementscollectionwidget.h b/sources/ElementsCollection/elementscollectionwidget.h
index 7c46a6e51..db6daa7dc 100644
--- a/sources/ElementsCollection/elementscollectionwidget.h
+++ b/sources/ElementsCollection/elementscollectionwidget.h
@@ -72,6 +72,7 @@ class ElementsCollectionWidget : public QWidget
void removeProject (QETProject *project);
void highlightUnusedElement();
void setCurrentLocation(const ElementsLocation &location);
+ void addTemplate(const ElementsLocation &location);
QVector rankedSearch(const QString &text,
const QModelIndex &within = QModelIndex());
diff --git a/sources/ElementsCollection/ui/renamedialog.cpp b/sources/ElementsCollection/ui/renamedialog.cpp
index fe8558b14..c7e761b22 100644
--- a/sources/ElementsCollection/ui/renamedialog.cpp
+++ b/sources/ElementsCollection/ui/renamedialog.cpp
@@ -20,12 +20,14 @@
#include
#include "ui_renamedialog.h"
+#include "../../qet.h"
RenameDialog::RenameDialog(QString path, QWidget *parent) :
QDialog(parent),
ui(new Ui::RenameDialog),
m_path(std::move(path))
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
m_name = m_path.split("/").last();
if (m_name.endsWith(".elmt")) m_name.remove(".elmt");
ui->m_label->setText(tr("L'élément « %1 » existe déjà. Que souhaitez-vous faire ?").arg(m_path));
diff --git a/sources/NameList/ui/namelistdialog.cpp b/sources/NameList/ui/namelistdialog.cpp
index bb942536b..71e606a4b 100644
--- a/sources/NameList/ui/namelistdialog.cpp
+++ b/sources/NameList/ui/namelistdialog.cpp
@@ -19,6 +19,7 @@
#include "ui_namelistdialog.h"
#include "namelistwidget.h"
+#include "../../qet.h"
#include
#include
@@ -27,6 +28,7 @@ NameListDialog::NameListDialog(QWidget *parent) :
ui(new Ui::NameListDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
m_namelist_widget = new NameListWidget(this);
ui->m_main_layout->insertWidget(1, m_namelist_widget);
diff --git a/sources/PropertiesEditor/propertieseditordialog.h b/sources/PropertiesEditor/propertieseditordialog.h
index 50cb96aef..b9e3bae86 100644
--- a/sources/PropertiesEditor/propertieseditordialog.h
+++ b/sources/PropertiesEditor/propertieseditordialog.h
@@ -18,6 +18,8 @@
#ifndef PROPERTIESEDITORDIALOG_H
#define PROPERTIESEDITORDIALOG_H
+#include "../qet.h"
+
#include
#include
#include
@@ -74,6 +76,14 @@ class PropertiesEditorDialog : public QDialog
this);
vlayout->addWidget(button_box);
+ // Keyed on the wrapped editor's own class, not this
+ // dialog's -- every instantiation of this constructor
+ // template is the same PropertiesEditorDialog class,
+ // so using its class name here would have every kind
+ // of wrapped editor (conductor properties, element
+ // properties, ...) fight over one saved size.
+ QET::trackDialogGeometry(this, editor->metaObject()->className());
+
//Setup connection between button box and the editor
connect(button_box,
&QDialogButtonBox::clicked,
diff --git a/sources/SearchAndReplace/ui/replaceadvanceddialog.cpp b/sources/SearchAndReplace/ui/replaceadvanceddialog.cpp
index 81f9f9e83..a728ab374 100644
--- a/sources/SearchAndReplace/ui/replaceadvanceddialog.cpp
+++ b/sources/SearchAndReplace/ui/replaceadvanceddialog.cpp
@@ -21,6 +21,7 @@
#include "../../qetinformation.h"
#include "ui_replaceadvanceddialog.h"
+#include "../../qet.h"
#include
/**
@@ -33,6 +34,7 @@ replaceAdvancedDialog::replaceAdvancedDialog(advancedReplaceStruct advanced, QWi
ui(new Ui::replaceAdvancedDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
connect(ui->m_button_box, &QDialogButtonBox::clicked, [this](QAbstractButton *button_) {
this->done(ui->m_button_box->buttonRole(button_));
diff --git a/sources/SearchAndReplace/ui/replaceconductordialog.cpp b/sources/SearchAndReplace/ui/replaceconductordialog.cpp
index 112452846..9b8fd7017 100644
--- a/sources/SearchAndReplace/ui/replaceconductordialog.cpp
+++ b/sources/SearchAndReplace/ui/replaceconductordialog.cpp
@@ -20,6 +20,7 @@
#include "../searchandreplaceworker.h"
#include "ui_replaceconductordialog.h"
+#include "../../qet.h"
#include
#include
@@ -37,6 +38,7 @@ ReplaceConductorDialog::ReplaceConductorDialog(
ui(new Ui::ReplaceConductorDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
initWidget();
setProperties(properties);
}
diff --git a/sources/SearchAndReplace/ui/replaceelementdialog.cpp b/sources/SearchAndReplace/ui/replaceelementdialog.cpp
index d1fa0e34b..ad5dfbf3f 100644
--- a/sources/SearchAndReplace/ui/replaceelementdialog.cpp
+++ b/sources/SearchAndReplace/ui/replaceelementdialog.cpp
@@ -24,6 +24,7 @@
#include "replaceelementdialog.h"
#include "ui_replaceelementdialog.h"
+#include "../../qet.h"
#include
ReplaceElementDialog::ReplaceElementDialog(DiagramContext context, QWidget *parent) :
@@ -31,6 +32,7 @@ ReplaceElementDialog::ReplaceElementDialog(DiagramContext context, QWidget *pare
ui(new Ui::ReplaceElementDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
buildWidget();
setContext(context);
}
diff --git a/sources/SearchAndReplace/ui/replacefoliowidget.cpp b/sources/SearchAndReplace/ui/replacefoliowidget.cpp
index ea75b87cf..be11c4352 100644
--- a/sources/SearchAndReplace/ui/replacefoliowidget.cpp
+++ b/sources/SearchAndReplace/ui/replacefoliowidget.cpp
@@ -18,6 +18,7 @@
#include "replacefoliowidget.h"
#include "../../ui/diagramcontextwidget.h"
+#include "../../qet.h"
#include "../searchandreplaceworker.h"
#include "ui_replacefoliowidget.h"
@@ -132,6 +133,8 @@ ReplaceFolioDialog::ReplaceFolioDialog(QWidget *parent) :
{
this->done(m_button_box->buttonRole(button_));
});
+
+ QET::trackDialogGeometry(this);
}
ReplaceFolioDialog::~ReplaceFolioDialog()
diff --git a/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp b/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp
index c61b67ad6..fe4c77d8b 100644
--- a/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp
+++ b/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp
@@ -23,6 +23,7 @@
#include "../GraphicsItem/terminalstripitem.h"
#include "../../diagram.h"
+#include "../../qet.h"
void AddTerminalStripItemDialog::openDialog(Diagram *diagram, QWidget *parent)
{
AddTerminalStripItemDialog d(diagram->project(), parent);
@@ -46,6 +47,7 @@ AddTerminalStripItemDialog::AddTerminalStripItemDialog(QETProject *project, QWid
ui{new Ui::AddTerminalStripItemDialog}
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
fillComboBox();
}
diff --git a/sources/TerminalStrip/ui/terminalstripcreatordialog.cpp b/sources/TerminalStrip/ui/terminalstripcreatordialog.cpp
index 52363065e..3cf5d7e4c 100644
--- a/sources/TerminalStrip/ui/terminalstripcreatordialog.cpp
+++ b/sources/TerminalStrip/ui/terminalstripcreatordialog.cpp
@@ -20,6 +20,7 @@
#include "../terminalstrip.h"
#include "../../qetproject.h"
+#include "../../qet.h"
/**
* @brief TerminalStripCreatorDialog::TerminalStripCreatorDialog
* @param project : Project to add a new terminal strip
@@ -31,6 +32,7 @@ TerminalStripCreatorDialog::TerminalStripCreatorDialog(QETProject *project, QWid
m_project(project)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
}
/**
diff --git a/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp b/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp
index b2e2aa1db..fd81bd61e 100644
--- a/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp
+++ b/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp
@@ -62,7 +62,9 @@ void TerminalStripTreeDockWidget::setProject(QETProject *project)
}
m_project = project;
if (m_project) {
- m_project_destroy_connection = connect(m_project, &QObject::destroyed, [this](){
+ //`this` as context: this dock can be deleted before the project
+ //(with the editor window that owns it), and the connection must go with it
+ m_project_destroy_connection = connect(m_project, &QObject::destroyed, this, [this](){
this->m_current_strip.clear();
this->reload();
});
diff --git a/sources/autoNum/ui/renumberelementsdialog.cpp b/sources/autoNum/ui/renumberelementsdialog.cpp
index a8c4c9dd1..7d9cfe48a 100644
--- a/sources/autoNum/ui/renumberelementsdialog.cpp
+++ b/sources/autoNum/ui/renumberelementsdialog.cpp
@@ -17,6 +17,7 @@
*/
#include "renumberelementsdialog.h"
+#include "../../qet.h"
#include
#include
@@ -60,6 +61,8 @@ RenumberElementsDialog::RenumberElementsDialog(const QStringList &scheme_titles,
connect(m_all_rb, &QRadioButton::toggled, this, &RenumberElementsDialog::updateUi);
connect(m_one_rb, &QRadioButton::toggled, this, &RenumberElementsDialog::updateUi);
+
+ QET::trackDialogGeometry(this);
}
QString RenumberElementsDialog::selectedSchemeTitle() const
diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp
index 9f5b0e16b..2927e6115 100644
--- a/sources/cli_export.cpp
+++ b/sources/cli_export.cpp
@@ -24,6 +24,7 @@
#include "dataBase/projectdatabase.h"
#include "diagram.h"
#include "diagramcontext.h"
+#include "editor/terminalnamecheck.h"
#include "dxfexport.h"
#include "exportproperties.h"
#include "pdf_links.h"
@@ -513,7 +514,20 @@ int checkOneElement(const QString &path)
return 2;
}
- const int terminals = root.elementsByTagName("terminal").count();
+ const QDomNodeList terminal_nodes = root.elementsByTagName("terminal");
+ const int terminals = terminal_nodes.count();
+ QStringList terminal_names;
+ for (int i = 0; i < terminals; ++i)
+ terminal_names << terminal_nodes.at(i).toElement().attribute("name");
+
+ // Two terminals with one name cannot be told apart in a wiring list
+ // (IEC 61666), the same rule the element editor applies on save.
+ const auto repeated = TerminalNameCheck::repeatedNames(terminal_names);
+ if (!repeated.isEmpty()) {
+ out << "FAIL " << path << " (repeated terminal names: "
+ << TerminalNameCheck::describe(repeated) << ")\n";
+ return 2;
+ }
// Negative dimensions are malformed but QET still loads them; surface as a
// warning rather than a failure so this agrees with QET's own loader.
@@ -528,6 +542,15 @@ int checkOneElement(const QString &path)
return 1;
}
+ const QString type = root.attribute("link_type");
+ const int unnamed = TerminalNameCheck::unnamedCount(terminal_names);
+ if (unnamed && !type.endsWith("_report")
+ && type != "conductor_definition" && type != "thumbnail") {
+ out << "WARN " << path << " (" << unnamed << " of " << terminals
+ << " terminals have no name)\n";
+ return 1;
+ }
+
out << "OK " << path << " (" << terminals << " terminals)\n";
return 0;
}
diff --git a/sources/configdialog.cpp b/sources/configdialog.cpp
index 10a0d0e0e..1e4f2bbe2 100644
--- a/sources/configdialog.cpp
+++ b/sources/configdialog.cpp
@@ -19,6 +19,7 @@
#include "configdialog.h"
#include "ui/configpage/configpages.h"
#include "qetapp.h"
+#include "qet.h"
#include "machine_info.h"
@@ -89,6 +90,8 @@ ConfigDialog::ConfigDialog(QWidget *parent) : QDialog(parent) {
setWindowFlags(Qt::Sheet);
}
#endif
+
+ QET::trackDialogGeometry(this);
}
/// Destructeur
diff --git a/sources/diagramevent/diagrameventaddmacro.cpp b/sources/diagramevent/diagrameventaddmacro.cpp
index 6b629cb6d..ecdf7a007 100644
--- a/sources/diagramevent/diagrameventaddmacro.cpp
+++ b/sources/diagramevent/diagrameventaddmacro.cpp
@@ -88,7 +88,12 @@ m_preview_item(nullptr)
dummy_diagram->render(&painter, QRectF(QPointF(0,0), scene_rect.size()), scene_rect);
m_preview_item = new QGraphicsPixmapItem(pixmap);
- m_preview_item->setOffset(scene_rect.topLeft());
+ // Anchor the preview on the template's own top-left
+ // corner, where addMacro() puts it, not on the origin of
+ // the folio it was saved from: a template saved from the
+ // middle of a folio was otherwise shown, and placed, that
+ // far below and to the right of the cursor.
+ m_preview_item->setOffset(scene_rect.topLeft() - m_items_top_left);
}
}
@@ -134,7 +139,7 @@ void DiagramEventAddMacro::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
m_preview_item->setPos(pos_);
if (m_status_bar) {
- m_status_bar->showMessage(QString("x %1 : y %2 (Makro-Anker)").arg(QString::number(pos_.x()), QString::number(pos_.y())));
+ m_status_bar->showMessage(QString("x %1 : y %2").arg(QString::number(pos_.x()), QString::number(pos_.y())));
}
}
event->setAccepted(true);
@@ -255,7 +260,14 @@ void DiagramEventAddMacro::addMacro(QPointF final_pos)
QDomElement cloned_node = diagram_node.cloneNode(true).toElement();
DiagramContent pasted_content;
- m_diagram->fromXml(cloned_node, final_pos + m_items_top_left, false, &pasted_content);
+ // fromXml() puts the template's top-left corner on final_pos,
+ // under the cursor. It skips that for a null position, which
+ // would leave the items where they were saved, so a click on
+ // the folio origin is nudged by less than it snaps away.
+ if (final_pos.isNull()) {
+ final_pos = QPointF(0.1, 0.1);
+ }
+ m_diagram->fromXml(cloned_node, final_pos, false, &pasted_content);
m_diagram->refreshContents();
// Prevent PasteDiagramCommand from erasing labels (BMK)
diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp
index b4c5cf126..044a24759 100644
--- a/sources/diagramview.cpp
+++ b/sources/diagramview.cpp
@@ -18,6 +18,7 @@
#include "diagramview.h"
#include "cellruler.h"
#include "lastusedstyle.h"
+#include "utils/colordialogdoubleclick.h"
#include "qetproject.h"
#include "QPropertyUndoCommand/qpropertyundocommand.h"
#include "diagramcommands.h"
@@ -45,13 +46,20 @@
#include "ElementsCollection/xmlelementcollection.h"
#include "NameList/nameslist.h"
#include "elementdialog.h"
+#include "qetapp.h"
+#include "qetgraphicsitem/element.h"
+#include "qetinformation.h"
+#include "qetversion.h"
#include
#include
#include
#include
#include
+#include
#include
+static bool hasCabinetInfo(const DiagramContext &infos);
+
/**
Constructeur
@param diagram Schema a afficher ; si diagram vaut 0, un nouveau Diagram est utilise
@@ -103,6 +111,10 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
m_create_template = new QAction(tr("Créer un template", "context menu action"), this);
connect(m_create_template, &QAction::triggered, this, &DiagramView::createTemplateFromSelection);
+ //Setup the action to generate cabinet placement thumbnails (discussion #602)
+ m_generate_cabinet_thumbnail = new QAction(tr("Générer une vignette d'armoire", "context menu action"), this);
+ connect(m_generate_cabinet_thumbnail, &QAction::triggered, this, &DiagramView::generateCabinetThumbnails);
+
//Filled each time the context menu opens, see updateFolioReportMenu()
m_folio_report_menu = new QMenu(tr("Renvoi de folio"), this);
@@ -165,6 +177,7 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
color_dialog -> setWindowFlags(Qt::Sheet);
#endif
color_dialog->setCurrentColor(initial_properties.color);
+ ColorDialogDoubleClick::install(color_dialog);
// asks the user what color he wishes to apply
if (color_dialog->exec() == QDialog::Accepted)
@@ -1765,7 +1778,8 @@ QList DiagramView::contextMenuActions() const
QList list;
if (QETDiagramEditor *qde = diagramEditor())
{
- if (m_diagram->selectedItems().isEmpty())
+ const QList selected_items = m_diagram->selectedItems();
+ if (selected_items.isEmpty())
{
//Drawing comes first. The row and column actions change
//the folio's layout and are rarely wanted, so they sit one
@@ -1786,6 +1800,15 @@ QList DiagramView::contextMenuActions() const
list << m_multi_paste;
list << m_separators.at(0);
list << m_create_template; // Add the create template action
+ //Disabled, and so left out below, unless a selected element
+ //has both a manufacturer and a manufacturer reference.
+ m_generate_cabinet_thumbnail->setEnabled(std::any_of(
+ selected_items.cbegin(), selected_items.cend(),
+ [](QGraphicsItem *qgi) {
+ const Element *element = qgraphicsitem_cast(qgi);
+ return element && hasCabinetInfo(element->elementInformations());
+ }));
+ list << m_generate_cabinet_thumbnail;
list << qde->m_conductor_reset;
list << m_separators.at(1);
list << qde->m_selection_actions_group.actions();
@@ -2065,6 +2088,10 @@ void DiagramView::createTemplateFromSelection()
file.close();
qDebug() << "Template successfully saved to:" << full_path;
+ // List it right away, not only after the collections are reloaded
+ for (QETDiagramEditor *qde : QETApp::diagramEditors())
+ qde->templateSaved(template_location);
+
QMessageBox::information(this, tr("Modèle enregistré"),
tr("Le modèle a été enregistré avec succès sous :\n%1").arg(full_path));
} else {
@@ -2073,6 +2100,166 @@ void DiagramView::createTemplateFromSelection()
}
}
+/**
+ @brief hasCabinetInfo
+ @return true if @a infos has both a manufacturer and a manufacturer
+ reference, the two values a cabinet thumbnail shows.
+*/
+static bool hasCabinetInfo(const DiagramContext &infos)
+{
+ return !infos[QETInformation::ELMT_MANUFACTURER].toString().trimmed().isEmpty()
+ && !infos[QETInformation::ELMT_MANUFACTURER_REF].toString().trimmed().isEmpty();
+}
+
+/**
+ @brief cabinetThumbnailDefinition
+ @return the definition of a thumbnail element, a 120x30 frame showing
+ the manufacturer and the manufacturer reference in a dynamic text.
+ The two values are also stored in the definition's element
+ information, so the thumbnail shows them as soon as it is placed.
+*/
+static QDomElement cabinetThumbnailDefinition(QDomDocument &doc,
+ const QString &display_name,
+ const QString &manufacturer,
+ const QString &reference)
+{
+ QDomElement definition = doc.createElement(QStringLiteral("definition"));
+ definition.setAttribute(QStringLiteral("version"), QetVersion::currentVersion().toString());
+ definition.setAttribute(QStringLiteral("type"), QStringLiteral("element"));
+ definition.setAttribute(QStringLiteral("link_type"), QStringLiteral("thumbnail"));
+ definition.setAttribute(QStringLiteral("width"), 120);
+ definition.setAttribute(QStringLiteral("height"), 30);
+ definition.setAttribute(QStringLiteral("hotspot_x"), 60);
+ definition.setAttribute(QStringLiteral("hotspot_y"), 15);
+ doc.appendChild(definition);
+
+ QDomElement uuid = doc.createElement(QStringLiteral("uuid"));
+ uuid.setAttribute(QStringLiteral("uuid"), QUuid::createUuid().toString());
+ definition.appendChild(uuid);
+
+ NamesList names;
+ names.addName(QStringLiteral("en"), display_name);
+ definition.appendChild(names.toXml(doc));
+
+ QDomElement informations = doc.createElement(QStringLiteral("elementInformations"));
+ DiagramContext infos;
+ infos.addValue(QETInformation::ELMT_MANUFACTURER, manufacturer);
+ infos.addValue(QETInformation::ELMT_MANUFACTURER_REF, reference);
+ infos.toXml(informations, QStringLiteral("elementInformation"));
+ definition.appendChild(informations);
+
+ QDomElement description = doc.createElement(QStringLiteral("description"));
+
+ QDomElement rect = doc.createElement(QStringLiteral("rect"));
+ rect.setAttribute(QStringLiteral("x"), -60);
+ rect.setAttribute(QStringLiteral("y"), -15);
+ rect.setAttribute(QStringLiteral("width"), 120);
+ rect.setAttribute(QStringLiteral("height"), 30);
+ rect.setAttribute(QStringLiteral("rx"), 0);
+ rect.setAttribute(QStringLiteral("ry"), 0);
+ rect.setAttribute(QStringLiteral("style"),
+ QStringLiteral("line-style:normal;line-weight:normal;filling:none;color:black"));
+ rect.setAttribute(QStringLiteral("antialias"), QStringLiteral("false"));
+ description.appendChild(rect);
+
+ QDomElement text = doc.createElement(QStringLiteral("dynamic_text"));
+ text.setAttribute(QStringLiteral("x"), -57);
+ text.setAttribute(QStringLiteral("y"), -14);
+ text.setAttribute(QStringLiteral("z"), 1);
+ text.setAttribute(QStringLiteral("text_width"), 114);
+ text.setAttribute(QStringLiteral("Halignment"), QStringLiteral("AlignLeft"));
+ text.setAttribute(QStringLiteral("Valignment"), QStringLiteral("AlignTop"));
+ text.setAttribute(QStringLiteral("frame"), QStringLiteral("false"));
+ text.setAttribute(QStringLiteral("rotation"), 0);
+ text.setAttribute(QStringLiteral("keep_visual_rotation"), QStringLiteral("false"));
+ text.setAttribute(QStringLiteral("text_from"), QStringLiteral("CompositeText"));
+ text.setAttribute(QStringLiteral("uuid"), QUuid::createUuid().toString());
+ text.setAttribute(QStringLiteral("font"), QETApp::dynamicTextsItemFont(7).toString());
+
+ QDomElement text_cache = doc.createElement(QStringLiteral("text"));
+ text_cache.appendChild(doc.createTextNode(manufacturer + QLatin1Char('\n') + reference));
+ text.appendChild(text_cache);
+
+ QDomElement composite_text = doc.createElement(QStringLiteral("composite_text"));
+ composite_text.appendChild(doc.createTextNode(QStringLiteral("%{manufacturer}\n%{manufacturer_reference}")));
+ text.appendChild(composite_text);
+
+ description.appendChild(text);
+ definition.appendChild(description);
+ return definition;
+}
+
+/**
+ @brief DiagramView::generateCabinetThumbnails
+ Context menu action (discussion #602). For every selected element with
+ both a manufacturer and a manufacturer reference, add a thumbnail
+ element showing them to the folder "Cabinet thumbnails" of this
+ project's embedded collection, from where it can be placed on a
+ cabinet layout folio like any other element. A device that already
+ has a thumbnail there is skipped, so placing it several times does
+ not pile up copies.
+*/
+void DiagramView::generateCabinetThumbnails()
+{
+ QETProject *project = m_diagram->project();
+ if (!project || project->isReadOnly()) {
+ return;
+ }
+ XmlElementCollection *collection = project->embeddedElementCollection();
+ if (!collection) {
+ return;
+ }
+
+ const QString dir_name = QStringLiteral("Cabinet thumbnails");
+ const QString dir_path = QStringLiteral("import/") + dir_name;
+ if (!collection->exist(dir_path))
+ {
+ NamesList dir_names;
+ dir_names.addName(QStringLiteral("en"), dir_name);
+ //Stored per language, like an element's names: not tr(), which
+ //would store the interface's language under "fr"
+ dir_names.addName(QStringLiteral("fr"), QStringLiteral("Vignettes d'armoire"));
+ if (!collection->createDir(QStringLiteral("import"), dir_name, dir_names)) {
+ return;
+ }
+ }
+
+ QStringList existing = collection->elementsNames(collection->directory(dir_path));
+ int created = 0;
+ const QList selected_items = m_diagram->selectedItems();
+ for (QGraphicsItem *qgi : selected_items)
+ {
+ const Element *element = qgraphicsitem_cast(qgi);
+ if (!element || !hasCabinetInfo(element->elementInformations())) {
+ continue;
+ }
+ const DiagramContext infos = element->elementInformations();
+ const QString manufacturer = infos[QETInformation::ELMT_MANUFACTURER].toString().trimmed();
+ const QString reference = infos[QETInformation::ELMT_MANUFACTURER_REF].toString().trimmed();
+
+ //The display name keeps the values as typed; the file name
+ //cannot hold characters such as '/', which would be read as
+ //a folder.
+ const QString display_name = manufacturer + QLatin1Char(' ') + reference;
+ const QString file_name = QET::stringToFileName(display_name) + QStringLiteral(".elmt");
+ if (existing.contains(file_name)) {
+ continue;
+ }
+
+ QDomDocument doc;
+ const QDomElement definition = cabinetThumbnailDefinition(doc, display_name, manufacturer, reference);
+ if (collection->addElementDefinition(dir_path, file_name, definition))
+ {
+ existing << file_name;
+ ++created;
+ }
+ }
+
+ if (created) {
+ project->setModified(true);
+ }
+}
+
/**
@return l'editeur de schemas parent ou 0
*/
diff --git a/sources/diagramview.h b/sources/diagramview.h
index 9ac5a5ad8..11f53ddf2 100644
--- a/sources/diagramview.h
+++ b/sources/diagramview.h
@@ -58,6 +58,7 @@ class DiagramView : public PaletteGraphicsView
QAction *m_paste_here = nullptr;
QAction *m_multi_paste = nullptr;
QAction *m_create_template = nullptr;
+ QAction *m_generate_cabinet_thumbnail = nullptr;
QMenu *m_folio_report_menu = nullptr;
QPoint m_paste_here_pos;
QPoint m_press_pos;
@@ -197,5 +198,6 @@ class DiagramView : public PaletteGraphicsView
void adjustGridToZoom();
void applyReadOnly();
void createTemplateFromSelection();
+ void generateCabinetThumbnails();
};
#endif
diff --git a/sources/editor/elementscene.cpp b/sources/editor/elementscene.cpp
index c0105ae83..1ae830123 100644
--- a/sources/editor/elementscene.cpp
+++ b/sources/editor/elementscene.cpp
@@ -18,6 +18,7 @@
#include "elementscene.h"
#include "../ElementsCollection/terminaluuids.h"
+#include "../borderproperties.h"
#include "../NameList/ui/namelistdialog.h"
#include "../NameList/ui/namelistwidget.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
@@ -72,6 +73,49 @@ ElementScene::ElementScene(QETElementEditor *editor, QObject *parent) :
m_decorator_lock = new QMutex();
connect(&m_undo_stack, &QUndoStack::indexChanged, this, &ElementScene::managePrimitivesGroups);
connect(this, &ElementScene::selectionChanged, this, &ElementScene::managePrimitivesGroups);
+
+ QSettings settings;
+ m_background_frame_visible = settings.value(
+ QStringLiteral("elementeditor/background_frame_visible"), false).toBool();
+ BorderProperties bp = BorderProperties::defaultProperties();
+ m_background_frame_size = QSizeF(
+ settings.value(QStringLiteral("elementeditor/background_frame_width"),
+ bp.columns_count * bp.columns_width).toReal(),
+ settings.value(QStringLiteral("elementeditor/background_frame_height"),
+ bp.rows_count * bp.rows_height).toReal());
+}
+
+/**
+ @brief ElementScene::setBackgroundFrameVisible
+ Toggle the visual-only background frame used to proportion this
+ element's drawing against a representative folio surface. Like the
+ hotspot indicator, this frame is never written to the saved .elmt file.
+ @param visible
+*/
+void ElementScene::setBackgroundFrameVisible(bool visible)
+{
+ if (m_background_frame_visible == visible) {
+ return;
+ }
+ m_background_frame_visible = visible;
+ QSettings().setValue(QStringLiteral("elementeditor/background_frame_visible"), visible);
+ update();
+}
+
+/**
+ @brief ElementScene::setBackgroundFrameSize
+ @param size the new size (in scene/grid units) of the background frame
+*/
+void ElementScene::setBackgroundFrameSize(const QSizeF &size)
+{
+ if (m_background_frame_size == size) {
+ return;
+ }
+ m_background_frame_size = size;
+ QSettings settings;
+ settings.setValue(QStringLiteral("elementeditor/background_frame_width"), size.width());
+ settings.setValue(QStringLiteral("elementeditor/background_frame_height"), size.height());
+ update();
}
/**
diff --git a/sources/editor/elementscene.h b/sources/editor/elementscene.h
index a53afff6f..e4f086078 100644
--- a/sources/editor/elementscene.h
+++ b/sources/editor/elementscene.h
@@ -92,6 +92,8 @@ class ElementScene : public QGraphicsScene
QPointer m_single_selected_item;
bool m_hotspot_visible = true;
+ bool m_background_frame_visible = false;
+ QSizeF m_background_frame_size;
// methods
public:
@@ -142,6 +144,11 @@ class ElementScene : public QGraphicsScene
/// not part of the drawn symbol.
void setHotspotVisible(bool visible) {m_hotspot_visible = visible;}
bool hotspotVisible() const {return m_hotspot_visible;}
+
+ bool backgroundFrameVisible() const {return m_background_frame_visible;}
+ void setBackgroundFrameVisible(bool visible);
+ QSizeF backgroundFrameSize() const {return m_background_frame_size;}
+ void setBackgroundFrameSize(const QSizeF &size);
protected:
void mouseMoveEvent (QGraphicsSceneMouseEvent *) override;
diff --git a/sources/editor/elementview.cpp b/sources/editor/elementview.cpp
index c9d254f70..6eed40457 100644
--- a/sources/editor/elementview.cpp
+++ b/sources/editor/elementview.cpp
@@ -606,6 +606,19 @@ void ElementView::drawBackground(QPainter *p, const QRectF &r) {
}
}
}
+
+ if (m_scene->backgroundFrameVisible()) {
+ const QSizeF frame_size = m_scene->backgroundFrameSize();
+ const QRectF frame_rect(-frame_size.width() / 2.0, -frame_size.height() / 2.0,
+ frame_size.width(), frame_size.height());
+ QPen frame_pen(Qt::blue);
+ frame_pen.setCosmetic(true);
+ frame_pen.setStyle(Qt::DashLine);
+ p -> setPen(frame_pen);
+ p -> setBrush(Qt::NoBrush);
+ p -> drawRect(frame_rect);
+ }
+
p -> restore();
}
diff --git a/sources/editor/graphicspart/partdynamictextfield.cpp b/sources/editor/graphicspart/partdynamictextfield.cpp
index 9c26a70d6..027a07ac4 100644
--- a/sources/editor/graphicspart/partdynamictextfield.cpp
+++ b/sources/editor/graphicspart/partdynamictextfield.cpp
@@ -19,6 +19,7 @@
#include "../../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../../qetapp.h"
+#include "../../textanchor.h"
#include "../elementscene.h"
#include "../../utils/qetutils.h"
#include
@@ -474,6 +475,41 @@ Qt::Alignment PartDynamicTextField::alignment() const
return m_alignment;
}
+/**
+ @brief PartDynamicTextField::anchorPos
+ @return the anchor point of this text, in parent coordinates:
+ the point of the text chosen by the alignment (top-left, right edge,
+ centre...). For a top-left aligned text this is pos().
+ Same as DiagramTextItem::anchorPos().
+*/
+QPointF PartDynamicTextField::anchorPos() const
+{
+ return TextAnchor::pos(this, m_alignment);
+}
+
+/**
+ @brief PartDynamicTextField::setAnchorPos
+ Move this text so that its anchor point (see anchorPos()) is at anchor.
+ @param anchor : in parent coordinates
+*/
+void PartDynamicTextField::setAnchorPos(const QPointF &anchor)
+{
+ setPos(TextAnchor::itemPosFor(this, m_alignment, anchor));
+}
+
+/**
+ @brief PartDynamicTextField::setAlignmentAtAnchor
+ Change the alignment and move the text so that its new anchor point
+ is where the previous one was.
+ @param alignment
+*/
+void PartDynamicTextField::setAlignmentAtAnchor(Qt::Alignment alignment)
+{
+ const QPointF anchor = anchorPos();
+ setAlignment(alignment);
+ setAnchorPos(anchor);
+}
+
void PartDynamicTextField::setFont(const QFont &font) {
if (font == this -> font()) {
return;
diff --git a/sources/editor/graphicspart/partdynamictextfield.h b/sources/editor/graphicspart/partdynamictextfield.h
index fa0c49fcf..afaff41b7 100644
--- a/sources/editor/graphicspart/partdynamictextfield.h
+++ b/sources/editor/graphicspart/partdynamictextfield.h
@@ -42,6 +42,8 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
Q_PROPERTY(bool frame READ frame WRITE setFrame NOTIFY frameChanged)
Q_PROPERTY(qreal textWidth READ textWidth WRITE setTextWidth NOTIFY textWidthChanged)
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
+ Q_PROPERTY(QPointF anchorPos READ anchorPos WRITE setAnchorPos)
+ Q_PROPERTY(Qt::Alignment alignmentAtAnchor READ alignment WRITE setAlignmentAtAnchor NOTIFY alignmentChanged)
Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged)
Q_PROPERTY(bool keepVisualRotation READ keepVisualRotation WRITE setKeepVisualRotation NOTIFY keepVisualRotationChanged)
Q_PROPERTY(bool rotationPointCenter READ rotationPointCenter WRITE setRotationPointCenter NOTIFY rotationPointCenterChanged)
@@ -101,6 +103,9 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
void setPlainText(const QString &text);
void setAlignment(Qt::Alignment alignment);
Qt::Alignment alignment() const;
+ QPointF anchorPos() const;
+ void setAnchorPos(const QPointF &anchor);
+ void setAlignmentAtAnchor(Qt::Alignment alignment);
void setFont(const QFont &font);
void setKeepVisualRotation(const bool &keep);
bool keepVisualRotation() const;
diff --git a/sources/editor/terminalnamecheck.h b/sources/editor/terminalnamecheck.h
new file mode 100644
index 000000000..8861d444f
--- /dev/null
+++ b/sources/editor/terminalnamecheck.h
@@ -0,0 +1,91 @@
+/*
+ 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 TERMINALNAMECHECK_H
+#define TERMINALNAMECHECK_H
+
+#include
+#include
+#include
+#include
+#include
+
+/**
+ The terminal name check run when an element is saved, and by
+ --check-elements. IEC 61666 requires every terminal to be identified
+ unambiguously within its object: two terminals of one element must not
+ share a name, and a terminal should have one.
+
+ Names are compared after trimming spaces at both ends, and are case
+ sensitive ("n" and "N" are different names).
+*/
+namespace TerminalNameCheck
+{
+ /// QSettings key: false turns the check off in the element editor.
+ inline const QString settings_key{QStringLiteral("elementeditor/check-terminal-names")};
+
+ /// The names used by more than one terminal, with how many terminals
+ /// use each, in the order the names first appear. Unnamed terminals
+ /// are not counted here.
+ inline QList> repeatedNames(const QStringList &names)
+ {
+ QHash count;
+ QStringList order;
+ for (const QString &raw : names) {
+ const QString name = raw.trimmed();
+ if (name.isEmpty()) {
+ continue;
+ }
+ if (!count.contains(name)) {
+ order << name;
+ }
+ ++count[name];
+ }
+
+ QList> repeated;
+ for (const QString &name : order) {
+ if (count.value(name) > 1) {
+ repeated << qMakePair(name, count.value(name));
+ }
+ }
+ return repeated;
+ }
+
+ /// The number of terminals without a name.
+ inline int unnamedCount(const QStringList &names)
+ {
+ int unnamed = 0;
+ for (const QString &name : names) {
+ if (name.trimmed().isEmpty()) {
+ ++unnamed;
+ }
+ }
+ return unnamed;
+ }
+
+ /// "N ×3, L ×2": the repeated names, for messages.
+ inline QString describe(const QList> &repeated)
+ {
+ QStringList parts;
+ for (const auto &entry : repeated) {
+ parts << QStringLiteral("%1 ×%2").arg(entry.first, QString::number(entry.second));
+ }
+ return parts.join(QStringLiteral(", "));
+ }
+}
+
+#endif // TERMINALNAMECHECK_H
diff --git a/sources/editor/ui/dynamictextfieldeditor.cpp b/sources/editor/ui/dynamictextfieldeditor.cpp
index 3f444a8e3..47d87a91c 100644
--- a/sources/editor/ui/dynamictextfieldeditor.cpp
+++ b/sources/editor/ui/dynamictextfieldeditor.cpp
@@ -134,8 +134,8 @@ QList DynamicTextFieldEditor::currentParts() const
void DynamicTextFieldEditor::updateForm()
{
if(m_text_field) {
- ui -> m_x_sb -> setValue(m_text_field.data() -> x());
- ui -> m_y_sb -> setValue(m_text_field.data() ->y ());
+ ui -> m_x_sb -> setValue(m_text_field.data() -> anchorPos().x());
+ ui -> m_y_sb -> setValue(m_text_field.data() -> anchorPos().y());
ui -> m_rotation_sb -> setValue(QET::correctAngle(m_text_field.data() -> rotation()));
ui -> m_frame_cb -> setChecked(m_text_field.data() -> frame());
ui -> m_user_text_le -> setText(m_text_field.data() -> text());
@@ -211,6 +211,9 @@ void DynamicTextFieldEditor::setUpConnections()
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::compositeTextChanged,this, [this](){this -> updateForm();});
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::keepVisualRotationChanged, this, [this](){this -> updateForm();});
m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::rotationPointCenterChanged, this, [this](){this -> updateForm();});
+ m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::alignmentChanged, this, [this](){this -> updateForm();});
+ m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::xChanged, this, [this](){this -> updateForm();});
+ m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::yChanged, this, [this](){this -> updateForm();});
// Refresh info combo when element data changes (e.g. type switched to PLC-Slave)
m_connection_list << connect(elementEditor()->elementScene(), &ElementScene::elementInfoChanged,
@@ -290,7 +293,8 @@ void DynamicTextFieldEditor::on_m_x_sb_editingFinished()
{
double value = ui -> m_x_sb -> value();
for (int i = 0; i < m_parts.length(); i++) {
- QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "x", m_parts[i] -> x(), value);
+ const QPointF anchor = m_parts[i] -> anchorPos();
+ QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "anchorPos", anchor, QPointF(value, anchor.y()));
undo -> setText(tr("Déplacer un champ texte"));
undo -> enableAnimation(true);
undoStack().push(undo);
@@ -301,7 +305,8 @@ void DynamicTextFieldEditor::on_m_y_sb_editingFinished()
{
double value = ui -> m_y_sb -> value();
for (int i = 0; i < m_parts.length(); i++) {
- QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "y", m_parts[i] -> y(), value);
+ const QPointF anchor = m_parts[i] -> anchorPos();
+ QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "anchorPos", anchor, QPointF(anchor.x(), value));
undo -> setText(tr("Déplacer un champ texte"));
undo -> enableAnimation(true);
undoStack().push(undo);
@@ -459,7 +464,7 @@ void DynamicTextFieldEditor::on_m_alignment_pb_clicked()
if(atd.alignment() != m_parts[i] -> alignment()) {
QPropertyUndoCommand *undo =\
new QPropertyUndoCommand(
- m_parts[i], "alignment", QVariant(m_parts[i] -> alignment()), QVariant(atd.alignment()));
+ m_parts[i], "alignmentAtAnchor", QVariant(m_parts[i] -> alignment()), QVariant(atd.alignment()));
undo -> setText(tr("Modifier l'alignement d'un champ texte"));
undoStack().push(undo);
}
diff --git a/sources/editor/ui/elementpropertieseditorwidget.cpp b/sources/editor/ui/elementpropertieseditorwidget.cpp
index 6e16ffa1b..78386abd1 100644
--- a/sources/editor/ui/elementpropertieseditorwidget.cpp
+++ b/sources/editor/ui/elementpropertieseditorwidget.cpp
@@ -22,6 +22,7 @@
#include "../../qetinformation.h"
#include "ui_elementpropertieseditorwidget.h"
+#include "../../qet.h"
#include
#include
#include
@@ -95,6 +96,7 @@ ElementPropertiesEditorWidget::ElementPropertiesEditorWidget(ElementData data, Q
m_data(data)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
setUpInterface();
upDateInterface();
}
diff --git a/sources/editor/ui/qetelementeditor.cpp b/sources/editor/ui/qetelementeditor.cpp
index 5428ec1b9..d6346d3f7 100644
--- a/sources/editor/ui/qetelementeditor.cpp
+++ b/sources/editor/ui/qetelementeditor.cpp
@@ -40,6 +40,7 @@
#include "../esevent/eseventadddynamictextfield.h"
#include "../../elementdialog.h"
#include "../graphicspart/partterminal.h"
+#include "../terminalnamecheck.h"
#include "../arceditor.h"
#include "ellipseeditor.h"
#include "lineeditor.h"
@@ -59,6 +60,10 @@
#include
#include
#include
+#include
+#include
+#include
+#include
/**
* @brief QETElementEditor::QETElementEditor
@@ -835,6 +840,53 @@ bool QETElementEditor::checkElement()
}
}
+ // Check terminal names: repeated names are an error, missing names a warning
+ if (QSettings().value(TerminalNameCheck::settings_key, true).toBool())
+ {
+ QList terminals;
+ QStringList names;
+ for (auto qgi : m_elmt_scene -> items()) {
+ if (auto terminal = qgraphicsitem_cast(qgi)) {
+ terminals << terminal;
+ names << terminal -> terminalName();
+ }
+ }
+
+ const auto repeated = TerminalNameCheck::repeatedNames(names);
+ if (!repeated.isEmpty())
+ {
+ errors << qMakePair (tr("Noms de bornes en double"),
+ tr(" Erreur :"
+ " Plusieurs bornes portent le même nom : %1."
+ " Solution :"
+ " Donner un nom unique à chaque borne, par exemple N.1 et N.2."
+ " Les bornes concernées sont sélectionnées.")
+ .arg(TerminalNameCheck::describe(repeated).toHtmlEscaped()));
+
+ m_elmt_scene -> clearSelection();
+ for (auto terminal : terminals) {
+ for (const auto &entry : repeated) {
+ if (terminal -> terminalName().trimmed() == entry.first) {
+ terminal -> setSelected(true);
+ }
+ }
+ }
+ }
+
+ const int unnamed = TerminalNameCheck::unnamedCount(names);
+ if (unnamed &&
+ !(m_elmt_scene->elementData().m_type & ElementData::AllReport) &&
+ m_elmt_scene->elementData().m_type != ElementData::ConductorDefinition &&
+ m_elmt_scene->elementData().m_type != ElementData::Thumbnail)
+ {
+ warnings << qMakePair (tr("Bornes sans nom"),
+ tr(" %n borne(s) sans nom. Sans noms de bornes uniques,"
+ " la liste de câblage (qui relie quoi à quoi) ne peut pas"
+ " désigner chaque borne, et ne peut donc pas servir à"
+ " câbler l'armoire en atelier.", "", unnamed));
+ }
+ }
+
if (!errors.count() && !warnings.count()) {
return(true);
}
@@ -1138,6 +1190,47 @@ void QETElementEditor::setupActions()
parts_toolbar -> setObjectName("parts");
parts_toolbar -> addActions(m_add_part_action_grp -> actions());
addToolBar(Qt::LeftToolBarArea, parts_toolbar);
+
+ //Background frame action: a visual-only reference rectangle, never
+ //written to the saved .elmt file, to help proportion the drawing
+ //against a representative folio surface.
+ auto *toggle_background_frame_action = new QAction(QET::Icons::DocumentPrintFrame, tr("Afficher le cadre de fond"), this);
+ toggle_background_frame_action -> setCheckable(true);
+ toggle_background_frame_action -> setChecked(m_elmt_scene -> backgroundFrameVisible());
+ connect(toggle_background_frame_action, &QAction::toggled, m_elmt_scene, &ElementScene::setBackgroundFrameVisible);
+ ShortcutManager::instance().registerAction(toggle_background_frame_action, "elementeditor.toggle_background_frame", tr("Éditeur d'élément"), QKeySequence());
+ ui->m_display_menu->addAction(toggle_background_frame_action);
+ ui->m_view_toolbar->addAction(toggle_background_frame_action);
+
+ auto *configure_background_frame_action = new QAction(tr("Taille du cadre de fond..."), this);
+ connect(configure_background_frame_action, &QAction::triggered, this, [this]() {
+ QDialog dialog(this);
+ dialog.setWindowTitle(tr("Taille du cadre de fond"));
+ auto *layout = new QFormLayout(&dialog);
+
+ auto *width_spin = new QDoubleSpinBox(&dialog);
+ width_spin -> setRange(1.0, 100000.0);
+ width_spin -> setSuffix(tr(" px"));
+ width_spin -> setValue(m_elmt_scene -> backgroundFrameSize().width());
+ layout -> addRow(tr("Largeur"), width_spin);
+
+ auto *height_spin = new QDoubleSpinBox(&dialog);
+ height_spin -> setRange(1.0, 100000.0);
+ height_spin -> setSuffix(tr(" px"));
+ height_spin -> setValue(m_elmt_scene -> backgroundFrameSize().height());
+ layout -> addRow(tr("Hauteur"), height_spin);
+
+ auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
+ connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
+ layout -> addRow(buttons);
+
+ if (dialog.exec() == QDialog::Accepted) {
+ m_elmt_scene -> setBackgroundFrameSize(QSizeF(width_spin -> value(), height_spin -> value()));
+ }
+ });
+ ShortcutManager::instance().registerAction(configure_background_frame_action, "elementeditor.configure_background_frame", tr("Éditeur d'élément"), QKeySequence());
+ ui->m_display_menu->addAction(configure_background_frame_action);
}
/**
diff --git a/sources/elementdialog.cpp b/sources/elementdialog.cpp
index 503aefc6b..64d62bd46 100644
--- a/sources/elementdialog.cpp
+++ b/sources/elementdialog.cpp
@@ -20,6 +20,7 @@
#include "ElementsCollection/elementcollectionitem.h"
#include "ElementsCollection/elementscollectionmodel.h"
#include "ElementsCollection/elementstreeview.h"
+#include "qet.h"
#include "qetapp.h"
#include "qetmessagebox.h"
#include "qfilenameedit.h"
@@ -41,6 +42,7 @@ ElementDialog::ElementDialog(uint mode, QWidget *parent) :
{
setUpWidget();
setUpConnection();
+ QET::trackDialogGeometry(this);
}
/**
diff --git a/sources/elementscategoryeditor.cpp b/sources/elementscategoryeditor.cpp
index 600b7e28e..455f4e115 100644
--- a/sources/elementscategoryeditor.cpp
+++ b/sources/elementscategoryeditor.cpp
@@ -41,6 +41,7 @@ ElementsCategoryEditor::ElementsCategoryEditor(const ElementsLocation &location,
m_location(location)
{
setUpWidget();
+ QET::trackDialogGeometry(this);
if (m_location.isElement()) {
QET::QetMessageBox::warning(this,
diff --git a/sources/exportdialog.cpp b/sources/exportdialog.cpp
index 95d0f8fb6..4242d726d 100644
--- a/sources/exportdialog.cpp
+++ b/sources/exportdialog.cpp
@@ -22,6 +22,7 @@
#include "dxfexport.h"
#include "exportpropertieswidget.h"
#include "factory/elementpicturefactory.h"
+#include "qet.h"
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "dxfpaintdevice.h"
#include "qetgraphicsitem/conductor.h"
@@ -108,6 +109,8 @@ ExportDialog::ExportDialog(
// ajustement des extensions des fichiers
slot_changeFilesExtension(true);
+
+ QET::trackDialogGeometry(this);
}
/**
diff --git a/sources/factory/ui/addtabledialog.cpp b/sources/factory/ui/addtabledialog.cpp
index b2de24aad..6ea094a01 100644
--- a/sources/factory/ui/addtabledialog.cpp
+++ b/sources/factory/ui/addtabledialog.cpp
@@ -22,6 +22,7 @@
#include "../../utils/qetutils.h"
#include "ui_addtabledialog.h"
+#include "../../qet.h"
#include
/**
@@ -35,6 +36,7 @@ AddTableDialog::AddTableDialog(QWidget *content_widget, QWidget *parent) :
ui(new Ui::AddTableDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
ui->m_header_font_pb->setText(m_header_font.family());
ui->m_table_font_pb->setText(m_table_font.family());
m_content_widget = content_widget;
diff --git a/sources/logging/ui/diagnosticsreportdialog.cpp b/sources/logging/ui/diagnosticsreportdialog.cpp
index 538b289f8..badfea564 100644
--- a/sources/logging/ui/diagnosticsreportdialog.cpp
+++ b/sources/logging/ui/diagnosticsreportdialog.cpp
@@ -17,6 +17,7 @@
*/
#include "diagnosticsreportdialog.h"
+#include "../../qet.h"
#include "../../qetmessagebox.h"
#include
@@ -63,6 +64,8 @@ DiagnosticsReportDialog::DiagnosticsReportDialog(
// holds a QString copy but we save the original UTF-8 bytes to avoid
// any round-trip surprises.
setProperty("qet_report_content", content);
+
+ QET::trackDialogGeometry(this);
}
void DiagnosticsReportDialog::saveToFile()
diff --git a/sources/materiallist/materialentrydialog.cpp b/sources/materiallist/materialentrydialog.cpp
index 2795df12f..477dd0805 100644
--- a/sources/materiallist/materialentrydialog.cpp
+++ b/sources/materiallist/materialentrydialog.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "materialentrydialog.h"
+#include "../qet.h"
#include "../qetmessagebox.h"
@@ -92,6 +93,8 @@ MaterialEntryDialog::MaterialEntryDialog(const QStringList &columns, QWidget *pa
if (!m_edits.isEmpty()) {
m_edits.first()->setFocus();
}
+
+ QET::trackDialogGeometry(this);
}
/**
diff --git a/sources/qet.cpp b/sources/qet.cpp
index 8f9b79908..4e5db31d8 100644
--- a/sources/qet.cpp
+++ b/sources/qet.cpp
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -272,6 +273,27 @@ bool QET::infoFlagIsTrue(const QString &value)
|| v == QLatin1String("on");
}
+/**
+ @brief QET::trackDialogGeometry
+ @see the declaration in qet.h for the rationale.
+*/
+void QET::trackDialogGeometry(QDialog *dialog, const QString &key)
+{
+ const QString settings_key = QStringLiteral("dialoggeometry/%1").arg(
+ key.isEmpty() ? QString::fromLatin1(dialog->metaObject()->className()) : key);
+
+ QSettings settings;
+ const QVariant geometry = settings.value(settings_key);
+ if (geometry.isValid()) {
+ dialog->restoreGeometry(geometry.toByteArray());
+ }
+
+ QObject::connect(dialog, &QDialog::finished, dialog, [dialog, settings_key]() {
+ QSettings settings;
+ settings.setValue(settings_key, dialog->saveGeometry());
+ });
+}
+
/**
@brief QET::ElementsAndConductorsSentence
Permet de composer rapidement la proposition "x elements et y conducteurs"
diff --git a/sources/qet.h b/sources/qet.h
index cb5a8a0a8..11438d2c2 100644
--- a/sources/qet.h
+++ b/sources/qet.h
@@ -24,6 +24,7 @@
#include
class QActionGroup;
+class QDialog;
/**
This file provides useful functions and enums that may be used from
anywhere else within the QElectroTech application.
@@ -204,6 +205,18 @@ namespace QET {
QActionGroup *depthActionGroup(QObject *parent = nullptr);
void saveCustomColors();
void loadCustomColors();
+
+ /**
+ Restore a dialog's last-used size/position from QSettings, and
+ save it back whenever the dialog closes (accepted, rejected, or
+ via the window's close button). @a key identifies the dialog in
+ QSettings; defaults to the dialog's class name, which is enough
+ to distinguish dialogs one-to-one -- pass an explicit @a key for
+ a dialog class reused for several different kinds of content
+ (e.g. a generic properties-editor wrapper), so each kind gets
+ its own remembered size instead of fighting over one entry.
+ */
+ void trackDialogGeometry(QDialog *dialog, const QString &key = QString());
}
Q_DECLARE_METATYPE(QET::DepthOption)
diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp
index f7c086756..615428f07 100644
--- a/sources/qetapp.cpp
+++ b/sources/qetapp.cpp
@@ -30,6 +30,7 @@
#include "qetpalette.h"
#include "qetstyle.h"
#include "utils/qetutils.h"
+#include "utils/configprofile.h"
#include "qetmessagebox.h"
#include "qetproject.h"
#include "qtextorientationspinboxwidget.h"
@@ -55,9 +56,12 @@
#include
#define QUOTE(x) STRINGIFY(x)
#define STRINGIFY(x) #x
+#include
+#include
#include
#include
#include
+#include
#include
#include
#ifdef BUILD_WITHOUT_KF
@@ -65,6 +69,9 @@
#else
# include
#endif
+#include "ui/backuprestoredialog.h"
+
+#include
#ifdef QET_ALLOW_OVERRIDE_CED_OPTION
QString QETApp::m_overrided_common_elements_dir = QString();
@@ -2242,6 +2249,109 @@ void QETApp::configureQET()
#endif
}
+/**
+ @brief QETApp::exportConfiguration
+ Save the settings of QElectroTech to a file the user chooses, to keep
+ them as a named profile or copy them to another computer (discussion
+ #610). The file is always written in the ini format, whatever the
+ platform stores its live settings in, so a profile saved on Windows
+ loads on Linux and macOS. See ConfigProfile for the keys left out.
+*/
+void QETApp::exportConfiguration()
+{
+ QWidget *parent_widget = qApp->activeWindow();
+
+ QString path = QFileDialog::getSaveFileName(
+ parent_widget,
+ tr("Enregistrer la configuration sous...", "dialog title"),
+ QString(),
+ tr("Configurations QElectroTech (*.conf)", "file dialog filter"));
+ if (path.isEmpty()) {
+ return;
+ }
+ if (!path.endsWith(QLatin1String(".conf"), Qt::CaseInsensitive)) {
+ path += QLatin1String(".conf");
+ }
+
+ QSettings live_settings;
+ QSettings file_settings(path, QSettings::IniFormat);
+ ConfigProfile::exportTo(live_settings, file_settings);
+
+ if (file_settings.status() != QSettings::NoError) {
+ QET::QetMessageBox::critical(
+ parent_widget,
+ tr("Erreur", "message box title"),
+ tr("Impossible d'enregistrer la configuration dans « %1 ».").arg(path));
+ }
+}
+
+/**
+ @brief QETApp::importConfiguration
+ Replace the settings of QElectroTech with a file saved by
+ exportConfiguration(), then close QElectroTech.
+
+ The settings are read by each part of QElectroTech when it starts, and
+ there is no signal telling all of them that a setting changed, so the
+ new settings are applied by starting QElectroTech again. That restart
+ is left to the user: a second copy started from here would find this
+ one still running, hand its arguments over to it and exit (main.cpp).
+*/
+void QETApp::importConfiguration()
+{
+ QWidget *parent_widget = qApp->activeWindow();
+
+ const QString path = QFileDialog::getOpenFileName(
+ parent_widget,
+ tr("Charger une configuration...", "dialog title"),
+ QString(),
+ tr("Configurations QElectroTech (*.conf)", "file dialog filter"));
+ if (path.isEmpty()) {
+ return;
+ }
+
+ QSettings file_settings(path, QSettings::IniFormat);
+ if (file_settings.status() != QSettings::NoError
+ || !ConfigProfile::isProfile(file_settings))
+ {
+ QET::QetMessageBox::critical(
+ parent_widget,
+ tr("Erreur", "message box title"),
+ tr("« %1 » n'est pas une configuration enregistrée par QElectroTech.").arg(path));
+ return;
+ }
+
+ //Said before anything closes: once the last window is closed,
+ //QElectroTech quits by itself (checkRemainingWindows()).
+ const auto answer = QET::QetMessageBox::question(
+ parent_widget,
+ tr("Charger une configuration", "message box title"),
+ tr("Cette configuration va remplacer vos réglages actuels, sauf la "
+ "disposition des fenêtres et la liste des fichiers récents.\n\n"
+ "QElectroTech va ensuite se fermer. Relancez-le pour utiliser "
+ "la nouvelle configuration.\n\n"
+ "Voulez-vous continuer ?"),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No);
+ if (answer != QMessageBox::Yes) {
+ return;
+ }
+
+ //Asks to save any modified project, as quitQET() does. The settings
+ //are only replaced once every editor is closed, so that the editors
+ //saving their own settings on close cannot overwrite them, and a
+ //cancelled close leaves them untouched.
+ if (!closeEveryEditor()) {
+ return;
+ }
+
+ QSettings live_settings;
+ ConfigProfile::importFrom(file_settings, live_settings);
+ //~QETApp() saves the colour dialog's custom colours on exit: load
+ //the profile's into it first, or the old ones overwrite them
+ QET::loadCustomColors();
+ qApp->quit();
+}
+
/**
@brief QETApp::aboutQET
Open the dialog about qet.
@@ -2839,48 +2949,47 @@ void QETApp::checkBackupFiles()
/**
@brief QETApp::offerBackupFiles
Ask whether to reopen the recovery files left by a previous run, and
- open or discard them accordingly.
+ open or discard them accordingly. A project can leave several recovery
+ files, one per snapshot (@see QETProject::writeBackup): they are grouped
+ by project, and the user picks which one to reopen, the newest by default.
@param stale_files : the recovery files to offer
*/
void QETApp::offerBackupFiles(const QList &stale_files)
{
- QString text;
- if(stale_files.size() == 1) {
- text.append(tr("Le fichier de restauration suivant a été trouvé, "
- "Voulez-vous l'ouvrir ? "));
- } else {
- text.append(tr("Les fichiers de restauration suivant on été trouvé, "
- "Voulez-vous les ouvrir ? "));
+ //Group the snapshots by the project they recover, newest first.
+ QHash> groups;
+ for (KAutoSaveFile *kasf : stale_files) {
+ groups[kasf->managedFile().path()].append(kasf);
}
- for(const KAutoSaveFile *kasf : stale_files)
- {
-# ifdef Q_OS_WIN
- //Remove the first character '/' before the name of the drive
- text.append(" " + kasf->managedFile().path().remove(0,1));
-# else
- text.append(" " + kasf->managedFile().path());
-# endif
+ for (auto &snapshots : groups) {
+ std::sort(snapshots.begin(), snapshots.end(),
+ [](KAutoSaveFile *a, KAutoSaveFile *b) {
+ return QFileInfo(*a).lastModified()
+ > QFileInfo(*b).lastModified();
+ });
}
- //Open backup file
- if (QET::QetMessageBox::question(nullptr,
- tr("Fichier de restauration"),
- text,
- QMessageBox::Ok
- |QMessageBox::Cancel
- )
- == QMessageBox::Ok)
+ BackupRestoreDialog dialog(groups, nullptr);
+ if (dialog.exec() == QDialog::Accepted)
{
+ //The snapshots not picked are no longer needed.
+ for (KAutoSaveFile *discarded : dialog.discardedFiles())
+ {
+ discarded->open(QIODevice::ReadWrite);
+ delete discarded;
+ }
+
+ const QList to_open = dialog.selectedFiles();
//If there are open editors, find those that are visible
if (diagramEditors().count())
{
diagramEditors().first()->setVisible(true);
- diagramEditors().first()->openBackupFiles(stale_files);
+ diagramEditors().first()->openBackupFiles(to_open);
}
else
{
QETDiagramEditor *editor = new QETDiagramEditor();
- editor->openBackupFiles(stale_files);
+ editor->openBackupFiles(to_open);
}
}
else //Clear backup file
diff --git a/sources/qetapp.h b/sources/qetapp.h
index 5c0b15106..2c2fa1d3e 100644
--- a/sources/qetapp.h
+++ b/sources/qetapp.h
@@ -291,6 +291,10 @@ class QETApp : public QObject
void openTitleBlockTemplate(const QString &);
void openTitleBlockTemplateFiles(const QStringList &);
void configureQET();
+ /// Save the settings to a file (discussion #610)
+ void exportConfiguration();
+ /// Replace the settings with a saved file, then close QElectroTech
+ void importConfiguration();
void aboutQET();
void showDiagnosticsReport();
void receiveMessage(int instanceId, QByteArray message);
diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp
index f08d00519..f826bbc52 100644
--- a/sources/qetdiagrameditor.cpp
+++ b/sources/qetdiagrameditor.cpp
@@ -1781,6 +1781,17 @@ ProjectView *QETDiagramEditor::currentProjectView() const
return(nullptr);
}
+/**
+ @brief QETDiagramEditor::templateSaved
+ List a template saved from a folio in this editor's templates tab.
+ @param location : the saved .qetmak file
+*/
+void QETDiagramEditor::templateSaved(const ElementsLocation &location)
+{
+ if (m_element_collection_widget)
+ m_element_collection_widget->addTemplate(location);
+}
+
/**
@brief QETDiagramEditor::currentProject
@return the current edited project.
diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h
index ba35ea2f3..d8c59e29d 100644
--- a/sources/qetdiagrameditor.h
+++ b/sources/qetdiagrameditor.h
@@ -75,6 +75,7 @@ class QETDiagramEditor : public QETMainWindow
ProjectView *viewForFile (const QString &) const;
ProjectView *currentProjectView() const;
QETProject *currentProject() const;
+ void templateSaved(const ElementsLocation &location);
bool drawGrid() const;
void openBackupFiles (QList backup_files);
diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp
index 4eb9009bc..a82dc5ced 100644
--- a/sources/qetgraphicsitem/conductor.cpp
+++ b/sources/qetgraphicsitem/conductor.cpp
@@ -1099,8 +1099,10 @@ bool Conductor::fromXml(QDomElement &dom_element)
m_uuid = QUuid::createUuid();
}
- setPos(dom_element.attribute("x", nullptr).toDouble(),
- dom_element.attribute("y", nullptr).toDouble());
+ //"nan" and "inf" parse as numbers; a non-finite position is kept at 0
+ const qreal x = dom_element.attribute("x", nullptr).toDouble();
+ const qreal y = dom_element.attribute("y", nullptr).toDouble();
+ setPos(qIsFinite(x) ? x : 0, qIsFinite(y) ? y : 0);
bool retval = pathFromXml(dom_element);
@@ -1229,7 +1231,10 @@ bool Conductor::pathFromXml(const QDomElement &e) {
// cette longueur doit etre un reel
bool ok;
qreal segment_length = current_segment.attribute("length").toDouble(&ok);
- if (!ok) continue;
+ // toDouble() accepts "nan" and "inf". A NaN length would also pass
+ // the coherence check below (every comparison with NaN is false)
+ // and turn the whole path into NaN.
+ if (!ok || !qIsFinite(segment_length)) continue;
if (current_segment.attribute("orientation") == "horizontal") {
segments_x << segment_length;
diff --git a/sources/qetgraphicsitem/conductortextitem.cpp b/sources/qetgraphicsitem/conductortextitem.cpp
index 624b5ea77..a3e3127f0 100644
--- a/sources/qetgraphicsitem/conductortextitem.cpp
+++ b/sources/qetgraphicsitem/conductortextitem.cpp
@@ -69,13 +69,17 @@ Conductor *ConductorTextItem::parentConductor() const
@param e
*/
void ConductorTextItem::fromXml(const QDomElement &e) {
- if (e.hasAttribute("userx")) {
- setPos(e.attribute("userx").toDouble(),
- e.attribute("usery").toDouble());
+ //A non-finite ("nan", "inf") position or rotation is ignored,
+ //as if the text had never been moved or rotated
+ const qreal x = e.attribute("userx").toDouble();
+ const qreal y = e.attribute("usery").toDouble();
+ if (e.hasAttribute("userx") && qIsFinite(x) && qIsFinite(y)) {
+ setPos(x, y);
moved_by_user_ = true;
}
- if (e.hasAttribute("rotation")) {
- setRotation(e.attribute("rotation").toDouble());
+ const qreal rotation = e.attribute("rotation").toDouble();
+ if (e.hasAttribute("rotation") && qIsFinite(rotation)) {
+ setRotation(rotation);
rotate_by_user_ = true;
}
}
diff --git a/sources/qetgraphicsitem/diagramtextitem.cpp b/sources/qetgraphicsitem/diagramtextitem.cpp
index 6e1a7aa02..c94695926 100644
--- a/sources/qetgraphicsitem/diagramtextitem.cpp
+++ b/sources/qetgraphicsitem/diagramtextitem.cpp
@@ -21,6 +21,7 @@
#include "../diagramcommands.h"
#include "../qetapp.h"
#include "../richtext/richtexteditor_p.h"
+#include "../textanchor.h"
/**
@brief DiagramTextItem::DiagramTextItem
@@ -193,6 +194,41 @@ Qt::Alignment DiagramTextItem::alignment() const
return m_alignment;
}
+/**
+ @brief DiagramTextItem::anchorPos
+ @return the anchor point of this text, in parent coordinates:
+ the point of the text chosen by the alignment (top-left, right edge,
+ centre...). For a top-left aligned text this is pos().
+ The anchor stays in place when the text changes, see finishAlignment().
+*/
+QPointF DiagramTextItem::anchorPos() const
+{
+ return TextAnchor::pos(this, m_alignment);
+}
+
+/**
+ @brief DiagramTextItem::setAnchorPos
+ Move this text so that its anchor point (see anchorPos()) is at anchor.
+ @param anchor : in parent coordinates
+*/
+void DiagramTextItem::setAnchorPos(const QPointF &anchor)
+{
+ setPos(TextAnchor::itemPosFor(this, m_alignment, anchor));
+}
+
+/**
+ @brief DiagramTextItem::setAlignmentAtAnchor
+ Change the alignment and move the text so that its new anchor point
+ is where the previous one was.
+ @param alignment
+*/
+void DiagramTextItem::setAlignmentAtAnchor(const Qt::Alignment &alignment)
+{
+ const QPointF anchor = anchorPos();
+ setAlignment(alignment);
+ setAnchorPos(anchor);
+}
+
/**
@brief DiagramTextItem::frameRect
@return the rect used to draw a frame around this text
diff --git a/sources/qetgraphicsitem/diagramtextitem.h b/sources/qetgraphicsitem/diagramtextitem.h
index 26bd7cdde..e0c807cca 100644
--- a/sources/qetgraphicsitem/diagramtextitem.h
+++ b/sources/qetgraphicsitem/diagramtextitem.h
@@ -36,6 +36,8 @@ class DiagramTextItem : public QGraphicsTextItem
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
+ Q_PROPERTY(QPointF anchorPos READ anchorPos WRITE setAnchorPos)
+ Q_PROPERTY(Qt::Alignment alignmentAtAnchor READ alignment WRITE setAlignmentAtAnchor NOTIFY alignmentChanged)
Q_PROPERTY(QString plainText READ toPlainText WRITE setPlainText)
Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged)
@@ -77,6 +79,10 @@ class DiagramTextItem : public QGraphicsTextItem
void setAlignment(const Qt::Alignment &alignment);
Qt::Alignment alignment() const;
bool m_block_alignment = false;
+
+ QPointF anchorPos() const;
+ void setAnchorPos(const QPointF &anchor);
+ void setAlignmentAtAnchor(const Qt::Alignment &alignment);
QRectF frameRect() const;
diff --git a/sources/qetmainwindow.cpp b/sources/qetmainwindow.cpp
index 1436f2668..36fccebf8 100644
--- a/sources/qetmainwindow.cpp
+++ b/sources/qetmainwindow.cpp
@@ -92,6 +92,16 @@ void QETMainWindow::initCommonActions()
}
});
+ 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);
+ ShortcutManager::instance().registerAction(export_config_action_, "mainwindow.export_configuration", tr("Général"), QKeySequence());
+
+ import_config_action_ = new QAction(QET::Icons::DocumentImport, tr("Charger une configuration..."), this);
+ import_config_action_ -> setStatusTip(tr("Remplace les réglages de QElectroTech par ceux d'un fichier, puis ferme QElectroTech", "status bar tip"));
+ connect(import_config_action_, &QAction::triggered, qet_app, &QETApp::importConfiguration);
+ ShortcutManager::instance().registerAction(import_config_action_, "mainwindow.import_configuration", tr("Général"), QKeySequence());
+
fullscreen_action_ = new QAction(this);
updateFullScreenAction();
connect(fullscreen_action_, &QAction::triggered, this, &QETMainWindow::toggleFullScreen);
@@ -170,6 +180,9 @@ void QETMainWindow::initCommonMenus()
settings_menu_ = new QMenu(tr("&Configuration", "window menu"), this);
settings_menu_ -> addAction(fullscreen_action_);
settings_menu_ -> addAction(configure_action_);
+ settings_menu_ -> addSeparator();
+ settings_menu_ -> addAction(export_config_action_);
+ settings_menu_ -> addAction(import_config_action_);
connect(settings_menu_, &QMenu::aboutToShow, this, &QETMainWindow::checkToolbarsmenu);
help_menu_ = new QMenu(tr("&Aide", "window menu"), this);
diff --git a/sources/qetmainwindow.h b/sources/qetmainwindow.h
index c3820c4bb..fe76fb7e0 100644
--- a/sources/qetmainwindow.h
+++ b/sources/qetmainwindow.h
@@ -56,6 +56,8 @@ class QETMainWindow : public QMainWindow {
// attributes
protected:
QAction *configure_action_; ///< Launch the QElectroTech configuration dialog
+ 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
QAction *whatsthis_action_; ///< Toggle "What's this" mode
QAction *about_qet_; ///< Launch the "About QElectroTech" dialog
diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp
index a44e127ef..d126b45f9 100644
--- a/sources/qetproject.cpp
+++ b/sources/qetproject.cpp
@@ -205,7 +205,7 @@ QETProject::QETProject(KAutoSaveFile *backup, QObject *parent) :
QETProject::~QETProject()
{
//Wait for any in-flight async crash-recovery backup to finish: the worker
- //writes through &m_backup_file, a member that would otherwise be destroyed
+ //writes through m_backup_files, a member that would otherwise be destroyed
//under it (issue #492).
m_backup_future.waitForFinished();
@@ -562,12 +562,16 @@ void QETProject::setFilePath(const QString &filepath)
if (filepath == m_file_path) {
return;
}
- //Don't close/re-point the backup file while a backup is still writing it.
+ //Don't close/re-point the backup files while a backup is still writing one.
m_backup_future.waitForFinished();
- if (m_backup_file.isOpen()) {
- m_backup_file.close();
+ const QUrl managed_file = QUrl::fromLocalFile(filepath);
+ for (auto &backup_file : m_backup_files) {
+ if (backup_file.isOpen()) {
+ backup_file.close();
+ }
+ backup_file.setManagedFile(managed_file);
}
- m_backup_file.setManagedFile(QUrl::fromLocalFile(filepath));
+ m_next_backup_slot = 0;
m_file_path = filepath;
QFileInfo fi(m_file_path);
@@ -2337,14 +2341,17 @@ void QETProject::detachDiagram(Diagram *diagram)
/**
@brief QETProject::writeBackup
- Write a backup file of this project, in the case that QET crash
+ Write a backup file of this project, in the case that QET crash.
+ The snapshots are written in turn to m_backup_files, so a write made
+ while the project is already in a bad state only replaces the oldest
+ snapshot, and the earlier ones are still there to recover from.
*/
void QETProject::writeBackup()
{
if (!m_backup_enabled)
return;
//Don't launch a new backup while the previous one is still writing:
- //both would write through &m_backup_file on different threads.
+ //both could write through the same m_backup_files slot on different threads.
if (m_backup_future.isRunning())
return;
//toXml() walks the whole project on the GUI thread, which freezes
@@ -2357,8 +2364,10 @@ void QETProject::writeBackup()
//Qt5-style QtConcurrent::run(function, reference-args) call did not
//survive the Qt6 API change, a lambda behaves identically on both.
QDomDocument xml_project(toXml());
- m_backup_future = QtConcurrent::run([this, xml_project]() mutable {
- return QET::writeToFile(xml_project, &m_backup_file, nullptr);
+ KAutoSaveFile *target = &m_backup_files[m_next_backup_slot];
+ m_next_backup_slot = (m_next_backup_slot + 1) % BackupGenerations;
+ m_backup_future = QtConcurrent::run([target, xml_project]() mutable {
+ return QET::writeToFile(xml_project, target, nullptr);
});
}
diff --git a/sources/qetproject.h b/sources/qetproject.h
index b30b07075..9e260bcc4 100644
--- a/sources/qetproject.h
+++ b/sources/qetproject.h
@@ -39,6 +39,8 @@
#include
#include
+#include
+
class Diagram;
class ElementsLocation;
class QETResult;
@@ -128,6 +130,10 @@ class QETProject : public QObject
/// process can destroy the project before the write finishes (crash).
static void setBackupEnabled(bool enabled);
+ /// Number of crash-recovery snapshots kept per project, written in
+ /// turn by writeBackup(), so one bad write cannot replace the only copy
+ static constexpr int BackupGenerations = 3;
+
///DEFAULT PROPERTIES
BorderProperties defaultBorderProperties() const;
void setDefaultBorderProperties(const BorderProperties &);
@@ -366,7 +372,9 @@ class QETProject : public QObject
QTimer m_save_backup_timer,
m_autosave_timer;
QFuture m_backup_future;
- KAutoSaveFile m_backup_file;
+ /// Crash-recovery snapshots, written in turn by writeBackup()
+ std::array m_backup_files;
+ int m_next_backup_slot = 0;
QUuid m_uuid = QUuid::createUuid();
QHash m_derived_uuid_keys;
QSet m_saved_item_uuids; //symbol and wire uuids the file carries, see derivedItemUuid()
diff --git a/sources/richtext/richtexteditor.cpp b/sources/richtext/richtexteditor.cpp
index 9c8b9b0a6..a982db81f 100644
--- a/sources/richtext/richtexteditor.cpp
+++ b/sources/richtext/richtexteditor.cpp
@@ -48,6 +48,7 @@
#include "richtexteditor_p.h"
#include "ui_addlinkdialog.h"
#include "../shortcutmanager.h"
+#include "../qet.h"
//#include
@@ -242,6 +243,7 @@ AddLinkDialog::AddLinkDialog(RichTextEditor *editor, QWidget *parent) :
m_ui(new Ui::AddLinkDialog)
{
m_ui->setupUi(this);
+ QET::trackDialogGeometry(this);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
@@ -862,6 +864,7 @@ RichTextEditorDialog::RichTextEditorDialog(QWidget *parent) :
m_editor->setFocus();
resize(600, 400);
+ QET::trackDialogGeometry(this);
}
RichTextEditorDialog::~RichTextEditorDialog()
diff --git a/sources/textanchor.h b/sources/textanchor.h
new file mode 100644
index 000000000..0c6983ab0
--- /dev/null
+++ b/sources/textanchor.h
@@ -0,0 +1,82 @@
+/*
+ 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 TEXTANCHOR_H
+#define TEXTANCHOR_H
+
+#include
+#include
+#include
+
+/**
+ The anchor point of a text: the point of the text chosen by its
+ alignment (top-left, right edge, centre...). It is the point that
+ stays in place when the text changes, and the point shown as
+ "Position X/Y" in the text properties.
+
+ The saved position of a text is still its top-left corner (pos()),
+ so existing projects and symbols are read and written unchanged.
+*/
+namespace TextAnchor
+{
+ /**
+ @return the point of rect chosen by alignment, in the
+ coordinates of rect. Left and top are x = 0 and y = 0, as
+ finishAlignment() of the text items assumes.
+ */
+ inline QPointF localPoint(const QRectF &rect, Qt::Alignment alignment)
+ {
+ qreal x = 0, y = 0;
+
+ if (alignment & Qt::AlignRight)
+ x = rect.right();
+ else if (alignment & Qt::AlignHCenter)
+ x = rect.center().x();
+
+ if (alignment & Qt::AlignBottom)
+ y = rect.bottom();
+ else if (alignment & Qt::AlignVCenter)
+ y = rect.center().y();
+
+ return QPointF(x, y);
+ }
+
+ /**
+ @return the anchor point of item for alignment, in parent
+ coordinates. For a top-left alignment this is item->pos().
+ The rotation of the item is taken into account.
+ */
+ inline QPointF pos(const QGraphicsItem *item, Qt::Alignment alignment)
+ {
+ return item->pos()
+ + item->mapToParent(localPoint(item->boundingRect(), alignment))
+ - item->mapToParent(QPointF(0, 0));
+ }
+
+ /**
+ @return the position (top-left corner) to give to item so that
+ its anchor point for alignment is at anchor.
+ */
+ inline QPointF itemPosFor(const QGraphicsItem *item,
+ Qt::Alignment alignment,
+ const QPointF &anchor)
+ {
+ return item->pos() + anchor - pos(item, alignment);
+ }
+}
+
+#endif // TEXTANCHOR_H
diff --git a/sources/titleblock/dimensionwidget.cpp b/sources/titleblock/dimensionwidget.cpp
index 228b9ad57..6e2566c06 100644
--- a/sources/titleblock/dimensionwidget.cpp
+++ b/sources/titleblock/dimensionwidget.cpp
@@ -17,6 +17,8 @@
*/
#include "dimensionwidget.h"
+#include "../qet.h"
+
/**
Constructor
@param complete True for this dialog to show the radio buttons that allow
@@ -31,6 +33,7 @@ TitleBlockDimensionWidget::TitleBlockDimensionWidget(bool complete, QWidget *par
{
initWidgets();
initLayouts();
+ QET::trackDialogGeometry(this);
}
/**
diff --git a/sources/ui/aboutqetdialog.cpp b/sources/ui/aboutqetdialog.cpp
index afec0e258..37911e122 100644
--- a/sources/ui/aboutqetdialog.cpp
+++ b/sources/ui/aboutqetdialog.cpp
@@ -34,6 +34,7 @@ AboutQETDialog::AboutQETDialog(QWidget *parent) :
ui(new Ui::AboutQETDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
setAbout();
setAuthors();
setTranslators();
diff --git a/sources/ui/aiassistantdialog.cpp b/sources/ui/aiassistantdialog.cpp
index 6d8542d7e..7e0284bdd 100644
--- a/sources/ui/aiassistantdialog.cpp
+++ b/sources/ui/aiassistantdialog.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "aiassistantdialog.h"
+#include "../qet.h"
#include
#include
@@ -157,6 +158,8 @@ AiAssistantDialog::AiAssistantDialog(QWidget *parent) :
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
refresh();
+
+ QET::trackDialogGeometry(this);
}
void AiAssistantDialog::refresh()
diff --git a/sources/ui/alignmenttextdialog.cpp b/sources/ui/alignmenttextdialog.cpp
index e3331800f..f732d9177 100644
--- a/sources/ui/alignmenttextdialog.cpp
+++ b/sources/ui/alignmenttextdialog.cpp
@@ -20,11 +20,13 @@
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "ui_alignmenttextdialog.h"
+#include "../qet.h"
AlignmentTextDialog::AlignmentTextDialog(Qt::Alignment alignment, QWidget *parent) :
QDialog(parent),
ui(new Ui::AlignmentTextDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
if(alignment == (Qt::AlignTop|Qt::AlignLeft))
ui->top_left->setChecked(true);
diff --git a/sources/ui/backuprestoredialog.cpp b/sources/ui/backuprestoredialog.cpp
new file mode 100644
index 000000000..72e46712b
--- /dev/null
+++ b/sources/ui/backuprestoredialog.cpp
@@ -0,0 +1,142 @@
+/*
+ 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 "backuprestoredialog.h"
+
+#ifdef BUILD_WITHOUT_KF
+# include "nokde/kautosavefile.h"
+#else
+# include
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ @brief BackupRestoreDialog::BackupRestoreDialog
+ @param groups : managed project path -> its recovery generations
+ (newest-first)
+ @param parent : parent widget
+*/
+BackupRestoreDialog::BackupRestoreDialog(
+ const QHash> &groups,
+ QWidget *parent) :
+ QDialog(parent),
+ m_groups(groups)
+{
+ setWindowTitle(tr("Fichiers de restauration", "window title"));
+
+ auto main_layout = new QVBoxLayout(this);
+
+ auto intro = new QLabel(
+ tr("Des fichiers de restauration ont été trouvés, "
+ "voulez-vous les ouvrir ? "
+ "Pour un projet ayant plusieurs versions de restauration, "
+ "la plus récente est sélectionnée par défaut.",
+ "dialog message"));
+ intro->setWordWrap(true);
+ main_layout->addWidget(intro);
+
+ auto grid = new QGridLayout();
+ int row = 0;
+ for (auto it = m_groups.constBegin(); it != m_groups.constEnd(); ++it)
+ {
+ const QString &path = it.key();
+ const QList &generations = it.value();
+
+# ifdef Q_OS_WIN
+ QString display_path = path;
+ display_path.remove(0, 1);
+# else
+ const QString &display_path = path;
+# endif
+ grid->addWidget(new QLabel(display_path), row, 0);
+
+ auto combo = new QComboBox(this);
+ for (int i = 0; i < generations.size(); ++i)
+ {
+ const QDateTime modified =
+ QFileInfo(*generations.at(i)).lastModified();
+ QString label = QLocale::system().toString(
+ modified, QLocale::ShortFormat);
+ if (i == 0) {
+ label = tr("%1 (la plus récente)", "recovery generation label")
+ .arg(label);
+ }
+ combo->addItem(label);
+ }
+ combo->setEnabled(generations.size() > 1);
+ grid->addWidget(combo, row, 1);
+
+ m_combo_for_path.insert(path, combo);
+ ++row;
+ }
+ main_layout->addLayout(grid);
+
+ auto buttons = new QDialogButtonBox(
+ QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ main_layout->addWidget(buttons);
+}
+
+/**
+ @brief BackupRestoreDialog::~BackupRestoreDialog
+*/
+BackupRestoreDialog::~BackupRestoreDialog() = default;
+
+/**
+ @return one recovery generation per project, matching the combo box
+ selection (the newest generation, unless the user picked another).
+*/
+QList BackupRestoreDialog::selectedFiles() const
+{
+ QList selected;
+ for (auto it = m_groups.constBegin(); it != m_groups.constEnd(); ++it)
+ {
+ const int index = m_combo_for_path.value(it.key())->currentIndex();
+ selected << it.value().at(index);
+ }
+ return selected;
+}
+
+/**
+ @return every recovery generation the user did not pick; the caller
+ should release and delete these.
+*/
+QList BackupRestoreDialog::discardedFiles() const
+{
+ QList discarded;
+ for (auto it = m_groups.constBegin(); it != m_groups.constEnd(); ++it)
+ {
+ const int index = m_combo_for_path.value(it.key())->currentIndex();
+ const QList &generations = it.value();
+ for (int i = 0; i < generations.size(); ++i) {
+ if (i != index) {
+ discarded << generations.at(i);
+ }
+ }
+ }
+ return discarded;
+}
diff --git a/sources/ui/backuprestoredialog.h b/sources/ui/backuprestoredialog.h
new file mode 100644
index 000000000..30eed692d
--- /dev/null
+++ b/sources/ui/backuprestoredialog.h
@@ -0,0 +1,60 @@
+/*
+ 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 BACKUPRESTOREDIALOG_H
+#define BACKUPRESTOREDIALOG_H
+
+#include
+#include
+#include
+#include
+
+class KAutoSaveFile;
+class QComboBox;
+
+/**
+ @brief Lets the user pick, per crashed project, which recovery
+ generation to reopen when the crash-recovery rotation
+ (@see QETProject::writeBackup) left more than one snapshot behind.
+*/
+class BackupRestoreDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ /// @param groups : managed project file path -> its stale recovery
+ /// generations, each list already sorted newest-first. Ownership of
+ /// the KAutoSaveFile objects stays with the caller.
+ explicit BackupRestoreDialog(
+ const QHash> &groups,
+ QWidget *parent = nullptr);
+ ~BackupRestoreDialog() override;
+
+ /// Valid once accepted: one entry per project, the chosen generation
+ /// (defaults to the most recent one).
+ QList selectedFiles() const;
+ /// The generations the user did not pick; the caller should discard
+ /// (release + delete) these.
+ QList discardedFiles() const;
+
+ private:
+ QHash> m_groups;
+ QHash m_combo_for_path;
+};
+
+#endif // BACKUPRESTOREDIALOG_H
diff --git a/sources/ui/bomexportdialog.cpp b/sources/ui/bomexportdialog.cpp
index da2be53ca..703b9e3c6 100644
--- a/sources/ui/bomexportdialog.cpp
+++ b/sources/ui/bomexportdialog.cpp
@@ -24,6 +24,7 @@
#include "../qetproject.h"
#include "ui_bomexportdialog.h"
+#include "../qet.h"
#include
#include
#include
@@ -40,6 +41,7 @@ BOMExportDialog::BOMExportDialog(QETProject *project, QWidget *parent) :
m_project(project)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
m_query_widget = new ElementQueryWidget(this);
ui->m_main_layout->insertWidget(0, m_query_widget);
diff --git a/sources/ui/compositetexteditdialog.cpp b/sources/ui/compositetexteditdialog.cpp
index 3e0deca34..ac4e9c89b 100644
--- a/sources/ui/compositetexteditdialog.cpp
+++ b/sources/ui/compositetexteditdialog.cpp
@@ -5,6 +5,7 @@
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/element.h"
#include "../qetinformation.h"
+#include "../qet.h"
#include "ui_compositetexteditdialog.h"
#include
@@ -15,6 +16,7 @@ CompositeTextEditDialog::CompositeTextEditDialog(DynamicElementTextItem *text, Q
m_text(text)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
m_default_text = m_text->compositeText();
ui->m_plain_text_edit->setPlainText(m_default_text);
ui->m_plain_text_edit->setPlaceholderText(tr("Entrée votre texte composé ici, en vous aidant des variables disponible"));
@@ -31,6 +33,7 @@ CompositeTextEditDialog::CompositeTextEditDialog(QString text, bool report, QWid
ui(new Ui::CompositeTextEditDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
m_default_text = std::move(text);
ui->m_plain_text_edit->setPlainText(m_default_text);
ui->m_plain_text_edit->setPlaceholderText(tr("Entrée votre texte composé ici, en vous aidant des variables disponible"));
diff --git a/sources/ui/conductorcolortoolbutton.cpp b/sources/ui/conductorcolortoolbutton.cpp
index e9b91dc08..f85ece30c 100644
--- a/sources/ui/conductorcolortoolbutton.cpp
+++ b/sources/ui/conductorcolortoolbutton.cpp
@@ -26,6 +26,7 @@
#include "../projectview.h"
#include "../qetgraphicsitem/conductor.h"
#include "../conductorproperties.h"
+#include "../utils/colordialogdoubleclick.h"
#include
#include
@@ -217,10 +218,11 @@ void ConductorColorToolButton::applyColor(const QColor &color)
*/
void ConductorColorToolButton::chooseOtherColor()
{
- const QColor c = QColorDialog::getColor(m_current, this,
- tr("Choisir une couleur de conducteur"));
- if (c.isValid()) {
- applyColor(c);
+ QColorDialog dialog(m_current, this);
+ dialog.setWindowTitle(tr("Choisir une couleur de conducteur"));
+ ColorDialogDoubleClick::install(&dialog);
+ if (dialog.exec() == QDialog::Accepted && dialog.selectedColor().isValid()) {
+ applyColor(dialog.selectedColor());
}
}
diff --git a/sources/ui/conductorpropertiesdialog.cpp b/sources/ui/conductorpropertiesdialog.cpp
index 329cf83a9..1973eb188 100644
--- a/sources/ui/conductorpropertiesdialog.cpp
+++ b/sources/ui/conductorpropertiesdialog.cpp
@@ -24,6 +24,7 @@
#include "conductorpropertieswidget.h"
#include "ui_conductorpropertiesdialog.h"
+#include "../qet.h"
/**
@brief ConductorPropertiesDialog::ConductorPropertiesDialog
Constructor
@@ -36,6 +37,7 @@ ConductorPropertiesDialog::ConductorPropertiesDialog(
ui(new Ui::ConductorPropertiesDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
m_cpw = new ConductorPropertiesWidget(conductor->properties());
m_cpw -> setHiddenOneTextPerFolio(true);
m_cpw->setHiddenAvailableAutonum(true);
diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp
index 68b86a43a..206735c64 100644
--- a/sources/ui/configpage/generalconfigurationpage.cpp
+++ b/sources/ui/configpage/generalconfigurationpage.cpp
@@ -25,6 +25,7 @@
#include "../../utils/qetutils.h"
#include "../../qetmessagebox.h"
#include "../../textgrid.h"
+#include "../../editor/terminalnamecheck.h"
#include "../../ElementsCollection/qetlabelsfile.h"
#include "../prefixconfigurationdialog.h"
#include "../nokde/kcolorbutton.h"
@@ -178,6 +179,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->MaxPartsElementEditorList_sb->setValue(settings.value("elementeditor/max-parts-element-editor-list", 200).toInt());
ui->ElementEditor_Grid_PointSize_min_sb->setValue(settings.value("elementeditor/grid_pointsize_min", 1).toInt());
ui->ElementEditor_Grid_PointSize_max_sb->setValue(settings.value("elementeditor/grid_pointsize_max", 1).toInt());
+ ui->m_check_terminal_names_cb->setChecked(settings.value(TerminalNameCheck::settings_key, true).toBool());
QString path = settings.value("elements-collections/common-collection-path", "default").toString();
if (path != "default")
@@ -290,6 +292,7 @@ void GeneralConfigurationPage::applyConf()
settings.setValue("elementeditor/max-parts-element-editor-list", ui->MaxPartsElementEditorList_sb->value());
settings.setValue("elementeditor/grid_pointsize_min", ui->ElementEditor_Grid_PointSize_min_sb->value());
settings.setValue("elementeditor/grid_pointsize_max", ui->ElementEditor_Grid_PointSize_max_sb->value());
+ settings.setValue(TerminalNameCheck::settings_key, ui->m_check_terminal_names_cb->isChecked());
//DIAGRAM VIEW
settings.setValue("diagramview/gestures", ui->m_use_gesture_trackpad->isChecked());
diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui
index 65381a646..2298006ee 100644
--- a/sources/ui/configpage/generalconfigurationpage.ui
+++ b/sources/ui/configpage/generalconfigurationpage.ui
@@ -1250,6 +1250,19 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
+
+
+
+ À l'enregistrement d'un élément : refuser deux bornes portant le même nom, et signaler les bornes sans nom (IEC 61666).
+
+
+ Vérifier les noms des bornes à l'enregistrement
+
+
+ true
+
+
+
@@ -1285,6 +1298,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
m_custom_tbt_path_cbm_highlight_integrated_elementsm_default_elements_info
+ m_check_terminal_names_cbm_lang_cbm_dyn_text_font_pbm_dyn_text_rotation_sb
diff --git a/sources/ui/contactgroupselectiondialog.cpp b/sources/ui/contactgroupselectiondialog.cpp
index d1c88a002..e681a79cb 100644
--- a/sources/ui/contactgroupselectiondialog.cpp
+++ b/sources/ui/contactgroupselectiondialog.cpp
@@ -17,6 +17,8 @@
*/
#include "contactgroupselectiondialog.h"
+#include "../qet.h"
+
#include
#include
#include
@@ -207,6 +209,8 @@ ContactGroupSelectionDialog::ContactGroupSelectionDialog(
accept();
}
});
+
+ QET::trackDialogGeometry(this);
}
int ContactGroupSelectionDialog::selectedIndex() const
diff --git a/sources/ui/diagrampropertiesdialog.cpp b/sources/ui/diagrampropertiesdialog.cpp
index e07de4a6a..75f05fa70 100644
--- a/sources/ui/diagrampropertiesdialog.cpp
+++ b/sources/ui/diagrampropertiesdialog.cpp
@@ -89,6 +89,8 @@ DiagramPropertiesDialog::DiagramPropertiesDialog(Diagram *diagram, QWidget *pare
vlayout.addLayout(glayout);
vlayout.addWidget(&boutons);
+ QET::trackDialogGeometry(this);
+
// if dialog is accepted
if (this -> exec() == QDialog::Accepted && !diagram_is_read_only)
{
diff --git a/sources/ui/dialogwaiting.cpp b/sources/ui/dialogwaiting.cpp
index b203d5d96..498504e1e 100644
--- a/sources/ui/dialogwaiting.cpp
+++ b/sources/ui/dialogwaiting.cpp
@@ -18,6 +18,7 @@
#include "dialogwaiting.h"
#include "ui_dialogwaiting.h"
+#include "../qet.h"
#include
@@ -31,6 +32,7 @@ DialogWaiting::DialogWaiting(QWidget *parent) :
ui(new Ui::DialogWaiting)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
setTitle( "..." );
setDetail( "..." );
}
diff --git a/sources/ui/duplicateoffsetdialog.cpp b/sources/ui/duplicateoffsetdialog.cpp
index 906bc97cd..86668d570 100644
--- a/sources/ui/duplicateoffsetdialog.cpp
+++ b/sources/ui/duplicateoffsetdialog.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "duplicateoffsetdialog.h"
+#include "../qet.h"
#include
#include
@@ -76,6 +77,8 @@ DuplicateOffsetDialog::DuplicateOffsetDialog(QWidget *parent) :
auto *layout = new QVBoxLayout(this);
layout->addLayout(form);
layout->addWidget(buttons);
+
+ QET::trackDialogGeometry(this);
}
QPoint DuplicateOffsetDialog::stepOffset() const
diff --git a/sources/ui/dynamicelementtextmodel.cpp b/sources/ui/dynamicelementtextmodel.cpp
index 3939d50b3..a3e825218 100644
--- a/sources/ui/dynamicelementtextmodel.cpp
+++ b/sources/ui/dynamicelementtextmodel.cpp
@@ -303,7 +303,7 @@ QList DynamicElementTextModel::itemsForText(
x_pos->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
QStandardItem *x_pos_a = new QStandardItem;
- x_pos_a->setData(deti->pos().x(), Qt::EditRole);
+ x_pos_a->setData(deti->anchorPos().x(), Qt::EditRole);
x_pos_a->setData(DynamicElementTextModel::pos, Qt::UserRole+1);
x_pos_a->setFlags(Qt::ItemIsSelectable
| Qt::ItemIsEnabled
@@ -318,7 +318,7 @@ QList DynamicElementTextModel::itemsForText(
y_pos->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
QStandardItem *y_pos_a = new QStandardItem;
- y_pos_a->setData(deti->pos().y(), Qt::EditRole);
+ y_pos_a->setData(deti->anchorPos().y(), Qt::EditRole);
y_pos_a->setData(DynamicElementTextModel::pos, Qt::UserRole+1);
y_pos_a->setFlags(Qt::ItemIsSelectable
| Qt::ItemIsEnabled
@@ -600,9 +600,9 @@ QUndoCommand *DynamicElementTextModel::undoForEditedText(
{
QPointF p(text_qsi->child(x_txt_row,1)->data(Qt::EditRole).toDouble(),
text_qsi->child(y_txt_row,1)->data(Qt::EditRole).toDouble());
- if(p != deti->pos())
+ if(p != deti->anchorPos())
{
- QPropertyUndoCommand *quc = new QPropertyUndoCommand(deti, "pos", QVariant(deti->pos()), QVariant(p), undo);
+ QPropertyUndoCommand *quc = new QPropertyUndoCommand(deti, "anchorPos", QVariant(deti->anchorPos()), QVariant(p), undo);
quc->setAnimated(true, false);
quc->setText(tr("Déplacer un texte d'élément"));
}
@@ -636,7 +636,7 @@ QUndoCommand *DynamicElementTextModel::undoForEditedText(
Qt::Alignment alignment = text_qsi->child(align_txt_row, 1)->data(Qt::UserRole+2).value();
if (alignment != deti->alignment())
{
- QPropertyUndoCommand *quc = new QPropertyUndoCommand(deti, "alignment", QVariant(deti->alignment()), QVariant(alignment), undo);
+ QPropertyUndoCommand *quc = new QPropertyUndoCommand(deti, "alignmentAtAnchor", QVariant(deti->alignment()), QVariant(alignment), undo);
quc->setText(tr("Modifier l'alignement d'un texte d'élément"));
}
}
@@ -1508,9 +1508,9 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti,
case pos:
{
if(qsi->child(x_txt_row,1))
- qsi->child(x_txt_row,1)->setData(deti->pos().x(), Qt::EditRole);
+ qsi->child(x_txt_row,1)->setData(deti->anchorPos().x(), Qt::EditRole);
if(qsi->child(y_txt_row,1))
- qsi->child(y_txt_row,1)->setData(deti->pos().y(), Qt::EditRole);
+ qsi->child(y_txt_row,1)->setData(deti->anchorPos().y(), Qt::EditRole);
break;
}
case frame:
@@ -1531,6 +1531,14 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti,
}
default:break;
}
+
+ //Rotating or resizing the text moves its anchor point (Position X/Y)
+ if ((type == rotation || type == textWidth)
+ && qsi->child(x_txt_row,1) && qsi->child(y_txt_row,1))
+ {
+ qsi->child(x_txt_row,1)->setData(deti->anchorPos().x(), Qt::EditRole);
+ qsi->child(y_txt_row,1)->setData(deti->anchorPos().y(), Qt::EditRole);
+ }
m_block_dataChanged = false;
}
diff --git a/sources/ui/formulaassistantdialog.cpp b/sources/ui/formulaassistantdialog.cpp
index 228da7aff..0f921f10b 100644
--- a/sources/ui/formulaassistantdialog.cpp
+++ b/sources/ui/formulaassistantdialog.cpp
@@ -18,6 +18,7 @@
#include "formulaassistantdialog.h"
#include "ui_formulaassistantdialog.h"
+#include "../qet.h"
#include
FormulaAssistantDialog::FormulaAssistantDialog(QWidget *parent) :
@@ -25,6 +26,7 @@ FormulaAssistantDialog::FormulaAssistantDialog(QWidget *parent) :
ui(new Ui::FormulaAssistantDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
}
FormulaAssistantDialog::~FormulaAssistantDialog()
diff --git a/sources/ui/imagetransparentcolordialog.cpp b/sources/ui/imagetransparentcolordialog.cpp
index ed9f5362c..0318acbee 100644
--- a/sources/ui/imagetransparentcolordialog.cpp
+++ b/sources/ui/imagetransparentcolordialog.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "imagetransparentcolordialog.h"
+#include "../qet.h"
#include
#include
@@ -141,6 +142,8 @@ ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixm
rebuildSwatches(); // shows existingColors immediately, if any
updatePreview(); // and the preview already reflects them too, rather than only appearing after the next pick
+
+ QET::trackDialogGeometry(this);
}
/**
diff --git a/sources/ui/importelementdialog.cpp b/sources/ui/importelementdialog.cpp
index 47d59220c..65f131f92 100644
--- a/sources/ui/importelementdialog.cpp
+++ b/sources/ui/importelementdialog.cpp
@@ -17,6 +17,7 @@
*/
#include "importelementdialog.h"
#include "ui_importelementdialog.h"
+#include "../qet.h"
#include
ImportElementDialog::ImportElementDialog(QWidget *parent) :
@@ -24,6 +25,7 @@ ImportElementDialog::ImportElementDialog(QWidget *parent) :
ui(new Ui::ImportElementDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
setUpWidget();
}
diff --git a/sources/ui/importelementtextpatterndialog.cpp b/sources/ui/importelementtextpatterndialog.cpp
index 4c4429d47..4581fa865 100644
--- a/sources/ui/importelementtextpatterndialog.cpp
+++ b/sources/ui/importelementtextpatterndialog.cpp
@@ -18,10 +18,12 @@
#include "importelementtextpatterndialog.h"
#include "ui_importelementtextpatterndialog.h"
+#include "../qet.h"
ImportElementTextPatternDialog::ImportElementTextPatternDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ImportElementTextPatternDialog) {
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
}
ImportElementTextPatternDialog::~ImportElementTextPatternDialog()
diff --git a/sources/ui/jumptoelementdialog.cpp b/sources/ui/jumptoelementdialog.cpp
index 65b2615b3..566a93305 100644
--- a/sources/ui/jumptoelementdialog.cpp
+++ b/sources/ui/jumptoelementdialog.cpp
@@ -20,6 +20,7 @@
#include "../diagram.h"
#include "../diagramview.h"
#include "../qetproject.h"
+#include "../qet.h"
#include "../qetgraphicsitem/element.h"
#include
@@ -59,6 +60,8 @@ JumpToElementDialog::JumpToElementDialog(Diagram *diagram, QWidget *parent) :
buildCandidates();
updateFilteredList(QString());
m_filter_edit->setFocus();
+
+ QET::trackDialogGeometry(this);
}
JumpToElementDialog::~JumpToElementDialog()
diff --git a/sources/ui/marginseditdialog.cpp b/sources/ui/marginseditdialog.cpp
index 65a8ac1a4..2177cb7d0 100644
--- a/sources/ui/marginseditdialog.cpp
+++ b/sources/ui/marginseditdialog.cpp
@@ -18,6 +18,7 @@
#include "marginseditdialog.h"
#include "ui_marginseditdialog.h"
+#include "../qet.h"
#include
MarginsEditDialog::MarginsEditDialog(QMargins margins, QWidget *parent) :
@@ -25,6 +26,7 @@ MarginsEditDialog::MarginsEditDialog(QMargins margins, QWidget *parent) :
ui(new Ui::MarginsEditDialog)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
ui->m_top_sb->setValue(margins.top());
ui->m_left_sb->setValue(margins.left());
ui->m_right_sb->setValue(margins.right());
diff --git a/sources/ui/masterpropertieswidget.cpp b/sources/ui/masterpropertieswidget.cpp
index 94d56e145..5d1d5a1ac 100644
--- a/sources/ui/masterpropertieswidget.cpp
+++ b/sources/ui/masterpropertieswidget.cpp
@@ -605,7 +605,6 @@ void MasterPropertiesWidget::updateUi()
// Type combo (read-only — type is defined in the editor)
auto *type_cb = new QComboBox(m_plc_table);
type_cb->setEnabled(false);
- type_cb->setStyleSheet("QComboBox { background-color: #f0f0f0; }");
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
type_cb->addItem(plc_types.at(t), t);
diff --git a/sources/ui/multipastedialog.cpp b/sources/ui/multipastedialog.cpp
index 953b5bccd..c4c8dda4f 100644
--- a/sources/ui/multipastedialog.cpp
+++ b/sources/ui/multipastedialog.cpp
@@ -26,6 +26,7 @@
#include "../qetgraphicsitem/conductor.h"
#include "../ui_multipastedialog.h"
+#include "../qet.h"
#include
#include
@@ -35,6 +36,7 @@ MultiPasteDialog::MultiPasteDialog(Diagram *diagram, QWidget *parent) :
m_diagram(diagram)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
connect(ui->m_x_sb, static_cast(&QSpinBox::valueChanged), this, &MultiPasteDialog::updatePreview);
connect(ui->m_y_sb, static_cast(&QSpinBox::valueChanged), this, &MultiPasteDialog::updatePreview);
diff --git a/sources/ui/pdfpagesdialog.cpp b/sources/ui/pdfpagesdialog.cpp
index 00a460862..586887b40 100644
--- a/sources/ui/pdfpagesdialog.cpp
+++ b/sources/ui/pdfpagesdialog.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "pdfpagesdialog.h"
+#include "../qet.h"
// Whole file is a no-op unless QtPdf is available (see pdfpagesdialog.h).
#ifdef QET_HAS_QTPDF
@@ -125,6 +126,8 @@ PdfPagesDialog::PdfPagesDialog(QPdfDocument &document, QWidget *parent)
// Render initial preview
updatePreview();
+
+ QET::trackDialogGeometry(this);
}
/**
diff --git a/sources/ui/potentialselectordialog.cpp b/sources/ui/potentialselectordialog.cpp
index 2a39252c7..7122676a0 100644
--- a/sources/ui/potentialselectordialog.cpp
+++ b/sources/ui/potentialselectordialog.cpp
@@ -19,6 +19,7 @@
#include "potentialselectordialog.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
+#include "../qet.h"
#include "../autoNum/assignvariables.h"
#include "../diagram.h"
#include "../qetgraphicsitem/conductor.h"
@@ -270,6 +271,7 @@ PotentialSelectorDialog::PotentialSelectorDialog(Conductor *conductor,
m_parent_undo(parent_undo)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
m_potential_selector = new NewConductorPotentialSelector(conductor);
buildWidget();
}
@@ -292,6 +294,7 @@ PotentialSelectorDialog::PotentialSelectorDialog(Element *report,
m_parent_undo(parent_undo)
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
m_potential_selector = new LinkReportPotentialSelector(report);
buildWidget();
}
diff --git a/sources/ui/terminalnumberingdialog.cpp b/sources/ui/terminalnumberingdialog.cpp
index 85f299214..fde1df6da 100644
--- a/sources/ui/terminalnumberingdialog.cpp
+++ b/sources/ui/terminalnumberingdialog.cpp
@@ -5,6 +5,7 @@
#include "../diagram.h"
#include "../qetgraphicsitem/element.h"
#include "../undocommand/changeelementinformationcommand.h"
+#include "../qet.h"
#include
#include
#include
@@ -67,6 +68,8 @@ TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *pr
m_stripCheckboxes.insert(prefix, cb);
}
}
+
+ QET::trackDialogGeometry(this);
}
/**
diff --git a/sources/ui/thirdpartybinaryinstalldialog.cpp b/sources/ui/thirdpartybinaryinstalldialog.cpp
index 80754a269..0eb82fc07 100644
--- a/sources/ui/thirdpartybinaryinstalldialog.cpp
+++ b/sources/ui/thirdpartybinaryinstalldialog.cpp
@@ -18,6 +18,7 @@
#include "thirdpartybinaryinstalldialog.h"
#include "ui_thirdpartybinaryinstalldialog.h"
+#include "../qet.h"
#include
#include
#include
@@ -32,6 +33,7 @@ ThirdPartyBinaryInstallDialog::ThirdPartyBinaryInstallDialog(const QString &text
ui{new Ui::ThirdPartyBinaryInstallDialog}
{
ui->setupUi(this);
+ QET::trackDialogGeometry(this);
ui->m_label->setText(text);
connect(ui->m_download_pb, &QPushButton::clicked, [downloadLink](){
diff --git a/sources/ui/wiringlistdialog.cpp b/sources/ui/wiringlistdialog.cpp
index a28f2425e..da2e7746c 100644
--- a/sources/ui/wiringlistdialog.cpp
+++ b/sources/ui/wiringlistdialog.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "wiringlistdialog.h"
+#include "../qet.h"
#include "../dataBase/projectdatabase.h"
#include "../qetproject.h"
@@ -111,4 +112,6 @@ WiringListDialog::WiringListDialog(QETProject *project, QWidget *parent) :
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addWidget(buttons);
+
+ QET::trackDialogGeometry(this);
}
diff --git a/sources/utils/colordialogdoubleclick.cpp b/sources/utils/colordialogdoubleclick.cpp
new file mode 100644
index 000000000..3cde3a591
--- /dev/null
+++ b/sources/utils/colordialogdoubleclick.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 "colordialogdoubleclick.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ @brief ColorDialogDoubleClick::install
+ Watch the swatches of @a dialog. The filter is a child of the dialog
+ and is deleted with it.
+ @param dialog
+*/
+void ColorDialogDoubleClick::install(QColorDialog *dialog)
+{
+ if (!dialog) {
+ return;
+ }
+
+ //The swatches are looked for when the dialog is shown: Qt builds
+ //them only then when the platform offered a native dialog it
+ //could not show after all.
+ dialog->installEventFilter(new ColorDialogDoubleClick(dialog));
+}
+
+ColorDialogDoubleClick::ColorDialogDoubleClick(QColorDialog *dialog) :
+ QObject(dialog),
+ m_dialog(dialog)
+{}
+
+/**
+ @brief ColorDialogDoubleClick::eventFilter
+ The first click of a double-click has already selected the swatch
+ (on its release), so the dialog's current colour is the one
+ double-clicked: accept it. Queued, so the swatch finishes handling
+ the second click before the dialog closes.
+*/
+bool ColorDialogDoubleClick::eventFilter(QObject *watched, QEvent *event)
+{
+ if (watched == m_dialog && event->type() == QEvent::Show) {
+ //QWellArray is Qt's private class behind both swatch grids;
+ //its name is the only handle on them (in the QtPrivate
+ //namespace in recent Qt 6). If a Qt release renames it,
+ //nothing matches and double-click just selects, as before.
+ //Installing twice only moves the filter to the front.
+ const auto children = m_dialog->findChildren();
+ for (QWidget *child : children) {
+ if (child->inherits("QtPrivate::QWellArray")
+ || child->inherits("QWellArray")) {
+ child->installEventFilter(this);
+ }
+ }
+ }
+ else if (event->type() == QEvent::MouseButtonDblClick
+ && static_cast(event)->button() == Qt::LeftButton)
+ {
+ QMetaObject::invokeMethod(m_dialog, &QDialog::accept, Qt::QueuedConnection);
+ }
+ return QObject::eventFilter(watched, event);
+}
diff --git a/sources/utils/colordialogdoubleclick.h b/sources/utils/colordialogdoubleclick.h
new file mode 100644
index 000000000..62f9ea40a
--- /dev/null
+++ b/sources/utils/colordialogdoubleclick.h
@@ -0,0 +1,50 @@
+/*
+ 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 COLOR_DIALOG_DOUBLE_CLICK_H
+#define COLOR_DIALOG_DOUBLE_CLICK_H
+
+#include
+
+class QColorDialog;
+
+/**
+ @brief The ColorDialogDoubleClick class
+ Lets a double-click on one of a QColorDialog's colour swatches (the
+ basic colours and the custom colours) choose that colour and close
+ the dialog, as OK does. Qt's dialog only selects the swatch.
+
+ Only Qt's own dialog has swatches to watch. Where the platform shows
+ its native dialog instead (macOS, some Linux desktops), this does
+ nothing and the native dialog behaves as it always has.
+*/
+class ColorDialogDoubleClick : public QObject
+{
+ Q_OBJECT
+
+ public:
+ static void install(QColorDialog *dialog);
+
+ protected:
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
+ private:
+ explicit ColorDialogDoubleClick(QColorDialog *dialog);
+ QColorDialog *m_dialog;
+};
+
+#endif // COLOR_DIALOG_DOUBLE_CLICK_H
diff --git a/sources/utils/configprofile.h b/sources/utils/configprofile.h
new file mode 100644
index 000000000..b235bc507
--- /dev/null
+++ b/sources/utils/configprofile.h
@@ -0,0 +1,98 @@
+/*
+ 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 CONFIGPROFILE_H
+#define CONFIGPROFILE_H
+
+#include
+#include
+#include
+
+/**
+ Configuration profiles (discussion #610): the settings of QElectroTech
+ saved to a file, and loaded back from one.
+
+ Two kinds of keys stay out of a profile, because they describe this
+ computer rather than how the user wants QElectroTech to behave: window
+ sizes, positions and dock layouts (".../geometry", ".../state"), and
+ the recent-files lists ("...-recentfiles/..."). They are not written to
+ the file, and loading a profile keeps the current ones.
+*/
+namespace ConfigProfile
+{
+ /// Key written to every exported file, so that loading refuses a
+ /// file that was not exported by QElectroTech. Never copied into
+ /// the live settings.
+ inline const QString marker_key{QStringLiteral("qelectrotech-configuration/format")};
+
+ /// True for a key that stays out of a profile (see above).
+ inline bool isLocalKey(const QString &key)
+ {
+ return key.endsWith(QLatin1String("/geometry"))
+ || key.endsWith(QLatin1String("/state"))
+ || key.contains(QLatin1String("-recentfiles/"))
+ || key == marker_key;
+ }
+
+ /// True if @a file was exported by exportTo().
+ inline bool isProfile(const QSettings &file)
+ {
+ return file.value(marker_key).toInt() == 1;
+ }
+
+ /// Replace the contents of @a file with every key of @a live,
+ /// except the local ones. Returns the number of keys written.
+ inline int exportTo(const QSettings &live, QSettings &file)
+ {
+ file.clear();
+ int written = 0;
+ const QStringList keys = live.allKeys();
+ for (const QString &key : keys) {
+ if (isLocalKey(key)) {
+ continue;
+ }
+ file.setValue(key, live.value(key));
+ ++written;
+ }
+ file.setValue(marker_key, 1);
+ file.sync();
+ return written;
+ }
+
+ /// Replace every key of @a live, except the local ones, with the
+ /// keys of @a file. A key that is not in @a file is removed rather
+ /// than kept: loading a profile must not leave behind a setting
+ /// the previous profile made. Local keys in @a file are ignored.
+ inline void importFrom(const QSettings &file, QSettings &live)
+ {
+ const QStringList live_keys = live.allKeys();
+ for (const QString &key : live_keys) {
+ if (!isLocalKey(key)) {
+ live.remove(key);
+ }
+ }
+ const QStringList file_keys = file.allKeys();
+ for (const QString &key : file_keys) {
+ if (!isLocalKey(key)) {
+ live.setValue(key, file.value(key));
+ }
+ }
+ live.sync();
+ }
+}
+
+#endif // CONFIGPROFILE_H
diff --git a/tests/catch/src/kautosavefile_test.cpp b/tests/catch/src/kautosavefile_test.cpp
index 64258c117..6e1e1c829 100644
--- a/tests/catch/src/kautosavefile_test.cpp
+++ b/tests/catch/src/kautosavefile_test.cpp
@@ -3,14 +3,18 @@
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
+#include
#ifdef Q_OS_UNIX
+#include
#include
#include
#include
@@ -106,3 +110,104 @@ TEST_CASE("Qt-only KAutoSaveFile recovers stale files", "[nokde][autosave]")
CHECK_FALSE(QFile::exists(lock_file_name));
#endif
}
+
+TEST_CASE("Multiple KAutoSaveFile generations for one managed file are all "
+ "found stale after a crash", "[nokde][autosave]")
+{
+#ifndef Q_OS_UNIX
+ SUCCEED("crash-style stale lock test is Unix-only");
+#else
+ //Simulates QETProject's rotating crash-recovery generations
+ //(BackupGenerations snapshots written round-robin): several
+ //KAutoSaveFile instances sharing one managed file, alive at once.
+ QTemporaryDir data_home;
+ REQUIRE(data_home.isValid());
+
+ qputenv("XDG_DATA_HOME", QFile::encodeName(data_home.path()));
+ QCoreApplication::setOrganizationName(QStringLiteral("QElectroTech"));
+ QCoreApplication::setApplicationName(
+ QStringLiteral("KAutoSaveFileGenerationsTest"));
+
+ const auto managed_path = data_home.filePath(QStringLiteral("project.qet"));
+ QFile managed_file(managed_path);
+ REQUIRE(managed_file.open(QIODevice::WriteOnly | QIODevice::Text));
+ REQUIRE(managed_file.write("\n") > 0);
+ managed_file.close();
+
+ constexpr int generations = 3;
+
+ int ready_pipe[2] = {-1, -1};
+ REQUIRE(pipe(ready_pipe) == 0);
+
+ const auto child_pid = fork();
+ REQUIRE(child_pid >= 0);
+
+ if (child_pid == 0) {
+ close(ready_pipe[0]);
+
+ std::vector> backups;
+ for (int i = 0; i < generations; ++i) {
+ auto backup = std::make_unique(
+ QUrl::fromLocalFile(managed_path));
+ if (!backup->open(QIODevice::WriteOnly
+ | QIODevice::Truncate
+ | QIODevice::Text)) {
+ _exit(2);
+ }
+ const QByteArray payload =
+ "\n";
+ if (backup->write(payload) != payload.size()) {
+ _exit(3);
+ }
+ if (!backup->flush()) {
+ _exit(4);
+ }
+ //Give each generation a distinct, increasing mtime.
+ struct timespec pause{0, 20 * 1000 * 1000};
+ nanosleep(&pause, nullptr);
+ backups.push_back(std::move(backup));
+ }
+
+ const char ready = '1';
+ if (write(ready_pipe[1], &ready, 1) != 1) {
+ _exit(5);
+ }
+ close(ready_pipe[1]);
+
+ for (;;) {
+ pause();
+ }
+ }
+
+ close(ready_pipe[1]);
+ char ready = 0;
+ REQUIRE(read(ready_pipe[0], &ready, 1) == 1);
+ close(ready_pipe[0]);
+ REQUIRE(ready == '1');
+
+ REQUIRE(kill(child_pid, SIGKILL) == 0);
+ int status = 0;
+ REQUIRE(waitpid(child_pid, &status, 0) == child_pid);
+ REQUIRE(WIFSIGNALED(status));
+ REQUIRE(WTERMSIG(status) == SIGKILL);
+
+ auto stale_files = KAutoSaveFile::allStaleFiles();
+ REQUIRE(stale_files.size() == generations);
+
+ std::sort(stale_files.begin(), stale_files.end(),
+ [](KAutoSaveFile *a, KAutoSaveFile *b) {
+ return QFileInfo(*a).lastModified() < QFileInfo(*b).lastModified();
+ });
+
+ for (int i = 0; i < generations; ++i) {
+ std::unique_ptr stale_file(stale_files.at(i));
+ CHECK(stale_file->managedFile().path()
+ == QFileInfo(managed_path).absoluteFilePath());
+ REQUIRE(stale_file->open(QIODevice::ReadOnly | QIODevice::Text));
+ const QByteArray content = stale_file->readAll();
+ CHECK(content.contains(
+ "generation n=\"" + QByteArray::number(i) + "\""));
+ }
+#endif
+}
diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt
index c8c159b15..1371aa6d6 100644
--- a/tests/qttest/CMakeLists.txt
+++ b/tests/qttest/CMakeLists.txt
@@ -111,12 +111,35 @@ add_test(NAME tst_alignment COMMAND tst_alignment)
target_include_directories(tst_alignment PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_alignment PRIVATE Qt::Test)
+# terminalnamecheck.h is header-only: the terminal name rule applied by the
+# element editor on save and by --check-elements.
+add_executable(tst_terminalnamecheck tst_terminalnamecheck.cpp)
+add_test(NAME tst_terminalnamecheck COMMAND tst_terminalnamecheck)
+target_include_directories(tst_terminalnamecheck PRIVATE ${QET_DIR}/sources)
+target_link_libraries(tst_terminalnamecheck PRIVATE Qt::Test)
+
+# configprofile.h is header-only: which settings a saved configuration
+# profile carries, and how loading one replaces the live settings.
+add_executable(tst_configprofile tst_configprofile.cpp)
+add_test(NAME tst_configprofile COMMAND tst_configprofile)
+target_include_directories(tst_configprofile PRIVATE ${QET_DIR}/sources)
+target_link_libraries(tst_configprofile PRIVATE Qt::Test Qt::Gui)
+
# itemgroups.cpp keeps group membership and the selection rule apart from
# Diagram, so it is tested here on a plain scene of rectangles.
add_executable(tst_itemgroups tst_itemgroups.cpp ${QET_DIR}/sources/itemgroups.cpp)
add_test(NAME tst_itemgroups COMMAND tst_itemgroups)
target_include_directories(tst_itemgroups PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_itemgroups PRIVATE Qt::Test Qt::Widgets Qt::Xml)
+
+# textanchor.h is header-only: the point of a text chosen by its alignment,
+# shown as Position X/Y in the text properties (#1155).
+add_executable(tst_textanchor tst_textanchor.cpp)
+add_test(NAME tst_textanchor COMMAND tst_textanchor)
+set_tests_properties(tst_textanchor PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
+target_include_directories(tst_textanchor PRIVATE ${QET_DIR}/sources)
+target_link_libraries(tst_textanchor PRIVATE Qt::Test Qt::Widgets)
+
# legacyelementtypes.h is header-only: the rewrite of element type names in
# nomenclature queries saved before June 2022.
add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp)
diff --git a/tests/qttest/tst_configprofile.cpp b/tests/qttest/tst_configprofile.cpp
new file mode 100644
index 000000000..e005e6f8d
--- /dev/null
+++ b/tests/qttest/tst_configprofile.cpp
@@ -0,0 +1,100 @@
+#include
+#include
+
+#include "utils/configprofile.h"
+
+class tst_configprofile : public QObject
+{
+ Q_OBJECT
+
+ QTemporaryDir m_dir;
+
+ QString path(const QString &name) const { return m_dir.filePath(name); }
+
+private slots:
+ void localKeys()
+ {
+ QVERIFY(ConfigProfile::isLocalKey("diagrameditor/geometry"));
+ QVERIFY(ConfigProfile::isLocalKey("elementeditor/state"));
+ QVERIFY(ConfigProfile::isLocalKey("projects-recentfiles/file1"));
+ QVERIFY(ConfigProfile::isLocalKey(ConfigProfile::marker_key));
+ QVERIFY(!ConfigProfile::isLocalKey("diagrameditor/Xgrid"));
+ QVERIFY(!ConfigProfile::isLocalKey("shortcuts/mainwindow.fullscreen"));
+ QVERIFY(!ConfigProfile::isLocalKey("masterpropertieswidget/plc-table-header-state"));
+ }
+
+ // Export leaves out window layout and recent files, and marks the file.
+ void exportLeavesOutLocalKeys()
+ {
+ QSettings live(path("live1.conf"), QSettings::IniFormat);
+ live.setValue("diagrameditor/Xgrid", 7);
+ live.setValue("lang", "de");
+ live.setValue("diagrameditor/geometry", QByteArray("xyz"));
+ live.setValue("projects-recentfiles/file1", "/home/a/b.qet");
+
+ QSettings file(path("profile1.conf"), QSettings::IniFormat);
+ QCOMPARE(ConfigProfile::exportTo(live, file), 2);
+ QVERIFY(ConfigProfile::isProfile(file));
+ QCOMPARE(file.value("diagrameditor/Xgrid").toInt(), 7);
+ QVERIFY(!file.contains("diagrameditor/geometry"));
+ QVERIFY(!file.contains("projects-recentfiles/file1"));
+ }
+
+ // Import replaces every setting, removes the ones the profile does not
+ // have, keeps window layout and recent files, and never copies the marker.
+ void importReplacesButKeepsLocal()
+ {
+ QSettings file(path("profile2.conf"), QSettings::IniFormat);
+ file.setValue("diagrameditor/Xgrid", 5);
+ file.setValue("diagrameditor/geometry", QByteArray("from-file"));
+ file.setValue(ConfigProfile::marker_key, 1);
+
+ QSettings live(path("live2.conf"), QSettings::IniFormat);
+ live.setValue("diagrameditor/Xgrid", 10);
+ live.setValue("lang", "fr");
+ live.setValue("diagrameditor/geometry", QByteArray("mine"));
+ live.setValue("projects-recentfiles/file1", "/home/a/b.qet");
+
+ ConfigProfile::importFrom(file, live);
+ QCOMPARE(live.value("diagrameditor/Xgrid").toInt(), 5);
+ QVERIFY(!live.contains("lang"));
+ QCOMPARE(live.value("diagrameditor/geometry").toByteArray(), QByteArray("mine"));
+ QCOMPARE(live.value("projects-recentfiles/file1").toString(), QString("/home/a/b.qet"));
+ QVERIFY(!live.contains(ConfigProfile::marker_key));
+ }
+
+ // Values survive the round trip with their types, including arrays.
+ void roundTrip()
+ {
+ QSettings a(path("a.conf"), QSettings::IniFormat);
+ a.setValue("print/default/fitinpage", true);
+ a.setValue("diagrameditor/sheet_background_color", QColor(12, 34, 56));
+ a.beginWriteArray("diagrameditor/defaultguides", 2);
+ a.setArrayIndex(0); a.setValue("pos", 100);
+ a.setArrayIndex(1); a.setValue("pos", 250);
+ a.endArray();
+
+ QSettings file(path("profile3.conf"), QSettings::IniFormat);
+ ConfigProfile::exportTo(a, file);
+ QSettings b(path("b.conf"), QSettings::IniFormat);
+ b.setValue("stray", 1);
+ ConfigProfile::importFrom(file, b);
+
+ QStringList ka = a.allKeys(), kb = b.allKeys();
+ ka.sort(); kb.sort();
+ QCOMPARE(kb, ka);
+ for (const QString &k : std::as_const(ka))
+ QCOMPARE(b.value(k), a.value(k));
+ }
+
+ void plainFileIsNotAProfile()
+ {
+ QSettings other(path("other.conf"), QSettings::IniFormat);
+ other.setValue("some/key", 1);
+ QVERIFY(!ConfigProfile::isProfile(other));
+ }
+};
+
+QTEST_GUILESS_MAIN(tst_configprofile)
+
+#include "tst_configprofile.moc"
diff --git a/tests/qttest/tst_terminalnamecheck.cpp b/tests/qttest/tst_terminalnamecheck.cpp
new file mode 100644
index 000000000..cb4a084bf
--- /dev/null
+++ b/tests/qttest/tst_terminalnamecheck.cpp
@@ -0,0 +1,55 @@
+#include
+
+#include "editor/terminalnamecheck.h"
+
+class tst_terminalnamecheck : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void uniqueNamesPass()
+ {
+ QVERIFY(TerminalNameCheck::repeatedNames({"A1", "A2", "13", "14"}).isEmpty());
+ }
+
+ // Shelly Pro 2PM in the shipped collection: three terminals named N.
+ void repeatedNamesAreCounted()
+ {
+ const auto repeated = TerminalNameCheck::repeatedNames(
+ {"L", "N", "O1", "N", "O2", "N", "L"});
+ QCOMPARE(repeated.size(), 2);
+ QCOMPARE(repeated.at(0), qMakePair(QString("L"), 2));
+ QCOMPARE(repeated.at(1), qMakePair(QString("N"), 3));
+ QCOMPARE(TerminalNameCheck::describe(repeated),
+ QString::fromUtf8("L ×2, N ×3"));
+ }
+
+ void surroundingSpacesAreIgnored()
+ {
+ QCOMPARE(TerminalNameCheck::repeatedNames({"PE", " PE "}).size(), 1);
+ }
+
+ void caseMatters()
+ {
+ QVERIFY(TerminalNameCheck::repeatedNames({"n", "N"}).isEmpty());
+ }
+
+ // Unnamed terminals are reported as unnamed, never as a repeat of "".
+ void unnamedAreNotRepeats()
+ {
+ const QStringList names{"", " ", "1", ""};
+ QVERIFY(TerminalNameCheck::repeatedNames(names).isEmpty());
+ QCOMPARE(TerminalNameCheck::unnamedCount(names), 3);
+ }
+
+ // describe() goes into a tr() message with %1: a name holding %2 stays literal.
+ void describeKeepsPercentLiteral()
+ {
+ QCOMPARE(TerminalNameCheck::describe({qMakePair(QString("%2"), 2)}),
+ QString::fromUtf8("%2 ×2"));
+ }
+};
+
+QTEST_GUILESS_MAIN(tst_terminalnamecheck)
+
+#include "tst_terminalnamecheck.moc"
diff --git a/tests/qttest/tst_textanchor.cpp b/tests/qttest/tst_textanchor.cpp
new file mode 100644
index 000000000..9c47a8f15
--- /dev/null
+++ b/tests/qttest/tst_textanchor.cpp
@@ -0,0 +1,135 @@
+/*
+ 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
+#include
+
+#include "textanchor.h"
+
+static bool samePoint(const QPointF &a, const QPointF &b)
+{
+ return qAbs(a.x() - b.x()) < 1e-9 && qAbs(a.y() - b.y()) < 1e-9;
+}
+
+class tst_textanchor : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ // The texts of issue #1155: Position X/Y is the point chosen by the
+ // alignment, so a right-aligned label keeps its right edge at X.
+ void pointOfTheText_data()
+ {
+ QTest::addColumn("alignment");
+ QTest::addColumn("fx"); // fraction of the width
+ QTest::addColumn("fy"); // fraction of the height
+
+ QTest::newRow("top left") << int(Qt::AlignTop | Qt::AlignLeft) << 0.0 << 0.0;
+ QTest::newRow("top right") << int(Qt::AlignTop | Qt::AlignRight) << 1.0 << 0.0;
+ QTest::newRow("centre") << int(Qt::AlignVCenter | Qt::AlignHCenter) << 0.5 << 0.5;
+ QTest::newRow("middle left") << int(Qt::AlignVCenter | Qt::AlignLeft) << 0.0 << 0.5;
+ QTest::newRow("middle right") << int(Qt::AlignVCenter | Qt::AlignRight) << 1.0 << 0.5;
+ QTest::newRow("bottom centre") << int(Qt::AlignBottom | Qt::AlignHCenter) << 0.5 << 1.0;
+ }
+
+ void pointOfTheText()
+ {
+ QFETCH(int, alignment);
+ QFETCH(qreal, fx);
+ QFETCH(qreal, fy);
+
+ QGraphicsTextItem text(QStringLiteral("XT2:12"));
+ text.setPos(-52, -3);
+ const QRectF r = text.boundingRect();
+
+ const QPointF expected(-52 + fx * r.width(), -3 + fy * r.height());
+ QVERIFY(samePoint(TextAnchor::pos(&text, Qt::Alignment(alignment)), expected));
+ }
+
+ // Top-left alignment keeps today's numbers: the anchor is pos(),
+ // whatever the rotation or the rotation point.
+ void topLeftIsPos()
+ {
+ QGraphicsTextItem text(QStringLiteral("-K1"));
+ text.setPos(12.5, -40);
+ for (qreal angle : {0.0, 90.0, 180.0, 270.0, 30.0}) {
+ text.setRotation(angle);
+ for (bool centre : {false, true}) {
+ text.setTransformOriginPoint(centre ? text.boundingRect().center() : QPointF());
+ QVERIFY(samePoint(TextAnchor::pos(&text, Qt::AlignTop | Qt::AlignLeft), text.pos()));
+ }
+ }
+ }
+
+ // A rotated text: the anchor is the point of the text where it is
+ // drawn, e.g. the right edge of a text turned by 90° is below pos().
+ void rotatedTextAnchorIsDrawnPoint()
+ {
+ QGraphicsTextItem text(QStringLiteral("XT2:12"));
+ text.setPos(100, 100);
+ text.setRotation(90);
+ const QRectF r = text.boundingRect();
+
+ const QPointF anchor = TextAnchor::pos(&text, Qt::AlignTop | Qt::AlignRight);
+ QVERIFY(samePoint(anchor, text.mapToParent(QPointF(r.right(), 0))));
+ QVERIFY(samePoint(anchor, QPointF(100, 100 + r.right())));
+ }
+
+ // Typing an anchor puts that point of the text there, for every
+ // alignment, rotation and rotation point.
+ void itemPosForRoundTrips()
+ {
+ const QList horizontal {Qt::AlignLeft, Qt::AlignHCenter, Qt::AlignRight};
+ const QList vertical {Qt::AlignTop, Qt::AlignVCenter, Qt::AlignBottom};
+
+ QGraphicsTextItem text(QStringLiteral("Right."));
+ text.setPos(-81, -13);
+ const QPointF anchor(-52, -3);
+
+ for (Qt::Alignment h : horizontal)
+ for (Qt::Alignment v : vertical)
+ for (qreal angle : {0.0, 90.0, 270.0, 45.0})
+ for (bool centre : {false, true}) {
+ text.setRotation(angle);
+ text.setTransformOriginPoint(centre ? text.boundingRect().center() : QPointF());
+ text.setPos(TextAnchor::itemPosFor(&text, h | v, anchor));
+ QVERIFY2(samePoint(TextAnchor::pos(&text, h | v), anchor),
+ qPrintable(QStringLiteral("h %1 v %2 angle %3 centre %4")
+ .arg(int(h)).arg(int(v)).arg(angle).arg(centre)));
+ }
+ }
+
+ // A longer text with the same anchor keeps its right edge in place:
+ // the gap between a label and its symbol does not change (#1155).
+ void rightEdgeStaysWhenTextGrows()
+ {
+ QGraphicsTextItem text(QStringLiteral("XT2:2"));
+ const QPointF anchor(-10, 0);
+ const Qt::Alignment right = Qt::AlignTop | Qt::AlignRight;
+ text.setPos(TextAnchor::itemPosFor(&text, right, anchor));
+ const qreal short_left = text.pos().x();
+
+ text.setPlainText(QStringLiteral("XT2:12"));
+ text.setPos(TextAnchor::itemPosFor(&text, right, anchor));
+
+ QVERIFY(text.pos().x() < short_left);
+ QCOMPARE(text.pos().x() + text.boundingRect().right(), anchor.x());
+ }
+};
+
+QTEST_MAIN(tst_textanchor)
+#include "tst_textanchor.moc"