Merge master into feature/terminal-name-check

Resolve the tests/qttest/CMakeLists.txt conflict with tst_configprofile
by keeping both tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-30 15:04:37 +13:00
83 changed files with 1440 additions and 61 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ message("QET_DIR :" ${QET_DIR})
message("GIT_COMMIT_SHA :" ${GIT_COMMIT_SHA})
if(BUILD_WITH_KF)
message("KF_GIT_TAG :" ${KF_GIT_TAG})
message("KF_GIT_TAG :" ${KF_GIT_TAG})
else()
add_definitions(-DBUILD_WITHOUT_KF)
endif()
+4
View File
@@ -752,6 +752,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
@@ -863,6 +865,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
+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();
}
@@ -47,7 +47,7 @@ PartDynamicTextField::PartDynamicTextField(QETElementEditor *editor, QGraphicsIt
//Option when text is displayed in multiple line
QTextOption option = document() -> defaultTextOption();
option.setAlignment(Qt::AlignHCenter);
option.setAlignment(m_alignment & Qt::AlignHorizontal_Mask);
option.setWrapMode(QTextOption::WordWrap);
document() -> setDefaultTextOption(option);
}
@@ -462,6 +462,10 @@ void PartDynamicTextField::setPlainText(const QString &text) {
void PartDynamicTextField::setAlignment(Qt::Alignment alignment) {
m_alignment = alignment;
//Lines of a multi-line text follow the horizontal alignment
QTextOption option = document() -> defaultTextOption();
option.setAlignment(m_alignment & Qt::AlignHorizontal_Mask);
document() -> setDefaultTextOption(option);
emit alignmentChanged(m_alignment);
}
@@ -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();
}
+45
View File
@@ -60,6 +60,10 @@
#include <QFileDialog>
#include <QSvgGenerator>
#include <QHBoxLayout>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
/**
* @brief QETElementEditor::QETElementEditor
@@ -1186,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;
}
}
@@ -63,9 +63,17 @@ DynamicElementTextItem::DynamicElementTextItem(Element *parent_element) :
//Option when text is displayed in multiple line
QTextOption option = document()->defaultTextOption();
option.setAlignment(Qt::AlignHCenter);
option.setAlignment(alignment() & Qt::AlignHorizontal_Mask);
option.setWrapMode(QTextOption::WordWrap);
document()->setDefaultTextOption(option);
//Lines of a multi-line text follow the horizontal alignment
connect(this, &DiagramTextItem::alignmentChanged, [this](Qt::Alignment alignment)
{
QTextOption option = document()->defaultTextOption();
option.setAlignment(alignment & Qt::AlignHorizontal_Mask);
document()->setDefaultTextOption(option);
});
}
DynamicElementTextItem::~DynamicElementTextItem()
+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()
+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);
@@ -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
+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
}
+7
View File
@@ -118,6 +118,13 @@ 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)
+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"