Merge upstream/master into prefix-button

Only one conflict: the include block of
sources/ui/configpage/generalconfigurationpage.cpp, where upstream adds
editor/terminalnamecheck.h next to the two includes of the prefix
editor. All three are kept. The file list in
cmake/qet_compilation_vars.cmake merged itself this time.
This commit is contained in:
Kellermorph
2026-09-30 11:10:02 +02:00
93 changed files with 2001 additions and 71 deletions
+4
View File
@@ -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
+1 -1
View File
@@ -1936,7 +1936,7 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
<source>
&lt;Shift&gt; to move</source>
<translation>
&lt;Shift&gt; to move</translation>
Hold Shift and drag to move this text on its own</translation>
</message>
</context>
<context>
+1 -1
View File
@@ -1935,7 +1935,7 @@ Remarque: Ces options n&apos;autorisent ou bloquent l&apos;auto numérotation, s
<source>
&lt;Shift&gt; to move</source>
<translation>
&lt;Shift&gt; pour déplacer</translation>
Maintenir Maj et faire glisser pour déplacer ce texte seul</translation>
</message>
</context>
<context>
@@ -18,6 +18,9 @@
#include "elementcollectionitem.h"
#include <QCoreApplication>
#include <QThread>
/**
@brief ElementCollectionItem::ElementCollectionItem
Constructor
@@ -247,6 +250,73 @@ QList<ElementCollectionItem *> 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();
}
@@ -18,6 +18,7 @@
#ifndef ELEMENTCOLLECTIONITEM2_H
#define ELEMENTCOLLECTIONITEM2_H
#include <QHash>
#include <QStandardItem>
/**
@@ -56,6 +57,16 @@ class ElementCollectionItem : public QStandardItem
QList<ElementCollectionItem *> elementsChild() const;
QList<ElementCollectionItem *> directoriesChild() const;
QList<ElementCollectionItem *> 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<int, QVariant> m_deferred_data;
};
void setUpData(ElementCollectionItem *eci);
@@ -312,8 +312,15 @@ void ElementsCollectionModel::loadCollections(bool common_collection,
this, &ElementsCollectionModel::loadingProgressValueChanged);
connect(watcher, &QFutureWatcher<void>::progressRangeChanged,
this, &ElementsCollectionModel::loadingProgressRangeChanged);
connect(watcher, &QFutureWatcher<void>::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<void>::finished, this, [this]()
{
for (ElementCollectionItem *eci : std::as_const(m_items_list_to_setUp))
eci->applyDeferredData();
emit loadingFinished();
});
connect(watcher, &QFutureWatcher<void>::finished, watcher, &QFutureWatcher<void>::deleteLater);
@@ -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))
@@ -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<ElementSearchHit> rankedSearch(const QString &text,
const QModelIndex &within = QModelIndex());
@@ -20,12 +20,14 @@
#include <QDate>
#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));
+2
View File
@@ -19,6 +19,7 @@
#include "ui_namelistdialog.h"
#include "namelistwidget.h"
#include "../../qet.h"
#include <QPushButton>
#include <QMessageBox>
@@ -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);
@@ -18,6 +18,8 @@
#ifndef PROPERTIESEDITORDIALOG_H
#define PROPERTIESEDITORDIALOG_H
#include "../qet.h"
#include <QDialog>
#include <QDialogButtonBox>
#include <QVBoxLayout>
@@ -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,
@@ -21,6 +21,7 @@
#include "../../qetinformation.h"
#include "ui_replaceadvanceddialog.h"
#include "../../qet.h"
#include <QAbstractButton>
/**
@@ -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_));
@@ -20,6 +20,7 @@
#include "../searchandreplaceworker.h"
#include "ui_replaceconductordialog.h"
#include "../../qet.h"
#include <QColorDialog>
#include <QPainter>
@@ -37,6 +38,7 @@ ReplaceConductorDialog::ReplaceConductorDialog(
ui(new Ui::ReplaceConductorDialog)
{
ui->setupUi(this);
QET::trackDialogGeometry(this);
initWidget();
setProperties(properties);
}
@@ -24,6 +24,7 @@
#include "replaceelementdialog.h"
#include "ui_replaceelementdialog.h"
#include "../../qet.h"
#include <QAbstractButton>
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);
}
@@ -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()
@@ -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();
}
@@ -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);
}
/**
@@ -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();
});
@@ -17,6 +17,7 @@
*/
#include "renumberelementsdialog.h"
#include "../../qet.h"
#include <QComboBox>
#include <QDialogButtonBox>
@@ -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
+24 -1
View File
@@ -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;
}
+3
View File
@@ -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
+15 -3
View File
@@ -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)
+188 -1
View File
@@ -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 <QApplication>
#include <QDropEvent>
#include <QPainter>
#include <QPointer>
#include <QSet>
#include <QUuid>
#include <algorithm>
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<QAction *> DiagramView::contextMenuActions() const
QList<QAction *> list;
if (QETDiagramEditor *qde = diagramEditor())
{
if (m_diagram->selectedItems().isEmpty())
const QList<QGraphicsItem *> 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<QAction *> 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<Element *>(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<QGraphicsItem *> selected_items = m_diagram->selectedItems();
for (QGraphicsItem *qgi : selected_items)
{
const Element *element = qgraphicsitem_cast<Element *>(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
*/
+2
View File
@@ -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
+44
View File
@@ -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();
}
/**
+7
View File
@@ -92,6 +92,8 @@ class ElementScene : public QGraphicsScene
QPointer<CustomElementGraphicPart> 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;
+13
View File
@@ -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();
}
@@ -19,6 +19,7 @@
#include "../../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../../qetapp.h"
#include "../../textanchor.h"
#include "../elementscene.h"
#include "../../utils/qetutils.h"
#include <QApplication>
@@ -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;
@@ -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;
+91
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef TERMINALNAMECHECK_H
#define TERMINALNAMECHECK_H
#include <QHash>
#include <QList>
#include <QPair>
#include <QString>
#include <QStringList>
/**
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<QPair<QString, int>> repeatedNames(const QStringList &names)
{
QHash<QString, int> 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<QPair<QString, int>> 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<QPair<QString, int>> &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
+10 -5
View File
@@ -134,8 +134,8 @@ QList<CustomElementPart*> 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);
}
@@ -22,6 +22,7 @@
#include "../../qetinformation.h"
#include "ui_elementpropertieseditorwidget.h"
#include "../../qet.h"
#include <QItemDelegate>
#include <QComboBox>
#include <QSpinBox>
@@ -95,6 +96,7 @@ ElementPropertiesEditorWidget::ElementPropertiesEditorWidget(ElementData data, Q
m_data(data)
{
ui->setupUi(this);
QET::trackDialogGeometry(this);
setUpInterface();
upDateInterface();
}
+93
View File
@@ -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 <QFileDialog>
#include <QSvgGenerator>
#include <QHBoxLayout>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
/**
* @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<PartTerminal *> terminals;
QStringList names;
for (auto qgi : m_elmt_scene -> items()) {
if (auto terminal = qgraphicsitem_cast<PartTerminal *>(qgi)) {
terminals << terminal;
names << terminal -> terminalName();
}
}
const auto repeated = TerminalNameCheck::repeatedNames(names);
if (!repeated.isEmpty())
{
errors << qMakePair (tr("Noms de bornes en double"),
tr("<br><b>Erreur</b> :"
"<br>Plusieurs bornes portent le même nom : %1."
"<br><b>Solution</b> :"
"<br>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("<br>%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);
}
/**
+2
View File
@@ -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);
}
/**
+1
View File
@@ -41,6 +41,7 @@ ElementsCategoryEditor::ElementsCategoryEditor(const ElementsLocation &location,
m_location(location)
{
setUpWidget();
QET::trackDialogGeometry(this);
if (m_location.isElement()) {
QET::QetMessageBox::warning(this,
+3
View File
@@ -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);
}
/**
+2
View File
@@ -22,6 +22,7 @@
#include "../../utils/qetutils.h"
#include "ui_addtabledialog.h"
#include "../../qet.h"
#include <QFontDialog>
/**
@@ -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;
@@ -17,6 +17,7 @@
*/
#include "diagnosticsreportdialog.h"
#include "../../qet.h"
#include "../../qetmessagebox.h"
#include <QDialogButtonBox>
@@ -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()
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#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);
}
/**
+22
View File
@@ -26,6 +26,7 @@
#include <QColorDialog>
#include <QGraphicsSceneContextMenuEvent>
#include <QAction>
#include <QDialog>
#include <QFileInfo>
#include <QSaveFile>
#include <QSettings>
@@ -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"
+13
View File
@@ -24,6 +24,7 @@
#include <QObject>
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)
+135 -26
View File
@@ -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 <iostream>
#define QUOTE(x) STRINGIFY(x)
#define STRINGIFY(x) #x
#include <QFileDialog>
#include <QFileInfo>
#include <QFontDatabase>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QSettings>
#include <QStyleFactory>
#include <QStyleHints>
#ifdef BUILD_WITHOUT_KF
@@ -65,6 +69,9 @@
#else
# include <KAutoSaveFile>
#endif
#include "ui/backuprestoredialog.h"
#include <algorithm>
#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<KAutoSaveFile *> &stale_files)
{
QString text;
if(stale_files.size() == 1) {
text.append(tr("<b>Le fichier de restauration suivant a été trouvé,<br>"
"Voulez-vous l'ouvrir ?</b><br>"));
} else {
text.append(tr("<b>Les fichiers de restauration suivant on été trouvé,<br>"
"Voulez-vous les ouvrir ?</b><br>"));
//Group the snapshots by the project they recover, newest first.
QHash<QString, QList<KAutoSaveFile *>> 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("<br>" + kasf->managedFile().path().remove(0,1));
# else
text.append("<br>" + 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<KAutoSaveFile *> 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
+4
View File
@@ -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);
+11
View File
@@ -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.
+1
View File
@@ -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<KAutoSaveFile *> backup_files);
+8 -3
View File
@@ -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;
@@ -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;
}
}
@@ -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
@@ -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;
+13
View File
@@ -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);
+2
View File
@@ -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
+18 -9
View File
@@ -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);
});
}
+9 -1
View File
@@ -39,6 +39,8 @@
#include <QSet>
#include <QFuture>
#include <array>
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<bool> m_backup_future;
KAutoSaveFile m_backup_file;
/// Crash-recovery snapshots, written in turn by writeBackup()
std::array<KAutoSaveFile, BackupGenerations> m_backup_files;
int m_next_backup_slot = 0;
QUuid m_uuid = QUuid::createUuid();
QHash<QString, int> m_derived_uuid_keys;
QSet<QUuid> m_saved_item_uuids; //symbol and wire uuids the file carries, see derivedItemUuid()
+3
View File
@@ -48,6 +48,7 @@
#include "richtexteditor_p.h"
#include "ui_addlinkdialog.h"
#include "../shortcutmanager.h"
#include "../qet.h"
//#include <QtDesigner/QDesignerFormEditorInterface>
@@ -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()
+82
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef TEXTANCHOR_H
#define TEXTANCHOR_H
#include <QGraphicsItem>
#include <QPointF>
#include <QRectF>
/**
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
+3
View File
@@ -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);
}
/**
+1
View File
@@ -34,6 +34,7 @@ AboutQETDialog::AboutQETDialog(QWidget *parent) :
ui(new Ui::AboutQETDialog)
{
ui->setupUi(this);
QET::trackDialogGeometry(this);
setAbout();
setAuthors();
setTranslators();
+3
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "aiassistantdialog.h"
#include "../qet.h"
#include <QApplication>
#include <QCheckBox>
@@ -157,6 +158,8 @@ AiAssistantDialog::AiAssistantDialog(QWidget *parent) :
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
refresh();
QET::trackDialogGeometry(this);
}
void AiAssistantDialog::refresh()
+2
View File
@@ -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);
+142
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "backuprestoredialog.h"
#ifdef BUILD_WITHOUT_KF
# include "nokde/kautosavefile.h"
#else
# include <KAutoSaveFile>
#endif
#include <QComboBox>
#include <QDateTime>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QGridLayout>
#include <QLabel>
#include <QLocale>
#include <QVBoxLayout>
/**
@brief BackupRestoreDialog::BackupRestoreDialog
@param groups : managed project path -> its recovery generations
(newest-first)
@param parent : parent widget
*/
BackupRestoreDialog::BackupRestoreDialog(
const QHash<QString, QList<KAutoSaveFile *>> &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("<b>Des fichiers de restauration ont été trouvés,<br>"
"voulez-vous les ouvrir ?</b><br>"
"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<KAutoSaveFile *> &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<KAutoSaveFile *> BackupRestoreDialog::selectedFiles() const
{
QList<KAutoSaveFile *> 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<KAutoSaveFile *> BackupRestoreDialog::discardedFiles() const
{
QList<KAutoSaveFile *> 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<KAutoSaveFile *> &generations = it.value();
for (int i = 0; i < generations.size(); ++i) {
if (i != index) {
discarded << generations.at(i);
}
}
}
return discarded;
}
+60
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef BACKUPRESTOREDIALOG_H
#define BACKUPRESTOREDIALOG_H
#include <QDialog>
#include <QHash>
#include <QList>
#include <QString>
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<QString, QList<KAutoSaveFile *>> &groups,
QWidget *parent = nullptr);
~BackupRestoreDialog() override;
/// Valid once accepted: one entry per project, the chosen generation
/// (defaults to the most recent one).
QList<KAutoSaveFile *> selectedFiles() const;
/// The generations the user did not pick; the caller should discard
/// (release + delete) these.
QList<KAutoSaveFile *> discardedFiles() const;
private:
QHash<QString, QList<KAutoSaveFile *>> m_groups;
QHash<QString, QComboBox *> m_combo_for_path;
};
#endif // BACKUPRESTOREDIALOG_H
+2
View File
@@ -24,6 +24,7 @@
#include "../qetproject.h"
#include "ui_bomexportdialog.h"
#include "../qet.h"
#include <QMessageBox>
#include <QSqlError>
#include <QSqlQueryModel>
@@ -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);
+3
View File
@@ -5,6 +5,7 @@
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/element.h"
#include "../qetinformation.h"
#include "../qet.h"
#include "ui_compositetexteditdialog.h"
#include <utility>
@@ -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"));
+6 -4
View File
@@ -26,6 +26,7 @@
#include "../projectview.h"
#include "../qetgraphicsitem/conductor.h"
#include "../conductorproperties.h"
#include "../utils/colordialogdoubleclick.h"
#include <QColorDialog>
#include <QMenu>
@@ -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());
}
}
+2
View File
@@ -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);
@@ -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());
@@ -1250,6 +1250,19 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="m_check_terminal_names_cb">
<property name="toolTip">
<string>À l'enregistrement d'un élément : refuser deux bornes portant le même nom, et signaler les bornes sans nom (IEC 61666).</string>
</property>
<property name="text">
<string>Vérifier les noms des bornes à l'enregistrement</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_6">
<property name="orientation">
@@ -1285,6 +1298,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
<tabstop>m_custom_tbt_path_cb</tabstop>
<tabstop>m_highlight_integrated_elements</tabstop>
<tabstop>m_default_elements_info</tabstop>
<tabstop>m_check_terminal_names_cb</tabstop>
<tabstop>m_lang_cb</tabstop>
<tabstop>m_dyn_text_font_pb</tabstop>
<tabstop>m_dyn_text_rotation_sb</tabstop>
@@ -17,6 +17,8 @@
*/
#include "contactgroupselectiondialog.h"
#include "../qet.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTableWidget>
@@ -207,6 +209,8 @@ ContactGroupSelectionDialog::ContactGroupSelectionDialog(
accept();
}
});
QET::trackDialogGeometry(this);
}
int ContactGroupSelectionDialog::selectedIndex() const
+2
View File
@@ -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)
{
+2
View File
@@ -18,6 +18,7 @@
#include "dialogwaiting.h"
#include "ui_dialogwaiting.h"
#include "../qet.h"
#include <QPushButton>
@@ -31,6 +32,7 @@ DialogWaiting::DialogWaiting(QWidget *parent) :
ui(new Ui::DialogWaiting)
{
ui->setupUi(this);
QET::trackDialogGeometry(this);
setTitle( "..." );
setDetail( "..." );
}
+3
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "duplicateoffsetdialog.h"
#include "../qet.h"
#include <QComboBox>
#include <QDialogButtonBox>
@@ -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
+15 -7
View File
@@ -303,7 +303,7 @@ QList<QStandardItem *> 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<QStandardItem *> 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<Qt::Alignment>();
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;
}
+2
View File
@@ -18,6 +18,7 @@
#include "formulaassistantdialog.h"
#include "ui_formulaassistantdialog.h"
#include "../qet.h"
#include <QPushButton>
FormulaAssistantDialog::FormulaAssistantDialog(QWidget *parent) :
@@ -25,6 +26,7 @@ FormulaAssistantDialog::FormulaAssistantDialog(QWidget *parent) :
ui(new Ui::FormulaAssistantDialog)
{
ui->setupUi(this);
QET::trackDialogGeometry(this);
}
FormulaAssistantDialog::~FormulaAssistantDialog()
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "imagetransparentcolordialog.h"
#include "../qet.h"
#include <QDialogButtonBox>
#include <QGridLayout>
@@ -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);
}
/**
+2
View File
@@ -17,6 +17,7 @@
*/
#include "importelementdialog.h"
#include "ui_importelementdialog.h"
#include "../qet.h"
#include <QButtonGroup>
ImportElementDialog::ImportElementDialog(QWidget *parent) :
@@ -24,6 +25,7 @@ ImportElementDialog::ImportElementDialog(QWidget *parent) :
ui(new Ui::ImportElementDialog)
{
ui->setupUi(this);
QET::trackDialogGeometry(this);
setUpWidget();
}
@@ -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()
+3
View File
@@ -20,6 +20,7 @@
#include "../diagram.h"
#include "../diagramview.h"
#include "../qetproject.h"
#include "../qet.h"
#include "../qetgraphicsitem/element.h"
#include <QEvent>
@@ -59,6 +60,8 @@ JumpToElementDialog::JumpToElementDialog(Diagram *diagram, QWidget *parent) :
buildCandidates();
updateFilteredList(QString());
m_filter_edit->setFocus();
QET::trackDialogGeometry(this);
}
JumpToElementDialog::~JumpToElementDialog()
+2
View File
@@ -18,6 +18,7 @@
#include "marginseditdialog.h"
#include "ui_marginseditdialog.h"
#include "../qet.h"
#include <QScopedPointer>
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());
-1
View File
@@ -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);
+2
View File
@@ -26,6 +26,7 @@
#include "../qetgraphicsitem/conductor.h"
#include "../ui_multipastedialog.h"
#include "../qet.h"
#include <QHash>
#include <QSettings>
@@ -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<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &MultiPasteDialog::updatePreview);
connect(ui->m_y_sb, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &MultiPasteDialog::updatePreview);
+3
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#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);
}
/**
+3
View File
@@ -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();
}
+3
View File
@@ -5,6 +5,7 @@
#include "../diagram.h"
#include "../qetgraphicsitem/element.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "../qet.h"
#include <QUndoCommand>
#include <QCheckBox>
#include <QVBoxLayout>
@@ -67,6 +68,8 @@ TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *pr
m_stripCheckboxes.insert(prefix, cb);
}
}
QET::trackDialogGeometry(this);
}
/**
@@ -18,6 +18,7 @@
#include "thirdpartybinaryinstalldialog.h"
#include "ui_thirdpartybinaryinstalldialog.h"
#include "../qet.h"
#include <QDialogButtonBox>
#include <QPushButton>
#include <QDesktopServices>
@@ -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](){
+3
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#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);
}
+77
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "colordialogdoubleclick.h"
#include <QColorDialog>
#include <QEvent>
#include <QMouseEvent>
#include <QWidget>
/**
@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<QWidget *>();
for (QWidget *child : children) {
if (child->inherits("QtPrivate::QWellArray")
|| child->inherits("QWellArray")) {
child->installEventFilter(this);
}
}
}
else if (event->type() == QEvent::MouseButtonDblClick
&& static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton)
{
QMetaObject::invokeMethod(m_dialog, &QDialog::accept, Qt::QueuedConnection);
}
return QObject::eventFilter(watched, event);
}
+50
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef COLOR_DIALOG_DOUBLE_CLICK_H
#define COLOR_DIALOG_DOUBLE_CLICK_H
#include <QObject>
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
+98
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef CONFIGPROFILE_H
#define CONFIGPROFILE_H
#include <QSettings>
#include <QString>
#include <QStringList>
/**
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
+105
View File
@@ -3,14 +3,18 @@
#include <catch2/catch.hpp>
#include <QCoreApplication>
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include <QTemporaryDir>
#include <QUrl>
#include <algorithm>
#include <memory>
#include <vector>
#ifdef Q_OS_UNIX
#include <ctime>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
@@ -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("<project/>\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<std::unique_ptr<KAutoSaveFile>> backups;
for (int i = 0; i < generations; ++i) {
auto backup = std::make_unique<KAutoSaveFile>(
QUrl::fromLocalFile(managed_path));
if (!backup->open(QIODevice::WriteOnly
| QIODevice::Truncate
| QIODevice::Text)) {
_exit(2);
}
const QByteArray payload =
"<project><generation n=\"" + QByteArray::number(i)
+ "\" /></project>\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<KAutoSaveFile> 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
}
+23
View File
@@ -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)
+100
View File
@@ -0,0 +1,100 @@
#include <QtTest>
#include <QTemporaryDir>
#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"
+55
View File
@@ -0,0 +1,55 @@
#include <QtTest>
#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"
+135
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <QtTest>
#include <QGraphicsTextItem>
#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<int>("alignment");
QTest::addColumn<qreal>("fx"); // fraction of the width
QTest::addColumn<qreal>("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<Qt::Alignment> horizontal {Qt::AlignLeft, Qt::AlignHCenter, Qt::AlignRight};
const QList<Qt::Alignment> 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"