mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-01 23:54:13 +02:00
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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
#ifndef NAMES_LIST_H
|
||||
#define NAMES_LIST_H
|
||||
#include "pugixml/src/pugixml.hpp"
|
||||
#include "pugixml.hpp"
|
||||
|
||||
#include <QtXml>
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 ¤tRule,
|
||||
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 ¤tRule = 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,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 ¤tRule,
|
||||
QSettings &settings,
|
||||
const QString &prefix);
|
||||
static QPair<QHash<QString, NumerotationContext>, QString> loadFromSettings(
|
||||
QSettings &settings,
|
||||
const QString &prefix);
|
||||
|
||||
private:
|
||||
QStringList content_;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user