Merge remote-tracking branch 'upstream/master' into proto-conductor-selection-properties

# Conflicts:
#	cmake/qet_compilation_vars.cmake
#	lang/qet_en.ts
#	sources/qetdiagrameditor.cpp
This commit is contained in:
ispyisail
2026-09-27 09:07:41 +13:00
897 changed files with 351648 additions and 61060 deletions
@@ -143,8 +143,12 @@ ElementsLocation ECHSFileToFile::copyElement(ElementsLocation &source, ElementsL
//On windows when user drag and drop an element from the common elements collection
//to the custom elements collection, the element file stay in read only mode, and so
//user can't save the element
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
QNtfsPermissionCheckGuard ntfs_guard;
#else
extern Q_CORE_EXPORT int qt_ntfs_permission_lookup;
qt_ntfs_permission_lookup++;
#endif
QFile file(destination.fileSystemPath() % "/" % new_elmt_name);
if (!file.isWritable()) {
if (!file.setPermissions(file.permissions() | QFileDevice::WriteUser)) {
@@ -152,7 +156,9 @@ ElementsLocation ECHSFileToFile::copyElement(ElementsLocation &source, ElementsL
<< " in ECHSFileToFile::copyElement";
}
}
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
qt_ntfs_permission_lookup--;
#endif
#endif
return ElementsLocation (destination.fileSystemPath() % "/" % new_elmt_name);
}
@@ -469,7 +475,7 @@ bool ElementCollectionHandler::setNames(ElementsLocation &location,
root.appendChild(name_list.toXml(document));
QString filepath = location.fileSystemPath()
+ "/qet_directory";
% "/qet_directory";
if (!QET::writeXmlFile(document, filepath)) {
qDebug() << "ElementCollectionHandler::setNames : write qet-directory file failed";
return false;
@@ -0,0 +1,54 @@
/*
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 "elementpreviewdelegate.h"
#include <QWidget>
#include "../qetpalette.h"
/**
@brief ElementPreviewDelegate::initStyleOption
After the base class has filled the option from the model, replace
the icon with one that reads on a dark palette. The adapted icon is
built from the pixmap the view is about to draw, at the view's
decoration size and device pixel ratio.
*/
void ElementPreviewDelegate::initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const
{
QStyledItemDelegate::initStyleOption(option, index);
if (option->icon.isNull() || !QET::Palette::isDark(option->palette))
return;
const qint64 key = option->icon.cacheKey();
const auto it = m_dark_icons.constFind(key);
if (it != m_dark_icons.constEnd())
{
option->icon = *it;
return;
}
const qreal dpr = option->widget ? option->widget->devicePixelRatio() : 1.0;
const QPixmap source = option->icon.pixmap(option->decorationSize, dpr);
const QPixmap adapted = QET::Palette::forPalette(source, option->palette);
QIcon icon = option->icon;
if (adapted.cacheKey() != source.cacheKey())
icon = QIcon(adapted);
m_dark_icons.insert(key, icon);
option->icon = icon;
}
@@ -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 ELEMENTPREVIEWDELEGATE_H
#define ELEMENTPREVIEWDELEGATE_H
#include <QHash>
#include <QIcon>
#include <QStyledItemDelegate>
/**
@brief The ElementPreviewDelegate class
Draws the items of an element collection tree with icons that read on
the current palette. Element previews are black line art drawn for a
white sheet; on a dark palette this delegate hands the view the same
picture with its lightness inverted (QET::Palette::forPalette), so the
ink is light on the dark row. Colored icons, such as folders, are left
alone, and nothing changes on a light palette. Adapted icons are kept
per source icon, so a repaint costs a hash lookup.
*/
class ElementPreviewDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
protected:
void initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const override;
private:
mutable QHash<qint64, QIcon> m_dark_icons;
};
#endif
@@ -25,12 +25,8 @@
#include "xmlelementcollection.h"
#include "xmlprojectelementcollectionitem.h"
#include <QFutureWatcher>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
#include <QtConcurrentMap>
#else
#include <QtConcurrentRun>
#endif
/**
@brief ElementsCollectionModel::ElementsCollectionModel
@@ -42,6 +38,28 @@ ElementsCollectionModel::ElementsCollectionModel(QObject *parent) :
{
}
/**
@brief ElementsCollectionModel::~ElementsCollectionModel
Destructor. loadCollections() may still have background threads
(via QtConcurrent::map()) running setUpData() on this model's items
when the model is destroyed (e.g. the user cancels the dialog before
loading finishes). Wait for them here so QStandardItemModel's
destructor doesn't free items out from under them, which used to
crash the whole application (bugtracker #291).
*/
ElementsCollectionModel::~ElementsCollectionModel()
{
// Without cancel(), the wait below runs the whole queued
// QtConcurrent::map() to completion, so closing this dialog on a
// large collection blocks until every remaining item has been
// processed -- a visible hang on the button pressed precisely to
// stop the work. cancel() drops the not-yet-started items so the
// wait that follows (still needed, so an in-flight item can't
// dereference this object after it's gone) is short.
m_future.cancel();
m_future.waitForFinished();
}
/**
@brief ElementsCollectionModel::data
Reimplemented from QStandardItemModel
@@ -53,6 +71,7 @@ QVariant ElementsCollectionModel::data(const QModelIndex &index, int role) const
{
if (role == Qt::DecorationRole) {
QStandardItem *item = itemFromIndex(index);
if (!item) return QStandardItemModel::data(index, role);
if (item->type() == FileElementCollectionItem::Type)
static_cast<FileElementCollectionItem*>(item)->setUpIcon();
@@ -297,11 +316,8 @@ void ElementsCollectionModel::loadCollections(bool common_collection,
this, &ElementsCollectionModel::loadingFinished);
connect(watcher, &QFutureWatcher<void>::finished, watcher, &QFutureWatcher<void>::deleteLater);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
m_future = QtConcurrent::map(m_items_list_to_setUp, setUpData);
#else
qDebug() << "Help code for QT 6 or later";
#endif
watcher->setFuture(m_future);
}
@@ -323,7 +339,6 @@ void ElementsCollectionModel::loadMacrosCollection()
void ElementsCollectionModel::addMacrosCollection(bool set_data)
{
QString macrosPath = QETApp::userMacrosDir();
qDebug() << "=== MAKRO PFAD CHECK ===" << macrosPath;
if (macrosPath.endsWith("/")) {
macrosPath.remove(macrosPath.length() - 1, 1);
}
@@ -424,7 +439,7 @@ void ElementsCollectionModel::addLocation(const ElementsLocation& location)
collection_name);
}
}
// ANPASSUNG: Makros und Custom Collection werden hier behandelt!
// Macros and Custom Collection are handled here
else if (location.isCustomCollection() || location.isMacrosCollection()) {
QList <ElementCollectionItem *> child_list;
@@ -488,6 +503,10 @@ void ElementsCollectionModel::addProject(QETProject *project, bool set_data)
connect(project->embeddedElementCollection(),
&XmlElementCollection::directoryRemoved,
this, &ElementsCollectionModel::itemRemovedFromCollection);
connect(project, &QETProject::projectTitleChanged,
this, &ElementsCollectionModel::projectNameChanged);
connect(project, &QETProject::projectFilePathChanged,
this, &ElementsCollectionModel::projectNameChanged);
}
/**
@@ -519,6 +538,10 @@ void ElementsCollectionModel::removeProject(QETProject *project)
&XmlElementCollection::directoryRemoved,
this,
&ElementsCollectionModel::itemRemovedFromCollection);
disconnect(project, &QETProject::projectTitleChanged,
this, &ElementsCollectionModel::projectNameChanged);
disconnect(project, &QETProject::projectFilePathChanged,
this, &ElementsCollectionModel::projectNameChanged);
}
}
@@ -538,6 +561,16 @@ QList<QETProject *> ElementsCollectionModel::project() const
*/
void ElementsCollectionModel::highlightUnusedElement()
{
//Reset only the items currently highlighted in red, so elements that
//are no longer unused lose the highlight. Scoping to the red
//Dense4Pattern avoids touching other backgrounds (e.g. the amber
//"show this dir" highlight) and avoids needless updates on big
//collections (issue #159).
for (ElementCollectionItem *eci : items())
if (eci->background().style() == Qt::Dense4Pattern &&
eci->background().color() == Qt::red)
eci->setBackground(QBrush());
QList <ElementsLocation> unused;
foreach (QETProject *project, m_project_list)
@@ -630,7 +663,7 @@ QModelIndex ElementsCollectionModel::indexFromLocation(
if (eci->type() == FileElementCollectionItem::Type) {
if (FileElementCollectionItem *feci = static_cast<FileElementCollectionItem *>(eci)) {
// ANPASSUNG: Makro-Prüfung hinzugefügt, damit das Modell den Pfad im Baum findet!
// Macro check added so the model finds the path in the tree
if ( (location.isCommonCollection() && feci->isCommonCollection()) ||
(location.isCompanyCollection() && feci->isCompanyCollection()) ||
(location.isMacrosCollection() && feci->isMacrosCollection()) ||
@@ -749,3 +782,15 @@ void ElementsCollectionModel::updateItem(const QString& path)
eci->setUpData();
}
}
/**
@brief ElementsCollectionModel::projectNameChanged
Update the displayed name of the collection of project,
when its title or its file path changed.
@param project
*/
void ElementsCollectionModel::projectNameChanged(QETProject *project)
{
if (XmlProjectElementCollectionItem *xpeci = m_project_hash.value(project))
xpeci->updateProjectName();
}
@@ -35,6 +35,7 @@ class ElementsCollectionModel : public QStandardItemModel
public:
ElementsCollectionModel(QObject *parent = Q_NULLPTR);
~ElementsCollectionModel() override;
QVariant data(const QModelIndex &index, int role) const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
@@ -72,6 +73,7 @@ class ElementsCollectionModel : public QStandardItemModel
void elementIntegratedToCollection (const QString& path);
void itemRemovedFromCollection (const QString& path);
void updateItem (const QString& path);
void projectNameChanged (QETProject *project);
private:
QList <QETProject *> m_project_list;
@@ -19,6 +19,7 @@
#include "../editor/ui/qetelementeditor.h"
#include "../elementscategoryeditor.h"
#include "../import/edz/edzimporter.h"
#include "../newelementwizard.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
@@ -32,8 +33,14 @@
#include "fileelementcollectionitem.h"
#include "xmlprojectelementcollectionitem.h"
#include <QCheckBox>
#include <QDesktopServices>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QLabel>
#include <QMenu>
#include <QPushButton>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
@@ -158,6 +165,8 @@ void ElementsCollectionWidget::setUpAction()
tr("Nouveau dossier"), this);
m_new_element = new QAction(QET::Icons::ElementNew,
tr("Nouvel élément"), this);
m_import_edz = new QAction(QET::Icons::ElementNew,
tr("Importer une pièce EPLAN (.edz)…"), this);
m_show_this_dir = new QAction(QET::Icons::FolderOnlyThis,
tr("Afficher uniquement ce dossier"),
this);
@@ -247,6 +256,8 @@ void ElementsCollectionWidget::setUpConnection()
this, &ElementsCollectionWidget::newDirectory);
connect(m_new_element, &QAction::triggered,
this, &ElementsCollectionWidget::newElement);
connect(m_import_edz, &QAction::triggered,
this, &ElementsCollectionWidget::importEdz);
connect(m_show_this_dir, &QAction::triggered,
this, &ElementsCollectionWidget::showThisDir);
connect(m_show_all_dir, &QAction::triggered,
@@ -329,6 +340,7 @@ void ElementsCollectionWidget::customContextMenu(const QPoint &point)
{
if (!feci->isMacrosCollection()) {
m_context_menu->addAction(m_new_element);
m_context_menu->addAction(m_import_edz);
}
m_context_menu->addAction(m_new_directory);
if (!feci->isCollectionRoot())
@@ -599,6 +611,108 @@ void ElementsCollectionWidget::newElement()
&ElementsCollectionWidget::locationWasSaved);
}
/**
@brief ElementsCollectionWidget::confirmEdzImportTerms
Show the EPLAN (.edz) import warning dialog. The user must tick the
acknowledgement checkbox before the Import button is enabled.
@return true if the user accepted the terms, false otherwise.
*/
bool ElementsCollectionWidget::confirmEdzImportTerms()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Avertissement — Importation d'un fichier EPLAN (.edz)"));
QLabel *text = new QLabel(
tr("Le format .edz peut provenir de deux sources différentes :\n"
"\n"
"• Le portail EPLAN Data Portal (dataportal.eplan.com), soumis "
"aux conditions d'utilisation de l'environnement EPLAN Cloud ;\n"
"• Le site d'un fabricant de composants (ou d'un distributeur) "
"qui met ses fichiers .edz à disposition directement, selon ses "
"propres conditions.\n"
"\n"
"QElectroTech ne peut pas déterminer automatiquement l'origine "
"du fichier que vous importez, ni les conditions qui s'y "
"appliquent.\n"
"\n"
"En important ce fichier, vous confirmez que :\n"
"\n"
"• vous connaissez son origine et êtes autorisé à l'utiliser "
"dans ce contexte, au regard des conditions applicables à cette "
"source ;\n"
"• cette importation est effectuée à vos propres risques et "
"responsabilité ;\n"
"• ni QElectroTech, ni ses mainteneurs, ni ses contributeurs ne "
"peuvent être tenus responsables d'une utilisation non conforme "
"de ces données."),
&dialog);
text->setWordWrap(true);
QCheckBox *accept_box = new QCheckBox(
tr("J'ai lu et j'accepte ces conditions."), &dialog);
QDialogButtonBox *buttons = new QDialogButtonBox(
QDialogButtonBox::Cancel, &dialog);
QPushButton *import_button = buttons->addButton(
tr("Importer"), QDialogButtonBox::AcceptRole);
import_button->setDefault(true);
import_button->setEnabled(false);
connect(accept_box, &QCheckBox::toggled,
import_button, &QPushButton::setEnabled);
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
layout->addWidget(text);
layout->addWidget(accept_box);
layout->addWidget(buttons);
return dialog.exec() == QDialog::Accepted;
}
/**
@brief ElementsCollectionWidget::importEdz
Import an EPLAN Data Portal part (.edz) as a QET element into the directory
pointed at by the context menu.
*/
void ElementsCollectionWidget::importEdz()
{
ElementCollectionItem *eci = elementCollectionItemForIndex(
m_index_at_context_menu);
if (!eci || eci->type() != FileElementCollectionItem::Type) {
return;
}
FileElementCollectionItem *feci =
static_cast<FileElementCollectionItem*>(eci);
if (feci->isCommonCollection() || !feci->isDir()) {
return;
}
if (!confirmEdzImportTerms()) {
return;
}
const QString edz_path = QFileDialog::getOpenFileName(
this, tr("Importer une pièce EPLAN"), QString(),
tr("Pièces EPLAN (*.edz)"));
if (edz_path.isEmpty()) {
return;
}
EdzImporter importer;
if (!importer.importToDirectory(edz_path, feci->fileSystemPath())) {
QET::QetMessageBox::critical(
this, tr("Import EPLAN"),
tr("Impossible d'importer cette pièce :\n%1")
.arg(importer.errorString()));
return;
}
reload();
}
/**
@brief ElementsCollectionWidget::showThisDir
Hide all directories except the pointed dir;
@@ -611,7 +725,10 @@ void ElementsCollectionWidget::showThisDir()
ElementCollectionItem *eci =
elementCollectionItemForIndex(m_showed_index);
if (eci)
{
eci->setBackground(QBrush());
eci->setForeground(QBrush());
}
}
m_showed_index = m_index_at_context_menu;
@@ -622,7 +739,11 @@ void ElementsCollectionWidget::showThisDir()
ElementCollectionItem *eci =
elementCollectionItemForIndex(m_showed_index);
if (eci)
{
// Amber under black, whatever the palette's text color.
eci->setBackground(QBrush(QColor(255, 204, 0, 255)));
eci->setForeground(QBrush(Qt::black));
}
search();
}
else
@@ -641,7 +762,10 @@ void ElementsCollectionWidget::resetShowThisDir()
ElementCollectionItem *eci = elementCollectionItemForIndex(
m_showed_index);
if (eci)
{
eci->setBackground(QBrush());
eci->setForeground(QBrush());
}
}
m_showed_index = QModelIndex();
@@ -830,19 +954,12 @@ void ElementsCollectionWidget::search()
}
//start the search when text have at least 3 letters.
if (text.count() < 3) {
if (text.size() < 3) {
return;
}
hideCollection(true);
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
const QStringList text_list = text.split("+", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
#endif
QModelIndexList match_index;
for (QString txt : text_list) {
match_index << m_model->match(m_showed_index.isValid()
@@ -72,6 +72,8 @@ class ElementsCollectionWidget : public QWidget
void editDirectory();
void newDirectory();
void newElement();
bool confirmEdzImportTerms();
void importEdz();
void showThisDir();
void resetShowThisDir();
void dirProperties();
@@ -112,6 +114,7 @@ class ElementsCollectionWidget : public QWidget
*m_edit_dir,
*m_new_directory,
*m_new_element,
*m_import_edz,
*m_show_this_dir,
*m_show_all_dir,
*m_dir_propertie;
+16 -31
View File
@@ -523,7 +523,9 @@ bool ElementsLocation::isCompanyCollection() const
*/
bool ElementsLocation::isCustomCollection() const
{
return fileSystemPath().startsWith(QETApp::customElementsDirN());
const QString dir = QETApp::customElementsDirN();
const QString path = fileSystemPath();
return path == dir || path.startsWith(dir + QLatin1Char('/'));
}
/**
@@ -651,9 +653,16 @@ QDomElement ElementsLocation::xml() const
if (!m_project)
{
QFile file (m_file_system_path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return QDomElement();
QDomDocument docu;
if (docu.setContent(&file))
{
file.close();
return docu.documentElement();
}
file.close();
}
else
{
@@ -701,11 +710,11 @@ pugi::xml_document ElementsLocation::pugiXml() const
if (!m_project)
{
#ifndef Q_OS_LINUX
if (docu.load_file(m_file_system_path.toStdString().c_str())) {
if (docu.load_file(m_file_system_path.toStdWString().c_str())) {
docu.save(m_string_stream);
}
#else
docu.load_file(m_file_system_path.toStdString().c_str());
docu.load_file(m_file_system_path.toStdWString().c_str());
#endif
}
else
@@ -776,40 +785,17 @@ bool ElementsLocation::setXml(const QDomDocument &xml_document) const
//Element doesn't exist, we create the element
else
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
QString path_ = collectionPath(false);
QRegExp rx ("^(.*)/(.*\\.elmt)$");
if (rx.exactMatch(path_)) {
return project()
->embeddedElementCollection()
->addElementDefinition(
rx.cap(1),
rx.cap(2),
xml_document
.documentElement());
}
else {
qDebug() << "ElementsLocation::setXml :"
" rx don't match";
}
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
# pragma message("@TODO ad Core5Compat to Cmake")
#endif
qDebug() << "Help code for QT 6 or later";
QString path_ = collectionPath(false);
QRegularExpression rx("^(.*)/(.*\\.elmt)$");
QRegularExpressionMatch match = rx.match(path_);
if (rx.exactMatch(path_))
if (match.hasMatch())
{
return project()
->embeddedElementCollection()
->addElementDefinition(
rx.cap(1),
rx.cap(2),
match.captured(1),
match.captured(2),
xml_document.documentElement());
}
else
@@ -817,7 +803,6 @@ bool ElementsLocation::setXml(const QDomDocument &xml_document) const
qDebug() << "ElementsLocation::setXml :"
" rx don't match";
}
#endif
}
}
@@ -20,7 +20,7 @@
#include "../NameList/nameslist.h"
#include "../diagramcontext.h"
#include "pugixml/src/pugixml.hpp"
#include "pugixml.hpp"
#include <QIcon>
#include <QString>
@@ -16,6 +16,8 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementstreeview.h"
#include "elementpreviewdelegate.h"
#include "../qetpalette.h"
#include "../factory/elementfactory.h"
#include "../qetgraphicsitem/element.h"
@@ -41,15 +43,11 @@ static int MAX_DND_PIXMAP_HEIGHT = 375;
ElementsTreeView::ElementsTreeView(QWidget *parent) :
QTreeView(parent)
{
// force du noir sur une alternance de blanc (comme le schema) et de gris
// clair, avec du blanc sur bleu pas trop fonce pour la selection
QPalette qp = palette();
qp.setColor(QPalette::Text, Qt::black);
qp.setColor(QPalette::Base, Qt::white);
qp.setColor(QPalette::AlternateBase, QColor("#e8e8e8"));
qp.setColor(QPalette::Highlight, QColor("#678db2"));
qp.setColor(QPalette::HighlightedText, Qt::black);
setPalette(qp);
// Rows follow the application palette. Element previews are black
// line art drawn for a white sheet; ElementPreviewDelegate adapts them
// to a dark palette, so this view no longer has to force a light one
// (bugtracker #335).
setItemDelegate(new ElementPreviewDelegate(this));
}
/**
@@ -134,11 +132,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
if (!dir_path.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
#else
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
#endif
QString current_path = "";
for (const QString &part : parts) {
QString parent_path = current_path;
@@ -215,7 +209,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
&elmt_creation_state));
if (elmt_creation_state) { return; }
QPixmap elmt_pixmap(temp_elmt->pixmap());
QPixmap elmt_pixmap(QET::Palette::forPalette(temp_elmt->pixmap(), palette()));
QPoint elmt_hotspot(temp_elmt->hotspot());
//Adjust the size of the pixmap if he is too big
@@ -22,7 +22,39 @@
#include "../qeticons.h"
#include "elementslocation.h"
#include <QApplication>
#include <QDir>
#include <QPainter>
#include <QPixmap>
#include <QStyle>
namespace {
/**
@return the folder icon overlaid with a small warning badge in the
bottom-right corner. Used for a directory whose qet_directory could
not be read (@see FileElementCollectionItem::m_qet_directory_unreadable),
so the problem is visible in the tree itself and not only on hover
via the tooltip. Built once: same folder icon, same badge, every time.
*/
const QIcon &unreadableFolderIcon()
{
static const QIcon icon = []() {
QPixmap pixmap = QET::Icons::Folder.pixmap(16, 16);
const QPixmap badge = QApplication::style()
->standardIcon(QStyle::SP_MessageBoxWarning)
.pixmap(9, 9);
QPainter painter(&pixmap);
painter.drawPixmap(pixmap.width() - badge.width(),
pixmap.height() - badge.height(),
badge);
painter.end();
return QIcon(pixmap);
}();
return icon;
}
}
/**
@brief FileElementCollectionItem::FileElementCollectionItem
@@ -130,24 +162,47 @@ QString FileElementCollectionItem::localName()
else if (m_path == QETApp::customElementsDirN())
setText(QObject::tr("Collection utilisateur"));
else if (m_path == macrosPath)
setText(QObject::tr("Makros"));
setText(QObject::tr("Macros"));
else
setText(QObject::tr("Collection inconnue"));
}
else
{
// Fall back to the raw directory name (m_path) whenever the
// translated name can't be obtained -- qet_directory missing,
// unreadable (e.g. a Windows path-encoding issue with special
// characters, see bugtracker #332), malformed, or present but
// without a usable name entry -- rather than leaving the item
// blank.
QString display_name;
bool readable = false;
QString str(fileSystemPath() % "/qet_directory");
pugi::xml_document docu;
if(docu.load_file(str.toStdString().c_str()))
if (docu.load_file(str.toStdWString().c_str()))
{
if (QString(docu.document_element().name())
== "qet-directory")
{
readable = true;
NamesList nl;
nl.fromXml(docu.document_element());
setText(nl.name());
// Deliberately no fallback argument: a non-empty one
// is returned *before* NamesList::name() reaches its
// "first available translation" step, so passing
// m_path here would replace a perfectly good name in
// some other language with the raw directory name.
// The fallback belongs after the chain, not inside it.
display_name = nl.name();
}
}
setText(display_name.isEmpty() ? m_path : display_name);
// Only a file-level failure counts: a readable qet-directory
// with no entry for the current language is not an error,
// NamesList::name() resolves that on its own. Recorded here
// and reported by setUpData(), which sets the tooltip after
// this runs.
m_qet_directory_unreadable = !readable;
}
}
else if (isElement()) {
@@ -274,7 +329,9 @@ bool FileElementCollectionItem::isCompanyCollection() const
*/
bool FileElementCollectionItem::isCustomCollection() const
{
return fileSystemPath().startsWith(QETApp::customElementsDirN());
const QString dir = QETApp::customElementsDirN();
const QString path = fileSystemPath();
return path == dir || path.startsWith(dir + QLatin1Char('/'));
}
/**
@@ -325,10 +382,44 @@ void FileElementCollectionItem::setUpData()
{ search_list.append(context.value(key).toString()); }
search_list.append(localName(loc));
setData(search_list.join(" "));
// Tooltip: show what a truncated tree label can't - the full
// localized name and the descriptive element information.
// Reuses the location/context already parsed for the search
// index above; the collection path stays as the last line
// (it used to be the whole tooltip).
QStringList tip;
tip << localName(loc);
for (const auto &key : { QStringLiteral("description"),
QStringLiteral("manufacturer"),
QStringLiteral("manufacturer_reference") })
{
const QString value = context.value(key).toString();
if (!value.isEmpty())
tip << value;
}
tip << collectionPath();
tip.removeDuplicates();
setToolTip(tip.join(QLatin1Char('\n')));
return;
}
}
setToolTip(collectionPath());
// Falling back to the raw directory name keeps the folder usable, but
// on its own it hides the fact that a file is broken: the user sees a
// plausible name and never learns there is anything to repair. Say so
// above the collection path, which stays as the last line the way the
// element tooltip above builds it.
QStringList tip;
if (isDir() && m_qet_directory_unreadable)
{
tip << QObject::tr("Le fichier « %1 » est absent ou illisible : "
"le nom traduit de ce dossier n'a pas pu être lu, "
"son nom de dossier est affiché à la place.")
.arg(fileSystemPath() % "/qet_directory");
}
tip << collectionPath();
setToolTip(tip.join(QLatin1Char('\n')));
}
/**
@@ -339,26 +430,45 @@ void FileElementCollectionItem::setUpData()
*/
void FileElementCollectionItem::setUpIcon()
{
if (!icon().isNull())
// Must return unconditionally once setUpIcon has run: setIcon() calls
// setData(), which emits dataChanged() regardless of whether the new
// icon differs from the old one (QIcon has no meaningful equality).
// QTreeView responds to dataChanged() by recomputing the row's size
// hint, which re-enters data() for this same index -- so without this
// guard, any repeated setIcon() here recurses until the stack
// overflows. Confirmed by crash report on PR #633.
//
// We use a dedicated bool instead of icon().isNull() because
// some items intentionally keep a null icon (e.g. .qetmak),
// which would bypass a null-based guard and still recurse.
//
// This item's m_qet_directory_unreadable is already final by the time
// this can run at all: ElementsCollectionModel only attaches itself
// to the tree view (making data() reachable) from loadingFinished(),
// which fires after the QtConcurrent::map over every item -- this one
// included -- has completed. So there is no race to work around here.
if (m_icon_initialized)
return;
m_icon_initialized = true;
if (isCollectionRoot()) {
QString macrosPath = QETApp::userMacrosDir();
if (macrosPath.endsWith("/")) macrosPath.remove(macrosPath.length() - 1, 1);
if (m_path == QETApp::commonElementsDirN())
setIcon(QIcon(":/ico/16x16/qet.png"));
setIcon(QIcon::fromTheme("qet"));
else if (m_path == QETApp::companyElementsDirN())
setIcon(QIcon(":/ico/16x16/go-company.png"));
setIcon(QIcon::fromTheme("go-company"));
else if (m_path == macrosPath)
setIcon(QIcon(":/ico/16x16/go-home.png"));
setIcon(QIcon::fromTheme("go-home"));
else
setIcon(QIcon(":/ico/16x16/go-home.png"));
setIcon(QIcon::fromTheme("go-home"));
}
else
{
if (isDir()) {
setIcon(QET::Icons::Folder);
setIcon(m_qet_directory_unreadable ? unreadableFolderIcon()
: QET::Icons::Folder);
} else {
if (m_path.endsWith(".qetmak")) {
setIcon(QIcon());
@@ -64,6 +64,12 @@ class FileElementCollectionItem : public ElementCollectionItem
private:
QString m_path;
/// True when this directory's qet_directory file is missing or
/// unreadable, so setUpData() can say so in the tooltip. Recorded
/// rather than acted on in localName(), because setUpData() resets
/// the tooltip afterwards and would otherwise discard it.
bool m_qet_directory_unreadable = false;
bool m_icon_initialized = false;
};
#endif // FILEELEMENTCOLLECTIONITEM2_H
@@ -186,23 +186,22 @@ QDomElement XmlElementCollection::child(const QDomElement &parent_element,
if (parent_element.ownerDocument() != m_dom_document)
return QDomElement();
//Get all childs element of parent_element
QDomNodeList child_list = parent_element.childNodes();
QString tag_name(child_name.endsWith(".elmt")? "element" : "category");
QList <QDomElement> found_dom_element;
for (int i=0 ; i<child_list.length() ; i++)
/* Walk the siblings directly instead of the previous
* childNodes()+item(i) double pass: QDomNodeList::item(i) restarts
* from the first child every call, which made this loop quadratic
* in the number of children. This lookup runs several times per
* element instance while loading a project, on the "import"
* category that holds every embedded definition. */
const QString tag_name = child_name.endsWith(QLatin1String(".elmt"))
? QStringLiteral("element") : QStringLiteral("category");
for (QDomElement child_element = parent_element.firstChildElement(tag_name) ;
!child_element.isNull() ;
child_element = child_element.nextSiblingElement(tag_name))
{
QDomElement child_element = child_list.item(i).toElement();
if (child_element.tagName() == tag_name)
found_dom_element << child_element;
if (child_element.attribute(QStringLiteral("name")) == child_name)
return child_element;
}
if (found_dom_element.isEmpty()) return QDomElement();
foreach (QDomElement elmt, found_dom_element)
if (elmt.attribute("name") == child_name)
return elmt;
return QDomElement();
}
@@ -21,6 +21,8 @@
#include "../qetproject.h"
#include "xmlelementcollection.h"
#include <QFileInfo>
/**
@brief XmlProjectElementCollectionItem::XmlProjectElementCollectionItem
Constructor
@@ -58,10 +60,7 @@ QString XmlProjectElementCollectionItem::localName()
return text();
if (isCollectionRoot()) {
if (m_project->title().isEmpty())
setText(QObject::tr("Projet sans titre"));
else
setText(m_project->title());
updateProjectName();
}
else {
ElementsLocation location (embeddedPath(), m_project);
@@ -71,6 +70,25 @@ QString XmlProjectElementCollectionItem::localName()
return text();
}
/**
@brief XmlProjectElementCollectionItem::updateProjectName
Set the displayed name of the collection root from the project :
its title, or its file name when it has no title, like the project panel.
Does nothing if this item is not the root of the collection.
*/
void XmlProjectElementCollectionItem::updateProjectName()
{
if (!isCollectionRoot() || !m_project)
return;
if (!m_project->title().isEmpty())
setText(m_project->title());
else if (!m_project->filePath().isEmpty())
setText(QFileInfo(m_project->filePath()).completeBaseName());
else
setText(QObject::tr("Projet sans titre"));
}
/**
@brief XmlProjectElementCollectionItem::name
@return The collection name of this item
@@ -45,6 +45,7 @@ class XmlProjectElementCollectionItem : public ElementCollectionItem
bool isCollectionRoot() const override;
void addChildAtPath(const QString &collection_name) override;
QETProject * project() const;
void updateProjectName();
void setProject (QETProject *project,
bool set_data = true,
+7
View File
@@ -263,6 +263,13 @@ QString NamesList::name(const QString &fallback_name) const
QString system_language = QETApp::langFromSetting();
if (! map_names[system_language].isEmpty())
return (map_names[system_language]);
// langFromSetting() may return a full locale (e.g. "de_DE") while element
// and folder names are keyed by the 2-letter language code ("de"). Try the
// base language before falling back to English, mirroring what setLanguage()
// does for the UI translations.
const QString base_language = system_language.section('_', 0, 0);
if (base_language != system_language && ! map_names[base_language].isEmpty())
return (map_names[base_language]);
if (! map_names["en"].isEmpty()) return (map_names["en"]);
if (! fallback_name.isEmpty()) return (fallback_name);
if (map_names.count()) return (map_names.begin().value());
+1 -1
View File
@@ -17,7 +17,7 @@
*/
#ifndef NAMES_LIST_H
#define NAMES_LIST_H
#include "pugixml/src/pugixml.hpp"
#include "pugixml.hpp"
#include <QtXml>
/**
+2 -2
View File
@@ -36,7 +36,7 @@
<string>Ajouter une ligne</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
</property>
</widget>
@@ -48,7 +48,7 @@
<string>Copier dans le presse papier</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="edit-paste" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-paste.png</normaloff>:/ico/16x16/edit-paste.png</iconset>
</property>
</item>
@@ -46,6 +46,18 @@ class PropertiesEditorDialog : public QDialog
PropertiesEditorDialog(T editor, QWidget *parent = nullptr) :
QDialog (parent)
{
// Without this, a QWidget-modal QDialog with a parent falls
// back to macOS's automatic sheet presentation, which some
// Qt/Cocoa versions render stuck centered on screen and
// non-draggable rather than as a proper attached sheet
// (bugtracker #275). Other dialogs in this codebase already
// opt in explicitly (see ElementDialog, DiagramPropertiesDialog)
// -- this one was missing it.
setWindowModality(Qt::WindowModal);
#ifdef Q_OS_MACOS
setWindowFlags(Qt::Sheet);
#endif
//Set dialog title
setWindowTitle(editor->title());
// Reparent the editor,
@@ -72,6 +72,7 @@ QPropertyUndoCommand::QPropertyUndoCommand(const QPropertyUndoCommand *other)
m_new_value = other->m_new_value;
m_animate = other->m_animate;
m_first_time = other->m_first_time;
m_undo_first_time = other->m_undo_first_time;
setText(other->text());
}
@@ -99,11 +100,20 @@ void QPropertyUndoCommand::enableAnimation (bool animate) {
@param first_time = if true,
the first animation is done at the first call of redo if false,
the first animation is done at the second call of redo.
The same rule applies to undo, tracked independently: redo() always
runs before the first undo() (QUndoStack::push() calls redo()
immediately), so by the time undo() can run at all, redo()'s own
m_first_time has already flipped true. Sharing that flag would leave
undo() with no instant path ever reachable in practice -- reusing it
is not actually symmetric, it just looks like it is. m_undo_first_time
gives undo() the same one-time grace period redo() has, on its own
first call instead of redo's.
*/
void QPropertyUndoCommand::setAnimated(bool animate, bool first_time)
{
m_animate = animate;
m_first_time = first_time;
m_undo_first_time = first_time;
}
/**
@@ -118,6 +128,18 @@ bool QPropertyUndoCommand::mergeWith(const QUndoCommand *other)
QPropertyUndoCommand const *undo = static_cast<const QPropertyUndoCommand *>(other);
if (m_object != undo->m_object
|| m_property_name != undo->m_property_name) return false;
// Same object and property name alone isn't enough: two entirely
// separate, deliberate actions (say, cropping an image and then
// mirroring it) both go through the same "pixmap" property and
// would otherwise silently coalesce into one undo entry, carrying
// only the first action's label -- the second vanishes from the
// undo list with no way to undo just it. Legitimate merging (a
// slider or spinbox pushing one command per tick while being
// dragged, e.g. ArcEditor's angle editors) always reuses the exact
// same text() across the whole sequence, so requiring a match here
// keeps that working unchanged while refusing to merge anything
// that isn't actually a continuation of the same action.
if (text() != other->text()) return false;
m_new_value = undo->m_new_value;
return true;
}
@@ -155,7 +177,7 @@ void QPropertyUndoCommand::undo()
{
if (m_object->property(m_property_name) != m_old_value)
{
if (m_animate)
if (m_animate && m_undo_first_time)
{
QPropertyAnimation *animation = new QPropertyAnimation(m_object, m_property_name);
animation->setStartValue(m_new_value);
@@ -163,7 +185,10 @@ void QPropertyUndoCommand::undo()
animation->start(QAbstractAnimation::DeleteWhenStopped);
}
else
{
m_object->setProperty(m_property_name, m_old_value);
m_undo_first_time = true;
}
}
QUndoCommand::undo();
@@ -58,7 +58,8 @@ class QPropertyUndoCommand : public QUndoCommand
const char *m_property_name;
QVariant m_old_value, m_new_value;
bool m_animate = false,
m_first_time = true;
m_first_time = true,
m_undo_first_time = true;
};
#endif // QPROPERTYUNDOCOMMAND_H
@@ -16,7 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "searchandreplaceworker.h"
#include "../qetproject.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../diagramcommands.h"
@@ -370,7 +370,7 @@
<string>phase</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="phase" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/phase.png</normaloff>:/ico/16x16/phase.png</iconset>
</property>
</widget>
@@ -397,7 +397,7 @@
<string>neutre</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="neutral" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/neutral.png</normaloff>:/ico/16x16/neutral.png</iconset>
</property>
</widget>
@@ -433,7 +433,7 @@
<string>terre</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="ground" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/ground.png</normaloff>:/ico/16x16/ground.png</iconset>
</property>
</widget>
@@ -180,7 +180,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="start" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/start.png</normaloff>:/ico/22x22/start.png</iconset>
</property>
</widget>
@@ -1,4 +1,4 @@
/*
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
@@ -475,7 +475,7 @@ void SearchAndReplaceWidget::setUpConenctions()
connect(ui->m_tree_widget, &QTreeWidget::customContextMenuRequested, [this](const QPoint &pos)
{
if (m_diagram_hash.keys().contains(ui->m_tree_widget->currentItem()))
if (m_diagram_hash.contains(ui->m_tree_widget->currentItem()))
{
QMenu *menu = new QMenu(ui->m_tree_widget);
menu->addAction(m_select_elements);
@@ -951,28 +951,28 @@ void SearchAndReplaceWidget::on_m_tree_widget_itemDoubleClicked(
{
Q_UNUSED(column)
if (m_diagram_hash.keys().contains(item))
if (m_diagram_hash.contains(item))
{
QPointer<Diagram> diagram = m_diagram_hash.value(item);
if(diagram) {
diagram.data()->showMe();
}
}
else if (m_element_hash.keys().contains(item))
else if (m_element_hash.contains(item))
{
QPointer<Element> elmt = m_element_hash.value(item);
if (elmt && elmt->diagram()) {
elmt.data()->diagram()->showMe();
}
}
else if (m_text_hash.keys().contains(item))
else if (m_text_hash.contains(item))
{
QPointer<IndependentTextItem> text = m_text_hash.value(item);
if (text && text->diagram()) {
text.data()->diagram()->showMe();
}
}
else if (m_conductor_hash.keys().contains(item))
else if (m_conductor_hash.contains(item))
{
QPointer<Conductor> cond = m_conductor_hash.value(item);
if (cond && cond->diagram()) {
@@ -1013,31 +1013,34 @@ void SearchAndReplaceWidget::on_m_tree_widget_currentItemChanged(
m_last_selected.data()->setSelected(false);
}
if (m_element_hash.keys().contains(current))
if (m_element_hash.contains(current))
{
QPointer<Element> elmt = m_element_hash.value(current);
if (elmt)
{
m_highlighted_element = elmt;
elmt.data()->setHighlighted(true);
elmt.data()->ensureVisible();
}
}
else if (m_text_hash.keys().contains(current))
else if (m_text_hash.contains(current))
{
QPointer<IndependentTextItem> text = m_text_hash.value(current);
if (text)
{
text.data()->setSelected(true);
m_last_selected = text;
text.data()->ensureVisible();
}
}
else if (m_conductor_hash.keys().contains(current))
else if (m_conductor_hash.contains(current))
{
QPointer<Conductor> cond = m_conductor_hash.value(current);
if (cond)
{
cond.data()->setSelected(true);
m_last_selected = cond;
cond.data()->ensureVisible();
}
}
@@ -1144,7 +1147,7 @@ void SearchAndReplaceWidget::on_m_replace_pb_clicked()
&& qtwi->checkState(0) == Qt::Checked)
{
if (ui->m_folio_pb->text().endsWith(tr(" [édité]")) &&
m_diagram_hash.keys().contains(qtwi))
m_diagram_hash.contains(qtwi))
{
QPointer<Diagram> d = m_diagram_hash.value(qtwi);
if (d) {
@@ -1152,7 +1155,7 @@ void SearchAndReplaceWidget::on_m_replace_pb_clicked()
}
}
else if (ui->m_element_pb->text().endsWith(tr(" [édité]")) &&
m_element_hash.keys().contains(qtwi))
m_element_hash.contains(qtwi))
{
QPointer<Element> e = m_element_hash.value(qtwi);
if (e) {
@@ -1160,7 +1163,7 @@ void SearchAndReplaceWidget::on_m_replace_pb_clicked()
}
}
else if (!ui->m_replace_le->text().isEmpty() &&
m_text_hash.keys().contains(qtwi))
m_text_hash.contains(qtwi))
{
m_worker.m_indi_text = ui->m_replace_le->text();
QPointer<IndependentTextItem> t =
@@ -1171,7 +1174,7 @@ void SearchAndReplaceWidget::on_m_replace_pb_clicked()
}
else if (ui->m_conductor_pb->text().endsWith(tr(" [édité]")) &&
m_conductor_hash.keys().contains(qtwi))
m_conductor_hash.contains(qtwi))
{
QPointer<Conductor> c = m_conductor_hash.value(qtwi);
if (c) {
@@ -1187,7 +1190,7 @@ void SearchAndReplaceWidget::on_m_replace_pb_clicked()
QList <IndependentTextItem *>tl;
QList <Conductor *>cl;
if (m_diagram_hash.keys().contains(qtwi))
if (m_diagram_hash.contains(qtwi))
{
QPointer<Diagram> d =
m_diagram_hash.value(qtwi);
@@ -1195,7 +1198,7 @@ void SearchAndReplaceWidget::on_m_replace_pb_clicked()
dl.append(d.data());
}
}
else if (m_element_hash.keys().contains(qtwi))
else if (m_element_hash.contains(qtwi))
{
QPointer<Element> e =
m_element_hash.value(qtwi);
@@ -1203,7 +1206,7 @@ void SearchAndReplaceWidget::on_m_replace_pb_clicked()
el.append(e.data());
}
}
else if (m_text_hash.keys().contains(qtwi))
else if (m_text_hash.contains(qtwi))
{
QPointer<IndependentTextItem> t =
m_text_hash.value(qtwi);
@@ -1211,7 +1214,7 @@ void SearchAndReplaceWidget::on_m_replace_pb_clicked()
tl.append(t.data());
}
}
else if (m_conductor_hash.keys().contains(qtwi))
else if (m_conductor_hash.contains(qtwi))
{
QPointer<Conductor> c =
m_conductor_hash.value(qtwi);
@@ -106,7 +106,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-bottom" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-bottom.png</normaloff>:/ico/16x16/go-bottom.png</iconset>
</property>
<property name="flat">
@@ -123,7 +123,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-top" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-top.png</normaloff>:/ico/16x16/go-top.png</iconset>
</property>
<property name="flat">
@@ -140,7 +140,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
<property name="flat">
@@ -157,7 +157,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="configure-toolbars" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/configure-toolbars.png</normaloff>:/ico/16x16/configure-toolbars.png</iconset>
</property>
<property name="checkable">
@@ -18,6 +18,7 @@
#include "terminalstripdrawer.h"
#include <QPainter>
#include <QHash>
namespace TerminalStripDrawer {
@@ -265,13 +266,13 @@ void TerminalStripDrawer::paint(QPainter *painter)
painter->restore();
//Draw the bridges
for (const auto &points_ : qAsConst(bridges_anchor_points))
for (const auto &points_ : std::as_const(bridges_anchor_points))
{
painter->save();
auto pen_{painter->pen()};
pen_.setWidth(2);
painter->setPen(pen_);
painter->drawPolyline(QPolygonF{points_});
painter->drawPolyline(QPolygonF(points_));
painter->restore();
}
}
@@ -19,6 +19,7 @@
#define TERMINALSTRIPDRAWER_H
#include <QPointer>
#include <QUuid>
#include "properties/terminalstriplayoutpattern.h"
@@ -16,7 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "terminalstripitem.h"
#include "../../qetproject.h"
#include "../diagram.h"
#include "../../project/projectpropertieshandler.h"
@@ -35,13 +35,17 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QSharedPointer<RealTerminal
const auto t_label = terminal->label();
const auto ts_name = strip->name();
const auto str_1 = t_label.isEmpty() ? QObject::tr("Ajouter une borne") :
QObject::tr("Ajouter la borne %1").arg(t_label);
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
QObject::tr("au groupe de bornes %1").arg(ts_name);
setText(str_1 % " " % str_2);
QString text;
if (ts_name.isEmpty()) {
text = t_label.isEmpty()
? QObject::tr("Ajouter une borne à un groupe de bornes")
: QObject::tr("Ajouter la borne %1 à un groupe de bornes").arg(t_label);
} else {
text = t_label.isEmpty()
? QObject::tr("Ajouter une borne au groupe de bornes %1").arg(ts_name)
: QObject::tr("Ajouter la borne %1 au groupe de bornes %2").arg(t_label, ts_name);
}
setText(text);
}
AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<RealTerminal>> terminals, TerminalStrip *strip, QUndoCommand *parent) :
@@ -50,14 +54,11 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<Real
m_new_strip{strip}
{
const auto ts_name = strip->name();
const auto count = m_terminal.size();
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Ajouter %1 bornes").arg(m_terminal.size()) :
QObject::tr("Ajouter une borne");
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
QObject::tr("au groupe de bornes %1").arg(ts_name);
setText(str_1 % " " % str_2);
setText(ts_name.isEmpty()
? QObject::tr("Ajouter %n borne(s) à un groupe de bornes", "", count)
: QObject::tr("Ajouter %n borne(s) au groupe de bornes %1", "", count).arg(ts_name));
}
@@ -126,7 +127,7 @@ void RemoveTerminalFromStripCommand::redo()
if (m_strip)
{
QVector<QSharedPointer<RealTerminal>> real_t;
for (const auto &real_t_vector : qAsConst(m_terminals)) {
for (const auto &real_t_vector : std::as_const(m_terminals)) {
real_t.append(real_t_vector);
}
@@ -137,13 +138,11 @@ void RemoveTerminalFromStripCommand::redo()
void RemoveTerminalFromStripCommand::setCommandTitle()
{
const auto strip_name = m_strip->name();
const auto count = m_terminals.size();
const auto str_1 = m_terminals.size()>1 ? QObject::tr("Enlever %1 bornes").arg(m_terminals.size()):
QObject::tr("Enlever une borne");
const auto str_2 = strip_name.isEmpty() ? QObject::tr("d'un groupe de bornes") :
QObject::tr("du groupe de bornes %1").arg(strip_name);
setText(str_1 % " " % str_2);
setText(strip_name.isEmpty()
? QObject::tr("Enlever %n borne(s) d'un groupe de bornes", "", count)
: QObject::tr("Enlever %n borne(s) du groupe de bornes %1", "", count).arg(strip_name));
}
/**
@@ -166,19 +165,30 @@ MoveTerminalCommand::MoveTerminalCommand(QSharedPointer<PhysicalTerminal> termin
t_label.append(", ");
t_label.append(real_t->label());
}
const auto strip_name = old_strip->name();
const auto new_strip_name = new_strip->name();
auto strip_name = old_strip->name();
auto new_strip_name = new_strip->name();
auto str_1 = t_label.isEmpty() ? QObject::tr("Déplacer une borne") :
QObject::tr("Déplacer la borne %1").arg(t_label);
auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
QObject::tr(" du groupe de bornes %1").arg(strip_name);
auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
setText(str_1 % " " % str_2 % " " % str_3);
QString text;
if (t_label.isEmpty()) {
if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers un autre groupe de bornes");
else if (strip_name.isEmpty())
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers le groupe de bornes %1").arg(new_strip_name);
else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers un autre groupe de bornes").arg(strip_name);
else
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers le groupe de bornes %2").arg(strip_name, new_strip_name);
} else {
if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers un autre groupe de bornes").arg(t_label);
else if (strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers le groupe de bornes %2").arg(t_label, new_strip_name);
else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers un autre groupe de bornes").arg(t_label, strip_name);
else
text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers le groupe de bornes %3").arg(t_label, strip_name, new_strip_name);
}
setText(text);
}
MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal>> terminals, TerminalStrip *old_strip,
@@ -191,17 +201,18 @@ MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal
{
const auto strip_name = old_strip->name();
const auto new_strip_name = new_strip->name();
const auto count = m_terminal.size();
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Déplacer des bornes") :
QObject::tr("Déplacer une borne");
const auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
QObject::tr(" du groupe de bornes %1").arg(strip_name);
const auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
setText(str_1 % " " % str_2 % " " % str_3);
QString text;
if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers un autre groupe de bornes", "", count);
else if (strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers le groupe de bornes %1", "", count).arg(new_strip_name);
else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers un autre groupe de bornes", "", count).arg(strip_name);
else
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers le groupe de bornes %2", "", count).arg(strip_name, new_strip_name);
setText(text);
}
void MoveTerminalCommand::undo()
@@ -68,7 +68,7 @@ void UnGroupTerminalsCommand::undo()
m_terminal_strip->groupTerminals(key, m_physical_real_H.value(key));
}
//Second, set level.
for (const auto &pair : qAsConst(m_real_t_level)) {
for (const auto &pair : std::as_const(m_real_t_level)) {
m_terminal_strip->setLevel(pair.first, pair.second);
}
}
@@ -78,7 +78,7 @@ void UnGroupTerminalsCommand::redo()
{
if (m_terminal_strip)
{
for (const auto &value : qAsConst(m_physical_real_H)) {
for (const auto &value : std::as_const(m_physical_real_H)) {
m_terminal_strip->unGroupTerminals(value);
}
}
+1 -6
View File
@@ -130,12 +130,7 @@ bool PhysicalTerminal::setLevelOf(const QSharedPointer<RealTerminal> &terminal,
const int i = m_real_terminal.indexOf(terminal);
if (i >= 0)
{
#if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
m_real_terminal.swapItemsAt(i, std::min(level, m_real_terminal.size()-1));
#else
auto j = std::min(level, m_real_terminal.size()-1);
std::swap(m_real_terminal.begin()[i], m_real_terminal.begin()[j]);
#endif
m_real_terminal.swapItemsAt(i, std::min<int>(level, static_cast<int>(m_real_terminal.size())-1));
return true;
}
return false;
+5 -5
View File
@@ -466,7 +466,7 @@ QSharedPointer<RealTerminal> TerminalStrip::realTerminalForUuid(const QUuid &uui
QVector<QSharedPointer<RealTerminal>> TerminalStrip::realTerminals() const
{
QVector<QSharedPointer<RealTerminal>> vector_;
for (const auto &phy : qAsConst(m_physical_terminals)) {
for (const auto &phy : std::as_const(m_physical_terminals)) {
vector_.append(phy->realTerminals());
}
return vector_;
@@ -639,7 +639,7 @@ bool TerminalStrip::isBridgeable(const QVector<QSharedPointer<RealTerminal>> &re
// Get the physical terminal and pos
auto first_physical_terminal = first_real_terminal->physicalTerminal();
QVector<shared_physical_terminal> physical_vector{first_physical_terminal};
QVector<int> pos_vector{m_physical_terminals.indexOf(first_physical_terminal)};
QVector<int> pos_vector{static_cast<int>(m_physical_terminals.indexOf(first_physical_terminal))};
auto bridge_ = isBridged(first_real_terminal);
//bool to know at the end of this function if at least one terminal is not bridged
@@ -856,7 +856,7 @@ QSharedPointer<TerminalStripBridge> TerminalStrip::isBridged(const QSharedPointe
{
if (real_terminal)
{
for (const auto &bridge_ : qAsConst(m_bridge)) {
for (const auto &bridge_ : std::as_const(m_bridge)) {
if (bridge_->realTerminals().contains(real_terminal))
return bridge_;
}
@@ -983,7 +983,7 @@ QDomElement TerminalStrip::toXml(QDomDocument &parent_document)
}
root_elmt.appendChild(xml_layout);
for (const auto &bridge_ : qAsConst(m_bridge)) {
for (const auto &bridge_ : std::as_const(m_bridge)) {
root_elmt.appendChild(bridge_->toXml(parent_document));
}
@@ -1024,7 +1024,7 @@ bool TerminalStrip::fromXml(QDomElement &xml_element)
for (auto &xml_real : QETXML::findInDomElement(xml_physical, RealTerminal::xmlTagName()))
{
const auto uuid_ = QUuid(xml_real.attribute(QStringLiteral("element_uuid")));
for (auto terminal_elmt : qAsConst(free_terminals))
for (auto terminal_elmt : std::as_const(free_terminals))
{
if (terminal_elmt->uuid() == uuid_)
{
@@ -85,7 +85,7 @@ QDomElement TerminalStripBridge::toXml(QDomDocument &parent_document) const
root_elmt.setAttribute(QStringLiteral("color"), m_color.name());
auto terminals_elmt = parent_document.createElement(QStringLiteral("real_terminals"));
for (const auto &real_t : qAsConst(m_real_terminals))
for (const auto &real_t : std::as_const(m_real_terminals))
{
if (real_t)
{
@@ -106,7 +106,7 @@ void TerminalStripBridge::fromXml(const QDomElement &dom_element)
}
m_uuid = QUuid(dom_element.attribute(QStringLiteral("uuid"), m_uuid.toString()));
m_color.setNamedColor(dom_element.attribute(QStringLiteral("color")));
m_color = QColor(dom_element.attribute(QStringLiteral("color")));
const auto real_t_vector = QETXML::subChild(dom_element,
QStringLiteral("real_terminals"),
+1 -4
View File
@@ -64,11 +64,8 @@ bool TerminalStripData::fromXml(const QDomElement &xml_element)
"due to wrong tag name. Expected " << this->xmlTagName() << " used " << xml_element.tagName();
return false;
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
m_uuid = QUuid::fromString(xml_element.attribute(QStringLiteral("uuid")));
#else
m_uuid = QUuid(xml_element.attribute(QStringLiteral("uuid")));
#endif
for (auto &xml_info :
QETXML::findInDomElement(xml_element.firstChildElement(QStringLiteral("informations")),
@@ -17,7 +17,7 @@
*/
#include "addterminalstripitemdialog.h"
#include "ui_addterminalstripitemdialog.h"
#include "../../qetproject.h"
#include "../../undocommand/addgraphicsobjectcommand.h"
#include "../terminalstrip.h"
#include "../GraphicsItem/terminalstripitem.h"
@@ -46,10 +46,11 @@ FreeTerminalEditor::FreeTerminalEditor(QETProject *project, QWidget *parent) :
connect(m_project, &QObject::destroyed, this, &FreeTerminalEditor::reload);
}
//Disabled the move if the table is currently edited (yellow cell)
connect(m_model, &FreeTerminalModel::dataChanged, this, [=] {
this->setDisabledMove();
});
//Disable the move button while cells are edited (yellow), re-evaluate on every change
connect(m_model, &FreeTerminalModel::dataChanged, this, &FreeTerminalEditor::selectionChanged);
connect(ui->m_table_view->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &FreeTerminalEditor::selectionChanged);
connect(ui->m_table_view, &QAbstractItemView::doubleClicked, this, [=](const QModelIndex &index)
{
@@ -102,7 +103,7 @@ void FreeTerminalEditor::reload()
QString str(strip->installation() + " " + strip->location() + " " + strip->name());
ui->m_move_in_cb->addItem(str, strip->uuid());
}
setDisabledMove(false);
selectionChanged();
}
}
@@ -273,10 +274,19 @@ void FreeTerminalEditor::on_m_move_pb_clicked()
reload();
}
void FreeTerminalEditor::selectionChanged()
{
const bool has_selection = !ui->m_table_view->selectionModel()->selectedIndexes().isEmpty();
const bool has_pending = !m_model->modifiedModelRealTerminalData().isEmpty();
setDisabledMove(!has_selection || has_pending);
}
void FreeTerminalEditor::setDisabledMove(bool b)
{
ui->m_move_label->setDisabled(b);
ui->m_move_in_cb->setDisabled(b);
ui->m_move_pb->setDisabled(b);
ui->m_move_pb->setToolTip(b ? tr("Appliquez ou annulez les modifications en cours avant de déplacer")
: tr("Déplacer les bornes sélectionnées vers le bornier choisi"));
}
@@ -84,7 +84,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="dialog-ok" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/dialog-ok.png</normaloff>:/ico/22x22/dialog-ok.png</iconset>
</property>
</widget>
@@ -182,7 +182,7 @@ bool FreeTerminalModel::setData(const QModelIndex &index, const QVariant &value,
modified_ = true;
modified_cell = FUNCTION_CELL;
}
else if (column_ == LED_CELL)
else if (column_ == LED_CELL && mrtd.led_ != value.toBool())
{
mrtd.led_ = value.toBool();
modified_ = true;
@@ -63,7 +63,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="dialog-ok" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/dialog-ok.png</normaloff>:/ico/16x16/dialog-ok.png</iconset>
</property>
</widget>
@@ -54,7 +54,7 @@
</widget>
<action name="m_add_terminal_strip">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
</property>
<property name="text">
@@ -66,7 +66,7 @@
</action>
<action name="m_remove_terminal">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
</property>
<property name="text">
@@ -78,7 +78,7 @@
</action>
<action name="m_reload">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
<property name="text">
@@ -29,6 +29,9 @@ TerminalStripLayoutEditor::TerminalStripLayoutEditor(QSharedPointer<TerminalStri
{
ui->setupUi(this);
ui->m_graphics_view->setScene(new QGraphicsScene{ this });
// The preview is printed output: black ink on a white page, whatever
// the application palette.
ui->m_graphics_view->setBackgroundBrush(Qt::white);
ui->m_graphics_view->scene()->addItem(&m_preview_strip_item);
updateUi();
}
@@ -535,7 +535,7 @@ modelRealTerminalData TerminalStripModel::dataAtRow(int row) const
else
{
auto current_row = 0;
for (const auto &physical_data : qAsConst(m_physical_data))
for (const auto &physical_data : std::as_const(m_physical_data))
{
for (const auto &real_data : physical_data.real_data)
{
@@ -567,7 +567,7 @@ void TerminalStripModel::replaceDataAtRow(modelRealTerminalData data, int row)
auto current_row = 0;
auto current_physical = 0;
for (const auto &physical_data : qAsConst(m_physical_data))
for (const auto &physical_data : std::as_const(m_physical_data))
{
auto current_real = 0;
for (int i=0 ; i<physical_data.real_data.count() ; ++i)
@@ -606,7 +606,7 @@ modelPhysicalTerminalData TerminalStripModel::physicalDataAtIndex(int index) con
int current_phy = -1;
bool match_ = false;
for (const auto &ptd_ : qAsConst(m_physical_data))
for (const auto &ptd_ : std::as_const(m_physical_data))
{
current_checked_index += ptd_.real_data.size();
++current_phy;
@@ -637,9 +637,9 @@ modelRealTerminalData TerminalStripModel::realDataAtIndex(int index) const
int current_checked_index = -1;
for (const auto & ptd_ : qAsConst(m_physical_data))
for (const auto & ptd_ : std::as_const(m_physical_data))
{
for (const auto & rtd_ : qAsConst(ptd_.real_data)) {
for (const auto & rtd_ : std::as_const(ptd_.real_data)) {
++current_checked_index;
if (current_checked_index == index) {
return rtd_;
@@ -29,8 +29,8 @@
#include "modelTerminalData.h"
//Code to use QColor as key for QHash
inline uint qHash(const QColor &key, uint seed) {
return qHash(key.name(), seed);
inline size_t qHash(const QColor &key, size_t seed = 0) {
return qHash(key.rgba(), seed);
}
//needed to use QPointer<Element> as key of QHash
@@ -28,6 +28,8 @@
#include "../terminalstrip.h"
#include "../../qetinformation.h"
#include <QUuid>
TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QWidget *parent) :
QDockWidget(parent),
ui(new Ui::TerminalStripTreeDockWidget)
@@ -35,11 +37,7 @@ TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QW
ui->setupUi(this);
setProject(project);
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
#else
ui->m_tree_view->expandAll();
#endif
}
TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget()
@@ -85,7 +83,7 @@ void TerminalStripTreeDockWidget::reload()
m_uuid_terminal_H.clear();
m_uuid_strip_H.clear();
for (const auto &connection_ : qAsConst(m_strip_changed_connection)) {
for (const auto &connection_ : std::as_const(m_strip_changed_connection)) {
disconnect(connection_);
}
m_strip_changed_connection.clear();
@@ -93,11 +91,7 @@ void TerminalStripTreeDockWidget::reload()
buildTree();
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
#else
ui->m_tree_view->expandAll();
#endif
//Reselect the tree widget item of the current edited strip
auto item = m_item_strip_H.key(current_);
@@ -250,7 +244,7 @@ void TerminalStripTreeDockWidget::buildTree()
return a->name() < b->name();
});
for (const auto &ts : qAsConst(ts_vector)) {
for (const auto &ts : std::as_const(ts_vector)) {
addTerminalStrip(ts);
}
addFreeTerminal();
@@ -352,7 +346,7 @@ void TerminalStripTreeDockWidget::addFreeTerminal()
auto free_terminal_item = ui->m_tree_view->topLevelItem(1);
for (const auto terminal : qAsConst(vector_))
for (const auto terminal : std::as_const(vector_))
{
QUuid uuid_ = terminal->uuid();
QStringList strl{terminal->actualLabel()};
@@ -20,6 +20,7 @@
#include <QDockWidget>
#include <QPointer>
#include <QHash>
class QETProject;
class QTreeWidgetItem;
+225 -106
View File
@@ -23,7 +23,9 @@
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h"
#include "../qetxml.h"
#include "../qetproject.h"
#include <QDir>
#include <QDomDocument>
#include <QStringList>
#include <QVariant>
#include <utility>
@@ -39,11 +41,13 @@ namespace autonum
sequentialNumbers::sequentialNumbers(const sequentialNumbers &other)
{
unit = other.unit;
wrap = other.wrap;
unit_folio = other.unit_folio;
ten = other.ten;
ten_folio = other.ten_folio;
hundred = other.hundred;
hundred_folio = other.hundred_folio;
alpha = other.alpha;
}
sequentialNumbers::~sequentialNumbers()
@@ -56,11 +60,13 @@ namespace autonum
return (*this);
unit = other.unit;
wrap = other.wrap;
unit_folio = other.unit_folio;
ten = other.ten;
ten_folio = other.ten_folio;
hundred = other.hundred;
hundred_folio = other.hundred_folio;
alpha = other.alpha;
return (*this);
}
@@ -68,11 +74,13 @@ namespace autonum
bool sequentialNumbers::operator==(const sequentialNumbers &other) const
{
if (unit == other.unit && \
wrap == other.wrap && \
unit_folio == other.unit_folio && \
ten == other.ten && \
ten_folio == other.ten_folio && \
hundred == other.hundred && \
hundred_folio == other.hundred_folio)
hundred_folio == other.hundred_folio && \
alpha == other.alpha)
return true;
else
return false;
@@ -104,6 +112,11 @@ namespace autonum
document,
"unit",
unit.join(";")));
if (!wrap.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
"wrap",
wrap.join(";")));
if (!unit_folio.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
@@ -129,6 +142,11 @@ namespace autonum
document,
"hundredFolio",
hundred_folio.join(";")));
if(!alpha.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
"alpha",
alpha.join(";")));
return element;
}
@@ -148,6 +166,11 @@ namespace autonum
from = element.firstChildElement("unit");
unit = from.text().split(";");
//Absent from files written before cyclic parts could be
//rendered; an empty list is the correct reading of that.
from = element.firstChildElement("wrap");
wrap = from.text().split(";");
from = element.firstChildElement("unitFolio");
unit_folio = from.text().split(";");
@@ -162,17 +185,22 @@ namespace autonum
from = element.firstChildElement("hundredFolio");
hundred_folio = from.text().split(";");
from = element.firstChildElement("alpha");
alpha = from.text().split(";");
}
//Clear this sequence
void sequentialNumbers::clear()
{
unit.clear();
wrap.clear();
unit_folio.clear();
ten.clear();
ten_folio.clear();
hundred.clear();
hundred_folio.clear();
alpha.clear();
}
/**
@@ -275,6 +303,17 @@ namespace autonum
str.replace("%{void}", QString());
str.replace("%{plc_type}", dc.value("plc_type").toString());
str.replace("%{plc_address}", dc.value("plc_address").toString());
str.replace("%{plc_function}", dc.value("plc_function").toString());
str.replace("%{plc_comment}", dc.value("plc_comment").toString());
str.replace("%{plc_crossref}", dc.value("plc_crossref").toString());
str.replace("%{plc_tc}", dc.value("plc_tc").toString());
str.replace("%{plc_t1}", dc.value("plc_t1").toString());
str.replace("%{plc_t2}", dc.value("plc_t2").toString());
str.replace("%{plc_t3}", dc.value("plc_t3").toString());
str.replace("%{plc_t4}", dc.value("plc_t4").toString());
return str;
}
@@ -408,8 +447,11 @@ namespace autonum
m_seq_struct.ten_folio.size()),
qMax(m_seq_struct.hundred_folio.size(),
m_seq_struct.unit.size())),
qMax(m_seq_struct.hundred.size(),
m_seq_struct.ten.size())
qMax(
qMax(m_seq_struct.hundred.size(),
m_seq_struct.ten.size()),
qMax(m_seq_struct.alpha.size(),
m_seq_struct.wrap.size()))
);
for (int i=1; i<=max ; i++)
@@ -417,6 +459,9 @@ namespace autonum
if (m_assigned_label.contains("%sequ_" + QString::number(i)) && m_seq_struct.unit.size() >= i) {
m_assigned_label.replace("%sequ_" + QString::number(i),m_seq_struct.unit.at(i-1));
}
if (m_assigned_label.contains("%seqw_" + QString::number(i)) && m_seq_struct.wrap.size() >= i) {
m_assigned_label.replace("%seqw_" + QString::number(i),m_seq_struct.wrap.at(i-1));
}
if (m_assigned_label.contains("%seqt_" + QString::number(i)) && m_seq_struct.ten.size() >= i) {
m_assigned_label.replace("%seqt_" + QString::number(i),m_seq_struct.ten.at(i-1));
}
@@ -432,6 +477,9 @@ namespace autonum
if (m_assigned_label.contains("%seqhf_" + QString::number(i)) && m_seq_struct.hundred_folio.size() >= i) {
m_assigned_label.replace("%seqhf_" + QString::number(i),m_seq_struct.hundred_folio.at(i-1));
}
if (m_assigned_label.contains("%seqa_" + QString::number(i)) && m_seq_struct.alpha.size() >= i) {
m_assigned_label.replace("%seqa_" + QString::number(i),m_seq_struct.alpha.at(i-1));
}
}
}
@@ -451,12 +499,26 @@ namespace autonum
{
if (context.itemAt(i).at(0) == type)
{
const QStringList item = context.itemAt(i);
//A zero-padding mask, spreadsheet style: its length is the
//minimum number of digits. It overrides the width implied
//by the part type, so "Chiffre 01" with a mask of "0000"
//pads to four. An absent mask -- which is every context
//written before the field existed -- falls through to the
//type's own width, so nothing about existing projects
//changes.
const QString mask = NumerotationContext::formatOf(item);
QString number;
if (type == "ten" || type == "tenfolio")
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 2, 10, QChar('0'));
if (type == "alpha")
//Alphabetic value, not an integer -- used as-is.
number = item.at(1);
else if (!mask.isEmpty())
number = QString("%1").arg(item.at(1).toInt(), mask.length(), 10, QChar('0'));
else if (type == "ten" || type == "tenfolio")
number = QString("%1").arg(item.at(1).toInt(), 2, 10, QChar('0'));
else if (type == "hundred" || type == "hundredfolio")
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 3, 10, QChar('0'));
else number = QString::number(context.itemAt(i).at(1).toInt());
number = QString("%1").arg(item.at(1).toInt(), 3, 10, QChar('0'));
else number = QString::number(item.at(1).toInt());
list.append(number);
}
}
@@ -522,6 +584,10 @@ namespace autonum
{
autonum::setSequentialToList(seqStruct.unit, context,"unit");
}
if (label.contains("%seqw_"))
{
autonum::setSequentialToList(seqStruct.wrap, context,"wrap");
}
if (label.contains("%sequf_"))
{
autonum::setSequentialToList(seqStruct.unit_folio, context,"unitfolio");
@@ -545,6 +611,10 @@ namespace autonum
autonum::setSequentialToList(seqStruct.hundred_folio, context,"hundredfolio");
autonum::setFolioSequentialToHash(seqStruct.hundred_folio, diagram->m_elmt_hundredfolio_max, hashKey);
}
if (label.contains("%seqa_"))
{
autonum::setSequentialToList(seqStruct.alpha, context,"alpha");
}
}
}
@@ -559,11 +629,13 @@ namespace autonum
QString value;
QString formula;
int count_unit = 0;
int count_wrap = 0;
int count_unitf = 0;
int count_ten = 0;
int count_tenf = 0;
int count_hundred = 0;
int count_hundredf = 0;
int count_alpha = 0;
for(int i=0 ; i<nc.size() ; i++)
{
@@ -600,6 +672,10 @@ namespace autonum
count_unit++;
formula.append("%sequ_" + QString::number(count_unit));
}
else if (type == "wrap") {
count_wrap++;
formula.append("%seqw_" + QString::number(count_wrap));
}
else if (type == "unitfolio") {
count_unitf++;
formula.append("%sequf_" + QString::number(count_unitf));
@@ -620,11 +696,83 @@ namespace autonum
count_hundredf++;
formula.append("%seqhf_" + QString::number(count_hundredf));
}
else if (type == "alpha") {
count_alpha++;
formula.append("%seqa_" + QString::number(count_alpha));
}
}
return formula;
}
/**
@brief prefixFromLabelFile
Look up a prefix for @a path (path[dirLevel] outermost, path[1] the
deepest directory; path[0], the element's own file name, is never
matched) in the qet_labels.xml at @a filepath.
Descends through nested \<category name="..."\> elements matching
path[dirLevel], path[dirLevel-1], ..., path[1] in turn, considering
only *direct* children at each step -- unlike a flat token scan,
this cannot be fooled by a same-named category living elsewhere in
the document at the wrong nesting depth (bugtracker #671 item 5).
At each matched level, that category's own \<prefix\> child -- even
an empty one -- overrides whatever a shallower ancestor already
provided, so an explicit empty \<prefix/\> cancels inheritance
rather than silently falling back to it (the behaviour requested in
PR #686 review). A category with no \<prefix\> child at all leaves
the inherited value untouched, which is how a directory with no
prefix of its own comes to inherit its parent's, as the file's own
header comment documents.
@return the prefix that applies, or a null QString if the file
cannot be read, is not well-formed, or does not describe this
path at all (as opposed to describing it with no prefix
anywhere along it, which is a non-null empty string).
*/
static QString prefixFromLabelFile(const QString &filepath, const QStringList &path, int dirLevel)
{
QFile file(filepath);
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
QDomDocument document;
if (!document.setContent(&file))
return QString();
QDomElement node = document.documentElement();
if (node.isNull())
return QString();
QString prefix;
for (int i = dirLevel ; i >= 1 ; --i) {
QDomElement child = node.firstChildElement(QStringLiteral("category"));
while (!child.isNull()
&& child.attribute(QStringLiteral("name")) != path[i]) {
child = child.nextSiblingElement(QStringLiteral("category"));
}
if (child.isNull())
return QString();
node = child;
const QDomElement own = node.firstChildElement(QStringLiteral("prefix"));
if (!own.isNull()) {
//readElementText()'s null-vs-empty distinction that PR
//#686 needed for the old QXmlStreamReader-based lookup
//has a QDomElement equivalent: text() on an empty
//element can itself come back null depending on how the
//XML was written, so the same explicit fallback applies
//-- an empty QString here means "found, deliberately
//blank", not "not found".
prefix = own.text();
if (prefix.isNull())
prefix = QString("");
}
}
return prefix;
}
/**
@brief elementPrefixForLocation
@param location
@@ -638,114 +786,85 @@ namespace autonum
if (!location.isProject())
return QString();
QXmlStreamReader rxml;
QString path[10];
int i = -1;
//Directory names from the element up to (not including) the
//collection root, outermost last -- path[dirLevel] is the
//top-level category, path[1] the element's immediate parent
//directory, path[0] the element's own file name (never matched
//against a category: the search stops descending once it has
//matched path[1], the deepest real directory). An unbounded
//QStringList rather than a fixed-size array, because a custom
//collection can nest deeper than the shipped one -- see
//bugtracker #671 item 3.
QStringList path;
ElementsLocation current_location = location;
int dirLevel = -1;
//Add location name to path array
while((current_location.parent() != current_location) && (current_location.parent().fileName() != "import"))
while ((current_location.parent() != current_location)
&& (current_location.parent().fileName() != "import"))
{
i++;
path[i]=current_location.fileName();
path << current_location.fileName();
current_location = current_location.parent();
dirLevel++;
}
//User Element without folder treatment
if (i == -1)
{
i = 0;
path[i]=current_location.fileName();
//User element without folder treatment
if (path.isEmpty()) {
path << current_location.fileName();
current_location = current_location.parent();
dirLevel = 0;
}
const int dirLevel = path.size() - 1;
//Name of the top-level tree the element's path was found
//under, e.g. "10_electric" -- or, for a custom/company
//collection not organised that way, whatever its top-level
//folder happens to be called.
const QString collection_root = current_location.fileName();
//Every top-level common-collection tree (10_electric,
//20_logic, 30_hydraulic, ...) may carry its own
//qet_labels.xml, with categories relative to that tree, the
//same way 10_electric/qet_labels.xml already does -- not just
//10_electric, which is all the hardcoded check this replaces
//used to allow (bugtracker #671 item 2). commonElementsDir()
//-- unlike customElementsDir(), which normalises this itself
//-- returns whatever path the user configured verbatim, with
//no guaranteed trailing separator; concatenating a suffix onto
//it directly used to silently mangle the path (and so the
//prefix lookup) for any install relocated to a directory
//without a trailing slash (#671 item 1). QDir::filePath()
//joins correctly either way.
{
const QString common_file = QDir(QETApp::commonElementsDir())
.filePath(collection_root + QStringLiteral("/qet_labels.xml"));
const QString prefix = prefixFromLabelFile(common_file, path, dirLevel);
if (!prefix.isNull()) {
return prefix;
}
}
// Create Custom labels if qet_labels.xml exits in customElementsDir
if (current_location.fileName() != "10_electric"){
QString custom_labels = "qet_labels.xml";
QString customfilepath = QETApp::customElementsDir().append(custom_labels);
QFile file(customfilepath);
file.isReadable();
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
rxml.setDevice(&file);
rxml.readNext();
/* Which collection an element actually came from is not
* recoverable post-import (addElement() strips the protocol),
* so custom and company labels files are tried against two
* possible layouts: with the collection's top-level tree name
* folded into the path (a custom/company file organised as a
* mirror of the common collection, tree name included) and
* without it (a file scoped to just this one tree, matching
* how the common collection's own files are written). Custom
* is tried before company, so a user override wins over a
* shared one.
*/
QStringList path_from_root = path;
path_from_root << collection_root;
while(!rxml.atEnd())
{
if (rxml.attributes().value("name").toString() == path[i])
{
rxml.readNext();
i=i-1;
//reached element directory
if (i==0)
{
for (int j=i; j<= dirLevel; j = j +1)
{
//if there is a prefix available apply prefix
if(rxml.name().toString()=="prefix")
{
return rxml.readElementText();
}
//if there isn't a prefix available, find parent prefix in parent folder
else
{
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
{
rxml.skipCurrentElement();
rxml.readNext();
}
}
}
const QStringList candidate_dirs = {
QETApp::customElementsDir(),
QETApp::companyElementsDir()
};
for (const QString &dir : candidate_dirs) {
const QString candidate =
QDir(dir).filePath(QStringLiteral("qet_labels.xml"));
for (const QStringList &segments : {path_from_root, path}) {
const QString prefix = prefixFromLabelFile(
candidate, segments, segments.size() - 1);
if (!prefix.isNull()) {
return prefix;
}
}
rxml.readNext();
}
}
else
{
QString qet_labels = "10_electric/qet_labels.xml";
QString filepath = QETApp::commonElementsDir().append(qet_labels);
QFile file(filepath);
file.isReadable();
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
rxml.setDevice(&file);
rxml.readNext();
while(!rxml.atEnd())
{
if (rxml.attributes().value("name").toString() == path[i])
{
rxml.readNext();
i=i-1;
//reached element directory
if (i==0)
{
for (int j=i; j<= dirLevel; j = j +1)
{
//if there is a prefix available apply prefix
if(rxml.name().toString()=="prefix")
{
return rxml.readElementText();
}
//if there isn't a prefix available, find parent prefix in parent folder
else
{
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
{
rxml.skipCurrentElement();
rxml.readNext();
}
}
}
}
}
rxml.readNext();
}
}
return QString();
}
+3
View File
@@ -47,11 +47,14 @@ namespace autonum
void clear();
QStringList unit;
/// Values of the cyclic (modulo) parts, referenced by %seqw_N.
QStringList wrap;
QStringList unit_folio;
QStringList ten;
QStringList ten_folio;
QStringList hundred;
QStringList hundred_folio;
QStringList alpha;
};
/**
+164 -12
View File
@@ -51,15 +51,22 @@ void NumerotationContext::clear ()
@param value the value itself
@param increase the increase number of value
@param initialvalue
@param modulus wrap-and-carry modulus (0 means "not a wrapping part")
@param format zero-padding mask, spreadsheet style: "00" pads to two
digits, "000" to three. Empty keeps the part type's natural width, so
an absent format reproduces exactly the behaviour of every context
written before this field existed.
@return true if value is append
*/
bool NumerotationContext::addValue(const QString &type,
const QVariant &value,
const int increase,
const int initialvalue) {
if (!keyIsAcceptable(type) && !value.canConvert(QVariant::String))
const int initialvalue,
const int modulus,
const QString &format) {
if (!keyIsAcceptable(type) && !value.canConvert<QString>())
return false;
if (keyIsNumber(type) && !value.canConvert(QVariant::Int))
if (keyIsNumber(type) && !value.canConvert<int>())
return false;
QString valuestr = value.toString();
@@ -70,7 +77,11 @@ bool NumerotationContext::addValue(const QString &type,
+ "|"
+ QString::number(increase)
+ "|"
+ QString::number(initialvalue);
+ QString::number(initialvalue)
+ "|"
+ QString::number(modulus)
+ "|"
+ QString(format).remove("|");
return true;
}
@@ -125,7 +136,7 @@ QStringList NumerotationContext::itemAt(const int i) const
*/
QString NumerotationContext::validRegExpNum () const
{
return ("unit|unitfolio|ten|tenfolio|hundred|hundredfolio|string|idfolio|folio|plant|locmach|elementline|elementcolumn|elementprefix");
return ("unit|unitfolio|ten|tenfolio|hundred|hundredfolio|wrap|alpha|string|idfolio|folio|plant|locmach|elementline|elementcolumn|elementprefix");
}
/**
@@ -134,7 +145,7 @@ QString NumerotationContext::validRegExpNum () const
*/
QString NumerotationContext::validRegExpNumber() const
{
return ("unit|unitfolio|ten|tenfolio|hundred|hundredfolio");
return ("unit|unitfolio|ten|tenfolio|hundred|hundredfolio|wrap");
}
/**
@@ -172,6 +183,12 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
strl.at(0) == ("hundredfolio")) {
part.setAttribute("initialvalue", strl.at(3));
}
if (strl.at(0) == ("wrap") && strl.size() > 4) {
part.setAttribute("modulus", strl.at(4));
}
if (strl.size() > 5 && !strl.at(5).isEmpty()) {
part.setAttribute("format", strl.at(5));
}
num_auto.appendChild(part);
}
return num_auto;
@@ -183,7 +200,7 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
*/
void NumerotationContext::fromXml(QDomElement &e) {
clear();
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt());
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt(), qde.attribute("modulus").toInt(), qde.attribute("format"));
}
/**
@@ -193,10 +210,145 @@ void NumerotationContext::fromXml(QDomElement &e) {
@param content to replace current value
*/
void NumerotationContext::replaceValue(int index, QString content) {
QString sep = "|";
QString type = content_[index].split("|").at(0);
QStringList strl = content_[index].split("|");
QString type = strl.at(0);
const QString& value = std::move(content);
QString increase = content_[index].split("|").at(2);
QString initvalue = content_[index].split("|").at(3);
content_[index].replace(content_[index], type + "|" + value + "|" + increase + "|" + initvalue);
QString increase = strl.at(2);
QString initvalue = strl.at(3);
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
QString format = strl.size() > 5 ? strl.at(5) : QString();
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus + "|" + format;
}
/**
@brief NumerotationContext::replaceIncrease
Change how much this part advances per step, leaving its current value,
initial value, modulus and format untouched. Sibling to replaceValue(),
which deliberately never touches this field.
@param index of NC item
@param increase new increase for that item
*/
void NumerotationContext::replaceIncrease(int index, int increase) {
QStringList strl = content_[index].split("|");
QString type = strl.at(0);
QString value = strl.at(1);
QString initvalue = strl.at(3);
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
QString format = strl.size() > 5 ? strl.at(5) : QString();
content_[index] = type + "|" + value + "|" + QString::number(increase) + "|" + initvalue + "|" + modulus + "|" + format;
}
/**
@brief NumerotationContext::formatOf
@param item : a context item as returned by itemAt()
@return the part's zero-padding mask, or an empty string when it has
none -- which every context written before the field existed will be.
*/
QString NumerotationContext::formatOf(const QStringList &item)
{
return item.size() > 5 ? item.at(5) : QString();
}
/**
@brief NumerotationContext::formatValue
@param item : a context item as returned by itemAt()
@return the part's value, zero-padded exactly as
autonum::setSequentialToList() pads it when composing a real label: an
explicit format mask wins, then "ten"/"hundred" parts get their
implicit 2/3-digit width, "alpha" is used as-is, everything else is a
plain number. Kept in step with that function by hand since the two
cannot share code without exposing an assignvariables.cpp-local helper.
*/
QString NumerotationContext::formatValue(const QStringList &item)
{
const QString &type = item.at(0);
const QString &value = item.at(1);
if (type == QLatin1String("alpha"))
return value;
const QString mask = formatOf(item);
if (!mask.isEmpty())
return QString("%1").arg(value.toInt(), mask.length(), 10, QChar('0'));
if (type == QLatin1String("ten") || type == QLatin1String("tenfolio"))
return QString("%1").arg(value.toInt(), 2, 10, QChar('0'));
if (type == QLatin1String("hundred") || type == QLatin1String("hundredfolio"))
return QString("%1").arg(value.toInt(), 3, 10, QChar('0'));
return QString::number(value.toInt());
}
/**
@brief NumerotationContext::saveToSettings
Save a hash of named NumerotationContexts to QSettings.
@param contexts : the named rules to save
@param currentRule : the name of the currently active rule
@param settings : QSettings instance
@param prefix : settings key prefix (e.g. "autonum/conductor")
*/
void NumerotationContext::saveToSettings(
const QHash<QString, NumerotationContext> &contexts,
const QString &currentRule,
QSettings &settings,
const QString &prefix)
{
settings.setValue(prefix + "/current", currentRule);
// Clear stale array entries before writing (beginWriteArray does not
// remove entries beyond the new size).
settings.remove(prefix + "/rules");
QStringList names = contexts.keys();
settings.beginWriteArray(prefix + "/rules", names.size());
for (int i = 0; i < names.size(); ++i) {
settings.setArrayIndex(i);
const QString &name = names.at(i);
const NumerotationContext &nc = contexts.value(name);
settings.setValue("name", name);
// Serialize context to XML string
QDomDocument doc;
NumerotationContext nc_copy = nc;
QDomElement root = nc_copy.toXml(doc, "context");
doc.appendChild(root);
settings.setValue("xml", doc.toString());
}
settings.endArray();
}
/**
@brief NumerotationContext::loadFromSettings
Load named NumerotationContexts from QSettings.
@param settings : QSettings instance
@param prefix : settings key prefix (e.g. "autonum/conductor")
@return pair of (hash of named rules, name of current rule)
*/
QPair<QHash<QString, NumerotationContext>, QString> NumerotationContext::loadFromSettings(
QSettings &settings,
const QString &prefix)
{
QPair<QHash<QString, NumerotationContext>, QString> result;
QHash<QString, NumerotationContext> &contexts = result.first;
QString &currentRule = result.second;
currentRule = settings.value(prefix + "/current").toString();
int size = settings.beginReadArray(prefix + "/rules");
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
QString name = settings.value("name").toString();
QString xmlStr = settings.value("xml").toString();
if (name.isEmpty() || xmlStr.isEmpty()) continue;
QDomDocument doc;
if (!doc.setContent(xmlStr)) continue;
QDomElement root = doc.documentElement();
NumerotationContext nc;
nc.fromXml(root);
contexts.insert(name, nc);
}
settings.endArray();
return result;
}
+21 -1
View File
@@ -21,6 +21,8 @@
#include <QStringList>
#include <QVariant>
#include <QDomElement>
#include <QHash>
#include <QSettings>
/**
This class represents a numerotation context, i.e. the data (type, value, increase)
@@ -36,7 +38,12 @@ class NumerotationContext
bool addValue(const QString &,
const QVariant & = QVariant(1),
const int = 1,
const int = 0);
const int = 0,
const int = 0,
const QString & = QString());
/// Zero-padding mask of a part, e.g. "00"; empty means the type's
/// own natural width. See addValue().
static QString formatOf(const QStringList &item);
QString operator[] (const int &) const;
void operator << (const NumerotationContext &);
int size() const;
@@ -49,6 +56,19 @@ class NumerotationContext
QDomElement toXml(QDomDocument &, const QString&);
void fromXml(QDomElement &);
void replaceValue(int, QString);
void replaceIncrease(int, int);
/// Zero-pad a part's value the same way the real numbering engine
/// does (autonum::setSequentialToList in assignvariables.cpp), so a
/// UI preview of a part's value matches what actually gets rendered.
static QString formatValue(const QStringList &item);
static void saveToSettings(const QHash<QString, NumerotationContext> &contexts,
const QString &currentRule,
QSettings &settings,
const QString &prefix);
static QPair<QHash<QString, NumerotationContext>, QString> loadFromSettings(
QSettings &settings,
const QString &prefix);
private:
QStringList content_;
+260 -3
View File
@@ -48,6 +48,15 @@ NumerotationContext NumerotationContextCommands::next()
QStringList str = context_.itemAt(i);
setNumStrategy(str.at(0));
contextnum << strategy_ -> next(context_, i);
//Wrap-and-carry: str still holds the pre-increment value, so
//this checks the same condition WrapNum::next() used to decide
//whether to wrap its own value back to 0.
if (str.at(0) == "wrap" && str.size() > 4) {
int modulus = str.at(4).toInt();
if (modulus > 0 && (str.at(1).toInt() + str.at(2).toInt()) >= modulus)
carry(contextnum, i - 1);
}
}
return contextnum;
}
@@ -64,10 +73,80 @@ NumerotationContext NumerotationContextCommands::previous()
QStringList str = context_.itemAt(i);
setNumStrategy(str.at(0));
contextnum << strategy_ -> previous(context_, i);
if (str.at(0) == "wrap" && str.size() > 4) {
int modulus = str.at(4).toInt();
if (modulus > 0 && (str.at(1).toInt() - str.at(2).toInt()) < 0)
borrow(contextnum, i - 1);
}
}
return contextnum;
}
/**
@brief NumerotationContextCommands::carry
Add one unit to the nearest numeric part at or before from_index in
contextnum, skipping non-numeric parts (e.g. a "." string separator)
along the way. If that part is itself a wrap part and this pushes it
to (or past) its own modulus, it wraps back to 0 and the carry
cascades further back -- so wrap parts can be chained (e.g. seconds
wrapping into minutes wrapping into hours).
@param contextnum the context being built by next(); already contains
entries for every index <= from_index
@param from_index index to start looking from, going backwards
*/
void NumerotationContextCommands::carry(NumerotationContext &contextnum, int from_index)
{
for (int j = from_index; j >= 0; --j) {
QStringList strl = contextnum.itemAt(j);
if (!contextnum.keyIsNumber(strl.at(0)))
continue;
int value = strl.at(1).toInt() + 1;
if (strl.at(0) == "wrap" && strl.size() > 4) {
int modulus = strl.at(4).toInt();
if (modulus > 0 && value >= modulus) {
contextnum.replaceValue(j, QString::number(value - modulus));
carry(contextnum, j - 1);
return;
}
}
contextnum.replaceValue(j, QString::number(value));
return;
}
//No preceding numeric part: the carry has nowhere to go and is dropped,
//same as any other counter in this engine has no overflow tracking
//beyond the parts the user actually configured.
}
/**
@brief NumerotationContextCommands::borrow
Inverse of carry(): subtract one unit from the nearest numeric part at
or before from_index. If that part is itself a wrap part and this
takes it below 0, it wraps to (modulus - 1) and the borrow cascades
further back.
*/
void NumerotationContextCommands::borrow(NumerotationContext &contextnum, int from_index)
{
for (int j = from_index; j >= 0; --j) {
QStringList strl = contextnum.itemAt(j);
if (!contextnum.keyIsNumber(strl.at(0)))
continue;
int value = strl.at(1).toInt() - 1;
if (strl.at(0) == "wrap" && strl.size() > 4) {
int modulus = strl.at(4).toInt();
if (modulus > 0 && value < 0) {
contextnum.replaceValue(j, QString::number(value + modulus));
borrow(contextnum, j - 1);
return;
}
}
contextnum.replaceValue(j, QString::number(value));
return;
}
}
/**
@brief NumerotationContextCommands::toFinalString
@return the string represented by the numerotation context
@@ -117,10 +196,18 @@ void NumerotationContextCommands::setNumStrategy(const QString &str) {
strategy_ = new HundredFNum (diagram_);
return;
}
else if (str == "wrap") {
strategy_ = new WrapNum (diagram_);
return;
}
else if (str == "string") {
strategy_ = new StringNum (diagram_);
return;
}
else if (str == "alpha") {
strategy_ = new AlphaNum (diagram_);
return;
}
else if (str == "idfolio") {
strategy_ = new IdFolioNum (diagram_);
return;
@@ -172,7 +259,7 @@ NumerotationContext NumStrategy::nextString (const NumerotationContext &nc,
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), strl.at(1), strl.at(2).toInt());
newnc.addValue(strl.at(0), strl.at(1), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -186,7 +273,7 @@ NumerotationContext NumStrategy::nextNumber (const NumerotationContext &nc,
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
QString value = QString::number( (strl.at(1).toInt()) + (strl.at(2).toInt()) );
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt());
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt(), 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -200,7 +287,7 @@ NumerotationContext NumStrategy::previousNumber(const NumerotationContext &nc,
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
QString value = QString::number( (strl.at(1).toInt()) - (strl.at(2).toInt()) );
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt());
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt(), 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -431,6 +518,62 @@ NumerotationContext HundredFNum::previous(const NumerotationContext &nc, const i
return (previousNumber(nc, i));
}
/**
Constructor
*/
WrapNum::WrapNum (Diagram *d):
NumStrategy (d)
{}
/**
@brief WrapNum::toRepresentedString
@return the represented string of num
*/
QString WrapNum::toRepresentedString(const QString num) const
{
return (num);
}
/**
@brief WrapNum::next
Wraps this part's own value back to 0 every `modulus` values (carrying
into the adjacent part is handled by NumerotationContextCommands::next(),
which has visibility into the other parts).
@return the next NumerotationContext nc at position i
*/
NumerotationContext WrapNum::next (const NumerotationContext &nc, const int i) const
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
int increase = strl.at(2).toInt();
int modulus = strl.size() > 4 ? strl.at(4).toInt() : 0;
int new_value = strl.at(1).toInt() + increase;
if (modulus > 0)
new_value %= modulus;
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus, NumerotationContext::formatOf(strl));
return (newnc);
}
/**
@brief WrapNum::previous
@return the previous NumerotationContext nc at posiiton i
*/
NumerotationContext WrapNum::previous(const NumerotationContext &nc, const int i) const
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
int increase = strl.at(2).toInt();
int modulus = strl.size() > 4 ? strl.at(4).toInt() : 0;
int new_value = strl.at(1).toInt() - increase;
if (modulus > 0) {
new_value %= modulus;
if (new_value < 0)
new_value += modulus;
}
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus, NumerotationContext::formatOf(strl));
return (newnc);
}
/**
Constructor
*/
@@ -465,6 +608,120 @@ NumerotationContext StringNum::previous(const NumerotationContext &nc, const int
return (nextString(nc, i));
}
namespace
{
/**
* @brief incrementAlpha
* Base-26 letter increment (a, b, ... z, aa, ab, ... az, ba, ...),
* the same algorithm as incrementing a spreadsheet column name.
* Carries right-to-left on 'z'/'Z' overflow; if the whole string
* overflows, a new leading letter is prepended (lowercase 'a').
* @param value : current alphabetic value; treated as "a" if empty.
* @return the next value.
*/
QString incrementAlpha(QString value)
{
if (value.isEmpty()) {
return QStringLiteral("a");
}
int i = value.length() - 1;
while (i >= 0 && (value.at(i) == QLatin1Char('z') || value.at(i) == QLatin1Char('Z'))) {
value[i] = value.at(i).isUpper() ? QLatin1Char('A') : QLatin1Char('a');
--i;
}
if (i < 0) {
value.prepend(QLatin1Char('a'));
} else {
value[i] = QChar(value.at(i).unicode() + 1);
}
return value;
}
/**
* @brief decrementAlpha
* Inverse of incrementAlpha(): borrows right-to-left on 'a'/'A'
* underflow. Symmetric shrink case (e.g. "aa" -> "z"): once every
* position has borrowed, the leading letter is dropped rather than
* left as an extra 'z'. A single-letter value already at "a"/"A" has
* no representable predecessor and is left unchanged, the same way
* the numeric parts don't clamp but a blank label would be worse
* here than a value that stops decreasing.
* @param value : current alphabetic value; treated as "a" if empty.
* @return the previous value.
*/
QString decrementAlpha(QString value)
{
if (value.isEmpty()) {
return QStringLiteral("a");
}
if (value.length() == 1) {
//A single letter has no representable predecessor once it
//reaches "a"/"A" -- clamp rather than mutate, since the loop
//below would otherwise turn it into "z"/"Z" (borrowing past
//the only position there is).
if (value.at(0) == QLatin1Char('a') || value.at(0) == QLatin1Char('A')) {
return value;
}
return QChar(value.at(0).unicode() - 1);
}
int i = value.length() - 1;
while (i >= 0 && (value.at(i) == QLatin1Char('a') || value.at(i) == QLatin1Char('A'))) {
value[i] = value.at(i).isUpper() ? QLatin1Char('Z') : QLatin1Char('z');
--i;
}
if (i < 0) {
//Every position borrowed: the whole value was "a...a", whose
//predecessor is one fewer "z" (e.g. "aa" -> "z").
value.remove(0, 1);
} else {
value[i] = QChar(value.at(i).unicode() - 1);
}
return value;
}
}
/**
Constructor
*/
AlphaNum::AlphaNum (Diagram *d):
NumStrategy (d)
{}
/**
@brief AlphaNum::toRepresentedString
@return the represented string of str
*/
QString AlphaNum::toRepresentedString(const QString str) const
{
return (str);
}
/**
@brief AlphaNum::next
@return the next NumerotationContext nc at position i
*/
NumerotationContext AlphaNum::next (const NumerotationContext &nc, const int i) const
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), incrementAlpha(strl.at(1)), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
/**
@brief AlphaNum::previous
@return the previous NumerotationContext nc at posiiton i
*/
NumerotationContext AlphaNum::previous(const NumerotationContext &nc, const int i) const
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), decrementAlpha(strl.at(1)), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
/**
Constructor
*/
@@ -38,6 +38,8 @@ class NumerotationContextCommands
private:
void setNumStrategy (const QString &);
static void carry(NumerotationContext &contextnum, int from_index);
static void borrow(NumerotationContext &contextnum, int from_index);
Diagram *diagram_;
NumerotationContext context_;
@@ -115,6 +117,24 @@ class HundredFNum: public NumStrategy
NumerotationContext previous (const NumerotationContext &, const int) const override;
};
/**
@brief The WrapNum class
A counter that wraps back to 0 (borrowing from initialvalue on the
way down) every `modulus` values, instead of counting up forever like
UnitNum/TenNum/HundredNum. Its own next()/previous() only computes its
own wrapped value; carrying into (or borrowing from) the preceding
numeric part is handled by NumerotationContextCommands::next()/
previous(), since only the composition loop can see adjacent parts.
*/
class WrapNum: public NumStrategy
{
public:
WrapNum (Diagram *);
QString toRepresentedString(const QString) const override;
NumerotationContext next (const NumerotationContext &, const int) const override;
NumerotationContext previous (const NumerotationContext &, const int) const override;
};
class StringNum: public NumStrategy
{
public:
@@ -124,6 +144,23 @@ class StringNum: public NumStrategy
NumerotationContext previous (const NumerotationContext &, const int) const override;
};
/**
@brief The AlphaNum class
Alphabetic auto-numbering (a, b, ... z, aa, ab, ...). Unlike StringNum
(a fixed, non-incrementing text segment), this is a real base-26
counter: next()/previous() carry/borrow entirely within this part's
own value, the same self-contained shape every other incrementing
NumStrategy already has.
*/
class AlphaNum: public NumStrategy
{
public:
AlphaNum (Diagram *);
QString toRepresentedString(const QString) const override;
NumerotationContext next (const NumerotationContext &, const int) const override;
NumerotationContext previous (const NumerotationContext &, const int) const override;
};
class IdFolioNum: public NumStrategy
{
public:
@@ -0,0 +1,78 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "renumberelementscommand.h"
#include "../qetproject.h"
#include "../qetgraphicsitem/element.h"
RenumberElementsCommand::RenumberElementsCommand(
QETProject *project,
QVector<ElementChange> changes,
QHash<QString, NumerotationContext> old_ctx,
QHash<QString, NumerotationContext> new_ctx,
const QString &text)
: QUndoCommand(text)
, project_(project)
, changes_(std::move(changes))
, old_ctx_(std::move(old_ctx))
, new_ctx_(std::move(new_ctx))
{
}
void RenumberElementsCommand::undo()
{
apply(false);
}
void RenumberElementsCommand::redo()
{
// QUndoStack calls redo() once immediately after push(); keep standard behaviour.
if (first_redo_) {
first_redo_ = false;
}
apply(true);
}
void RenumberElementsCommand::apply(bool use_new)
{
if (!project_) return;
// Restore project contexts
const auto &ctx = use_new ? new_ctx_ : old_ctx_;
for (auto it = ctx.constBegin(); it != ctx.constEnd(); ++it) {
project_->addElementAutoNum(it.key(), it.value());
}
// Apply per-element changes
for (const ElementChange &c : changes_) {
if (!c.element) continue;
const bool frozen = use_new ? c.new_frozen : c.old_frozen;
const auto &infos = use_new ? c.new_infos : c.old_infos;
const auto &seq = use_new ? c.new_seq : c.old_seq;
// Temporarily unfreeze so that label/infos update correctly.
const bool was_frozen = c.element->isFreezeLabel();
if (was_frozen) c.element->freezeLabel(false);
c.element->rSequenceStruct() = seq;
c.element->setElementInformations(infos);
c.element->freezeLabel(frozen);
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
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 RENUMBERELEMENTSCOMMAND_H
#define RENUMBERELEMENTSCOMMAND_H
#include <QUndoCommand>
#include <QHash>
#include <QVector>
#include "../diagramcontext.h"
#include "assignvariables.h" // defines autonum::sequentialNumbers
class Element;
class QETProject;
/**
* @brief Undoable renumbering of element labels and sequence structs.
*/
class RenumberElementsCommand final : public QUndoCommand
{
public:
struct ElementChange
{
Element *element = nullptr;
DiagramContext old_infos;
DiagramContext new_infos;
autonum::sequentialNumbers old_seq;
autonum::sequentialNumbers new_seq;
bool old_frozen = false;
bool new_frozen = false;
};
RenumberElementsCommand(
QETProject *project,
QVector<ElementChange> changes,
QHash<QString, NumerotationContext> old_ctx,
QHash<QString, NumerotationContext> new_ctx,
const QString &text);
void undo() override;
void redo() override;
private:
void apply(bool use_new);
private:
QETProject *project_ = nullptr;
QVector<ElementChange> changes_;
QHash<QString, NumerotationContext> old_ctx_;
QHash<QString, NumerotationContext> new_ctx_;
bool first_redo_ = true;
};
#endif // RENUMBERELEMENTSCOMMAND_H
+401 -51
View File
@@ -20,10 +20,18 @@
#include "../../diagram.h"
#include "../../diagramview.h"
#include "../../qetapp.h"
#include "../../shortcutmanager.h"
#include "../../titleblockproperties.h"
#include "../../ui/projectpropertiesdialog.h"
#include "../numerotationcontext.h"
#include "../numerotationcontextcommands.h"
#include "ui_autonumberingdockwidget.h"
#include "../../undocommand/changetitleblockcommand.h"
#include <QComboBox>
#include <QLineEdit>
#include <QSignalBlocker>
#include <QSpinBox>
/**
@brief AutoNumberingDockWidget::AutoNumberingDockWidget
@@ -57,6 +65,32 @@ void AutoNumberingDockWidget::clear()
ui->m_conductor_cb->clear();
ui->m_element_cb->clear();
ui->m_folio_cb->clear();
ui->m_conductor_value_le->clear();
ui->m_element_value_le->clear();
ui->m_folio_value_le->clear();
ui->m_conductor_next_le->clear();
ui->m_element_next_le->clear();
ui->m_folio_next_le->clear();
}
/**
@brief AutoNumberingDockWidget::rowFor
@return the combo/value/increase/next widgets that make up category's row.
*/
AutoNumberingDockWidget::Row AutoNumberingDockWidget::rowFor(AutoNumCategory category) const
{
switch (category) {
case AutoNumCategory::Conductor:
return {ui->m_conductor_cb, ui->m_conductor_value_le,
ui->m_conductor_increase_sb, ui->m_conductor_next_le};
case AutoNumCategory::Element:
return {ui->m_element_cb, ui->m_element_value_le,
ui->m_element_increase_sb, ui->m_element_next_le};
case AutoNumCategory::Folio:
return {ui->m_folio_cb, ui->m_folio_value_le,
ui->m_folio_increase_sb, ui->m_folio_next_le};
}
return {nullptr, nullptr, nullptr, nullptr};
}
void AutoNumberingDockWidget::projectClosed()
@@ -79,35 +113,23 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
//Disconnect previous project
if (m_project && m_project_view)
{
//Conductor Signals
disconnect(m_project, SIGNAL(conductorAutoNumChanged()),
this,SLOT(conductorAutoNumChanged()));
disconnect (m_project,SIGNAL(conductorAutoNumRemoved()),
this,SLOT(conductorAutoNumChanged()));
disconnect (m_project,SIGNAL(conductorAutoNumAdded()),
this,SLOT(conductorAutoNumChanged()));
disconnect(m_project_view,SIGNAL(diagramActivated(DiagramView*)),
this,SLOT(setConductorActive(DiagramView*)));
//Element Signals
disconnect (m_project,SIGNAL(elementAutoNumRemoved(QString)),
this,SLOT(elementAutoNumChanged()));
disconnect (m_project,SIGNAL(elementAutoNumAdded(QString)),
this,SLOT(elementAutoNumChanged()));
disconnect(m_project, &QETProject::conductorAutoNumChanged, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project, &QETProject::conductorAutoNumRemoved, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project, &QETProject::conductorAutoNumAdded, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::setConductorActive);
//Element Signals
disconnect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
disconnect(m_project, &QETProject::elementAutoNumAdded, this, &AutoNumberingDockWidget::elementAutoNumChanged);
//Folio Signals
disconnect (m_project,SIGNAL(folioAutoNumRemoved()),
this,SLOT(folioAutoNumChanged()));
disconnect (m_project,SIGNAL(folioAutoNumAdded()),
this,SLOT(folioAutoNumChanged()));
disconnect (this,
SIGNAL(folioAutoNumChanged(QString)),
&m_project_view->currentDiagram()->diagram()->border_and_titleblock,
SLOT (slot_setAutoPageNum(QString)));
disconnect(m_project, SIGNAL(defaultTitleBlockPropertiesChanged()),
this,SLOT(setActive()));
disconnect(m_project, &QETProject::folioAutoNumRemoved, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
disconnect(m_project, &QETProject::folioAutoNumAdded, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
disconnect(m_project, &QETProject::defaultTitleBlockPropertiesChanged, this, &AutoNumberingDockWidget::setActive);
//Conductor, Element and Folio Signals
disconnect(m_project, &QETProject::autoNumContextUpdated,
this, &AutoNumberingDockWidget::refreshValueFields);
disconnect(m_project, &QETProject::destroyed,
this, &AutoNumberingDockWidget::projectClosed);
}
@@ -116,42 +138,31 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
m_project_view = projectview;
this->setEnabled(true);
//Conductor Signals
connect(m_project, SIGNAL(conductorAutoNumChanged()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project,SIGNAL(conductorAutoNumRemoved()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project,SIGNAL(conductorAutoNumAdded()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project_view,SIGNAL(diagramActivated(DiagramView*)),
this,SLOT(setConductorActive(DiagramView*)));
connect(m_project, &QETProject::conductorAutoNumChanged, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project, &QETProject::conductorAutoNumRemoved, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project, &QETProject::conductorAutoNumAdded, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::setConductorActive);
//Element Signals
connect (m_project,SIGNAL(elementAutoNumRemoved(QString)),
this,SLOT(elementAutoNumChanged()));
connect (m_project,SIGNAL(elementAutoNumAdded(QString)),
this,SLOT(elementAutoNumChanged()));
//Element Signals
connect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
connect(m_project, &QETProject::elementAutoNumAdded, this, &AutoNumberingDockWidget::elementAutoNumChanged);
//Folio Signals
connect (m_project,SIGNAL(folioAutoNumRemoved()),
this,SLOT(folioAutoNumChanged()));
connect (m_project,SIGNAL(folioAutoNumAdded()),
this,SLOT(folioAutoNumChanged()));
connect (this,
SIGNAL(folioAutoNumChanged(QString)),
&m_project_view->currentDiagram()->diagram()->border_and_titleblock,
SLOT (slot_setAutoPageNum(QString)));
connect(m_project, SIGNAL(defaultTitleBlockPropertiesChanged()),
this,SLOT(setActive()));
connect(m_project, &QETProject::folioAutoNumRemoved, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
connect(m_project, &QETProject::folioAutoNumAdded, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
connect(m_project, &QETProject::defaultTitleBlockPropertiesChanged, this, &AutoNumberingDockWidget::setActive);
//Conductor, Element and Folio Signals
connect(m_project, &QETProject::autoNumContextUpdated,
this, &AutoNumberingDockWidget::refreshValueFields);
connect(m_project, &QETProject::destroyed,
this, &AutoNumberingDockWidget::projectClosed);
//Set Combobox Context
setContext();
ui->m_configure_pb->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P);
ShortcutManager::instance().registerAction(ui->m_configure_pb, "autonum.configure",
tr("Autonumérotation"), Qt::CTRL | Qt::SHIFT | Qt::Key_P);
}
/**
@@ -187,6 +198,12 @@ void AutoNumberingDockWidget::setContext()
{ ui->m_folio_cb -> addItem(str);}
}
//The combo boxes have just been repopulated, so the value fields next
//to them are showing whatever the previous project left there.
refreshRow(AutoNumCategory::Conductor);
refreshRow(AutoNumCategory::Element);
refreshRow(AutoNumCategory::Folio);
this->setActive();
}
@@ -261,6 +278,7 @@ void AutoNumberingDockWidget::on_m_conductor_cb_activated(int)
m_project->setCurrentConductorAutoNum(current_autonum);
m_project_view->currentDiagram()->diagram()->setConductorsAutonumName(current_autonum);
m_project_view->currentDiagram()->diagram()->loadCndFolioSeq();
refreshRow(AutoNumCategory::Conductor);
}
/**
@@ -289,6 +307,7 @@ void AutoNumberingDockWidget::on_m_element_cb_activated(int)
{
m_project->setCurrrentElementAutonum(ui->m_element_cb->currentText());
m_project_view->currentDiagram()->diagram()->loadElmtFolioSeq();
refreshRow(AutoNumCategory::Element);
}
/**
@@ -325,7 +344,17 @@ void AutoNumberingDockWidget::on_m_folio_cb_activated(int) {
ip.folio = "%id/%total";
m_project->setDefaultTitleBlockProperties(ip);
}
emit(folioAutoNumChanged(current_autonum));
if (m_project_view && m_project_view->currentDiagram()) {
Diagram *diagram = m_project_view->currentDiagram()->diagram();
TitleBlockProperties old_properties = diagram->border_and_titleblock.exportTitleBlock();
TitleBlockProperties new_properties = old_properties;
new_properties.auto_page_num = ip.auto_page_num;
new_properties.folio = ip.folio;
if (new_properties != old_properties)
diagram->undoStack().push(new ChangeTitleBlockCommand(diagram, old_properties, new_properties));
}
refreshRow(AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_configure_pb_clicked()
@@ -337,3 +366,324 @@ void AutoNumberingDockWidget::on_m_configure_pb_clicked()
ppd.exec();
}
}
void AutoNumberingDockWidget::on_m_conductor_reset_start_pb_clicked()
{
resetAutoNum(ui->m_conductor_cb, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_reset_start_pb_clicked()
{
resetAutoNum(ui->m_element_cb, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_reset_start_pb_clicked()
{
resetAutoNum(ui->m_folio_cb, AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_conductor_value_le_editingFinished()
{
applyValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_value_le_editingFinished()
{
applyValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_value_le_editingFinished()
{
applyValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_conductor_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_conductor_cb, ui->m_conductor_increase_sb, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_element_cb, ui->m_element_increase_sb, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_folio_cb, ui->m_folio_increase_sb, AutoNumCategory::Folio);
}
/**
@brief AutoNumberingDockWidget::contextFor
@return the numerotation context named by combo_box, for category
*/
NumerotationContext AutoNumberingDockWidget::contextFor(QComboBox *combo_box, AutoNumCategory category) const
{
const QString key = combo_box->currentText();
switch (category) {
case AutoNumCategory::Conductor: return m_project->conductorAutoNum(key);
case AutoNumCategory::Element: return m_project->elementAutoNum(key);
case AutoNumCategory::Folio: return m_project->folioAutoNum(key);
}
return NumerotationContext();
}
/**
@brief AutoNumberingDockWidget::storeContext
Write context back under the name selected in combo_box and flag the
project as modified -- without that last step the change is not saved
and the user is never asked to save it on close.
*/
void AutoNumberingDockWidget::storeContext(QComboBox *combo_box, AutoNumCategory category, const NumerotationContext &context)
{
const QString key = combo_box->currentText();
switch (category) {
case AutoNumCategory::Conductor: m_project->addConductorAutoNum(key, context); break;
case AutoNumCategory::Element: m_project->addElementAutoNum(key, context); break;
case AutoNumCategory::Folio: m_project->addFolioAutoNum(key, context); break;
}
m_project->setModified(true);
}
/**
@brief AutoNumberingDockWidget::counterIndex
@return the index of the part the value field shows: the last one that
actually progresses, i.e. the least significant digit of the number.
-1 when the context has no progressing part at all.
*/
int AutoNumberingDockWidget::counterIndex(const NumerotationContext &context)
{
for (int i = context.size() - 1 ; i >= 0 ; --i)
{
const QString type = context.itemAt(i).at(0);
if (type == QLatin1String("unit")
|| type == QLatin1String("ten")
|| type == QLatin1String("hundred")
|| type == QLatin1String("unitfolio")
|| type == QLatin1String("tenfolio")
|| type == QLatin1String("hundredfolio")
|| type == QLatin1String("wrap")
|| type == QLatin1String("alpha"))
return i;
}
return -1;
}
/**
@brief AutoNumberingDockWidget::refreshValueFields
Re-read all three value fields from the project. Called whenever a
numerotation context's values change, which includes every element or
conductor that consumes the next number -- without this the field only
caught up when the user re-picked a rule from the combo box, because
the combo's activated() signal fires on user interaction alone.
*/
void AutoNumberingDockWidget::refreshValueFields()
{
//Leave alone a row the user is typing in: numbering an element
//refreshes all three rows, and overwriting a half-typed value or
//increment under the cursor is worse than showing it a moment out
//of date. Only this automatic path skips; an explicit refresh after
//a reset or an edit still writes, so the row always ends up
//canonical. The next-value preview has no such guard: it is
//read-only, so there is nothing a refresh could clobber.
for (AutoNumCategory category : {AutoNumCategory::Conductor,
AutoNumCategory::Element,
AutoNumCategory::Folio})
{
const Row row = rowFor(category);
if (!row.value->hasFocus() && !row.increase->hasFocus())
refreshRow(category);
}
}
/**
@brief AutoNumberingDockWidget::refreshValueField
Show the current value of the selected context's counter, so the field
always reflects where the numbering has actually got to.
*/
void AutoNumberingDockWidget::refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
{
line_edit->clear();
line_edit->setEnabled(false);
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
line_edit->setEnabled(index >= 0);
line_edit->setText(index >= 0 ? context.itemAt(index).at(1) : QString());
}
/**
@brief AutoNumberingDockWidget::applyValueField
Write the value typed in line_edit to the counter it displays. An empty
field is treated as "no change" rather than as an empty value, so
clearing the box by accident cannot wipe the counter.
*/
void AutoNumberingDockWidget::applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
return;
const QString typed = line_edit->text();
if (typed.isEmpty() || typed == context.itemAt(index).at(1))
{
refreshRow(category);
return;
}
context.replaceValue(index, typed);
storeContext(combo_box, category, context);
refreshRow(category);
}
/**
@brief AutoNumberingDockWidget::refreshIncreaseField
Show the counter's current step size (bug #331: previously only
reachable from the full configuration dialog, via "Configurer").
*/
void AutoNumberingDockWidget::refreshIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category)
{
//QSpinBox::setValue() emits valueChanged() even when called
//programmatically. Without blocking it, this refresh would
//immediately re-trigger on_..._increase_sb_valueChanged() ->
//applyIncreaseField() -> storeContext() -> the project's
//autoNumContextUpdated signal -> refreshValueFields() -> back here.
const QSignalBlocker blocker(increase_sb);
if (!m_project || combo_box->currentText().isEmpty())
{
increase_sb->setEnabled(false);
increase_sb->setValue(increase_sb->minimum());
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
increase_sb->setEnabled(index >= 0);
increase_sb->setValue(index >= 0 ? context.itemAt(index).at(2).toInt()
: increase_sb->minimum());
}
/**
@brief AutoNumberingDockWidget::applyIncreaseField
Write the spin box's step size to the counter it displays (bug #331).
*/
void AutoNumberingDockWidget::applyIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
return;
if (increase_sb->value() == context.itemAt(index).at(2).toInt())
return;
context.replaceIncrease(index, increase_sb->value());
storeContext(combo_box, category, context);
refreshRow(category);
}
/**
@brief AutoNumberingDockWidget::refreshNextField
Show what this counter will read after one more step (bug #331: "visualiser
la prochaine numérotation qui sera appliquée"). Advances a copy of the
whole context through NumerotationContextCommands -- the same engine the
Suivant button in the full configuration dialog uses to step a context --
so wrap-and-carry into this part from a following part, or out of it into
a preceding one, comes out identical to what will actually happen when the
number is next consumed.
*/
void AutoNumberingDockWidget::refreshNextField(QComboBox *combo_box, QLineEdit *next_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
{
next_edit->clear();
next_edit->setEnabled(false);
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
{
next_edit->clear();
next_edit->setEnabled(false);
return;
}
Diagram *diagram = (m_project_view && m_project_view->currentDiagram())
? m_project_view->currentDiagram()->diagram()
: nullptr;
NumerotationContextCommands ncc(context, diagram);
const NumerotationContext next_context = ncc.next();
next_edit->setEnabled(true);
next_edit->setText(NumerotationContext::formatValue(next_context.itemAt(index)));
}
/**
@brief AutoNumberingDockWidget::refreshRow
Refresh a category's value, increment and next-value preview together --
every call site that used to refresh just the value field needs the
other two kept in step with it as well.
*/
void AutoNumberingDockWidget::refreshRow(AutoNumCategory category)
{
const Row row = rowFor(category);
refreshValueField(row.combo, row.value, category);
refreshIncreaseField(row.combo, row.increase, category);
refreshNextField(row.combo, row.next, category);
}
/**
@brief AutoNumberingDockWidget::resetAutoNum
Reset the numerotation context currently selected in combo_box back to
a per-type starting value, then write it back. Does nothing if no
context is selected.
Only parts that actually progress are touched: folio-anchored numeric
types go back to their own stored initialvalue, plain numeric types go
back to "1", a wrap part goes back to "0" because a modulo counter
cycles over [0, modulus) -- a PLC card addressed %IX0.0..%IX0.31 starts
at 0, not 1 -- and alpha goes back to "a". Non-incrementing types
(string, plant, locmach, idfolio, folio, elementline, elementcolumn,
elementprefix) are left alone: there is no "start" for them distinct
from the fixed or contextual value the user configured.
*/
void AutoNumberingDockWidget::resetAutoNum(QComboBox *combo_box, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
for (int i = 0 ; i < context.size() ; ++i)
{
const QStringList item = context.itemAt(i);
const QString &type = item.at(0);
if (type == QLatin1String("unitfolio")
|| type == QLatin1String("tenfolio")
|| type == QLatin1String("hundredfolio"))
context.replaceValue(i, item.size() > 3 ? item.at(3) : QStringLiteral("1"));
else if (type == QLatin1String("unit")
|| type == QLatin1String("ten")
|| type == QLatin1String("hundred"))
context.replaceValue(i, QStringLiteral("1"));
else if (type == QLatin1String("wrap"))
context.replaceValue(i, QStringLiteral("0"));
else if (type == QLatin1String("alpha"))
context.replaceValue(i, QStringLiteral("a"));
}
storeContext(combo_box, category, context);
refreshRow(category);
}
+70 -2
View File
@@ -23,6 +23,10 @@
#include <QDockWidget>
class QComboBox;
class QLineEdit;
class QSpinBox;
namespace Ui {
class AutoNumberingDockWidget;
}
@@ -51,13 +55,77 @@ class AutoNumberingDockWidget : public QDockWidget
void folioAutoNumChanged();
void clear();
void projectClosed();
void refreshValueFields();
void on_m_configure_pb_clicked();
void on_m_conductor_reset_start_pb_clicked();
void on_m_element_reset_start_pb_clicked();
void on_m_folio_reset_start_pb_clicked();
void on_m_conductor_value_le_editingFinished();
void on_m_element_value_le_editingFinished();
void on_m_folio_value_le_editingFinished();
void on_m_conductor_increase_sb_valueChanged(int);
void on_m_element_increase_sb_valueChanged(int);
void on_m_folio_increase_sb_valueChanged(int);
signals:
void folioAutoNumChanged(QString);
private:
enum class AutoNumCategory { Conductor, Element, Folio };
/// The four widgets that make up one category's row, bundled so
/// refreshRow() can be called with just a category instead of
/// four pointers that must always be passed in matching sets.
struct Row
{
QComboBox *combo;
QLineEdit *value;
QSpinBox *increase;
QLineEdit *next;
};
Row rowFor(AutoNumCategory category) const;
/**
@brief resetAutoNum
Reset the numerotation context currently selected in combo_box
(for the given category) to a per-type starting value. Does
nothing if no context is selected.
*/
void resetAutoNum(QComboBox *combo_box, AutoNumCategory category);
/// Read/write the numerotation context named in combo_box.
NumerotationContext contextFor(QComboBox *combo_box, AutoNumCategory category) const;
void storeContext(QComboBox *combo_box, AutoNumCategory category, const NumerotationContext &context);
/// Index of the counter the value field shows and edits: the last
/// part that actually progresses. Returns -1 when the context has
/// no such part (e.g. it is only fixed text).
static int counterIndex(const NumerotationContext &context);
/// Refresh a value field from its context, and apply a typed value.
void refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
void applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
/// Refresh/apply the increment spin box the same way.
void refreshIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category);
void applyIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category);
/// Show what the counter will read after one more step, using
/// the same NumerotationContextCommands engine the Suivant
/// button in the full configuration dialog already advances
/// the whole context with -- so the preview can never disagree
/// with what actually happens when the number is next consumed.
void refreshNextField(QComboBox *combo_box, QLineEdit *next_edit, AutoNumCategory category);
/// Refresh a whole row -- value, increment and next-value
/// preview -- in one call. Every refresh call site needs the
/// increment and preview kept in step with the value now, so
/// this replaces refreshValueField() at each of them.
void refreshRow(AutoNumCategory category);
Ui::AutoNumberingDockWidget *ui;
QETProject* m_project = nullptr;
ProjectView* m_project_view = nullptr;
+253 -1
View File
@@ -15,6 +15,36 @@
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="3">
<widget class="QLabel" name="value_header_label">
<property name="text">
<string>Valeur</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QLabel" name="increase_header_label">
<property name="text">
<string>Incrément</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QLabel" name="next_header_label">
<property name="text">
<string>Suivant</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="m_element_cb"/>
</item>
@@ -28,6 +58,80 @@
<item row="2" column="1">
<widget class="QComboBox" name="m_conductor_cb"/>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="m_conductor_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLineEdit" name="m_conductor_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="2" column="4">
<widget class="QSpinBox" name="m_conductor_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="2" column="5">
<widget class="QLineEdit" name="m_conductor_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label">
<property name="text">
@@ -42,9 +146,157 @@
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="m_element_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="m_element_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QSpinBox" name="m_element_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="3" column="5">
<widget class="QLineEdit" name="m_element_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="m_folio_cb"/>
</item>
<item row="4" column="2">
<widget class="QPushButton" name="m_folio_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QLineEdit" name="m_folio_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="4" column="4">
<widget class="QSpinBox" name="m_folio_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="4" column="5">
<widget class="QLineEdit" name="m_folio_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
@@ -67,7 +319,7 @@
<string>Configurer</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="configure" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/configure.png</normaloff>:/ico/16x16/configure.png</iconset>
</property>
</widget>
@@ -23,6 +23,7 @@
#include "formulaautonumberingw.h"
#include "numparteditorw.h"
#include "qdebug.h"
#include "renumberelementsdialog.h"
#include "ui_autonumberingmanagementw.h"
#include "ui_formulaautonumberingw.h"
@@ -48,6 +49,8 @@ AutoNumberingManagementW::AutoNumberingManagementW(QETProject *project,
ui->m_selected_folios_le->setDisabled(true);
ui->m_selected_folios_le->setReadOnly(true);
ui->m_apply_project_rb->setChecked(true);
// Enabled only when project is in "Under Development" status and not read-only.
ui->m_renumber_elements_pb->setEnabled(ui->m_status_cb->currentIndex() == 0 && project_ && !project_->isReadOnly());
setProjectContext();
}
@@ -87,6 +90,7 @@ void AutoNumberingManagementW::on_m_status_cb_currentIndexChanged(int index)
ui->m_both_conductor_rb->setChecked(true);
ui->m_both_element_rb->setChecked(true);
ui->m_both_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(true);
}
//Installing
else if (index == 1) {
@@ -96,15 +100,31 @@ void AutoNumberingManagementW::on_m_status_cb_currentIndexChanged(int index)
ui->m_new_conductor_rb->setChecked(true);
ui->m_new_element_rb->setChecked(true);
ui->m_new_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(true);
}
//Built
else if (index == 2) {
ui->m_disable_conductor_rb->setChecked(true);
ui->m_disable_element_rb->setChecked(true);
ui->m_disable_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(false);
}
}
void AutoNumberingManagementW::on_m_renumber_elements_pb_clicked()
{
if (!project_ || project_->isReadOnly()) return;
// Only allowed during "Under Development"
// if (ui->m_status_cb->currentIndex() != 1) return;
QStringList titles = project_->elementAutoNum().keys();
titles.sort(Qt::CaseInsensitive);
RenumberElementsDialog dlg(titles, this);
if (dlg.exec() != QDialog::Accepted) return;
project_->renumberElementsBySchemeTitle(dlg.selectedSchemeTitle());
}
/**
@brief AutoNumberingManagementW::on_m_apply_folios_rb_clicked
Set From Folios Combobox
@@ -50,6 +50,7 @@ class AutoNumberingManagementW : public QWidget
void on_m_from_folios_cb_currentIndexChanged(int);
void on_m_to_folios_cb_currentIndexChanged(int);
void on_m_status_cb_currentIndexChanged(int);
void on_m_renumber_elements_pb_clicked();
void on_m_apply_folios_rb_clicked();
void on_m_apply_project_rb_clicked();
void on_buttonBox_clicked(QAbstractButton *);
@@ -141,7 +141,6 @@
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -163,7 +162,6 @@
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -238,7 +236,6 @@
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -308,6 +305,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_renumber_elements_pb">
<property name="text">
<string>Renumber element(s)…</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
@@ -328,7 +332,6 @@
<widget class="QWidget" name="folioWidget" native="true">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
<kerning>true</kerning>
</font>
@@ -338,7 +341,6 @@
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -54,6 +54,10 @@ FolioAutonumberingW::~FolioAutonumberingW()
*/
void FolioAutonumberingW::setContext(QList <QString> autonums)
{
// Replace the list rather than append to it: this is called again
// whenever the project's numberings change (import, for instance),
// and appending gave a second copy of every name.
ui->m_autonums_cb->clear();
foreach (QString str, autonums) { ui->m_autonums_cb->addItem(str);}
}
+95 -2
View File
@@ -18,6 +18,10 @@
#include "numparteditorw.h"
#include "ui_numparteditorw.h"
#include "../numerotationcontext.h"
#include <QRegularExpressionValidator>
/**
@brief NumPartEditorW::NumPartEditorW
Constructor
@@ -28,10 +32,14 @@ NumPartEditorW::NumPartEditorW(int type, QWidget *parent) :
QWidget(parent),
ui(new Ui::NumPartEditorW),
intValidator (new QIntValidator(0,99999,this)),
alphaValidator (new QRegularExpressionValidator(QRegularExpression("[A-Za-z]+"), this)),
m_edited_type(type)
{
ui -> setupUi(this);
setVisibleItems();
//The mask is a run of zeros and nothing else, so it cannot be typed
//into a state the renderer would have to reject.
ui -> format_le -> setValidator(new QRegularExpressionValidator(QRegularExpression("0*"), this));
setType(NumPartEditorW::unit, true);
}
@@ -51,6 +59,7 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
QWidget(parent),
ui(new Ui::NumPartEditorW),
intValidator (new QIntValidator(0,99999,this)),
alphaValidator (new QRegularExpressionValidator(QRegularExpression("[A-Za-z]+"), this)),
m_edited_type(type)
{
ui -> setupUi(this);
@@ -71,6 +80,10 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
setType(NumPartEditorW::hundred, true);
else if (strl.at(0)=="hundredfolio")
setType(NumPartEditorW::hundredfolio, true);
else if (strl.at(0)=="wrap")
setType(NumPartEditorW::wrap, true);
else if (strl.at(0)=="alpha")
setType(NumPartEditorW::alpha);
else if (strl.at(0)=="string")
setType(NumPartEditorW::string);
else if (strl.at(0)=="idfolio")
@@ -89,6 +102,9 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
setType(NumPartEditorW::elementprefix);
ui -> value_field -> setText(strl.at(1));
ui -> increase_spinBox -> setValue(strl.at(2).toInt());
if (strl.at(0)=="wrap" && strl.size() > 4)
ui -> modulus_spinBox -> setValue(strl.at(4).toInt());
ui -> format_le -> setText(NumerotationContext::formatOf(strl));
}
}
@@ -98,6 +114,7 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
NumPartEditorW::~NumPartEditorW()
{
delete intValidator;
delete alphaValidator;
delete ui;
}
@@ -110,6 +127,8 @@ void NumPartEditorW::setVisibleItems()
items << tr("Chiffre 1")
<< tr("Chiffre 01")
<< tr("Chiffre 001")
<< tr("Cyclique (modulo)")
<< tr("Alphabétique")
<< tr("Texte");
}
else if (m_edited_type == 1)
@@ -120,6 +139,8 @@ void NumPartEditorW::setVisibleItems()
<< tr("Chiffre 01 - Folio")
<< tr("Chiffre 001")
<< tr("Chiffre 001 - Folio")
<< tr("Cyclique (modulo)")
<< tr("Alphabétique")
<< tr("Texte")
<< tr("N° folio")
<< tr("Folio")
@@ -133,6 +154,8 @@ void NumPartEditorW::setVisibleItems()
<< tr("Chiffre 01 - Folio")
<< tr("Chiffre 001")
<< tr("Chiffre 001 - Folio")
<< tr("Cyclique (modulo)")
<< tr("Alphabétique")
<< tr("Texte")
<< tr("N° folio")
<< tr("Folio")
@@ -195,18 +218,37 @@ NumerotationContext NumPartEditorW::toNumContext()
case elementprefix:
type_str = "elementprefix";
break;
case wrap:
type_str = "wrap";
break;
case alpha:
type_str = "alpha";
break;
}
const QString number_format = ui -> format_le -> text();
if (type_str == "unitfolio"
|| type_str == "tenfolio"
|| type_str == "hundredfolio")
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
ui->value_field->displayText().toInt());
ui->value_field->displayText().toInt(),
0,
number_format);
else if (type_str == "wrap")
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
0,
ui -> modulus_spinBox -> value(),
number_format);
else
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value());
ui -> increase_spinBox -> value(),
0,
0,
number_format);
return nc;
}
@@ -260,6 +302,10 @@ void NumPartEditorW::on_type_cb_activated(int) {
setType(elementcolumn);
else if (ui->type_cb->currentText() == tr("Element Prefix"))
setType(elementprefix);
else if (ui->type_cb->currentText() == tr("Cyclique (modulo)"))
setType(wrap);
else if (ui->type_cb->currentText() == tr("Alphabétique"))
setType(alpha);
emit changed();
}
@@ -280,6 +326,22 @@ void NumPartEditorW::on_increase_spinBox_valueChanged(int) {
if (!ui -> value_field -> text().isEmpty()) emit changed();
}
/**
@brief NumPartEditorW::on_modulus_spinBox_valueChanged
emit changed when modulus_spinBox value changed
*/
/**
@brief NumPartEditorW::on_format_le_textEdited
emit changed when the display format is edited
*/
void NumPartEditorW::on_format_le_textEdited(const QString &) {
emit changed();
}
void NumPartEditorW::on_modulus_spinBox_valueChanged(int) {
if (!ui -> value_field -> text().isEmpty()) emit changed();
}
/**
@brief NumPartEditorW::setType
Set good behavior by type t
@@ -299,6 +361,7 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
|| t==tenfolio
|| t==hundred
|| t==hundredfolio
|| t==wrap
)
&& (type_==string
|| type_==folio
@@ -320,6 +383,7 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
}
//@t isn't a numeric type
else if (t == string
|| t == alpha
|| t == folio
|| t == idfolio
|| t == elementline
@@ -333,6 +397,13 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> value_field -> setValidator(nullptr);
ui -> value_field -> setEnabled(true);
}
else if (t==alpha) {
//Alphabetic step is always exactly one letter (a, b, ...);
//there is no numeric "increase" to configure, unlike the
//digit-based part types.
ui -> value_field -> setValidator(alphaValidator);
ui -> value_field -> setEnabled(true);
}
else if (t==folio) {
ui -> value_field -> setDisabled(true);
ui -> increase_spinBox -> setDisabled(true);
@@ -362,6 +433,24 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> increase_spinBox -> setDisabled(true);
}
}
//A modulus of 0 means "no cycle", which makes a Cyclique part behave
//exactly like a plain digit. Defaulting it used to live in the numeric
//behavior block above, which is skipped when the previous type was
//itself numeric -- so the ordinary path of turning the default
//"Chiffre 1" into a "Cyclique (modulo)" left the modulus at 0 and
//produced a wrap part that never wrapped. Kept out of that block so it
//applies whatever the part was before, and only when the current value
//is unusable, so a modulus the user chose on purpose is not clobbered.
if (t == wrap && ui -> modulus_spinBox -> value() <= 0)
ui -> modulus_spinBox -> setValue(8);
ui -> modulus_spinBox -> setEnabled(t == wrap);
//A padding mask only means anything for a part rendered as a number.
const bool numeric = (t == unit || t == unitfolio || t == ten
|| t == tenfolio || t == hundred || t == hundredfolio
|| t == wrap);
ui -> format_le -> setEnabled(numeric);
if (!numeric)
ui -> format_le -> clear();
type_= t;
}
@@ -384,6 +473,8 @@ void NumPartEditorW::setCurrentIndex(NumPartEditorW::type t) {
i = ui->type_cb->findText(tr("Chiffre 001"));
else if (t == hundredfolio)
i = ui->type_cb->findText(tr("Chiffre 001 - Folio"));
else if (t == alpha)
i = ui->type_cb->findText(tr("Alphabétique"));
else if (t == string)
i = ui->type_cb->findText(tr("Texte"));
else if (t == idfolio)
@@ -400,5 +491,7 @@ void NumPartEditorW::setCurrentIndex(NumPartEditorW::type t) {
i = ui->type_cb->findText(tr("Element Column"));
else if (t == elementprefix)
i = ui->type_cb->findText(tr("Element Prefix"));
else if (t == wrap)
i = ui->type_cb->findText(tr("Cyclique (modulo)"));
ui->type_cb->setCurrentIndex(i);
}
+5
View File
@@ -47,8 +47,10 @@ class NumPartEditorW : public QWidget
~NumPartEditorW() override;
enum type {unit,unitfolio,ten,tenfolio, hundred, hundredfolio,
alpha,
string,idfolio,folio,plant,locmach,
elementline,elementcolumn,elementprefix,
wrap,
};
NumerotationContext toNumContext();
bool isValid ();
@@ -63,6 +65,8 @@ class NumPartEditorW : public QWidget
void on_type_cb_activated(int);
void on_value_field_textEdited();
void on_increase_spinBox_valueChanged(int);
void on_modulus_spinBox_valueChanged(int);
void on_format_le_textEdited(const QString &);
void setType (NumPartEditorW::type t, bool=false);
signals:
@@ -71,6 +75,7 @@ class NumPartEditorW : public QWidget
private:
Ui::NumPartEditorW *ui;
QValidator *intValidator;
QValidator *alphaValidator;
int m_edited_type = -1; ///<0 == element : 1 == conductor : 2 == folio
+57 -1
View File
@@ -75,6 +75,9 @@
<property name="wrapping">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Mettre à 0 pour un chiffre qui n'avance que par le report d'un chiffre cyclique suivant (ex: le "0" de "0.7")</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
@@ -88,7 +91,60 @@
<string/>
</property>
<property name="minimum">
<number>1</number>
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="modulus_spinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Valeur à laquelle ce chiffre revient à 0 en incrémentant le chiffre précédent (0 = pas de cycle)</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="prefix">
<string>mod. </string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>99999</number>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="format_le">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Format d'affichage : une suite de zéros donne le nombre minimum de chiffres (00 = 07, 000 = 007). Vide = largeur naturelle du type.</string>
</property>
<property name="placeholderText">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
@@ -0,0 +1,75 @@
/*
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 "renumberelementsdialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QRadioButton>
#include <QVBoxLayout>
RenumberElementsDialog::RenumberElementsDialog(const QStringList &scheme_titles, QWidget *parent)
: QDialog(parent)
, m_scheme_titles(scheme_titles)
{
setWindowTitle(tr("Renumber element(s)"));
setModal(true);
auto *vl = new QVBoxLayout(this);
auto *scope = new QGroupBox(tr("Scope"), this);
auto *scope_l = new QVBoxLayout(scope);
m_all_rb = new QRadioButton(tr("All schemes"), scope);
m_one_rb = new QRadioButton(tr("One scheme"), scope);
scope_l->addWidget(m_all_rb);
scope_l->addWidget(m_one_rb);
vl->addWidget(scope);
auto *form = new QFormLayout();
m_combo = new QComboBox(this);
m_combo->addItems(m_scheme_titles);
form->addRow(new QLabel(tr("Scheme:"), this), m_combo);
vl->addLayout(form);
m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
vl->addWidget(m_buttons);
m_all_rb->setChecked(true);
updateUi();
connect(m_all_rb, &QRadioButton::toggled, this, &RenumberElementsDialog::updateUi);
connect(m_one_rb, &QRadioButton::toggled, this, &RenumberElementsDialog::updateUi);
}
QString RenumberElementsDialog::selectedSchemeTitle() const
{
if (m_all_rb && m_all_rb->isChecked()) return QString();
return m_combo ? m_combo->currentText() : QString();
}
void RenumberElementsDialog::updateUi()
{
const bool one = m_one_rb && m_one_rb->isChecked();
if (m_combo) m_combo->setEnabled(one);
}
@@ -0,0 +1,59 @@
/*
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 RENUMBERELEMENTSDIALOG_H
#define RENUMBERELEMENTSDIALOG_H
#include <QDialog>
#include <QStringList>
class QComboBox;
class QRadioButton;
class QDialogButtonBox;
/**
* @brief Simple dialog to pick renumbering scope for elements.
*
* The user can choose between:
* - all element autonumbering schemes
* - one selected scheme title
*/
class RenumberElementsDialog final : public QDialog
{
Q_OBJECT
public:
explicit RenumberElementsDialog(const QStringList &scheme_titles, QWidget *parent = nullptr);
/**
* @return Empty string if "all" is selected, otherwise the chosen scheme title.
*/
QString selectedSchemeTitle() const;
private slots:
void updateUi();
private:
QStringList m_scheme_titles;
QRadioButton *m_all_rb = nullptr;
QRadioButton *m_one_rb = nullptr;
QComboBox *m_combo = nullptr;
QDialogButtonBox *m_buttons = nullptr;
};
#endif // RENUMBERELEMENTSDIALOG_H
+3 -3
View File
@@ -112,7 +112,7 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
else {
for (int i=0; i<m_context.size(); ++i) { //build with the content of @context
NumPartEditorW *part= new NumPartEditorW(m_context, i, m_edited_type, this);
connect (part, SIGNAL(changed()), this, SLOT(applyEnable()));
connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
num_part_list_ << part;
ui -> editor_layout -> addWidget(part);
}
@@ -145,7 +145,7 @@ void SelectAutonumW::on_add_button_clicked()
{
applyEnable(false);
NumPartEditorW *part = new NumPartEditorW(m_edited_type, this);
connect (part, SIGNAL(changed()), this, SLOT(applyEnable()));
connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
num_part_list_ << part;
ui -> editor_layout -> addWidget(part);
ui -> remove_button -> setEnabled(true);
@@ -160,7 +160,7 @@ void SelectAutonumW::on_remove_button_clicked()
//remove if @num_part_list contains more than one item
if (num_part_list_.size() > 1) {
NumPartEditorW *part = num_part_list_.takeLast();
disconnect(part, SIGNAL(changed()), this, SLOT(applyEnable()));
// deliberately not disconnecting as not possible to resolve with lambda and will happen automatically when the object "part" is destroyed.
delete part;
if (num_part_list_.size() == 1) {
ui -> remove_button -> setDisabled(true);
+5 -5
View File
@@ -63,7 +63,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="edit-delete" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-delete.png</normaloff>:/ico/16x16/edit-delete.png</iconset>
</property>
</widget>
@@ -136,7 +136,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/list-remove.png</normaloff>:/ico/22x22/list-remove.png</iconset>
</property>
<property name="flat">
@@ -153,7 +153,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/list-add.png</normaloff>:/ico/22x22/list-add.png</iconset>
</property>
<property name="flat">
@@ -170,7 +170,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="arrow-left" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/arrow-left.png</normaloff>:/ico/16x16/arrow-left.png</iconset>
</property>
</widget>
@@ -184,7 +184,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="arrow-right" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/arrow-right.png</normaloff>:/ico/16x16/arrow-right.png</iconset>
</property>
</widget>
+237
View File
@@ -0,0 +1,237 @@
/*
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 "autobreakconductor.h"
#include "diagram.h"
#include "qetproject.h"
#include "conductorautonumerotation.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/terminal.h"
#include "qetgraphicsitem/conductor.h"
#include "undocommand/addgraphicsobjectcommand.h"
#include "undocommand/deleteqgraphicsitemcommand.h"
#include <QPainterPath>
#include <QMap>
#include <limits>
namespace {
/**
@brief distanceToSegment
@param point : point to measure from
@param segment : the finite segment (not the infinite line through it)
@return the distance from @a point to the closest point actually on
@a segment. Unlike QET::orthogonalProjection(), which reports a hit
for any point on the segment's infinite extension, this clamps the
projection to the segment itself: a point collinear with a wire but
past its actual drawn end is correctly reported as far away, not "on"
the wire.
*/
qreal distanceToSegment(const QPointF &point, const QLineF &segment)
{
const QPointF a = segment.p1();
const QPointF b = segment.p2();
const QPointF ab = b - a;
const qreal len2 = QPointF::dotProduct(ab, ab);
if (len2 <= 0.0)
return QLineF(point, a).length();
qreal t = QPointF::dotProduct(point - a, ab) / len2;
t = qBound(0.0, t, 1.0);
return QLineF(point, a + t * ab).length();
}
} // anonymous namespace
/**
@brief autoBreakConductors
For each terminal of @a element, check if its dock point lies on an
existing conductor. If so, break the conductor and reconnect through
the element's terminal. Broken conductors from the same circuit
(sharing the same far-end endpoint) are broken together; independent
crossing conductors are left untouched.
*/
QSet<Terminal *> autoBreakConductors(
Diagram *diagram,
Element *element,
QUndoCommand *parent,
QList<Conductor *> &conductors_handled,
QSet<Terminal *> &used_terminals)
{
QSet<Terminal *> broken_endpoints;
if (!diagram->project()->autoBreakConductor())
return broken_endpoints;
foreach (Terminal *t, element->terminals())
{
if (used_terminals.contains(t))
continue;
QPointF t_dock = t->dockConductor();
struct ConductorMatch {
Conductor *conductor;
Terminal *connect_to;
Terminal *other;
};
QList<ConductorMatch> all_matches;
foreach (Conductor *c, diagram->conductors())
{
if (conductors_handled.contains(c) ||
c->terminal1->parentElement() == element ||
c->terminal2->parentElement() == element)
continue;
QPointF local_dock = c->mapFromScene(t_dock);
bool point_on_conductor = false;
QPainterPath path = c->path();
for (int i = 0; i < path.elementCount() - 1; ++i)
{
const QPainterPath::Element &e1 = path.elementAt(i);
const QPainterPath::Element &e2 = path.elementAt(i + 1);
QLineF segment(QPointF(e1.x, e1.y), QPointF(e2.x, e2.y));
if (distanceToSegment(local_dock, segment) < 5.0)
{
point_on_conductor = true;
break;
}
}
if (!point_on_conductor)
continue;
Terminal *c1 = c->terminal1;
Terminal *c2 = c->terminal2;
QPointF c1_dock = c1->dockConductor();
QPointF c2_dock = c2->dockConductor();
Terminal *connect_to = nullptr;
Terminal *other = nullptr;
switch (t->orientation()) {
case Qet::North:
if (c1_dock.y() < t_dock.y()) { connect_to = c1; other = c2; }
else if (c2_dock.y() < t_dock.y()) { connect_to = c2; other = c1; }
break;
case Qet::South:
if (c1_dock.y() > t_dock.y()) { connect_to = c1; other = c2; }
else if (c2_dock.y() > t_dock.y()) { connect_to = c2; other = c1; }
break;
case Qet::East:
if (c1_dock.x() > t_dock.x()) { connect_to = c1; other = c2; }
else if (c2_dock.x() > t_dock.x()) { connect_to = c2; other = c1; }
break;
case Qet::West:
if (c1_dock.x() < t_dock.x()) { connect_to = c1; other = c2; }
else if (c2_dock.x() < t_dock.x()) { connect_to = c2; other = c1; }
break;
}
if (!connect_to) {
qreal d1 = QLineF(t_dock, c1_dock).length();
qreal d2 = QLineF(t_dock, c2_dock).length();
if (d1 <= d2) { connect_to = c1; other = c2; }
else { connect_to = c2; other = c1; }
}
all_matches.append({c, connect_to, other});
}
if (all_matches.isEmpty())
continue;
QMap<Terminal *, QList<ConductorMatch>> groups;
for (const auto &m : all_matches) {
groups[m.other].append(m);
}
Terminal *best_other = nullptr;
int best_count = 0;
for (auto it = groups.constBegin(); it != groups.constEnd(); ++it) {
if (it.value().size() > best_count) {
best_count = it.value().size();
best_other = it.key();
}
}
const QList<ConductorMatch> &matches = groups[best_other];
used_terminals.insert(t);
DiagramContent content;
for (const auto &m : matches) {
content.m_other_conductors.append(m.conductor);
conductors_handled.append(m.conductor);
}
new DeleteQGraphicsItemCommand(diagram, content, parent);
for (const auto &m : matches) {
Conductor *new_c = new Conductor(m.connect_to, t);
new AddGraphicsObjectCommand(new_c, diagram, QPointF(), parent);
ConductorAutoNumerotation can(new_c, diagram, parent);
can.numerate();
if (diagram->freezeNewConductors() || diagram->project()->isFreezeNewConductors())
new_c->setFreezeLabel(true);
broken_endpoints.insert(m.connect_to);
}
QPointF other_dock = best_other->dockConductor();
Terminal *other_terminal = nullptr;
qreal best_dist = std::numeric_limits<qreal>::max();
foreach (Terminal *ot, element->terminals())
{
if (used_terminals.contains(ot))
continue;
QPointF ot_dock = ot->dockConductor();
bool orientation_ok = false;
switch (ot->orientation()) {
case Qet::North: orientation_ok = other_dock.y() < ot_dock.y(); break;
case Qet::South: orientation_ok = other_dock.y() > ot_dock.y(); break;
case Qet::East: orientation_ok = other_dock.x() > ot_dock.x(); break;
case Qet::West: orientation_ok = other_dock.x() < ot_dock.x(); break;
}
if (!orientation_ok)
continue;
qreal dist = QLineF(ot_dock, other_dock).length();
if (dist < best_dist) {
best_dist = dist;
other_terminal = ot;
}
}
if (other_terminal) {
Conductor *new_c2 = new Conductor(best_other, other_terminal);
new AddGraphicsObjectCommand(new_c2, diagram, QPointF(), parent);
ConductorAutoNumerotation can2(new_c2, diagram, parent);
can2.numerate();
if (diagram->freezeNewConductors() || diagram->project()->isFreezeNewConductors())
new_c2->setFreezeLabel(true);
broken_endpoints.insert(best_other);
used_terminals.insert(other_terminal);
}
}
return broken_endpoints;
}
+57
View File
@@ -0,0 +1,57 @@
/*
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 AUTOBREAKCONDUCTOR_H
#define AUTOBREAKCONDUCTOR_H
#include <QSet>
#include <QList>
class Diagram;
class Element;
class Terminal;
class Conductor;
class QUndoCommand;
/**
@brief autoBreakConductors
For each terminal of @a element, check if its dock point lies on an
existing conductor. If so, break the conductor and reconnect through the
element's terminal. Broken conductors from the same circuit (sharing the
same far-end endpoint) are broken together; independent crossing
conductors are left untouched.
@param diagram the diagram containing the conductors
@param element the element whose terminals trigger breaks
@param parent undo command under which break/reconnect
sub-commands are created
@param conductors_handled shared list of conductors already claimed by
a previous call in the same batch (prevents
double-processing when multiple elements are
broken in one pass)
@param used_terminals shared set of terminals already used as
connect_to or other_terminal in the same batch
@return set of terminals that were connected to a broken conductor's
far-end (useful for preventing duplicate auto-connect)
*/
QSet<Terminal *> autoBreakConductors(
Diagram *diagram,
Element *element,
QUndoCommand *parent,
QList<Conductor *> &conductors_handled,
QSet<Terminal *> &used_terminals);
#endif // AUTOBREAKCONDUCTOR_H
+133
View File
@@ -0,0 +1,133 @@
/*
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 "bomexport.h"
#include <QSaveFile>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QVariant>
namespace {
QString csvField(QString value)
{
value.replace(QLatin1Char('"'), QStringLiteral("\"\""));
return QLatin1Char('"') + value + QLatin1Char('"');
}
QByteArray csvRecord(const QStringList &values)
{
QStringList escaped;
for (const auto &value : values) {
escaped.append(csvField(value));
}
return (escaped.join(QLatin1Char(';')) + QLatin1Char('\n')).toUtf8();
}
}
QStringList BomExport::defaultColumns()
{
return {
QStringLiteral("label"),
QStringLiteral("designation"),
QStringLiteral("manufacturer"),
QStringLiteral("manufacturer_reference"),
QStringLiteral("model"),
QStringLiteral("description"),
QStringLiteral("category"),
QStringLiteral("quantity"),
QStringLiteral("voltage_rating"),
QStringLiteral("current_rating"),
QStringLiteral("folio"),
QStringLiteral("notes")
};
}
QString BomExport::defaultQuery()
{
//Slaves and terminals are included because both are routinely
//separately orderable hardware. A circuit breaker can carry ten or
//twenty auxiliary blocks, each with its own order code, and a
//terminal block is a purchased part in its own right. Neither shares
//a line with its master: the query is ungrouped, one row per element,
//so each appears as the distinct item it is.
//
//Anything that should not be ordered is kept out by setting
//exclude_from_bom on the element, which the view already honours --
//a relay's own auxiliary contact, say.
//
//Thumbnails are deliberately left out for now even though ten of
//them in the shipped examples carry manufacturer and reference data
//(the assembly-plan mounting-plate symbols), because that has not
//been asked for and is a separate question. The folio report arrows
//and the conductor definition stay out because they are not hardware.
//
//See discussion #847.
return QStringLiteral("SELECT %1 FROM element_nomenclature_view "
"WHERE element_type IN "
"('simple', 'master', 'slave', 'terminal') "
"ORDER BY diagram_position, position, label")
.arg(defaultColumns().join(QStringLiteral(", ")));
}
QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers, int *row_count)
{
QByteArray csv("\xEF\xBB\xBF");
if (include_headers) {
csv += csvRecord(headers);
}
int rows = 0;
while (query.next())
{
QStringList values;
for (int i = 0; i < query.record().count(); ++i) {
values.append(query.value(i).toString());
}
csv += csvRecord(values);
++rows;
}
if (row_count) {
*row_count = rows;
}
return csv;
}
bool BomExport::writeCsv(const QString &file_path, const QByteArray &csv,
QString *error)
{
if (error) {
error->clear();
}
QSaveFile file(file_path);
if (!file.open(QIODevice::WriteOnly) || file.write(csv) != csv.size())
{
if (error) {
*error = file.errorString();
}
file.cancelWriting();
return false;
}
if (!file.commit())
{
if (error) {
*error = file.errorString();
}
return false;
}
return true;
}
+37
View File
@@ -0,0 +1,37 @@
/*
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 BOMEXPORT_H
#define BOMEXPORT_H
#include <QByteArray>
#include <QString>
#include <QStringList>
class QSqlQuery;
namespace BomExport
{
QStringList defaultColumns();
QString defaultQuery();
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers = true, int *row_count = nullptr);
bool writeCsv(const QString &file_path, const QByteArray &csv,
QString *error = nullptr);
}
#endif // BOMEXPORT_H
+51
View File
@@ -0,0 +1,51 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BORDERCELLLABELS_H
#define BORDERCELLLABELS_H
#include <QString>
/// The labels the border of a folio writes on its rows and columns, shared
/// by BorderTitleBlock::draw() and the cell rulers of DiagramView so the
/// two cannot disagree. Header-only so it can be unit-tested directly --
/// see tests/qttest/tst_bordercelllabels.cpp.
namespace BorderCellLabels {
/// @return the label of row \a row, counted from 1: A..Z, then AA, AB...
/// (the sequence BorderTitleBlock::incrementLetters() walks through).
inline QString rowLabel(int row)
{
QString label;
while (row > 0) {
--row;
label.prepend(QChar('A' + row % 26));
row /= 26;
}
return label;
}
/// @return the label of column \a column, counted from 1. When
/// \a starts_at_zero (the "border-columns_0" setting) the first column
/// is labelled 0.
inline QString columnLabel(int column, bool starts_at_zero)
{
return QString::number(starts_at_zero ? column - 1 : column);
}
}
#endif // BORDERCELLLABELS_H
+65 -21
View File
@@ -17,6 +17,7 @@
*/
#include "bordertitleblock.h"
#include "bordercelllabels.h"
#include "createdxf.h"
#include "diagram.h"
#include "diagramposition.h"
@@ -29,6 +30,7 @@
#include <QLocale>
#include <QPainter>
#include <QRegularExpression>
#include <utility>
#define MIN_COLUMN_COUNT 3
@@ -55,12 +57,6 @@ BorderTitleBlock::BorderTitleBlock(QObject *parent) :
m_titleblock_template_renderer = new TitleBlockTemplateRenderer(this);
m_titleblock_template_renderer -> setTitleBlockTemplate(QETApp::defaultTitleBlockTemplate());
// disable the QPicture-based cache from Qt 4.8 to avoid rendering errors and crashes
#if QT_VERSION < QT_VERSION_CHECK(4, 8, 0) // ### Qt 6: remove
#else
m_titleblock_template_renderer -> setUseCache(false);
#endif
// dimensions par defaut du schema
importBorder(BorderProperties());
@@ -410,12 +406,10 @@ QString BorderTitleBlock::titleBlockTemplateName() const
@brief BorderTitleBlock::titleBlockTemplateChanged
This slot may be used to inform this class that the given title block
template has changed.
The title block-dedicated rendering cache will thus be flushed.
@param template_name : Name of the title block template that has changed
*/
void BorderTitleBlock::titleBlockTemplateChanged(const QString &template_name) {
if (titleBlockTemplateName() != template_name) return;
m_titleblock_template_renderer -> invalidateRenderedTemplate();
}
/**
@@ -513,7 +507,10 @@ void BorderTitleBlock::draw(QPainter *painter)
{
//Set the QPainter
painter -> save();
QPen pen(Qt::black);
//Use a pen color that contrasts with the background
QColor border_color = Diagram::background_color.lightness() < 128
? QColor(Qt::white) : QColor(Qt::black);
QPen pen(border_color);
painter -> setPen(pen);
painter -> setBrush(Qt::NoBrush);
@@ -538,6 +535,8 @@ void BorderTitleBlock::draw(QPainter *painter)
//Draw the nums of columns
if (display_border_ && display_columns_) {
const bool columns_start_at_zero =
settings.value("border-columns_0", true).toBool();
for (int i = 1 ; i <= columns_count_ ; ++ i) {
QRectF numbered_rectangle = QRectF(
diagram_rect_.topLeft().x()
@@ -548,23 +547,15 @@ void BorderTitleBlock::draw(QPainter *painter)
columns_header_height_
);
painter -> drawRect(numbered_rectangle);
if (settings.value("border-columns_0", true).toBool()){
painter -> drawText(numbered_rectangle,
Qt::AlignVCenter
| Qt::AlignCenter,
QString("%1").arg(i - 1));
}else{
painter -> drawText(numbered_rectangle,
Qt::AlignVCenter
| Qt::AlignCenter,
QString("%1").arg(i));
}
BorderCellLabels::columnLabel(i, columns_start_at_zero));
}
}
//Draw the nums of rows
if (display_border_ && display_rows_) {
QString row_string("A");
for (int i = 1 ; i <= rows_count_ ; ++ i) {
QRectF lettered_rectangle = QRectF(
diagram_rect_.topLeft().x(),
@@ -580,8 +571,7 @@ void BorderTitleBlock::draw(QPainter *painter)
painter -> drawText(lettered_rectangle,
Qt::AlignVCenter
| Qt::AlignCenter,
row_string);
row_string = incrementLetters(row_string);
BorderCellLabels::rowLabel(i));
}
}
@@ -898,9 +888,23 @@ void BorderTitleBlock::updateDiagramContextForTitleBlock(
const DiagramContext &initial_context) {
// Our final DiagramContext is the initial one (which is supposed to bring
// project-wide properties), overridden by the "additional fields" one...
// An empty page-level value means the variable was auto-added to the
// folio's Custom tab (#495) but never actually set by the user, so it
// must not shadow a real project-level value of the same name (#531).
//
// That guard has to stop short of removing the key outright, though
// (#973). TitleBlockTemplate::interpreteVariables() only replaces a
// "%name"/"%{name}" placeholder when "name" is a key in this context at
// all -- an unset variable that never makes it in is left as its own
// literal placeholder text in the rendered title block, not blank.
// So an empty page-level value is skipped only when a real project-level
// one is already there to show through; otherwise it still goes in
// empty, which is what makes the placeholder resolve to nothing.
DiagramContext context = initial_context;
foreach (QString key, additional_fields_.keys()) {
context.addValue(key, additional_fields_[key]);
const QVariant value = additional_fields_[key];
if (!value.toString().isEmpty() || !context.contains(key))
context.addValue(key, value);
}
// ... overridden by the historical and/or dynamically generated fields
@@ -926,6 +930,46 @@ void BorderTitleBlock::updateDiagramContextForTitleBlock(
m_titleblock_template_renderer -> setContext(context);
}
/**
@brief BorderTitleBlock::cellRect
Convert a cell written the way the border labels it (ex : B13, the row
letter(s) then the column number) to its rect in scene coordinate.
This is the reverse of convertPosition().
@param cell : the cell to convert, case and surrounding spaces ignored
@return the rect of the cell, or a null QRectF if \a cell is not a
cell reference or lies outside of the border.
*/
QRectF BorderTitleBlock::cellRect(const QString &cell) const
{
static const QRegularExpression cell_re(
QStringLiteral("^\\s*([A-Za-z]+)\\s*(\\d{1,4})\\s*$"));
const QRegularExpressionMatch match = cell_re.match(cell);
if (!match.hasMatch())
return QRectF();
//Row letters count like A..Z, AA, AB... (see incrementLetters())
int row = 0;
for (const QChar c : match.captured(1).toUpper()) {
row = row * 26 + (c.unicode() - 'A' + 1);
if (row > rows_count_)
return QRectF();
}
int column = match.captured(2).toInt();
QSettings settings;
if (settings.value("border-columns_0", true).toBool())
++column;
if (row < 1 || column < 1 || column > columns_count_)
return QRectF();
const QPointF top_left = insideBorderRect().topLeft();
return QRectF(top_left.x() + (column - 1) * columns_width_,
top_left.y() + (row - 1) * rows_height_,
columns_width_,
rows_height_);
}
/**
@brief BorderTitleBlock::incrementLetters
increments string with Letters A to Z
+3
View File
@@ -73,6 +73,8 @@ class BorderTitleBlock : public QObject
return(rows_count_ * rows_height_); }
/// @return la rows header width, in pixels
qreal rowsHeaderWidth() const { return(rows_header_width_); }
/// @return the edge where title block is docked
Qt::Edge titleBlockEdge() const { return(m_edge); }
// border - title block = diagram
/**
@@ -157,6 +159,7 @@ class BorderTitleBlock : public QObject
void setDiagramHeight(const qreal &);
DiagramPosition convertPosition(const QPointF &);
QRectF cellRect(const QString &cell) const;
// methods to set title block basic data
void setFolio(const QString &folio);
+192
View File
@@ -0,0 +1,192 @@
/*
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 "cellruler.h"
#include "bordercelllabels.h"
#include "diagram.h"
#include "diagramview.h"
#include <QPainter>
#include <QSettings>
/**
@brief CellRuler::CellRuler
@param orientation : Qt::Horizontal for the column numbers along the
top, Qt::Vertical for the row letters along the left
@param view : the view this ruler is placed on
*/
CellRuler::CellRuler(Qt::Orientation orientation, DiagramView *view) :
QWidget(view),
m_orientation(orientation),
m_view(view)
{
setAttribute(Qt::WA_OpaquePaintEvent);
hide();
}
/**
@brief CellRuler::thickness
@return the height of the top ruler, which is also the width of the
side ruler, so the corner they share is square. Constant whatever the
zoom.
*/
int CellRuler::thickness() const
{
const QFontMetrics metrics = fontMetrics();
return qMax(metrics.height(), metrics.horizontalAdvance(QStringLiteral("WW"))) + 6;
}
/**
@brief CellRuler::setLeadingSpace
@param space : pixels left empty before the first pixel of the
viewport, along this ruler
*/
void CellRuler::setLeadingSpace(int space)
{
m_leading_space = space;
update();
}
/**
@brief CellRuler::paintEvent
Draw a cell for each column (or row) of the folio border, at the
position the view shows it. When the cells get too small for their
labels, only one label every 2, 5, 10... cells is written.
*/
void CellRuler::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.fillRect(rect(), background());
const bool horizontal = m_orientation == Qt::Horizontal;
const int length = horizontal ? width() : height();
const int depth = horizontal ? height() : width();
//Separate the ruler from the drawing
painter.setPen(palette().color(QPalette::Dark));
if (horizontal) {
painter.drawLine(0, depth - 1, length, depth - 1);
} else {
painter.drawLine(depth - 1, 0, depth - 1, length);
}
Diagram *diagram = m_view->diagram();
if (!diagram) {
return;
}
const BorderTitleBlock &border = diagram->border_and_titleblock;
const QTransform transform = m_view->viewportTransform();
const int count = horizontal ? border.columnsCount() : border.rowsCount();
const qreal cell_size = horizontal ? border.columnsWidth() : border.rowsHeight();
//Where BorderTitleBlock::draw() puts the first cell: after the
//other header's room even when that header is hidden, which
//insideBorderRect() does not account for
const qreal first = Diagram::margin
+ (horizontal ? border.rowsHeaderWidth() : border.columnsHeaderHeight());
const qreal scale = horizontal ? transform.m11() : transform.m22();
const qreal offset = (horizontal ? transform.dx() : transform.dy()) + m_leading_space;
const qreal cell_pixels = cell_size * scale;
if (count < 1 || cell_pixels <= 0) {
return;
}
const bool columns_start_at_zero =
QSettings().value("border-columns_0", true).toBool();
auto label = [&](int index) {
return horizontal ? BorderCellLabels::columnLabel(index, columns_start_at_zero)
: BorderCellLabels::rowLabel(index);
};
//Room one label needs along the ruler, and the smallest step
//between written labels that gives it that room
const QFontMetrics metrics = fontMetrics();
const int label_room = horizontal
? metrics.horizontalAdvance(label(count)) + 6
: metrics.height() + 2;
int step = 1;
for (int candidate : {1, 2, 5, 10, 20, 50, 100, 200, 500}) {
step = candidate;
if (candidate * cell_pixels >= label_room) {
break;
}
}
for (int i = 1 ; i <= count ; ++i) {
const qreal start = offset + (first + (i - 1) * cell_size) * scale;
const qreal end = start + cell_pixels;
if (end < m_leading_space || start > length) {
continue;
}
//Cell edges, drawn only when the cells are wide enough
//for them to read as cells rather than as a hatching
painter.setPen(palette().color(QPalette::Dark));
if (cell_pixels >= 4) {
if (horizontal) {
painter.drawLine(QPointF(start, 0), QPointF(start, depth - 1));
if (i == count) painter.drawLine(QPointF(end, 0), QPointF(end, depth - 1));
} else {
painter.drawLine(QPointF(0, start), QPointF(depth - 1, start));
if (i == count) painter.drawLine(QPointF(0, end), QPointF(depth - 1, end));
}
}
//Written labels are the ones a multiple of step: 0, 5, 10...
//for the columns, A, F, K... for the rows
const int position = (horizontal && !columns_start_at_zero) ? i : i - 1;
if (position % step != 0) {
continue;
}
painter.setPen(palette().color(QPalette::ButtonText));
const qreal centre = (start + end) / 2;
const QRectF text_rect = horizontal
? QRectF(centre - label_room / 2.0, 0, label_room, depth - 1)
: QRectF(0, centre - label_room / 2.0, depth - 1, label_room);
painter.drawText(text_rect, Qt::AlignCenter | Qt::TextDontClip, label(i));
}
//Keep the corner empty: the other ruler's labels do not belong there
if (m_leading_space > 0) {
painter.fillRect(horizontal ? QRect(0, 0, m_leading_space, depth - 1)
: QRect(0, 0, depth - 1, m_leading_space),
background());
}
}
/**
@brief CellRuler::background
@return the button colour laid over the window colour, always opaque.
The Windows 11 style gives buttons a translucent colour; filled with it
as is, a ruler (painted with Qt::WA_OpaquePaintEvent, so never cleared
first) would let every previous frame show through, and zooming would
leave a shadow of the old labels behind the new ones.
*/
QColor CellRuler::background() const
{
const QColor window = palette().color(QPalette::Window);
const QColor button = palette().color(QPalette::Button);
const qreal alpha = button.alphaF();
return QColor::fromRgbF(
button.redF() * alpha + window.redF() * (1 - alpha),
button.greenF() * alpha + window.greenF() * (1 - alpha),
button.blueF() * alpha + window.blueF() * (1 - alpha));
}
+56
View File
@@ -0,0 +1,56 @@
/*
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 CELLRULER_H
#define CELLRULER_H
#include <QWidget>
class DiagramView;
/**
@brief The CellRuler class
A bar along the top or the left edge of a DiagramView that repeats the
column numbers or the row letters of the folio border, aligned with the
cells at any zoom, so they stay in sight however far the view is
scrolled. It sits in the view's margins, outside of the scene: printing
and exporting are unaffected.
*/
class CellRuler : public QWidget
{
Q_OBJECT
public:
CellRuler(Qt::Orientation orientation, DiagramView *view);
int thickness() const;
void setLeadingSpace(int space);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QColor background() const;
Qt::Orientation m_orientation;
DiagramView *m_view;
/// Pixels before the viewport starts, left empty: the corner the
/// side ruler fills when both rulers are shown.
int m_leading_space = 0;
};
#endif // CELLRULER_H
+139 -54
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cli_export.h"
#include "bomexport.h"
#include "bordertitleblock.h"
#include "conductornumexport.h"
@@ -39,6 +40,7 @@
#include <QDomDocument>
#include <QDate>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
@@ -70,6 +72,7 @@ const QHash<QString, QString> &exportFlags()
{"--export-cables", "cables"},
{"--export-wires", "wires"},
{"--export-bom", "bom"},
{"--export-wiring", "wiring"},
{"--export-nets", "nets"},
{"--export-links", "links"},
{"--info", "info"},
@@ -109,19 +112,38 @@ QString diagramStem(Diagram *diagram, int index)
}
/// Render @p diagram into @p painter, fitting @p target to the page rect.
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
/// @p showTerminals: paint terminal markers (red stroke + blue docking
/// dot) and terminal names, as the interactive editor does. Off by
/// default — Terminal::paint() draws them whenever the diagram's
/// drawTerminals()/drawTerminalNames() flags are set (default true, so
/// headless export used to ship them as editor UI); the GUI export
/// dialog already defaults to clearing them via
/// Diagram::applyProperties(). Pass true (--show-terminals) to keep
/// them, e.g. to visually debug an unconnected pin.
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target,
bool showTerminals = false)
{
const QRect source = diagramRect(diagram);
// Export without the editor grid: drawBackground() only paints it when
// draw_grid_ is set (default true), so toggle it off around the render
// and restore it afterwards.
const bool was_drawing_grid = diagram->displayGrid();
const bool was_drawing_guides = diagram->displayGuides();
const bool was_drawing_terminals = diagram->drawTerminals();
const bool was_drawing_terminal_names = diagram->drawTerminalNames();
diagram->setDisplayGrid(false);
diagram->setDisplayGuides(false);
diagram->setDrawTerminals(showTerminals);
diagram->setDrawTerminalNames(showTerminals);
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
diagram->setDisplayGrid(was_drawing_grid);
diagram->setDisplayGuides(was_drawing_guides);
diagram->setDrawTerminals(was_drawing_terminals);
diagram->setDrawTerminalNames(was_drawing_terminal_names);
}
int exportPdf(QETProject &project, const QString &output)
int exportPdf(QETProject &project, const QString &output,
bool showTerminals = false)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
@@ -161,7 +183,7 @@ int exportPdf(QETProject &project, const QString &output)
}
const QRectF target(0, 0,
writer.width(), writer.height());
renderDiagram(diagram, painter, target);
renderDiagram(diagram, painter, target, showTerminals);
// Inject clickable cross-reference / folio-report hyperlinks for this
// page. The geometry is rebuilt from the QPdfWriter (not a QPrinter):
@@ -205,13 +227,14 @@ int exportPdf(QETProject &project, const QString &output)
// Rewrite the URI link annotations into native internal GoTo actions, so
// the cross-references jump inside the document in any PDF viewer.
PdfLinks::convertUriToGoTo(output);
PdfLinks::removeUnusedPdfxNamespace(output);
out << "Exported " << diagrams.size() << " page(s) -> " << output << "\n";
return 0;
}
int exportImages(QETProject &project, const QString &format,
const QString &out_dir)
const QString &out_dir, bool showTerminals = false)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
@@ -234,14 +257,16 @@ int exportImages(QETProject &project, const QString &format,
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
gen.setTitle(diagram->title());
QPainter painter(&gen);
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
showTerminals);
painter.end();
} else { // png
QImage image(r.size(), QImage::Format_ARGB32);
image.fill(Qt::white);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
showTerminals);
painter.end();
if (!image.save(path)) {
err << "Failed to write '" << path << "'.\n";
@@ -281,61 +306,40 @@ int exportCsv(QETProject &project, const QString &format, const QString &output)
return 0;
}
/// Quote a field for CSV output (RFC-4180 style, ';' delimiter).
QString csvField(const QString &value)
{
if (value.contains(';') || value.contains('"')
|| value.contains('\n') || value.contains('\r')) {
QString v = value;
v.replace('"', "\"\"");
return '"' % v % '"';
}
return value;
}
/// Bill of materials: one row per element, key component-data fields.
/// Pulls from QET's own project database (the same source as the GUI BOM
/// export), so the output matches what the editor produces.
/// Bill of materials from the same project database and default query as the
/// GUI nomenclature export.
int exportBom(QETProject &project, const QString &output)
{
// The project database is built lazily; force a (re)build before querying.
project.dataBase()->updateDB();
static const QStringList columns {
"label", "designation", "manufacturer", "manufacturer_reference",
"quantity", "location", "function", "title", "folio"
};
QSqlQuery query = project.dataBase()->newQuery(
"SELECT " % columns.join(", ") %
" FROM element_nomenclature_view ORDER BY label");
QSqlQuery query = project.dataBase()->newQuery(BomExport::defaultQuery());
if (!query.exec()) {
err << "BOM query failed: " << query.lastError().text() << "\n";
return 1;
}
QString csv = columns.join(";") % "\n";
int rows = 0;
while (query.next()) {
QStringList values;
for (int i = 0; i < columns.size(); ++i)
values << csvField(query.value(i).toString());
csv += values.join(";") % "\n";
++rows;
}
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
const auto csv = BomExport::toCsv(
query, BomExport::defaultColumns(), true, &rows);
QString error;
if (!BomExport::writeCsv(output, csv, &error)) {
err << "Cannot write '" << output << "': " << error << "\n";
return 1;
}
QTextStream fout(&file);
fout << csv;
file.close();
out << "Exported " << rows << " component(s) -> " << output << "\n";
return 0;
}
QString csvField(const QString &value)
{
if (value.contains(QLatin1Char(';')) || value.contains(QLatin1Char('"'))
|| value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r')))
{
QString escaped = value;
escaped.replace(QLatin1Char('"'), QStringLiteral("\"\""));
return QLatin1Char('"') % escaped % QLatin1Char('"');
}
return value;
}
/// Count terminals on @p element that no conductor connects to.
int freeTerminals(Element *element)
{
@@ -419,6 +423,14 @@ int checkOneElement(const QString &path)
return 2;
}
QDomDocument doc;
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
if (const auto result = doc.setContent(&file); !result) {
file.close();
out << "FAIL " << path << " (XML error line "
<< result.errorLine << ": " << result.errorMessage << ")\n";
return 2;
}
#else
QString error;
int line = 0;
if (!doc.setContent(&file, &error, &line)) {
@@ -427,6 +439,7 @@ int checkOneElement(const QString &path)
<< line << ": " << error << ")\n";
return 2;
}
#endif
file.close();
const QDomElement root = doc.documentElement();
@@ -513,6 +526,70 @@ QHash<Element *, int> folioIndex(QETProject &project)
return folio;
}
/// From-to wiring list: one row per conductor, each endpoint resolved to its
/// element label and terminal name.
///
/// Reads wiring_list_view out of the project database. --export-cables produces
/// the same logical list from the document XML instead, and the two are meant
/// to agree: running both and diffing them is a direct check that the database
/// still describes the project, which is otherwise only observable through the
/// GUI.
int exportWiring(QETProject &project, const QString &output)
{
// The project database is built lazily; force a (re)build before querying.
project.dataBase()->updateDB();
static const QStringList columns {
"wire_number", "from_element_label", "from_terminal",
"to_element_label", "to_terminal", "diagram_position", "conductor_uuid"
};
QSqlQuery query = project.dataBase()->newQuery(
"SELECT " % columns.join(", ") %
" FROM wiring_list_view"
//Wire numbers are text, so a plain sort puts "10" before "9".
//Numeric ones first, ordered by value; anything non-numeric after,
//ordered as text. The trailing wire_number keeps ties stable.
" ORDER BY diagram_position,"
" CASE WHEN wire_number GLOB '[0-9]*' THEN 0 ELSE 1 END,"
" CAST(wire_number AS INTEGER),"
" wire_number");
if (!query.exec()) {
err << "Wiring list query failed: " << query.lastError().text() << "\n";
return 1;
}
QString csv = columns.join(";") % "\n";
int rows = 0;
while (query.next()) {
QStringList values;
for (int i = 0; i < columns.size(); ++i)
values << csvField(query.value(i).toString());
csv += values.join(";") % "\n";
++rows;
}
//Written through QSaveFile so a failure part-way leaves the previous
//file intact rather than a truncated one, and with a UTF-8 byte order
//mark: without it Excel opens a .csv as the local 8-bit codepage and
//mangles any accented element label. Qt writes UTF-8 by default, so
//the bytes were already right -- the mark is what tells Excel so.
QSaveFile file(output);
if (!file.open(QIODevice::WriteOnly)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
static const char utf8_bom[] = "\xEF\xBB\xBF";
file.write(utf8_bom, 3);
file.write(csv.toUtf8());
if (!file.commit()) {
err << "Cannot write '" << output << "': " << file.errorString() << "\n";
return 1;
}
out << "Exported " << rows << " conductor(s) -> " << output << "\n";
return 0;
}
/// Electrical nets: groups of terminals joined into one potential.
/// Walks QET's own potential graph, so each net is a connected component
/// of terminals across all folios. The ground truth for connectivity.
@@ -756,13 +833,19 @@ bool isExportRequest(const QStringList &args)
int run(const QStringList &args)
{
// --show-terminals is a standalone switch (not tied to a position),
// so pull it out before the positional project/output arguments are
// collected below.
QStringList filtered = args;
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
QString flag;
QStringList rest;
for (int i = 0; i < args.size(); ++i) {
if (exportFlags().contains(args.at(i))) {
flag = args.at(i);
for (int j = i + 1; j < args.size(); ++j)
rest << args.at(j);
for (int i = 0; i < filtered.size(); ++i) {
if (exportFlags().contains(filtered.at(i))) {
flag = filtered.at(i);
for (int j = i + 1; j < filtered.size(); ++j)
rest << filtered.at(j);
break;
}
}
@@ -806,11 +889,13 @@ int run(const QStringList &args)
return 2;
}
if (format == "pdf")
return exportPdf(project, output);
return exportPdf(project, output, showTerminals);
if (format == "cables" || format == "wires")
return exportCsv(project, format, output);
if (format == "bom")
return exportBom(project, output);
if (format == "wiring")
return exportWiring(project, output);
if (format == "nets")
return exportNets(project, output);
if (format == "links")
@@ -819,7 +904,7 @@ int run(const QStringList &args)
return resaveProject(project, output);
if (format == "settb")
return setTitleBlock(project, output, rest.mid(2));
return exportImages(project, format, output);
return exportImages(project, format, output, showTerminals);
}
} // namespace CLIExport
+13 -3
View File
@@ -42,12 +42,13 @@ namespace CLIExport {
@return process exit code (0 on success).
Usage:
qelectrotech --export-pdf <project.qet> <output.pdf>
qelectrotech --export-png <project.qet> <output_dir>
qelectrotech --export-svg <project.qet> <output_dir>
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --export-wiring <project.qet> <output.csv>
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
qelectrotech --info <project.qet> [output.json]
@@ -57,9 +58,18 @@ namespace CLIExport {
PDF: one multi-page document (one diagram per page).
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
--show-terminals: also paint terminal markers (red stroke + blue
docking dot) and terminal names, as the interactive editor
does; off by default, matching the GUI export dialog's
default. Has no effect on the non-image export modes.
cables: wiring list (one row per conductor) as CSV.
wires: list of distinct wire numbers as CSV.
bom: bill of materials (one row per element) as CSV.
wiring: from-to wiring list (one row per conductor) as CSV, read from
the project database. Same logical list as `cables`, which
reads the document XML instead; the two are meant to agree,
so diffing them checks that the database still describes the
project.
nets: electrical nets (connected-terminal groups) as JSON.
links: element cross-references (coil/contact) as CSV, with
unresolved links flagged.
+236
View File
@@ -0,0 +1,236 @@
/*
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 "commandsearchpopup.h"
#include "shortcutmanager.h"
#include <QAction>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QScreen>
#include <QVBoxLayout>
#include <algorithm>
/**
@brief CommandSearchPopup::CommandSearchPopup
@param owner : the window whose commands are searched; also the parent
*/
CommandSearchPopup::CommandSearchPopup(QWidget *owner) :
QFrame(owner, Qt::Popup)
{
setFrameShape(QFrame::StyledPanel);
setFrameShadow(QFrame::Raised);
setMinimumWidth(380);
m_search = new QLineEdit(this);
m_search->setPlaceholderText(tr("Rechercher une commande…"));
m_search->setClearButtonEnabled(true);
m_list = new QListWidget(this);
m_list->setIconSize(QSize(20, 20));
m_list->setMinimumHeight(280);
m_list->setFocusPolicy(Qt::NoFocus);
auto *hint = new QLabel(tr("Entrée pour lancer · Échap pour fermer"), this);
hint->setEnabled(false);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(6, 6, 6, 6);
layout->setSpacing(4);
layout->addWidget(m_search);
layout->addWidget(m_list);
layout->addWidget(hint);
connect(m_search, &QLineEdit::textChanged, this, &CommandSearchPopup::filter);
connect(m_list, &QListWidget::itemClicked, this, [this]() { runCurrent(); });
}
/**
@brief CommandSearchPopup::fold
@return @a text lower-cased, without accents and without the "&" of
mnemonics, for matching
*/
QString CommandSearchPopup::fold(const QString &text)
{
QString out;
const QString decomposed = text.normalized(QString::NormalizationForm_D);
out.reserve(decomposed.size());
for (const QChar c : decomposed) {
if (c.category() != QChar::Mark_NonSpacing && c != QLatin1Char('&')) {
out.append(c.toLower());
}
}
return out;
}
/**
@brief CommandSearchPopup::popUpAt
Rebuild the command list -- actions come and go with windows, and their
enabled state changes -- then show at @a global_pos, kept on screen.
*/
void CommandSearchPopup::popUpAt(const QPoint &global_pos)
{
collect();
m_search->clear();
filter();
adjustSize();
QPoint pos = global_pos;
if (QScreen *screen = QGuiApplication::screenAt(global_pos)) {
const QRect avail = screen->availableGeometry();
pos.setX(qBound(avail.left(), pos.x(), avail.right() - width()));
pos.setY(qBound(avail.top(), pos.y(), avail.bottom() - height()));
}
move(pos);
show();
m_search->setFocus();
}
/**
@brief CommandSearchPopup::collect
Every command registered by the owning window, except this search.
*/
void CommandSearchPopup::collect()
{
m_commands.clear();
for (const ShortcutManager::ShortcutInfo &info :
ShortcutManager::instance().allShortcuts())
{
if (info.id == QLatin1String("diagrameditor.command_search")) {
continue;
}
QAction *action = ShortcutManager::instance().action(info.id, parentWidget());
if (!action || !action->isVisible()) {
continue;
}
const QString text = action->text().remove(QLatin1Char('&'));
if (text.isEmpty()) {
continue;
}
m_commands.append({action, text, fold(text)});
}
}
/**
@brief CommandSearchPopup::filter
Show the commands matching the search text, best first: a name starting
with it, then a word starting with it, then containing it. An empty
search lists everything, alphabetically.
*/
void CommandSearchPopup::filter()
{
const QString needle = fold(m_search->text().trimmed());
QList<QPair<int, const Command *>> hits;
for (const Command &command : std::as_const(m_commands))
{
int score = 0;
if (needle.isEmpty()) {
score = 1;
} else if (command.folded.startsWith(needle)) {
score = 3;
} else if (command.folded.contains(QLatin1Char(' ') + needle)) {
score = 2;
} else if (command.folded.contains(needle)) {
score = 1;
}
if (score) {
hits.append({score, &command});
}
}
std::stable_sort(hits.begin(), hits.end(), [](const auto &a, const auto &b) {
if (a.first != b.first) {
return a.first > b.first;
}
return a.second->folded < b.second->folded;
});
m_list->clear();
for (const auto &hit : std::as_const(hits))
{
QAction *action = hit.second->action;
const QKeySequence key = action->shortcut();
auto *item = new QListWidgetItem(
action->icon(),
key.isEmpty() ? hit.second->text
: QStringLiteral("%1 (%2)").arg(hit.second->text,
key.toString(QKeySequence::NativeText)));
item->setData(Qt::UserRole, QVariant::fromValue(static_cast<void *>(action)));
if (!action->isEnabled()) {
item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
}
m_list->addItem(item);
}
//Preselect the first command that can run, so Enter works at once
for (int i = 0 ; i < m_list->count() ; ++i) {
if (m_list->item(i)->flags() & Qt::ItemIsEnabled) {
m_list->setCurrentRow(i);
break;
}
}
}
/**
@brief CommandSearchPopup::runCurrent
Close, then trigger the highlighted command: it may open a dialog or
start a tool on the folio, which need the focus the popup holds.
*/
void CommandSearchPopup::runCurrent()
{
QListWidgetItem *item = m_list->currentItem();
if (!item || !(item->flags() & Qt::ItemIsEnabled)) {
return;
}
auto *action = static_cast<QAction *>(item->data(Qt::UserRole).value<void *>());
hide();
if (action) {
action->trigger();
}
}
/**
@brief CommandSearchPopup::keyPressEvent
Up and Down move through the list while typing goes on in the search
field; Enter runs, Esc closes.
*/
void CommandSearchPopup::keyPressEvent(QKeyEvent *event)
{
switch (event->key())
{
case Qt::Key_Escape:
hide();
return;
case Qt::Key_Return:
case Qt::Key_Enter:
runCurrent();
return;
case Qt::Key_Down:
case Qt::Key_Up:
case Qt::Key_PageDown:
case Qt::Key_PageUp:
QCoreApplication::sendEvent(m_list, event);
return;
default:
break;
}
QFrame::keyPressEvent(event);
}
+65
View File
@@ -0,0 +1,65 @@
/*
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 COMMANDSEARCHPOPUP_H
#define COMMANDSEARCHPOPUP_H
#include <QFrame>
#include <QList>
class QAction;
class QLineEdit;
class QListWidget;
/**
@brief Type part of a command's name, press Enter to run it.
Lists every command the owning window registered with ShortcutManager,
best match first, with its key if it has one. Matching ignores case and
accents, so "editer" finds "Éditer". Disabled commands are listed,
greyed, but cannot be run.
*/
class CommandSearchPopup : public QFrame
{
Q_OBJECT
public:
explicit CommandSearchPopup(QWidget *owner);
void popUpAt(const QPoint &global_pos);
static QString fold(const QString &text);
protected:
void keyPressEvent(QKeyEvent *event) override;
private:
void collect();
void filter();
void runCurrent();
struct Command {
QAction *action;
QString text; ///< as shown
QString folded; ///< for matching
};
QLineEdit *m_search = nullptr;
QListWidget *m_list = nullptr;
QList<Command> m_commands;
};
#endif // COMMANDSEARCHPOPUP_H
+27 -3
View File
@@ -16,7 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "conductorautonumerotation.h"
#include "qetproject.h"
#include "QPropertyUndoCommand/qpropertyundocommand.h"
#include "autoNum/assignvariables.h"
#include "autoNum/numerotationcontextcommands.h"
@@ -24,6 +24,7 @@
#include "qet.h"
#include "qetdiagrameditor.h"
#include "ui/potentialselectordialog.h"
#include "undocommand/setautonumcontextcommand.h"
/**
@brief ConductorAutoNumerotation::ConductorAutoNumerotation
@@ -156,7 +157,16 @@ void ConductorAutoNumerotation::newProperties(
autonum::setSequential(formula, seq, context, diagram, autoNum_name);
NumerotationContextCommands ncc (context, diagram);
diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = diagram->project();
auto *undo = new SetAutoNumContextCommand(
[project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);},
autoNum_name,
context,
new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
diagram->undoStack().push(undo);
}
/**
@@ -245,7 +255,21 @@ void ConductorAutoNumerotation::numerateNewConductor()
autoNum_name);
NumerotationContextCommands ncc (context, m_diagram);
m_diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = m_diagram->project();
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);};
if (m_parent_undo)
{
new SetAutoNumContextCommand(setter, autoNum_name, context, new_context, m_parent_undo);
}
else
{
auto *undo = new SetAutoNumContextCommand(setter, autoNum_name, context, new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
m_diagram->undoStack().push(undo);
}
}
applyText(autonum::AssignVariables::formulaToLabel(
+11 -17
View File
@@ -16,7 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "conductornumexport.h"
#include "qetproject.h"
#include "qetapp.h"
#include "diagram.h"
#include "diagramcontent.h"
@@ -72,14 +72,8 @@ bool ConductorNumExport::toCsv()
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream stream(&file);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
stream << wiresNum() << endl;
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
stream << wiresNum() << &Qt::endl(stream);
#endif
}
else {
return false;
@@ -130,15 +124,15 @@ void ConductorNumExport::fillHash()
continue;
}
//We must define if the connected terminal is a folio report, if it is the case
//we don't add the num to the hash because the terminal doesn't represent a real terminal.
if(!(c->terminal1->parentElement()->linkType() & Element::AllReport))
{
int value = m_hash.value(num, 0);
++value;
m_hash.insert(num, value);
}
if(!(c->terminal2->parentElement()->linkType() & Element::AllReport))
//We must define if the connected terminals are folio reports, if
//both are the case we don't add the num to the hash because
//neither terminal represents a real terminal. Otherwise the
//conductor counts once, regardless of how many of its two ends
//are real -- counting per terminal instead of per conductor
//doubled every ordinary two-real-terminal conductor's tally.
bool terminal1_is_report = c->terminal1->parentElement()->linkType() & Element::AllReport;
bool terminal2_is_report = c->terminal2->parentElement()->linkType() & Element::AllReport;
if (!(terminal1_is_report && terminal2_is_report))
{
int value = m_hash.value(num, 0);
++value;
+13 -7
View File
@@ -499,6 +499,7 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
horiz_rotate_text = cp.horiz_rotate_text;
m_vertical_alignment = cp.m_vertical_alignment;
m_horizontal_alignment = cp.m_horizontal_alignment;
style = cp.style;
return;
}
@@ -555,6 +556,18 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
m_dash_size = i_value;
equal = true;
//style
Qt::PenStyle pen_style;
pen_style = clist.first().style;
for(ConductorProperties cp : clist)
{
if (cp.style != pen_style)
equal = false;
}
if (equal)
style = pen_style;
equal = true;
//text
s_value = clist.first().text;
for(ConductorProperties cp : clist)
@@ -811,14 +824,7 @@ void ConductorProperties::readStyle(const QString &style_string) {
if (style_string.isEmpty()) return;
// recupere la liste des couples style / valeur
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QStringList styles = style_string.split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code QString::SkipEmptyParts for QT 5.14 or later")
#endif
QStringList styles = style_string.split(";", Qt::SkipEmptyParts);
#endif
QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$");
if (!Rx.isValid())
+3 -4
View File
@@ -74,10 +74,9 @@ ConfigDialog::ConfigDialog(QWidget *parent) : QDialog(parent) {
setLayout(dialog_layout);
// connexion signaux / slots
connect(buttons, SIGNAL(accepted()), this, SLOT(applyConf()));
connect(buttons, SIGNAL(rejected()), this, SLOT(reject()));
connect(pages_list, SIGNAL(currentRowChanged(int)),
pages_widget, SLOT(setCurrentIndex(int)));
connect(buttons, &QDialogButtonBox::accepted, this, &ConfigDialog::applyConf);
connect(buttons, &QDialogButtonBox::rejected, this, &ConfigDialog::reject);
connect(pages_list, &QListWidget::currentRowChanged, pages_widget, &QStackedWidget::setCurrentIndex);
// set maximum a bit smaller than available size = (screen-size - Task-Bar):
setMaximumSize((int)(0.94 * MachineInfo::instance()->i_max_available_width()),
+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 CONTACTUSAGE_H
#define CONTACTUSAGE_H
#include <algorithm>
/**
@brief The ContactUsage struct
How many slave contacts a master element currently uses, broken down
by contact type.
Header-only and free of any graphics dependency so that the counting
rules can be unit tested on their own. MasterElement::contactUsage()
is the thin wrapper that feeds it the linked elements.
This counts contacts, which is what tells you how many contacts an
auxiliary block must provide. It is deliberately not the count that
MasterElement::isFull() uses: a master's max_slaves is a number of
slots, and a slave fills exactly one slot however many contacts it
carries.
Two rules are easy to get wrong, and both live here so that every
caller gets them right:
- a slave stands for as many contacts as its "number" kind
information says, so a 4 pole contact counts as 4, not as 1
- a changeover contact is counted once, as sw. CrossRefItem's
NOElements() and NCElements() both return it, so adding those two
lists together would count it twice.
*/
struct ContactUsage
{
/**
Contact types a slave can declare. Mirrors
ElementData::SlaveState, which is not used directly so that this
header stays free of the element data dependencies and can be
unit tested on its own. MasterElement::contactUsage() maps
between the two.
*/
enum Type
{
NO, ///< Normally open
NC, ///< Normally closed
SW, ///< Changeover
Other ///< Neither of the above
};
int no = 0; ///< Normally open
int nc = 0; ///< Normally closed
int sw = 0; ///< Changeover
int other = 0; ///< Neither of the above
int total() const { return no + nc + sw + other; }
/**
Add one slave element to the tally.
@param type the contact type the slave declares
@param contacts how many contacts it stands for. Values below 1
are treated as 1: an element which declares no
contact count is still one contact.
*/
void addSlave(Type type, int contacts)
{
const int n = std::max(1, contacts);
switch (type)
{
case NO: no += n; break;
case NC: nc += n; break;
case SW: sw += n; break;
case Other: other += n; break;
}
}
};
#endif // CONTACTUSAGE_H
File diff suppressed because it is too large Load Diff
+70 -4
View File
@@ -27,7 +27,8 @@
class Element;
class QETProject;
class Diagram;
class sqlite3;
class Conductor;
class Terminal;
/**
@brief The projectDataBase class
@@ -45,8 +46,42 @@ class projectDataBase : public QObject
virtual ~projectDataBase() override;
void updateDB();
/**
Suppress the full rebuild performed by updateDB().
While blocked, updateDB() returns immediately instead of
repopulating every table. Meant for bulk operations -- notably
loading a project, where each table model re-queries the
database as it is built and would otherwise trigger one
complete rebuild of it. The caller unblocks and calls
updateDB() once when done; @see QETProject::readProjectXml().
*/
void setUpdateBlocked(bool blocked);
QETProject *project() const;
QSqlQuery newQuery(const QString &query = QString());
QSqlQuery newQuery(const QString &query = QString(), QString *error = nullptr);
static bool isReadOnlySelect(const QString &query, QString *error = nullptr);
/**
The most rows any caller reads out of one query result.
A SELECT is not bounded by how much data the project holds:
SQLite produces rows lazily, so a query that never stops
producing them makes the loop that reads them never stop
either. A recursive CTE does exactly that in one line, and
a <graphics_table>'s <query> is stored in the .qet and run
on load -- so the text can arrive from a file rather than
from the person at the keyboard, and opening that file is
the whole attack.
100000 is far above any real result: the largest table in
the shipped examples is 396 rows. It is a backstop, not a
page size -- a caller that hits it has almost certainly
been handed something it should not run to completion, and
says so rather than truncating quietly.
*/
static constexpr int MaxResultRows = 100000;
QSqlDatabase database() const {return m_data_base;}
int excludedConductorCount() const;
void addElement (Element *element);
void removeElement (Element *element);
@@ -58,6 +93,16 @@ class projectDataBase : public QObject
void diagramInfoChanged (Diagram *diagram);
void diagramOrderChanged();
void addConductor (Conductor *conductor);
void removeConductor (Conductor *conductor);
void updateConductor (Conductor *conductor);
private slots:
//Refresh the sender()'s row after Conductor::setProperties().
void conductorPropertiesChanged();
public:
signals:
void dataBaseUpdated();
@@ -65,32 +110,53 @@ class projectDataBase : public QObject
bool createDataBase();
void createElementNomenclatureView();
void createSummaryView();
void createWiringListView();
void populateDiagramTable();
void populateElementTable();
void populateElementInfoTable();
void populateDiagramInfoTable();
void populateConductorTable();
void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram);
void watchConductor(Conductor *conductor);
void insertTerminal(Terminal *terminal);
void prepareQuery();
static QHash<QString, QString> elementInfoToString(
Element *elmt);
void bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram);
static void bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram);
static void bindElementInfoValues(QSqlQuery &query, Element *element);
private:
QPointer<QETProject> m_project;
bool m_update_blocked = false;
//Starts true : the database is empty until the first rebuild.
//Set by every method of this class that writes rows, cleared by
//updateDB(). Callers reach the database from outside only through
//newQuery(), and every such call site reads.
bool m_content_changed = true;
QSqlDatabase m_data_base;
QSqlQuery m_insert_elements_query,
m_insert_element_info_query,
m_remove_element_query,
m_remove_element_info_query,
m_update_element_query,
m_insert_diagram_query,
m_remove_diagram_query,
m_insert_diagram_info_query,
m_update_diagram_info_query,
m_diagram_order_changed,
m_diagram_info_order_changed;
m_diagram_info_order_changed,
m_insert_terminal_query,
m_insert_conductor_query,
m_update_conductor_query,
m_remove_conductor_query,
m_cascade_remove_element_info_query,
m_cascade_remove_terminal_query,
m_cascade_remove_conductor_query,
m_cascade_remove_element_query;
#ifdef QET_EXPORT_PROJECT_DB
public:
static sqlite3 *sqliteHandle(QSqlDatabase *db);
static void exportDb(projectDataBase *db,
QWidget *parent = nullptr,
const QString &caption = QString(),
+110
View File
@@ -0,0 +1,110 @@
/*
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 "sqlreadonly.h"
#include <QCoreApplication>
#include <QSqlError>
namespace QETSql {
/**
@brief QETSql::execReadOnly
Run @p query on @p db with SQLite's query_only pragma set, so that
SQLite itself refuses anything that would write.
Why SQLite is asked rather than the text inspected: a check on the
query's first keyword cannot see what the statement actually does.
SQLite has allowed a CTE prefix in front of a data-modifying statement
since 3.8.3, so
@code
WITH x AS (SELECT 1) DELETE FROM element
@endcode
begins with WITH, contains no semicolon, and deletes every row. With
query_only set, SQLite fails that statement with SQLITE_READONLY when it
tries to start writing, before any row is touched, while an ordinary
WITH ... SELECT still runs.
Why a pragma rather than sqlite3_stmt_readonly(): that needs the
driver's native sqlite3 handle passed to the libsqlite3 QElectroTech
links. The QSQLITE plugin of the Qt online installer carries its own
private copy of SQLite, so that handle belongs to another library and
the call crashes (qelectrotech-source-mirror#1045). A pragma goes
through the driver, whichever SQLite it uses.
A statement that succeeded here is read-only, so running the returned
query again, as several callers do, runs a read-only statement again.
A refused one comes back as an empty query with nothing to run again:
query_only is only set for the duration of this call.
Qt's SQLite driver refuses a second statement after the first one, so
"SELECT 1; DROP TABLE element" is refused too.
This is a read-only test, NOT a statement-type allowlist: query_only
does not refuse ATTACH, BEGIN or most PRAGMAs, because none of them
change the contents of the database. Callers that need to restrict
which *kind* of statement is acceptable must say so separately --
projectDataBase::newQuery() keeps isReadOnlySelect() in front of this
for exactly that reason.
@param db the connection to run the query on
@param query the raw SQL text
@param error set to a human-readable reason when the query is refused
@return the executed query, or an empty query on @p db if @p query was
refused or failed
*/
QSqlQuery execReadOnly(const QSqlDatabase &db, const QString &query, QString *error)
{
if (error) {
error->clear();
}
if (!QSqlQuery(db).exec(QStringLiteral("PRAGMA query_only = ON"))) {
if (error) {
*error = QCoreApplication::translate("QETSql",
"Impossible de vérifier la requête : "
"la base de données ne peut pas être mise en lecture seule.");
}
return QSqlQuery(db);
}
QSqlQuery result(db);
const bool ok = result.exec(query);
QSqlQuery(db).exec(QStringLiteral("PRAGMA query_only = OFF"));
if (ok) {
return result;
}
if (error) {
// SQLITE_READONLY is 8; extended codes keep it in the low byte.
if ((result.lastError().nativeErrorCode().toInt() & 0xff) == 8) {
*error = QCoreApplication::translate("QETSql",
"Seules les requêtes en lecture seule sont autorisées : "
"cette requête modifierait la base de données.");
} else {
*error = QCoreApplication::translate("QETSql",
"Requête SQL invalide : %1")
.arg(result.lastError().databaseText());
}
}
return QSqlQuery(db);
}
} // namespace QETSql
+43
View File
@@ -0,0 +1,43 @@
/*
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 SQLREADONLY_H
#define SQLREADONLY_H
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QString>
/**
Running a piece of SQL only if it reads.
Deliberately its own translation unit, depending on nothing but Qt SQL:
it is the enforcement point for every query QElectroTech runs against a
project database, including queries that arrive from outside the
application (a .qet file's saved report/table query), so it is worth
being able to test it in isolation -- see
tests/qttest/tst_sqlreadonly.cpp, which links this file and nothing
else of QElectroTech.
*/
namespace QETSql {
QSqlQuery execReadOnly(const QSqlDatabase &db,
const QString &query,
QString *error = nullptr);
}
#endif // SQLREADONLY_H
+199 -26
View File
@@ -20,8 +20,15 @@
#include "../../properties/elementdata.h"
#include "../../qetapp.h"
#include "../../qetinformation.h"
#include "../projectdatabase.h"
#include "ui_elementquerywidget.h"
#include <QFile>
#include <QFileDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QRegularExpression>
/**
@@ -47,14 +54,9 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
m_button_group.addButton(ui->m_coil_cb, 4);
m_button_group.addButton(ui->m_protection_cb, 5);
m_button_group.addButton(ui->m_thumbnail_cb, 6);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), [this](int id)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
m_button_group.addButton(ui->m_plc_cb, 7);
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
#endif
{
auto check_box = static_cast<QCheckBox *>(m_button_group.button(0));
if (id == 0)
@@ -76,7 +78,7 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
else
{
int checked = 0;
for (int i=1 ; i<7 ; ++i) {
for (int i=1 ; i<8 ; ++i) {
if (m_button_group.button(i)->isChecked()) {++checked;}
}
@@ -85,7 +87,7 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
case 0 :
check_box->setCheckState(Qt::Unchecked);
break;
case 6:
case 7:
check_box->setCheckState(Qt::Checked);
break;
default:
@@ -99,6 +101,9 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
setUpItems();
fillSavedQuery();
connect(ui->m_sql_query, &QLineEdit::textChanged, this, &ElementQueryWidget::checkQueryValidity);
checkQueryValidity();
}
/**
@@ -165,6 +170,8 @@ void ElementQueryWidget::setQuery(const QString &query)
{
auto str_type = rxm.captured(1);
where.remove(str_type);
const bool all_master_types = str_type.contains(
ElementData::typeToString(ElementData::Master));
int c=0;
ui->m_simple_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Simple)) ? true : false);
@@ -175,24 +182,28 @@ void ElementQueryWidget::setQuery(const QString &query)
if (ui->m_terminal_cb->isChecked()) {
++c;
}
ui->m_coil_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::Coil)) ? true : false);
ui->m_coil_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Coil)));
if (ui->m_coil_cb->isChecked()) {
++c;
}
ui->m_button_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::Commutator)) ? true : false);
ui->m_button_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Commutator)));
if (ui->m_button_cb->isChecked()) {
++c;
}
ui->m_protection_cb->setChecked(str_type.contains(ElementData::masterTypeToString(ElementData::Protection)) ? true : false);
if (ui->m_protection_cb) {
++c;
}
ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false);
if (ui->m_thumbnail_cb->isChecked()) {
ui->m_protection_cb->setChecked(all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Protection)));
if (ui->m_protection_cb->isChecked()) {
++c;
}
ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false);
if (ui->m_thumbnail_cb->isChecked()) {
++c;
}
ui->m_plc_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::PLC)));
if (ui->m_plc_cb->isChecked()) {
++c;
}
if (c == 6) {
if (c == 7) {
ui->m_all_cb->setCheckState(Qt::Checked);
} else if (c > 0) {
ui->m_all_cb->setCheckState(Qt::PartiallyChecked);
@@ -353,19 +364,35 @@ QString ElementQueryWidget::queryStr() const
where += QStringLiteral(" element_type = '") += ElementData::typeToString(ElementData::Simple) += "'";
b = true;
}
if (ui->m_button_cb->isChecked()) {
const bool all_master_types = ui->m_button_cb->isChecked()
&& ui->m_coil_cb->isChecked()
&& ui->m_protection_cb->isChecked()
&& ui->m_plc_cb->isChecked();
if (all_master_types) {
if (b) where += " OR";
where += QStringLiteral(" element_type = '")
+= ElementData::typeToString(ElementData::Master) += "'";
b = true;
}
if (!all_master_types && ui->m_button_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Commutator) += "'";
b = true;
}
if (ui->m_coil_cb->isChecked()) {
if (!all_master_types && ui->m_coil_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Coil) += "'";
b = true;
}
if (ui->m_protection_cb->isChecked()) {
if (!all_master_types && ui->m_protection_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Protection) += "'";
b = true;
}
if (!all_master_types && ui->m_plc_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
b = true;
}
where.append(")");
@@ -373,10 +400,10 @@ QString ElementQueryWidget::queryStr() const
where.clear();
}
QString exclude_condition = "(exclude_from_bom IS NULL OR exclude_from_bom != '1')";
filter_ += " AND " + exclude_condition;
// -------------------------------------------------------------
// exclude_from_bom is already filtered by element_nomenclature_view
// (see createElementNomenclatureView() in projectdatabase.cpp); this
// widget's query reads FROM that view, so a flagged element never
// reaches this point in the first place.
if (where.isEmpty() && !filter_.isEmpty()) {
filter_.remove(0, 4); //Remove the first " AND" of filter.
@@ -582,6 +609,8 @@ void ElementQueryWidget::on_m_edit_sql_query_cb_clicked()
m_custom_query = ui->m_sql_query->text();
updateQueryLine();
}
checkQueryValidity();
}
/**
@@ -655,6 +684,10 @@ void ElementQueryWidget::on_m_load_pb_clicked()
*/
void ElementQueryWidget::on_m_save_current_conf_pb_clicked()
{
if (!projectDataBase::isReadOnlySelect(queryStr())) {
return;
}
QFile file_(QETApp::configDir() % "/nomenclature.json");
if (file_.open(QFile::ReadWrite))
@@ -681,7 +714,147 @@ void ElementQueryWidget::on_m_save_current_conf_pb_clicked()
}
void ElementQueryWidget::on_m_save_name_le_textChanged(const QString &arg1) {
ui->m_save_current_conf_pb->setDisabled(arg1.isEmpty());
Q_UNUSED(arg1)
checkQueryValidity();
}
/**
@brief ElementQueryWidget::checkQueryValidity
Live feedback for the custom-SQL box: only the free-text path can carry
anything other than a SELECT (the column/filter builder always produces
one), so this only actually restricts something once "Requête SQL
personnalisée" is checked. Same rule projectDataBase::isReadOnlySelect()
enforces at execution time -- this just tells the user *before* they hit
Preview/Export/OK instead of after (qelectrotech-source-mirror#886).
*/
void ElementQueryWidget::checkQueryValidity()
{
if (!ui->m_edit_sql_query_cb->isChecked()) {
ui->m_query_warning_label->clear();
ui->m_save_current_conf_pb->setEnabled(!ui->m_save_name_le->text().isEmpty());
return;
}
QString reason;
const bool valid = projectDataBase::isReadOnlySelect(ui->m_sql_query->text(), &reason);
ui->m_query_warning_label->setText(valid ? QString() : reason);
ui->m_save_current_conf_pb->setEnabled(valid && !ui->m_save_name_le->text().isEmpty());
}
/**
@brief ElementQueryWidget::on_m_export_reports_pb_clicked
Write every saved report in nomenclature.json to a file the user picks,
so it can be handed to a colleague or another install
(qelectrotech-source-mirror#886's "import and export report definitions
for sharing between users or company installations").
*/
void ElementQueryWidget::on_m_export_reports_pb_clicked()
{
QFile source(QETApp::configDir() % "/nomenclature.json");
if (!source.open(QFile::ReadOnly)) {
QMessageBox::information(this, tr("Exporter"), tr("Aucun rapport enregistré à exporter."));
return;
}
const auto content = source.readAll();
source.close();
if (QJsonDocument::fromJson(content).object().isEmpty()) {
QMessageBox::information(this, tr("Exporter"), tr("Aucun rapport enregistré à exporter."));
return;
}
const QString file_path = QFileDialog::getSaveFileName(
this, tr("Exporter les rapports"), QStringLiteral("rapports_qet.json"),
tr("Fichiers JSON (*.json)"));
if (file_path.isEmpty()) {
return;
}
QFile dest(file_path);
if (!dest.open(QFile::WriteOnly) || dest.write(content) == -1) {
QMessageBox::critical(this, tr("Erreur"), tr("Impossible d'écrire dans %1.").arg(file_path));
}
}
/**
@brief ElementQueryWidget::on_m_import_reports_pb_clicked
Merge a previously-exported reports file into this install's
nomenclature.json. A name already used locally is not overwritten
silently -- the user is asked, per report, whether to replace it.
*/
void ElementQueryWidget::on_m_import_reports_pb_clicked()
{
const QString file_path = QFileDialog::getOpenFileName(
this, tr("Importer des rapports"), QString(), tr("Fichiers JSON (*.json)"));
if (file_path.isEmpty()) {
return;
}
QFile source(file_path);
if (!source.open(QFile::ReadOnly)) {
QMessageBox::critical(this, tr("Erreur"), tr("Impossible de lire %1.").arg(file_path));
return;
}
QJsonParseError parse_error;
const auto incoming_doc = QJsonDocument::fromJson(source.readAll(), &parse_error);
source.close();
if (parse_error.error != QJsonParseError::NoError || !incoming_doc.isObject()) {
QMessageBox::critical(
this, tr("Erreur"),
tr("%1 ne contient pas des rapports QElectroTech valides.").arg(file_path));
return;
}
const auto incoming = incoming_doc.object();
if (incoming.isEmpty()) {
QMessageBox::information(this, tr("Importer"), tr("Ce fichier ne contient aucun rapport."));
return;
}
QFile dest_file(QETApp::configDir() % "/nomenclature.json");
QJsonObject existing;
if (dest_file.open(QFile::ReadOnly)) {
existing = QJsonDocument::fromJson(dest_file.readAll()).object();
dest_file.close();
}
int imported = 0, skipped = 0;
for (auto it = incoming.begin(); it != incoming.end(); ++it)
{
if (existing.contains(it.key()))
{
const auto answer = QMessageBox::question(
this, tr("Rapport déjà existant"),
tr("Un rapport nommé « %1 » existe déjà. Le remplacer ?").arg(it.key()),
QMessageBox::Yes | QMessageBox::No);
if (answer != QMessageBox::Yes) {
++skipped;
continue;
}
}
existing[it.key()] = it.value();
++imported;
}
if (dest_file.open(QFile::WriteOnly | QFile::Truncate))
{
dest_file.write(QJsonDocument(existing).toJson());
dest_file.close();
}
else
{
QMessageBox::critical(this, tr("Erreur"), tr("Impossible d'écrire la configuration locale."));
return;
}
ui->m_conf_cb->clear();
fillSavedQuery();
QMessageBox::information(
this, tr("Importer"),
tr("%1 rapport(s) importé(s), %2 ignoré(s).").arg(imported).arg(skipped));
}
void ElementQueryWidget::on_m_choosen_list_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
+3
View File
@@ -59,6 +59,9 @@ class ElementQueryWidget : public QWidget
void on_m_load_pb_clicked();
void on_m_save_current_conf_pb_clicked();
void on_m_save_name_le_textChanged(const QString &arg1);
void on_m_import_reports_pb_clicked();
void on_m_export_reports_pb_clicked();
void checkQueryValidity();
void on_m_choosen_list_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
void on_m_var_list_itemDoubleClicked(QListWidgetItem *item);
void on_m_choosen_list_itemDoubleClicked(QListWidgetItem *item);
+129 -86
View File
@@ -88,7 +88,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-up" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-up.png</normaloff>:/ico/16x16/go-up.png</iconset>
</property>
<property name="checkable">
@@ -105,7 +105,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
</property>
</widget>
@@ -119,7 +119,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
</property>
</widget>
@@ -133,7 +133,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-down" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-down.png</normaloff>:/ico/16x16/go-down.png</iconset>
</property>
</widget>
@@ -234,86 +234,96 @@
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Type d'éléments</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="5" column="0">
<widget class="QCheckBox" name="m_simple_cb">
<property name="text">
<string>Simples</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="m_protection_cb">
<property name="text">
<string>Organes de protection</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="m_all_cb">
<property name="text">
<string>Tous</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="m_button_cb">
<property name="text">
<string>Boutons et commutateurs</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="m_terminal_cb">
<property name="text">
<string>Borniers</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="m_thumbnail_cb">
<property name="text">
<string>Vignettes</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="m_coil_cb">
<property name="text">
<string>Contacteurs et relais</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Type d'éléments</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QCheckBox" name="m_all_cb">
<property name="text">
<string>Tous</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="m_simple_cb">
<property name="text">
<string>Simples</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="m_terminal_cb">
<property name="text">
<string>Borniers</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="m_protection_cb">
<property name="text">
<string>Organes de protection</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="m_coil_cb">
<property name="text">
<string>Contacteurs et relais</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="m_thumbnail_cb">
<property name="text">
<string>Vignettes</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="m_button_cb">
<property name="text">
<string>Boutons et commutateurs</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="m_plc_cb">
<property name="text">
<string>Automates (MAE/SPS)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
@@ -343,7 +353,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="folder-open" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/folder-open.png</normaloff>:/ico/16x16/folder-open.png</iconset>
</property>
</widget>
@@ -370,7 +380,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="document-save" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/document-save.png</normaloff>:/ico/16x16/document-save.png</iconset>
</property>
<property name="flat">
@@ -378,6 +388,26 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="m_import_reports_pb">
<property name="toolTip">
<string>Importer des rapports depuis un fichier</string>
</property>
<property name="text">
<string>Importer...</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="m_export_reports_pb">
<property name="toolTip">
<string>Exporter tous les rapports enregistrés vers un fichier</string>
</property>
<property name="text">
<string>Exporter...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -419,6 +449,19 @@
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="m_query_warning_label">
<property name="styleSheet">
<string notr="true">color: red;</string>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
+29 -3
View File
@@ -63,7 +63,6 @@ QString SummaryQueryWidget::queryStr() const
QStringList keys = selectedKeys();
QString select ="SELECT ";
QString order_by = " ORDER BY ";
QString column;
bool first = true;
@@ -72,14 +71,29 @@ QString SummaryQueryWidget::queryStr() const
first = false;
} else {
column += ", ";
order_by +=", ";
}
column += key;
order_by += key;
}
QString from = " FROM project_summary_view";
//Always ordered by the folio's own position, never by the columns
//the user happened to choose to display. This is a table of
//contents: it lists the folios of the project and its order is the
//project's order. Ordering by the displayed columns instead meant
//that choosing, say, Title as the first column silently sorted the
//summary alphabetically (bugtracker #238).
//
//project_summary_view exposes the position as "pos", from
//diagram.pos, which is an INTEGER -- so this sorts numerically and
//folio 10 does not land between folio 1 and folio 2. One row per
//folio means pos fully determines the order and no secondary key is
//needed.
//
//A user who wants a different order can still write the query by
//hand; that path returns above, untouched.
QString order_by = " ORDER BY pos";
QString q(select + column + from + order_by);
return q;
}
@@ -95,6 +109,7 @@ void SummaryQueryWidget::setQuery(const QString &query)
if (query.startsWith("SELECT"))
{
reset();
ui->m_edit_sql_query_cb->setChecked(false);
ui->m_user_query_le->setText(query);
QString select = query;
@@ -115,6 +130,17 @@ void SummaryQueryWidget::setQuery(const QString &query)
}
}
}
//If the query this widget would build from the columns just
//parsed above does not match the query as loaded byte-for-byte,
//the user wrote it by hand (a join, a subquery, a different
//view) and accepting the dialog unmodified must not silently
//replace it with a generated one (bugtracker #885).
const bool custom_query = query != queryStr();
m_custom_query = custom_query ? query : QString();
ui->m_edit_sql_query_cb->setChecked(custom_query);
ui->m_user_query_le->setEnabled(custom_query);
ui->m_info_widget->setDisabled(custom_query);
}
}
+4 -4
View File
@@ -76,7 +76,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-up" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-up.png</normaloff>:/ico/16x16/go-up.png</iconset>
</property>
</widget>
@@ -87,7 +87,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
</property>
</widget>
@@ -98,7 +98,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
</property>
</widget>
@@ -109,7 +109,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-down" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-down.png</normaloff>:/ico/16x16/go-down.png</iconset>
</property>
</widget>
+470 -70
View File
@@ -26,6 +26,7 @@
#include "diagramposition.h"
#include "factory/elementfactory.h"
#include "qetapp.h"
#include "qetpalette.h"
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/conductortextitem.h"
@@ -39,10 +40,107 @@
#include "qetxml.h"
#include "undocommand/addelementtextcommand.h"
#include "qetinformation.h"
#include "qetproject.h"
#include "diagramsortkeys.h"
#include "textgrid.h"
#include <QTextStream>
#include <algorithm>
#include <climits>
#include <cassert>
#include <math.h>
namespace {
using DiagramSortKeys::positionKey;
/// Sort key for Diagram::toXml()'s <elements> block: the element's own
/// diagram-local position, exactly what it's already saved as (x/y),
/// never invented or regenerated. uuid() is deliberately NOT used here:
/// for an element with no persisted uuid attribute, fromXml() invents a
/// fresh random one on every load, so sorting by uuid would still be
/// non-deterministic across process runs for any legacy file.
QString elementSortKey(Element *elmt)
{
//Position alone is not a total order: two elements can sit at the
//same x/y (lmdg.qet has a pair of text elements both at 780,350).
//With equal keys std::stable_sort falls back to the order the
//scene handed us, which varies per run, so those two swapped
//places on every save. The uuid breaks the tie.
//
//For an element with a persisted uuid attribute this is fully
//deterministic. For a legacy element without one, fromXml()
//invents a fresh uuid per load, so a collision between two such
//elements is no better ordered than before -- but no worse
//either, and the tiebreaker is only consulted when the positions
//are equal.
return positionKey(elmt->pos())
+ QLatin1Char(':') + elmt->uuid().toString();
}
/// Sort key for a terminal: its parent element's position, then the
/// terminal's own position local to that element (from the .elmt
/// definition, fixed regardless of where the element is placed).
QString terminalSortKey(Terminal *terminal)
{
if (!terminal)
return QString();
Element *parent = terminal->parentElement();
return (parent ? positionKey(parent->pos()) : QStringLiteral("?"))
+ QLatin1Char(':') + positionKey(terminal->pos());
}
/// Sort key for Diagram::toXml()'s <conductors> block. Built from both
/// endpoints' terminalSortKey(), not Conductor::uuid(): in every example
/// project checked, conductors have no persisted uuid attribute at all,
/// so uuid() is a freshly-minted random value on every load -- exactly
/// as unusable for cross-run determinism as the element case above, just
/// with no persisted fallback to reach for instead. Canonicalised
/// (smaller key first) since a conductor's two ends are unordered for
/// this purpose.
QString conductorSortKey(Conductor *cond)
{
QString a = terminalSortKey(cond->terminal1);
QString b = terminalSortKey(cond->terminal2);
return (a <= b) ? (a + QLatin1Char('>') + b) : (b + QLatin1Char('>') + a);
}
/// Serialize @p items and append them to a new @p tag block of @p root,
/// in stacking order (@p stack_rank). items() cannot be trusted for
/// this: with NoIndex, the first removeItem() on the scene sorts Qt's
/// item list by pointer address, so from then on items() hands them over
/// in a per-run order (bugtracker #343). Stacking order is what reloading
/// the file rebuilds, so the drawing is unchanged and a resave is stable.
template <typename T>
void appendInStackingOrder(QDomDocument &document, QDomElement &root,
const QString &tag, const QVector<T *> &items,
const QHash<const QGraphicsItem *, int> &stack_rank)
{
if (items.isEmpty())
return;
struct Entry { int rank; QString xml_text; QDomElement xml; };
QVector<Entry> sorted;
for (T *item : items) {
Entry entry{stack_rank.value(item, INT_MAX), QString(),
item->toXml(document)};
// Only an item the stacking query missed needs a tiebreak.
if (entry.rank == INT_MAX) {
QTextStream stream(&entry.xml_text);
entry.xml.save(stream, 0);
}
sorted.append(entry);
}
std::stable_sort(sorted.begin(), sorted.end(),
[](const Entry &a, const Entry &b) {
return a.rank != b.rank ? a.rank < b.rank
: a.xml_text < b.xml_text;
});
auto block = document.createElement(tag);
for (const auto &entry : sorted)
block.appendChild(entry.xml);
root.appendChild(block);
}
}
int Diagram::xGrid = 10;
int Diagram::yGrid = 10;
int Diagram::xKeyGrid = 10;
@@ -65,14 +163,19 @@ QColor Diagram::background_color = Qt::white;
Diagram::Diagram(QETProject *project) :
QGraphicsScene (project),
m_project (project),
draw_grid_ (true),
use_border_ (true),
draw_terminals_ (true),
draw_terminal_names_ (true),
draw_colored_conductors_ (true),
m_event_interface (nullptr),
m_freeze_new_elements (false),
m_freeze_new_conductors_ (false)
{
QSettings settings;
draw_grid_ = settings.value(QStringLiteral("diagrameditor/grid_display_startup"), true).toBool();
draw_guides_ = settings.value(QStringLiteral("diagrameditor/guides_display_startup"), false).toBool();
setItemIndexMethod(QGraphicsScene::NoIndex);
/* Set to no index,
* because they can be the source of the crash with conductor and shape ghost.
@@ -121,8 +224,30 @@ Diagram::Diagram(QETProject *project) :
connect(this, &Diagram::diagramActivated,
this, &Diagram::loadCndFolioSeq);
adjustSceneRect();
}
m_guides_list.clear();
if (m_project) {
for (const auto &pg : m_project->defaultGuides()) {
Diagram::Guide g;
g.orientation = static_cast<Diagram::Guide::Orientation>(pg.orientation);
g.position = pg.position;
g.color = pg.color;
m_guides_list.append(g);
}
} else {
QSettings settings;
int size = settings.beginReadArray(QStringLiteral("diagrameditor/defaultguides"));
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
Diagram::Guide g;
g.orientation = static_cast<Diagram::Guide::Orientation>(settings.value(QStringLiteral("orientation"), 0).toInt());
g.position = settings.value(QStringLiteral("position"), 0.0).toReal();
g.color = QColor(settings.value(QStringLiteral("color"), QStringLiteral("#ff0000")).toString());
m_guides_list.append(g);
}
settings.endArray();
}
}
/**
@brief Diagram::~Diagram
Destructor
@@ -152,7 +277,7 @@ Diagram::~Diagram()
continue;
deletable_items.append(qgi);
}
for (const auto &item : qAsConst(deletable_items))
for (const auto &item : std::as_const(deletable_items))
{
removeItem(item);
delete item;
@@ -186,33 +311,33 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
p -> setBrush(Diagram::background_color);
p -> drawRect(r);
QSettings settings;
QRectF rect = settings.value(
QStringLiteral("diagrameditor/zoom-out-beyond-of-folio"),
false).toBool() ? r
: border_and_titleblock
.insideBorderRect()
.intersected(r);
if (draw_grid_) {
/* Draw the points of the grid
* if background color is black,
* then grid spots shall be white,
* else they shall be black in color.
* A view that shows the sheet with its lightness inverted
* gets softer dots, see QET::Palette::gridDotColor.
*/
QPen pen;
Diagram::background_color == Qt::black? pen.setColor(Qt::white)
: pen.setColor(Qt::black);
pen.setColor(QET::Palette::gridDotColor(Diagram::background_color, m_inverted_lightness));
pen.setCosmetic(true);
p->setPen(pen);
p -> setBrush(Qt::NoBrush);
// If user allow zoom out beyond of folio,
// we draw grid outside of border.
QSettings settings;
int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
int yGrid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
QRectF rect = settings.value(
QStringLiteral("diagrameditor/zoom-out-beyond-of-folio"),
false).toBool() ? r
: border_and_titleblock
.insideBorderRect()
.intersected(r);
qreal limit_x = rect.x() + rect.width();
qreal limit_y = rect.y() + rect.height();
@@ -252,7 +377,23 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
p -> drawPoints(points);
}
if (use_border_) border_and_titleblock.draw(p);
if (draw_guides_) {
for (const Diagram::Guide &guide : m_guides_list) {
QPen guidePen(guide.color, 1, Qt::DashLine);
guidePen.setCosmetic(true);
p->setPen(guidePen);
if (guide.orientation == Diagram::Guide::Vertical) {
p->drawLine(guide.position, rect.top(), guide.position, rect.bottom());
} else {
p->drawLine(rect.left(), guide.position, rect.right(), guide.position);
}
}
}
if (use_border_) {
border_and_titleblock.draw(p);
}
p -> restore();
}
@@ -351,6 +492,34 @@ void Diagram::wheelEvent(QGraphicsSceneWheelEvent *event)
QGraphicsScene::wheelEvent(event);
}
/**
@brief Diagram::event
QGraphicsScene has its own Tab/Shift+Tab item-focus-chain traversal
(mirroring QWidget's), checked before keyPressEvent() is ever reached:
by default it would silently consume Tab/Backtab to move focus among
the scene's own focusable items. Intercepting the key press here,
ahead of that, is the only way to reliably override it: unlike
QWidget::focusNextPrevChild(), QGraphicsScene::focusNextPrevChild() is
only virtual starting in Qt 6 (guarded by the QT6_VIRTUAL macro), so a
Diagram:: override of it would silently do nothing on a Qt 5 build.
@param event
*/
bool Diagram::event(QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
auto *key_event = static_cast<QKeyEvent *>(event);
if ((key_event->key() == Qt::Key_Tab || key_event->key() == Qt::Key_Backtab)
&& !isReadOnly() && !focusItem()) {
bool forward = key_event->key() == Qt::Key_Tab
&& !(key_event->modifiers() & Qt::ShiftModifier);
selectNextItem(forward);
event->accept();
return true;
}
}
return QGraphicsScene::event(event);
}
/**
@brief Diagram::keyPressEvent
This event is managed by diagram event interface if any.
@@ -564,6 +733,79 @@ QUuid Diagram::uuid()
return m_uuid;
}
/**
@brief Diagram::uuidUsedByOtherDiagram
A hand-edited or merged project file can contain two folios with the same
uuid. The uuid is used as a key (e.g. in the project database), so the
second one must get another one.
@param uuid
@return true if another diagram of the parent project already uses @p uuid
*/
bool Diagram::uuidUsedByOtherDiagram(const QUuid &uuid) const
{
if (!m_project) {
return false;
}
const auto diagrams = m_project->diagrams();
for (const Diagram *diagram : diagrams) {
if (diagram != this && diagram->m_uuid == uuid) {
return true;
}
}
return false;
}
/**
@brief Diagram::derivedUuid
Name-based (version 5) uuid for a folio that has no usable uuid in the
file it is loaded from : either the file predates the uuid attribute, or
the uuid it carries is already taken by another folio of the project.
A random uuid would do as an identity, but it would make saving an
unmodified legacy project non-reproducible : every load would invent a
different one and write it out (see #754). The name is therefore built
only from data read from the file itself -- the project title, the
position of the folio in the file and its title -- so the same input file
always yields the same uuid. It is computed once, when the folio is
loaded, and saved from then on : renaming or moving the folio later does
not change it.
@param root : the <diagram> element being loaded
@param reason : distinguishes the two cases above, so that they can not
produce the same name
@return a uuid not used by any other diagram of the project
*/
QUuid Diagram::derivedUuid(const QDomElement &root, const QString &reason) const
{
//Fixed namespace for QElectroTech folio uuids, never change it :
//doing so would change the uuid given to every legacy folio.
static const QUuid folio_namespace(
QStringLiteral("{d5951240-154d-44d6-8277-0092a31d1920}"));
const int index = m_project
? m_project->diagrams().indexOf(const_cast<Diagram *>(this))
: -1;
const QString project_title = root.ownerDocument()
.documentElement()
.attribute(QStringLiteral("title"));
const QString base = QStringLiteral("qet-folio\n%1\n%2\n%3\n%4")
.arg(reason,
project_title,
QString::number(index),
root.attribute(QStringLiteral("title")));
//A clash is only possible with a hand-crafted file, but the uuid is a
//key : salt the name until it is free. Still deterministic.
QUuid uuid = QUuid::createUuidV5(folio_namespace, base);
for (int salt = 1 ; uuidUsedByOtherDiagram(uuid) ; ++salt) {
uuid = QUuid::createUuidV5(folio_namespace,
base + QStringLiteral("\n")
+ QString::number(salt));
}
return uuid;
}
/**
@brief Diagram::setEventInterface
Set event_interface has current interface.
@@ -601,6 +843,11 @@ void Diagram::clearEventInterface()
}
}
bool Diagram::eventInterfaceIsRunning() const
{
return m_event_interface && m_event_interface->isRunning();
}
/**
@brief Diagram::conductorsAutonumName
@return the name of autonum to use.
@@ -779,6 +1026,11 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
// schema properties
// proprietes du schema
if (whole_content) {
//Persist the folio identity, so that a folio keeps the same uuid
//across save/load. Without it every load invents a new one, and
//nothing outside the running instance (version control, a lock,
//a diff tool...) can tell which folio is which.
dom_root.setAttribute(QStringLiteral("uuid"), m_uuid.toString());
border_and_titleblock.titleBlockToXml(dom_root);
border_and_titleblock.borderToXml(dom_root);
@@ -972,6 +1224,20 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
}
}
// items() returns items in stacking order, which is not guaranteed
// reproducible across processes (ties between same-Z items follow
// the scene's internal index, not any content-derived order) -- so
// without this, saving an unmodified project produces a different
// byte stream on every run. Elements and conductors are the two
// blocks observed to actually churn across the example corpus;
// sort them into a deterministic, content-derived order before
// serializing. This also fixes the legacy terminal-id churn below,
// since those ids are assigned sequentially in element order.
std::stable_sort(list_elements.begin(), list_elements.end(),
[](Element *a, Element *b) { return elementSortKey(a) < elementSortKey(b); });
std::stable_sort(list_conductors.begin(), list_conductors.end(),
[](Conductor *a, Conductor *b) { return conductorSortKey(a) < conductorSortKey(b); });
// correspondence table between the addresses of the terminals and their ids
// table de correspondance entre les adresses des bornes et leurs ids
QHash<Terminal *, int> table_adr_id;
@@ -997,37 +1263,20 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
dom_root.appendChild(dom_conductors);
}
if (!list_texts.isEmpty()) {
auto dom_texts = document.createElement(QStringLiteral("inputs"));
for (auto dti : list_texts) {
dom_texts.appendChild(dti->toXml(document));
}
dom_root.appendChild(dom_texts);
}
if (!list_images.isEmpty()) {
auto dom_images = document.createElement(QStringLiteral("images"));
for (auto dii : list_images) {
dom_images.appendChild(dii->toXml(document));
}
dom_root.appendChild(dom_images);
}
if (!list_shapes.isEmpty()) {
auto dom_shapes = document.createElement(QStringLiteral("shapes"));
for (auto dii : list_shapes) {
dom_shapes.appendChild(dii -> toXml(document));
}
dom_root.appendChild(dom_shapes);
}
if (table_vector.size()) {
auto tables = document.createElement(QStringLiteral("tables"));
for (auto table : table_vector) {
tables.appendChild(table->toXml(document));
}
dom_root.appendChild(tables);
// A rect query, unlike items(), returns true stacking order (z, then
// insertion order) even with NoIndex.
QHash<const QGraphicsItem *, int> stack_rank;
{
const QList<QGraphicsItem *> stacked = items(
QRectF(-1e9, -1e9, 2e9, 2e9), Qt::IntersectsItemBoundingRect,
Qt::AscendingOrder);
for (int i = 0 ; i < stacked.size() ; ++i)
stack_rank.insert(stacked.at(i), i);
}
appendInStackingOrder(document, dom_root, QStringLiteral("inputs"), list_texts, stack_rank);
appendInStackingOrder(document, dom_root, QStringLiteral("images"), list_images, stack_rank);
appendInStackingOrder(document, dom_root, QStringLiteral("shapes"), list_shapes, stack_rank);
appendInStackingOrder(document, dom_root, QStringLiteral("tables"), table_vector, stack_rank);
if (!strip_vector.isEmpty()) {
dom_root.appendChild(TerminalStripItemXml::toXml(strip_vector, document));
@@ -1265,6 +1514,21 @@ bool Diagram::fromXml(QDomElement &document,
// Read attributes of this diagram
if (consider_informations)
{
// Restore the persisted folio uuid. Done first, before any item is
// loaded, so that everything created below sees the final uuid.
// A folio without a usable one (file written before the uuid was
// persisted, or uuid already taken by another folio) gets a
// deterministic one instead, see derivedUuid().
const QUuid persisted_uuid(root.attribute(QStringLiteral("uuid")));
if (persisted_uuid.isNull()) {
m_uuid = derivedUuid(root, QStringLiteral("legacy"));
} else if (uuidUsedByOtherDiagram(persisted_uuid)) {
m_uuid = derivedUuid(root, QStringLiteral("duplicate ")
+ persisted_uuid.toString());
} else {
m_uuid = persisted_uuid;
}
// Load border and titleblock
border_and_titleblock.titleBlockFromXml(root);
border_and_titleblock.borderFromXml(root);
@@ -1458,20 +1722,17 @@ bool Diagram::fromXml(QDomElement &document,
if (position != QPointF())
{
QVector <QGraphicsItem *> added_items;
for (auto element : qAsConst(added_elements )) added_items << element;
for (auto shape : qAsConst(added_shapes )) added_items << shape;
for (auto text : qAsConst(added_texts )) added_items << text;
for (auto image : qAsConst(added_images )) added_items << image;
for (auto table : qAsConst(added_tables )) added_items << table;
for (const auto &strip : qAsConst(added_strips)) added_items << strip;
for (auto element : std::as_const(added_elements )) added_items << element;
for (auto shape : std::as_const(added_shapes )) added_items << shape;
for (auto text : std::as_const(added_texts )) added_items << text;
for (auto image : std::as_const(added_images )) added_items << image;
for (auto table : std::as_const(added_tables )) added_items << table;
for (const auto &strip : std::as_const(added_strips)) added_items << strip;
//Get the top left corner of the rectangle that contain all added items
QRectF items_rect;
for (auto item : added_items) {
items_rect = items_rect.united(
item->mapToScene(
item->boundingRect()
).boundingRect());
items_rect = items_rect.united(item->mapToScene(item->boundingRect()).boundingRect());
}
QPointF point_ = items_rect.topLeft();
@@ -1479,8 +1740,12 @@ bool Diagram::fromXml(QDomElement &document,
position.y() - point_.y()));
//Translate all added items
for (auto qgi : added_items)
for (auto qgi : added_items) {
qgi->setPos(qgi->pos() += pos_);
}
}
else
{
}
// Load conductor
@@ -1514,14 +1779,6 @@ bool Diagram::fromXml(QDomElement &document,
if (content_ptr) {
content_ptr -> m_elements = added_elements;
content_ptr -> m_conductors_to_move = added_conductors;
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
content_ptr -> m_text_fields = added_texts.toSet();
content_ptr -> m_images = added_images.toSet();
content_ptr -> m_shapes = added_shapes.toSet();
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
content_ptr -> m_text_fields = QSet<IndependentTextItem *>(
added_texts.begin(),
added_texts.end());
@@ -1532,7 +1789,6 @@ bool Diagram::fromXml(QDomElement &document,
added_shapes.begin(),
added_shapes.end());
content_ptr->m_terminal_strip.swap(added_strips);
#endif
content_ptr->m_tables.swap(added_tables);
}
@@ -1599,11 +1855,11 @@ void Diagram::refreshContents()
conductor->refreshText();
}
for (auto &table : qAsConst(dc_.m_tables)) {
for (auto &table : std::as_const(dc_.m_tables)) {
table->initLink();
}
for (auto &strip :qAsConst(dc_.m_terminal_strip)) {
for (auto &strip :std::as_const(dc_.m_terminal_strip)) {
strip->refreshPending();
}
}
@@ -1633,6 +1889,7 @@ void Diagram::addItem(QGraphicsItem *item)
conductor->terminal1->addConductor(conductor);
conductor->terminal2->addConductor(conductor);
conductor->calculateTextItemPosition();
m_project->dataBase()->addConductor(conductor);
break;
}
default: {break;}
@@ -1663,6 +1920,7 @@ void Diagram::removeItem(QGraphicsItem *item)
Conductor *conductor = static_cast<Conductor *>(item);
conductor->terminal1->removeConductor(conductor);
conductor->terminal2->removeConductor(conductor);
m_project->dataBase()->removeConductor(conductor);
break;
}
default: {break;}
@@ -1788,7 +2046,7 @@ void Diagram::invertSelection()
item_list << item;
}
}
for (auto item : qAsConst(item_list)) {
for (auto item : std::as_const(item_list)) {
item -> setSelected(!item -> isSelected());
}
@@ -1796,6 +2054,85 @@ void Diagram::invertSelection()
emit selectionChanged();
}
/**
@brief Diagram::selectAllConductors
Select every conductor on this diagram, deselecting anything else.
*/
void Diagram::selectAllConductors()
{
if (items().isEmpty()) return;
blockSignals(true);
for (auto item : items()) {
item -> setSelected(dynamic_cast<Conductor *>(item) != nullptr);
}
blockSignals(false);
emit selectionChanged();
}
/**
@brief Diagram::selectAllTextFields
Select every text field on this diagram (independent/static text,
conductor labels, and dynamic element texts), deselecting anything else.
*/
void Diagram::selectAllTextFields()
{
if (items().isEmpty()) return;
blockSignals(true);
for (auto item : items()) {
item -> setSelected(dynamic_cast<DiagramTextItem *>(item) != nullptr);
}
blockSignals(false);
emit selectionChanged();
}
/**
@brief Diagram::selectNextItem
Select the next (or, if @a forward is false, the previous) selectable
item on this diagram, cycling through items() (z-order) and wrapping
around at either end. If nothing is currently selected, selects the
first (or last) item. Uses the same "what counts as a real selectable
diagram item" filter as invertSelection(), so the candidate list and
its order always match what the user could reach by clicking.
@param forward true to select the next item, false for the previous one
*/
void Diagram::selectNextItem(bool forward)
{
QList<QGraphicsItem *> candidates;
for (auto item : items()) {
if (dynamic_cast<QetGraphicsItem *>(item) ||
dynamic_cast<DiagramTextItem *>(item) ||
dynamic_cast<Conductor *>(item)) {
candidates << item;
}
}
if (candidates.isEmpty())
return;
int current_index = -1;
for (int i = 0; i < candidates.size(); ++i) {
if (candidates.at(i) -> isSelected()) {
current_index = i;
break;
}
}
int next_index;
if (current_index == -1) {
next_index = forward ? 0 : candidates.size() - 1;
} else {
next_index = forward
? (current_index + 1) % candidates.size()
: (current_index - 1 + candidates.size()) % candidates.size();
}
clearSelection();
QGraphicsItem *next_item = candidates.at(next_index);
next_item -> setSelected(true);
next_item -> ensureVisible();
}
/**
@brief Diagram::insertFolioSeqHash
This class inserts a stringlist containing all
@@ -2274,9 +2611,11 @@ ExportProperties Diagram::applyProperties(
// exporte les options de rendu en cours
ExportProperties old_properties;
old_properties.draw_grid = displayGrid();
old_properties.draw_guides = displayGuides();
old_properties.draw_border = border_and_titleblock.borderIsDisplayed();
old_properties.draw_titleblock = border_and_titleblock.titleBlockIsDisplayed();
old_properties.draw_terminals = drawTerminals();
old_properties.draw_terminal_names = drawTerminalNames();
old_properties.draw_colored_conductors = drawColoredConductors();
old_properties.exported_area = useBorder() ? QET::BorderArea
: QET::ElementsArea;
@@ -2285,8 +2624,10 @@ ExportProperties Diagram::applyProperties(
// applique les nouvelles options de rendu
setUseBorder (new_properties.exported_area == QET::BorderArea);
setDrawTerminals (new_properties.draw_terminals);
setDrawTerminalNames (new_properties.draw_terminal_names);
setDrawColoredConductors (new_properties.draw_colored_conductors);
setDisplayGrid (new_properties.draw_grid);
setDisplayGuides (new_properties.draw_guides);
border_and_titleblock.displayBorder(new_properties.draw_border);
border_and_titleblock.displayTitleBlock (new_properties.draw_titleblock);
@@ -2344,6 +2685,29 @@ QPointF Diagram::snapToGrid(const QPointF &p)
return (QPointF(p_x, p_y));
}
/**
@brief Diagram::snapToTextGrid
Return the nearest point of p on the text grid, see TextGrid.
Ctrl held rounds to the nearest pixel instead, as snapToGrid() does.
@param p point to find the nearest snapped point
@return
*/
QPointF Diagram::snapToTextGrid(const QPointF &p)
{
QSettings settings;
const qreal divisor =
QApplication::keyboardModifiers().testFlag(Qt::ControlModifier)
? 0
: settings.value(TextGrid::settings_key, 1).toReal();
return TextGrid::snap(p,
settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt(),
settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt(),
divisor);
}
/**
@@ -2354,9 +2718,24 @@ QPointF Diagram::snapToGrid(const QPointF &p)
\~French true pour afficher les bornes, false sinon
*/
void Diagram::setDrawTerminals(bool dt) {
draw_terminals_ = dt;
foreach(QGraphicsItem *qgi, items()) {
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
t -> setVisible(dt);
t -> update();
}
}
}
/**
@brief Diagram::setDrawTerminalNames
Defines whether or not to display the terminal names/labels
@param dt : true to display the terminal names, false otherwise
*/
void Diagram::setDrawTerminalNames(bool dt) {
draw_terminal_names_ = dt;
foreach(QGraphicsItem *qgi, items()) {
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
t -> update();
}
}
}
@@ -2635,3 +3014,24 @@ void Diagram::restoreText(Element* elmt)
}
}
}
QUndoStack &Diagram::undoStack() {
return *(project()->undoStack());
}
/**
* @brief Diagram::updateProjectGuides
* Aktualisiert die internen Hilfslinien dieses Schaltplans
* basierend auf den Projekt-Einstellungen und erzwingt ein Neuzeichnen.
*/
void Diagram::updateProjectGuides(const QList<GuideProperties> &guides) {
m_guides_list.clear();
for (const GuideProperties &pg : guides) {
Diagram::Guide g;
g.orientation = static_cast<Diagram::Guide::Orientation>(pg.orientation);
g.position = pg.position;
g.color = pg.color;
m_guides_list.append(g);
}
update();
}
+60 -12
View File
@@ -24,7 +24,6 @@
#include "elementtextsmover.h"
#include "exportproperties.h"
#include "properties/xrefproperties.h"
#include "qetproject.h"
#include "qgimanager.h"
#include <QHash>
@@ -39,12 +38,11 @@ class DiagramPosition;
class DiagramTextItem;
class Element;
class ElementsLocation;
class QETProject;
class Terminal;
class DiagramImageItem;
class DiagramEventInterface;
class DiagramFolioList;
class QETProject;
struct GuideProperties;
/**
@brief The Diagram class
@@ -67,6 +65,13 @@ class Diagram : public QGraphicsScene
// ATTRIBUTES
public:
struct Guide {
enum Orientation { Horizontal, Vertical };
Orientation orientation;
qreal position;
QColor color;
};
/**
@brief The BorderOptions enum
Represents available options when rendering a particular diagram:
@@ -118,8 +123,12 @@ class Diagram : public QGraphicsScene
qreal diagram_qet_version_;
bool draw_grid_;
bool m_inverted_lightness = false;
bool use_border_;
bool draw_guides_;
QList<Diagram::Guide> m_guides_list;
bool draw_terminals_;
bool draw_terminal_names_;
bool draw_colored_conductors_;
QString m_conductors_autonum_name;
@@ -128,6 +137,9 @@ class Diagram : public QGraphicsScene
bool m_freeze_new_elements;
bool m_freeze_new_conductors_;
QUuid m_uuid = QUuid::createUuid();
bool uuidUsedByOtherDiagram(const QUuid &uuid) const;
QUuid derivedUuid(const QDomElement &root, const QString &reason) const;
// METHODS
protected:
@@ -142,8 +154,11 @@ class Diagram : public QGraphicsScene
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void keyReleaseEvent (QKeyEvent *) override;
bool event(QEvent *event) override;
private:
void selectNextItem(bool forward);
public:
void correctTextPos(Element* elmt);
void restoreText(Element* elmt);
@@ -202,20 +217,28 @@ class Diagram : public QGraphicsScene
// methods related to graphics items addition/removal on the diagram
virtual void addItem (QGraphicsItem *item);
virtual void removeItem (QGraphicsItem *item);
bool eventInterfaceIsRunning() const;
// methods related to graphics options
ExportProperties applyProperties(const ExportProperties &);
void setDisplayGrid(bool);
bool displayGrid();
void setInvertedLightness(bool);
void setDisplayGuides(bool);
bool displayGuides();
void updateProjectGuides(const QList<GuideProperties> &guides);
void setUseBorder(bool);
bool useBorder();
void setBorderOptions(BorderOptions);
BorderOptions borderOptions();
DiagramPosition convertPosition(const QPointF &);
static QPointF snapToGrid(const QPointF &p);
static QPointF snapToTextGrid(const QPointF &p);
bool drawTerminals() const;
void setDrawTerminals(bool);
bool drawTerminalNames() const;
void setDrawTerminalNames(bool);
bool drawColoredConductors() const;
void setDrawColoredConductors(bool);
@@ -274,6 +297,8 @@ class Diagram : public QGraphicsScene
void selectAll();
void deselectAll();
void invertSelection();
void selectAllConductors();
void selectAllTextFields();
signals:
void showDiagram (Diagram *);
@@ -333,6 +358,18 @@ inline void Diagram::setDisplayGrid(bool dg) {
draw_grid_ = dg;
}
/**
@brief Diagram::setInvertedLightness
Tell the diagram whether the view painting it will show the result
with its lightness inverted (PaletteGraphicsView on a dark palette).
drawBackground draws a softer grid in that case. Printing
and export never set this.
@param inverted
*/
inline void Diagram::setInvertedLightness(bool inverted) {
m_inverted_lightness = inverted;
}
/**
@brief Diagram::displayGrid
@return draw_grid_ true if the grid is drawn, false otherwise.
@@ -341,6 +378,16 @@ inline bool Diagram::displayGrid() {
return(draw_grid_);
}
inline void Diagram::setDisplayGuides(bool dg) {
if (draw_guides_ != dg) {
draw_guides_ = dg;
update();
}
}
inline bool Diagram::displayGuides() {
return(draw_guides_);
}
/**
@brief Diagram::setUseBorder
Set whether the diagram border (including rows/columns headers and the title
@@ -389,14 +436,6 @@ inline Diagram::BorderOptions Diagram::borderOptions() {
return(options);
}
/**
@brief Diagram::undoStack
@return the diagram undo stack
*/
inline QUndoStack &Diagram::undoStack() {
return *(project()->undoStack());
}
/**
@brief Diagram::qgiManager
@return the diagram graphics item manager
@@ -414,6 +453,15 @@ inline bool Diagram::drawTerminals() const
return(draw_terminals_);
}
/**
@brief Diagram::drawTerminalNames
@return true if terminal names are rendered, false otherwise
*/
inline bool Diagram::drawTerminalNames() const
{
return(draw_terminal_names_);
}
/**
@brief Diagram::drawColoredConductors
@return true if conductors colors are rendered, false otherwise.
+105 -3
View File
@@ -19,8 +19,10 @@
#include "diagram.h"
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/elementtextitemgroup.h"
#include "qetinformation.h"
#include "qgimanager.h"
/**
@@ -75,13 +77,92 @@ void PasteDiagramCommand::redo()
{
first_redo = false;
//this is the first paste, we do some actions for the new element
//Resolve a linked master/slave pair pasted together (bugtracker
//#607) before anything below renews their uuids: at this exact
//moment a pasted element's tmp_uuids_link still holds its
//source's original partner uuid, which still equals the
//not-yet-renewed uuid of that partner's own pasted copy if it
//was carried along in the same batch. Scoped to this batch only
//(not a project-wide search), so a pair pasted together links to
//each other and not to an original element left elsewhere that
//happens to still carry that same soon-to-be-replaced uuid. If
//only one half of a linked group was pasted, its link entry
//simply finds no match here and is dropped -- same "leave it
//unlinked" outcome as always.
const QList <Element *> elmts_list = content.m_elements;
for (Element *e : elmts_list) {
e->initLink(elmts_list);
}
//make new uuid for every pasted conductor, because old uuid are
//the uuid of the copied conductor
const QList <Conductor *> all_pasted_conductors = content.conductors();
for (Conductor *c : all_pasted_conductors) {
c -> newUuid();
}
//this is the first paste, we do some actions for the new element
for (Element *e : elmts_list)
{
//make new uuid, because old uuid are the uuid of the copied element
e -> newUuid();
// PLC slaves carry master-specific data (type, address,
// function, cross-ref, etc.) in their elementInformations.
// Always clear those on paste so the duplicate starts clean,
// regardless of the user's erase-label-on-copy preference.
const bool is_slave = (e->linkType() == Element::Slave);
if (is_slave) {
DiagramContext dc = e->elementInformations();
dc.remove(QETInformation::ELMT_PLC_TYPE);
dc.remove(QETInformation::ELMT_PLC_ADDRESS);
dc.remove(QETInformation::ELMT_PLC_FUNCTION);
dc.remove(QETInformation::ELMT_PLC_COMMENT);
dc.remove(QETInformation::ELMT_PLC_CROSSREF);
dc.remove(QETInformation::ELMT_LABEL);
dc.remove(QETInformation::ELMT_PLC_TC);
dc.remove(QETInformation::ELMT_PLC_T1);
dc.remove(QETInformation::ELMT_PLC_T2);
dc.remove(QETInformation::ELMT_PLC_T3);
dc.remove(QETInformation::ELMT_PLC_T4);
dc.remove(QStringLiteral("xref"));
// Block alignment before setElementInformations so
// that elementInfoChanged() resolves texts without
// finishAlignment() shifting right/center-aligned items.
for (DynamicElementTextItem *deti : e->dynamicTextItems())
deti->m_block_alignment = true;
for (auto *group : e->textGroups())
group->blockAlignmentUpdate(true);
e->setElementInformations(dc);
for (DynamicElementTextItem *deti : e->dynamicTextItems())
deti->m_block_alignment = false;
for (auto *group : e->textGroups())
group->blockAlignmentUpdate(false);
// After setElementInformations, elementInfoChanged()
// resolves composite text with cleaned dc. Clear
// all non-UserText items directly as a safety net.
for (DynamicElementTextItem *deti : e->dynamicTextItems()) {
if (deti->textFrom() != DynamicElementTextItem::UserText) {
deti->m_block_alignment = true;
deti->setPlainText(QString());
deti->m_block_alignment = false;
}
}
for (auto *group : e->textGroups()) {
for (DynamicElementTextItem *deti : group->texts()) {
if (deti->textFrom() != DynamicElementTextItem::UserText) {
deti->m_block_alignment = true;
deti->setPlainText(QString());
deti->m_block_alignment = false;
}
}
}
}
if (settings.value("diagramcommands/erase-label-on-copy", true).toBool())
{
//Reset the information about the label, the comment and location
@@ -90,14 +171,35 @@ void PasteDiagramCommand::redo()
dc.addValue("label", "");
dc.addValue("comment", "");
dc.addValue("location", "");
// Block alignment during setElementInformations
// for non-slaves, same as Element::fromXml() (line 890-896).
if (!is_slave) {
for (DynamicElementTextItem *deti : e->dynamicTextItems())
deti->m_block_alignment = true;
for (auto *group : e->textGroups())
group->blockAlignmentUpdate(true);
}
e->setElementInformations(dc);
for (DynamicElementTextItem *deti : e->dynamicTextItems())
deti->m_block_alignment = false;
for (auto *group : e->textGroups())
group->blockAlignmentUpdate(false);
//Reset the text of conductors
//Reset the text of conductors, the same way the label/comment/
//location above are reset to "" rather than to some other
//value - "erase on copy" means erase, not "replace with the
//project's default new-conductor text" (which happens to
//default to a literal "_" character, unrelated to whether the
//user wanted this copy's old label kept or cleared; see
//issue #413).
const QList <Conductor *> conductors_list = content.m_conductors_to_move;
for (Conductor *c : conductors_list)
{
ConductorProperties cp = c -> properties();
cp.text = c->diagram() ? c -> diagram() -> defaultConductorProperties.text : "_";
cp.text = "";
c -> setProperties(cp);
}
}
+4 -4
View File
@@ -55,7 +55,7 @@ DiagramContent::DiagramContent(Diagram *diagram, bool selected) :
item_list = diagram->items();
}
for (const auto &item : qAsConst(item_list))
for (const auto &item : std::as_const(item_list))
{
switch (item->type())
{
@@ -399,10 +399,10 @@ QList<QGraphicsItem *> DiagramContent::items(int filter) const
if (filter & ElementTextFields) for(auto qgi : m_element_texts) items_list << qgi;
if (filter & TextGroup) for(auto qgi : m_texts_groups) items_list << qgi;
if (filter & Tables) for(auto qgi : m_tables) items_list << qgi;
if (filter & TerminalStrip) for(const auto qgi : qAsConst(m_terminal_strip)) items_list << qgi;
if (filter & TerminalStrip) for(const auto qgi : std::as_const(m_terminal_strip)) items_list << qgi;
if (filter & SelectedOnly) {
for(const auto &qgi : qAsConst(items_list)) {
for(const auto &qgi : std::as_const(items_list)) {
if (!qgi -> isSelected()) items_list.removeOne(qgi);
}
}
@@ -428,7 +428,7 @@ int DiagramContent::count(int filter) const
if (filter & ElementTextFields) for(auto deti : m_element_texts) { if (deti -> isSelected()) ++ count; }
if (filter & TextGroup) for(auto etig : m_texts_groups) { if (etig -> isSelected()) ++ count; }
if (filter & Tables) for(auto table : m_tables) { if (table -> isSelected()) ++ count; }
if (filter & TerminalStrip) for(const auto &strip : qAsConst(m_terminal_strip)) {if (strip->isSelected()) ++ count;}
if (filter & TerminalStrip) for(const auto &strip : std::as_const(m_terminal_strip)) {if (strip->isSelected()) ++ count;}
}
else {
if (filter & Elements) count += m_elements.count();
+11 -3
View File
@@ -151,8 +151,8 @@ bool DiagramContext::operator!=(const DiagramContext &dc) const
void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
{
foreach (QString key, keys()) {
if ((tag_name == "elementInformation") &&
(m_content[key].toString().trimmed().isEmpty())) {
const QString raw = m_content[key].toString();
if ((tag_name == "elementInformation") && raw.trimmed().isEmpty()) {
continue;
}
QDomElement property = e.ownerDocument().createElement(tag_name);
@@ -161,7 +161,15 @@ void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
property.removeAttribute("name");
property.setAttribute("show", m_content_show[key]);
property.setAttribute("name", key);
QDomText value = e.ownerDocument().createTextNode(m_content[key].toString().trimmed());
// Trim stray leading/trailing whitespace around real content, but
// not a value that IS whitespace: unconditionally trimming an
// all-whitespace string collapses it to "", which is silently
// indistinguishable from a value that was never set. A title-block
// custom variable set to a single space -- a workaround for #973,
// where an unset variable renders as its own literal placeholder --
// would otherwise vanish on the very next save.
const QString stored = raw.trimmed().isEmpty() ? raw : raw.trimmed();
QDomText value = e.ownerDocument().createTextNode(stored);
property.appendChild(value);
e.appendChild(property);
}

Some files were not shown because too many files have changed in this diff Show More