Merge branch 'master' into fix-nonfinite-coordinate-validation

This commit is contained in:
ispyisail
2026-09-12 23:07:47 +12:00
217 changed files with 57618 additions and 20059 deletions
@@ -63,6 +63,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();
@@ -330,7 +331,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);
}
@@ -430,7 +430,7 @@ void FileElementCollectionItem::setUpData()
*/
void FileElementCollectionItem::setUpIcon()
{
// Must return unconditionally once an icon is set: setIcon() calls
// 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
@@ -438,13 +438,18 @@ void FileElementCollectionItem::setUpIcon()
// 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 (!icon().isNull())
if (m_icon_initialized)
return;
m_icon_initialized = true;
if (isCollectionRoot()) {
QString macrosPath = QETApp::userMacrosDir();
@@ -69,6 +69,7 @@ class FileElementCollectionItem : public ElementCollectionItem
/// 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
@@ -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
+139 -98
View File
@@ -24,6 +24,8 @@
#include "../qetgraphicsitem/element.h"
#include "../qetxml.h"
#include "../qetproject.h"
#include <QDir>
#include <QDomDocument>
#include <QStringList>
#include <QVariant>
#include <utility>
@@ -703,6 +705,74 @@ namespace autonum
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
@@ -716,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();
}
@@ -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>
@@ -328,7 +325,6 @@
<widget class="QWidget" name="folioWidget" native="true">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
<kerning>true</kerning>
</font>
@@ -338,7 +334,6 @@
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
+114
View File
@@ -0,0 +1,114 @@
/*
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()
{
return QStringLiteral("SELECT %1 FROM element_nomenclature_view "
"WHERE ( element_type = 'simple' OR element_type = 'master') "
"ORDER BY diagram_position, position, label")
.arg(defaultColumns().join(QStringLiteral(", ")));
}
QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers, int *row_count)
{
QByteArray csv("\xEF\xBB\xBF");
if (include_headers) {
csv += csvRecord(headers);
}
int rows = 0;
while (query.next())
{
QStringList values;
for (int i = 0; i < query.record().count(); ++i) {
values.append(query.value(i).toString());
}
csv += csvRecord(values);
++rows;
}
if (row_count) {
*row_count = rows;
}
return csv;
}
bool BomExport::writeCsv(const QString &file_path, const QByteArray &csv,
QString *error)
{
if (error) {
error->clear();
}
QSaveFile file(file_path);
if (!file.open(QIODevice::WriteOnly) || file.write(csv) != csv.size())
{
if (error) {
*error = file.errorString();
}
file.cancelWriting();
return false;
}
if (!file.commit())
{
if (error) {
*error = file.errorString();
}
return false;
}
return true;
}
+37
View File
@@ -0,0 +1,37 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BOMEXPORT_H
#define BOMEXPORT_H
#include <QByteArray>
#include <QString>
#include <QStringList>
class QSqlQuery;
namespace BomExport
{
QStringList defaultColumns();
QString defaultQuery();
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers = true, int *row_count = nullptr);
bool writeCsv(const QString &file_path, const QByteArray &csv,
QString *error = nullptr);
}
#endif // BOMEXPORT_H
+126 -54
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cli_export.h"
#include "bomexport.h"
#include "bordertitleblock.h"
#include "conductornumexport.h"
@@ -39,6 +40,7 @@
#include <QDomDocument>
#include <QDate>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
@@ -70,6 +72,7 @@ const QHash<QString, QString> &exportFlags()
{"--export-cables", "cables"},
{"--export-wires", "wires"},
{"--export-bom", "bom"},
{"--export-wiring", "wiring"},
{"--export-nets", "nets"},
{"--export-links", "links"},
{"--info", "info"},
@@ -109,7 +112,16 @@ 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
@@ -117,14 +129,21 @@ void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
// 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()) {
@@ -164,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):
@@ -214,7 +233,7 @@ int exportPdf(QETProject &project, const QString &output)
}
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()) {
@@ -237,14 +256,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";
@@ -284,61 +305,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)
{
@@ -525,6 +525,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.
@@ -768,13 +832,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;
}
}
@@ -818,11 +888,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")
@@ -831,7 +903,7 @@ int run(const QStringList &args)
return resaveProject(project, output);
if (format == "settb")
return setTitleBlock(project, output, rest.mid(2));
return exportImages(project, format, output);
return exportImages(project, format, output, showTerminals);
}
} // namespace CLIExport
+13 -3
View File
@@ -42,12 +42,13 @@ namespace CLIExport {
@return process exit code (0 on success).
Usage:
qelectrotech --export-pdf <project.qet> <output.pdf>
qelectrotech --export-png <project.qet> <output_dir>
qelectrotech --export-svg <project.qet> <output_dir>
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --export-wiring <project.qet> <output.csv>
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
qelectrotech --info <project.qet> [output.json]
@@ -57,9 +58,18 @@ namespace CLIExport {
PDF: one multi-page document (one diagram per page).
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
--show-terminals: also paint terminal markers (red stroke + blue
docking dot) and terminal names, as the interactive editor
does; off by default, matching the GUI export dialog's
default. Has no effect on the non-image export modes.
cables: wiring list (one row per conductor) as CSV.
wires: list of distinct wire numbers as CSV.
bom: bill of materials (one row per element) as CSV.
wiring: from-to wiring list (one row per conductor) as CSV, read from
the project database. Same logical list as `cables`, which
reads the document XML instead; the two are meant to agree,
so diffing them checks that the database still describes the
project.
nets: electrical nets (connected-terminal groups) as JSON.
links: element cross-references (coil/contact) as CSV, with
unresolved links flagged.
+9 -9
View File
@@ -124,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;
+91
View File
@@ -0,0 +1,91 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTUSAGE_H
#define CONTACTUSAGE_H
#include <algorithm>
/**
@brief The ContactUsage struct
How many slave contacts a master element currently uses, broken down
by contact type.
Header-only and free of any graphics dependency so that the counting
rules can be unit tested on their own. MasterElement::contactUsage()
is the thin wrapper that feeds it the linked elements.
This counts contacts, which is what tells you how many contacts an
auxiliary block must provide. It is deliberately not the count that
MasterElement::isFull() uses: a master's max_slaves is a number of
slots, and a slave fills exactly one slot however many contacts it
carries.
Two rules are easy to get wrong, and both live here so that every
caller gets them right:
- a slave stands for as many contacts as its "number" kind
information says, so a 4 pole contact counts as 4, not as 1
- a changeover contact is counted once, as sw. CrossRefItem's
NOElements() and NCElements() both return it, so adding those two
lists together would count it twice.
*/
struct ContactUsage
{
/**
Contact types a slave can declare. Mirrors
ElementData::SlaveState, which is not used directly so that this
header stays free of the element data dependencies and can be
unit tested on its own. MasterElement::contactUsage() maps
between the two.
*/
enum Type
{
NO, ///< Normally open
NC, ///< Normally closed
SW, ///< Changeover
Other ///< Neither of the above
};
int no = 0; ///< Normally open
int nc = 0; ///< Normally closed
int sw = 0; ///< Changeover
int other = 0; ///< Neither of the above
int total() const { return no + nc + sw + other; }
/**
Add one slave element to the tally.
@param type the contact type the slave declares
@param contacts how many contacts it stands for. Values below 1
are treated as 1: an element which declares no
contact count is still one contact.
*/
void addSlave(Type type, int contacts)
{
const int n = std::max(1, contacts);
switch (type)
{
case NO: no += n; break;
case NC: nc += n; break;
case SW: sw += n; break;
case Other: other += n; break;
}
}
};
#endif // CONTACTUSAGE_H
+299 -36
View File
@@ -53,6 +53,7 @@ projectDataBase::projectDataBase(QETProject *project, QObject *parent) :
});
connect(m_project, &QETProject::projectDiagramsOrderChanged, [this]()
{
m_content_changed = true;
for (auto diagram : m_project->diagrams())
{
m_diagram_order_changed.bindValue(":pos", m_project->folioIndex(diagram)+1);
@@ -85,14 +86,42 @@ projectDataBase::~projectDataBase()
*/
void projectDataBase::updateDB()
{
//A bulk operation is in progress and updates the database itself once
//it is done : rebuilding now would only be thrown away by that final
//rebuild. @see setUpdateBlocked().
if (m_update_blocked) {
return;
}
//Nothing in the project has changed since the last rebuild, so
//repopulating would insert exactly the rows that are already there.
//The signal is still emitted : callers and models rely on it to
//refresh, and what they read back is unchanged either way.
if (!m_content_changed)
{
emit dataBaseUpdated();
return;
}
populateDiagramTable();
populateDiagramInfoTable();
populateElementTable();
populateElementInfoTable();
populateConductorTable();
m_content_changed = false;
emit dataBaseUpdated();
}
/**
@brief projectDataBase::setUpdateBlocked
@param blocked : whether updateDB() should skip the full rebuild
*/
void projectDataBase::setUpdateBlocked(bool blocked)
{
m_update_blocked = blocked;
}
/**
@brief projectDataBase::project
@return the project of this database
@@ -111,35 +140,65 @@ QSqlQuery projectDataBase::newQuery(const QString &query) {
return QSqlQuery(query, m_data_base);
}
/**
@brief projectDataBase::excludedConductorCount
@return how many conductors of the project are absent from the conductor
table because an endpoint has no parent element to key on.
Counted from the live scene rather than from the database, precisely
because the database is where these conductors are *not*.
This used to count conductors whose terminals had no uuid, which was most
of them on most projects. Terminal::stableUuid() now derives an identity
from the terminal's geometry when the definition provides no uuid, so that
is no longer a reason to exclude anything, and this counts only the case
that remains genuinely unkeyable.
This is what lets a caller tell the user "N wires are missing and here
is why", instead of silently presenting a short list as if it were
complete.
*/
int projectDataBase::excludedConductorCount() const
{
if (!m_project) {
return 0;
}
int count = 0;
for (auto *diagram : m_project->diagrams())
{
const auto conductor_list = diagram->conductors();
for (auto *conductor : conductor_list)
{
//Must match addConductor()'s guard exactly, or this reports
//wires as missing that the list is in fact showing.
if (!conductor->terminal1->parentElement()
|| !conductor->terminal2->parentElement()) {
++count;
}
}
}
return count;
}
/**
@brief projectDataBase::addElement
@param element
*/
void projectDataBase::addElement(Element *element)
{
m_content_changed = true;
if (!element || !element->diagram()) {
qDebug() << "projectDataBase::addElement: null element or diagram";
return;
}
m_insert_elements_query.bindValue(":uuid", element->uuid().toString());
m_insert_elements_query.bindValue(":diagram_uuid", element->diagram()->uuid().toString());
m_insert_elements_query.bindValue(":pos", element->diagram()->convertPosition(element->scenePos()).toString());
m_insert_elements_query.bindValue(":type", element->elementData().typeToString());
m_insert_elements_query.bindValue(":sub_type", element->kindInformations()["type"].toString());
bindElementValues(m_insert_elements_query, element, element->diagram());
if (!m_insert_elements_query.exec()) {
qDebug() << "projectDataBase::addElement insert element error : " << m_insert_elements_query.lastError();
}
m_insert_element_info_query.bindValue(":uuid", element->uuid().toString());
auto hash = elementInfoToString(element);
for (auto key : hash.keys())
{
QString value = hash.value(key);
QString bind = key.prepend(":");
m_insert_element_info_query.bindValue(bind, value);
}
bindElementInfoValues(m_insert_element_info_query, element);
if (!m_insert_element_info_query.exec()) {
qDebug() << "projectDataBase::addElement insert element info error : " << m_insert_element_info_query.lastError();
} else {
@@ -153,6 +212,7 @@ void projectDataBase::addElement(Element *element)
*/
void projectDataBase::removeElement(Element *element)
{
m_content_changed = true;
m_remove_element_query.bindValue(":uuid", element->uuid().toString());
if(!m_remove_element_query.exec()) {
qDebug() << "projectDataBase::removeElement remove error : " << m_remove_element_query.lastError();
@@ -167,6 +227,7 @@ void projectDataBase::removeElement(Element *element)
*/
void projectDataBase::elementInfoChanged(Element *element)
{
m_content_changed = true;
auto hash = elementInfoToString(element);
for (auto str : QETInformation::elementInfoKeys()) {
m_update_element_query.bindValue(":" + str, hash.value(str));
@@ -181,6 +242,7 @@ void projectDataBase::elementInfoChanged(Element *element)
void projectDataBase::elementInfoChanged(QList<Element *> elements)
{
m_content_changed = true;
this->blockSignals(true);
//Block signal for not emit dataBaseUpdated at
//each call of the method elementInfoChanged(Element *element)
@@ -197,6 +259,7 @@ void projectDataBase::elementInfoChanged(QList<Element *> elements)
void projectDataBase::addDiagram(Diagram *diagram)
{
m_content_changed = true;
m_insert_diagram_query.bindValue(":uuid", diagram->uuid().toString());
m_insert_diagram_query.bindValue(":pos", m_project->folioIndex(diagram)+1);
if(!m_insert_diagram_query.exec()) {
@@ -225,16 +288,63 @@ void projectDataBase::addDiagram(Diagram *diagram)
void projectDataBase::removeDiagram(Diagram *diagram)
{
m_remove_diagram_query.bindValue(":uuid", diagram->uuid().toString());
m_content_changed = true;
const QString uuid_str = diagram->uuid().toString();
//Order matters: element_info and terminal are scoped through a
//subquery on element, so they must run before element itself is
//deleted below. The whole cascade runs in one transaction and is
//rolled back on the first error, so a mid-cascade failure (e.g. a
//locked DB) can't leave the diagram row deleted while its
//element/terminal/element_info/conductor rows survive.
m_data_base.transaction();
m_cascade_remove_element_info_query.bindValue(":uuid", uuid_str);
if (!m_cascade_remove_element_info_query.exec()) {
qDebug() << "projectDataBase::removeDiagram element_info cascade error : "
<< m_cascade_remove_element_info_query.lastError();
m_data_base.rollback();
return;
}
m_cascade_remove_terminal_query.bindValue(":uuid", uuid_str);
if (!m_cascade_remove_terminal_query.exec()) {
qDebug() << "projectDataBase::removeDiagram terminal cascade error : "
<< m_cascade_remove_terminal_query.lastError();
m_data_base.rollback();
return;
}
m_cascade_remove_conductor_query.bindValue(":uuid", uuid_str);
if (!m_cascade_remove_conductor_query.exec()) {
qDebug() << "projectDataBase::removeDiagram conductor cascade error : "
<< m_cascade_remove_conductor_query.lastError();
m_data_base.rollback();
return;
}
m_cascade_remove_element_query.bindValue(":uuid", uuid_str);
if (!m_cascade_remove_element_query.exec()) {
qDebug() << "projectDataBase::removeDiagram element cascade error : "
<< m_cascade_remove_element_query.lastError();
m_data_base.rollback();
return;
}
m_remove_diagram_query.bindValue(":uuid", uuid_str);
if (!m_remove_diagram_query.exec()) {
qDebug() << "projectDataBase::removeDiagram delete error : " << m_remove_diagram_query.lastError();
} else {
emit dataBaseUpdated();
m_data_base.rollback();
return;
}
m_data_base.commit();
emit dataBaseUpdated();
}
void projectDataBase::diagramInfoChanged(Diagram *diagram)
{
m_content_changed = true;
bindDiagramInfoValues(m_update_diagram_info_query, diagram);
if (!m_update_diagram_info_query.exec()) {
@@ -246,6 +356,7 @@ void projectDataBase::diagramInfoChanged(Diagram *diagram)
void projectDataBase::diagramOrderChanged()
{
m_content_changed = true;
}
/**
@@ -254,6 +365,7 @@ void projectDataBase::diagramOrderChanged()
*/
void projectDataBase::addConductor(Conductor *conductor)
{
m_content_changed = true;
if (!conductor || !conductor->diagram()) {
qDebug() << "projectDataBase::addConductor: null conductor or diagram";
return;
@@ -287,6 +399,7 @@ void projectDataBase::addConductor(Conductor *conductor)
*/
void projectDataBase::removeConductor(Conductor *conductor)
{
m_content_changed = true;
m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString());
if (!m_remove_conductor_query.exec()) {
qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError();
@@ -307,6 +420,7 @@ void projectDataBase::removeConductor(Conductor *conductor)
*/
void projectDataBase::updateConductor(Conductor *conductor)
{
m_content_changed = true;
if (!conductor) {
return;
}
@@ -349,6 +463,7 @@ void projectDataBase::watchConductor(Conductor *conductor)
*/
void projectDataBase::conductorPropertiesChanged()
{
m_content_changed = true;
if (auto *conductor = qobject_cast<Conductor *>(sender())) {
updateConductor(conductor);
}
@@ -505,6 +620,7 @@ bool projectDataBase::createDataBase()
createElementNomenclatureView();
createSummaryView();
createWiringListView();
prepareQuery();
updateDB();
return true;
@@ -525,6 +641,11 @@ void projectDataBase::createElementNomenclatureView()
"ei.designation AS designation,"
"ei.manufacturer AS manufacturer,"
"ei.manufacturer_reference AS manufacturer_reference,"
"ei.model AS model,"
"ei.category AS category,"
"ei.voltage_rating AS voltage_rating,"
"ei.current_rating AS current_rating,"
"ei.notes AS notes,"
"ei.machine_manufacturer_reference AS machine_manufacturer_reference,"
"ei.supplier AS supplier,"
"ei.quantity AS quantity,"
@@ -583,7 +704,14 @@ void projectDataBase::createElementNomenclatureView()
"di.folio AS folio,"
"e.pos AS position "
" FROM element_info ei, diagram_info di, element e, diagram d"
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid AND (ei.exclude_from_bom IS NOT 'true')");
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid"
" AND COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1', 'yes', 'on')"
//The element table holds every element of the project; which
//kinds belong in a nomenclature is this view's business, not
//the table's. Kept identical to the mask populateElementTable()
//used to apply, so what this view returns does not change --
//a slave element (a relay contact) is still not a line item.
" AND e.type IN ('simple', 'terminal', 'master', 'thumbnail')");
QSqlQuery query(m_data_base);
if (!query.exec(create_view)) {
@@ -622,6 +750,64 @@ void projectDataBase::createSummaryView()
}
}
/**
@brief projectDataBase::createWiringListView
A from-to wiring list: one row per conductor, each endpoint resolved to
its element label and terminal name.
Two deliberate differences from an ordinary inner-join view like
element_nomenclature_view:
- No join to the element table. A terminal row already carries its
element_uuid, so joining element back just to read the same uuid adds
nothing -- and would actively drop rows, because populateElementTable()
only inserts elements matching Simple|Terminal|Master|Thumbnail. Slave
elements (relay contacts and the like, extremely common at the end of a
wire) and report elements are absent from that table after a project
load, so an inner join through it silently loses their conductors.
- element_info is LEFT joined for the same reason. A wire whose endpoint
element carries no info row still belongs in a wiring list; it comes
back with an empty label rather than vanishing. Losing a wire from a
wiring list is a worse failure than showing one with a blank end.
- diagram is LEFT joined for the same reason. It should
always match, since QETProject::diagramAdded is wired to addDiagram()
and a conductor cannot exist before its folio -- but an inner join here
would make that an assumption the view silently enforces, and a wire
missing from a wiring list is the one failure this view must not have.
The result is that this view returns exactly as many rows as the
conductor table holds -- what is already excluded upstream (conductors
on legacy terminals without uuids) stays excluded, and nothing new is
dropped here. Only the terminal joins are inner, and both are guaranteed
by insertTerminal() running for each endpoint before the conductor row
is written.
*/
void projectDataBase::createWiringListView()
{
QString create_view ("CREATE VIEW wiring_list_view AS SELECT "
"c.uuid AS conductor_uuid,"
"c.text AS wire_number,"
"t1.element_uuid AS from_element_uuid,"
"ei1.label AS from_element_label,"
"t1.name AS from_terminal,"
"t2.element_uuid AS to_element_uuid,"
"ei2.label AS to_element_label,"
"t2.name AS to_terminal,"
"d.pos AS diagram_position"
" FROM conductor c"
" JOIN terminal t1 ON c.terminal1_uuid = t1.uuid AND c.terminal1_element_uuid = t1.element_uuid"
" JOIN terminal t2 ON c.terminal2_uuid = t2.uuid AND c.terminal2_element_uuid = t2.element_uuid"
" LEFT JOIN element_info ei1 ON t1.element_uuid = ei1.element_uuid"
" LEFT JOIN element_info ei2 ON t2.element_uuid = ei2.element_uuid"
" LEFT JOIN diagram d ON c.diagram_uuid = d.uuid");
QSqlQuery query(m_data_base);
if (!query.exec(create_view)) {
qDebug() << query.lastError();
}
}
void projectDataBase::populateDiagramTable()
{
QSqlQuery query_(m_data_base);
@@ -637,6 +823,30 @@ void projectDataBase::populateDiagramTable()
}
}
/**
@brief allElementTypes
Every ElementData::Type, i.e. no filtering at all.
The element table used to be populated with only
Simple|Terminal|Master|Thumbnail, which quietly made it "the elements a
nomenclature cares about" rather than "the elements of the project".
Anything else reading the table -- the wiring list, and terminal plans
later -- then could not see slave elements (relay contacts) or report
elements, which are ordinary conductor endpoints. The filter now lives in
element_nomenclature_view, where it belongs; see createElementNomenclatureView().
*/
static ElementData::Types allElementTypes()
{
return ElementData::Simple
| ElementData::NextReport
| ElementData::PreviousReport
| ElementData::Master
| ElementData::Slave
| ElementData::Terminal
| ElementData::Thumbnail
| ElementData::ConductorDefinition;
}
/**
@brief projectDataBase::populateElementTable
Populate the element table
@@ -649,16 +859,11 @@ void projectDataBase::populateElementTable()
for (auto diagram : m_project->diagrams())
{
const ElementProvider ep(diagram);
const auto elmt_vector = ep.find(ElementData::Simple | ElementData::Terminal | ElementData::Master | ElementData::Thumbnail);
const auto elmt_vector = ep.find(allElementTypes());
//Insert all values into the database
for (const auto &elmt : elmt_vector)
{
const auto elmt_data = elmt->elementData();
m_insert_elements_query.bindValue(":uuid", elmt->uuid().toString());
m_insert_elements_query.bindValue(":diagram_uuid", diagram->uuid().toString());
m_insert_elements_query.bindValue(":pos", diagram->convertPosition(elmt->scenePos()).toString());
m_insert_elements_query.bindValue(":type", elmt_data.typeToString());
m_insert_elements_query.bindValue(":sub_type", elmt_data.masterTypeToString());
bindElementValues(m_insert_elements_query, elmt, diagram);
if (!m_insert_elements_query.exec()) {
qDebug() << "projectDataBase::populateElementTable insert error : " << m_insert_elements_query.lastError();
}
@@ -678,20 +883,12 @@ void projectDataBase::populateElementInfoTable()
for (const auto &diagram : m_project->diagrams())
{
const ElementProvider ep(diagram);
const auto elmt_vector = ep.find(ElementData::Simple | ElementData::Terminal | ElementData::Master | ElementData::Thumbnail);
const auto elmt_vector = ep.find(allElementTypes());
//Insert all values into the database
for (const auto &elmt : elmt_vector)
{
m_insert_element_info_query.bindValue(QStringLiteral(":uuid"), elmt->uuid().toString());
const auto hash = elementInfoToString(elmt);
for (const auto &key : hash.keys())
{
QString value = hash.value(key);
QString bind = QStringLiteral(":") + key;
m_insert_element_info_query.bindValue(bind, value);
}
bindElementInfoValues(m_insert_element_info_query, elmt);
if (!m_insert_element_info_query.exec()) {
qDebug() << "projectDataBase::populateElementInfoTable insert error : " << m_insert_element_info_query.lastError();
}
@@ -772,7 +969,27 @@ void projectDataBase::prepareQuery()
m_insert_diagram_query = QSqlQuery(m_data_base);
m_insert_diagram_query.prepare("INSERT INTO diagram (uuid, pos) VALUES (:uuid, :pos)");
//REMOVE DIAGRAM
//REMOVE DIAGRAM (cascade first: element_info and terminal have no
//diagram_uuid column of their own, so both are scoped through
//element while the element rows for this diagram still exist).
m_cascade_remove_element_info_query = QSqlQuery(m_data_base);
m_cascade_remove_element_info_query.prepare(
"DELETE FROM element_info WHERE element_uuid IN "
"(SELECT uuid FROM element WHERE diagram_uuid = :uuid)");
m_cascade_remove_terminal_query = QSqlQuery(m_data_base);
m_cascade_remove_terminal_query.prepare(
"DELETE FROM terminal WHERE element_uuid IN "
"(SELECT uuid FROM element WHERE diagram_uuid = :uuid)");
m_cascade_remove_conductor_query = QSqlQuery(m_data_base);
m_cascade_remove_conductor_query.prepare(
"DELETE FROM conductor WHERE diagram_uuid = :uuid");
m_cascade_remove_element_query = QSqlQuery(m_data_base);
m_cascade_remove_element_query.prepare(
"DELETE FROM element WHERE diagram_uuid = :uuid");
m_remove_diagram_query = QSqlQuery(m_data_base);
m_remove_diagram_query.prepare("DELETE FROM diagram WHERE uuid=:uuid");
@@ -878,6 +1095,52 @@ QHash<QString, QString> projectDataBase::elementInfoToString(Element *elmt)
return hash;
}
/**
@brief projectDataBase::bindElementValues
Bind one element's row for the element table.
Shared by addElement() (a single element added to a live diagram) and
populateElementTable() (a full rebuild), because those two used to bind
the same row differently: the incremental path wrote
kindInformations()["type"] into sub_type while the bulk path wrote
elementData().masterTypeToString(). The element table therefore held
different values depending on whether the project had been reloaded
since the element was placed. One binder means live and reloaded agree
by construction rather than by coincidence.
The bulk path's values are the ones kept: they are what every already
saved project contains, so nothing a reload produces changes.
@param query : prepared insert query to bind into
@param element : element to bind
@param diagram : diagram holding @element
*/
void projectDataBase::bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram)
{
const auto element_data = element->elementData();
query.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString());
query.bindValue(QStringLiteral(":pos"), diagram->convertPosition(element->scenePos()).toString());
query.bindValue(QStringLiteral(":type"), element_data.typeToString());
query.bindValue(QStringLiteral(":sub_type"), element_data.masterTypeToString());
}
/**
@brief projectDataBase::bindElementInfoValues
Bind one element's row for the element info table.
Shared by addElement() and populateElementInfoTable() for the same
reason as bindElementValues().
@param query : prepared insert query to bind into
@param element : element to bind
*/
void projectDataBase::bindElementInfoValues(QSqlQuery &query, Element *element)
{
query.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
const auto hash = elementInfoToString(element);
for (const auto &key : hash.keys()) {
query.bindValue(QStringLiteral(":") + key, hash.value(key));
}
}
void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram)
{
query.bindValue(":uuid", diagram->uuid());
+27 -1
View File
@@ -47,8 +47,21 @@ 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());
QSqlDatabase database() const {return m_data_base;}
int excludedConductorCount() const;
void addElement (Element *element);
void removeElement (Element *element);
@@ -77,6 +90,7 @@ class projectDataBase : public QObject
bool createDataBase();
void createElementNomenclatureView();
void createSummaryView();
void createWiringListView();
void populateDiagramTable();
void populateElementTable();
void populateElementInfoTable();
@@ -89,9 +103,17 @@ class projectDataBase : public QObject
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,
@@ -106,7 +128,11 @@ class projectDataBase : public QObject
m_insert_terminal_query,
m_insert_conductor_query,
m_update_conductor_query,
m_remove_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:
+25 -13
View File
@@ -160,6 +160,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);
@@ -170,23 +172,23 @@ 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) {
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 (str_type.contains(ElementData::masterTypeToString(ElementData::PLC)) ? true : false);
ui->m_plc_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::PLC)));
if (ui->m_plc_cb->isChecked()) {
++c;
}
@@ -352,22 +354,32 @@ 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 (ui->m_plc_cb->isChecked()) {
if (!all_master_types && ui->m_plc_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
b = true;
@@ -378,10 +390,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.
+58
View File
@@ -40,9 +40,53 @@
#include "undocommand/addelementtextcommand.h"
#include "qetinformation.h"
#include "qetproject.h"
#include "diagramsortkeys.h"
#include <algorithm>
#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)
{
return positionKey(elmt->pos());
}
/// 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);
}
}
int Diagram::xGrid = 10;
int Diagram::yGrid = 10;
int Diagram::xKeyGrid = 10;
@@ -1014,6 +1058,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;
+139 -21
View File
@@ -19,10 +19,14 @@
#include "diagrameventaddimage.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../diagram.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../qetgraphicsitem/diagramimageitem.h"
#include <QStatusBar>
#include <QTimer>
/**
@brief DiagramEventAddImage::DiagramEventAddImage
Default constructor
@@ -34,6 +38,16 @@ DiagramEventAddImage::DiagramEventAddImage(Diagram *diagram) :
m_is_added (false)
{
openDialog();
if (m_running)
{
// Deferred for the same reason as the shape tools' own
// constructor-time hint: Diagram::setEventInterface() destroys
// whatever tool was previously active *after* this constructor
// returns, and that tool's own destructor clears the status bar
// -- an immediate show here would just get wiped out moments
// later by that cleanup.
QTimer::singleShot(0, this, [this]() { showHint(); });
}
}
/**
@@ -47,33 +61,61 @@ DiagramEventAddImage::~DiagramEventAddImage()
delete m_image;
}
if (!m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
foreach (QGraphicsView *view, m_diagram->views())
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
/**
@brief DiagramEventAddImage::showHint
Re-asserted on every move (see mouseMoveEvent), not just once at
activation: Qt's own built-in "show an action's statusTip on hover"
has its own internal "restore whatever was there before" logic for
when the hover ends. Since this message is first shown *during* that
same hover session (the user is still over the toolbar icon when the
deferred constructor-time call above fires), Qt's hover-tracking has
no idea this code changed the status bar in the meantime -- the
moment the mouse leaves the icon for the canvas, it silently
restores whatever it remembers being there before its own tip
started, overwriting this one. Re-showing it on every move within
the canvas simply outlasts that one-time restore -- the exact same
issue already found and fixed for the shape tools.
*/
void DiagramEventAddImage::showHint() const
{
if (m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(tr("Clic : positionner à la taille d'origine. "
"Cliquer-glisser : positionner et redimensionner. "
"Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille."));
}
/**
@brief DiagramEventAddImage::mousePressEvent
Action when mouse is pressed
Left button: starts a potential drag-to-resize, anchored here -- but
doesn't commit to anything yet. A quick click-release (see
mouseMoveEvent's threshold check) still places the image at its
original size, matching the previous behavior exactly; only an
actual drag switches to resizing. Right button still rotates in 90
degree steps, unchanged, and only while not already left-dragging.
@param event : event of mouse pressed
*/
void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (m_image && event -> button() == Qt::LeftButton)
if (m_image && event->button() == Qt::LeftButton)
{
QPointF pos = event->scenePos();
pos.rx() -= m_image->boundingRect().width()/2;
pos.ry() -= m_image->boundingRect().height()/2;
m_diagram -> undoStack().push (new AddGraphicsObjectCommand(m_image, m_diagram, pos));
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
m_running = false;
emit finish();
m_pressed = true;
m_resize_engaged = false;
m_press_pos = event->scenePos();
event->setAccepted(true);
}
else if (m_image && event -> button() == Qt::RightButton)
else if (m_image && !m_pressed && event->button() == Qt::RightButton)
{
m_image->setRotation(m_image->rotation() + 90);
event->setAccepted(true);
@@ -87,26 +129,95 @@ void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
*/
void DiagramEventAddImage::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_image || event->buttons() != Qt::NoButton) {
if (!m_image) {
return;
}
showHint();
QPointF pos = event->scenePos();
if (!m_is_added)
{
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::NoContextMenu));
}
m_diagram->addItem(m_image);
m_is_added = true;
}
m_image->setPos(pos - m_image->boundingRect().center());
if (m_pressed)
{
// Anchored on m_press_pos, not the item's own current position:
// dragging in any direction has to visibly grow the image from
// where the click started, not from wherever the "no button
// held" preview phase happened to leave it centered.
const QPointF delta = pos - m_press_pos;
if (!m_resize_engaged && QLineF(m_press_pos, pos).length() >= 4.0)
m_resize_engaged = true; // latched: crossing back within the threshold afterward must not un-engage it
if (!m_resize_engaged)
{
// Still just a (so far) plain click -- keep behaving like
// the pre-drag preview: original size, centered here, so
// releasing right now reproduces the old click-to-place
// behavior exactly.
m_image->setPos(m_press_pos - m_image->boundingRect().center());
}
else
{
const QSizeF naturalSize = m_image->boundingRect().size();
if (naturalSize.width() > 0 && naturalSize.height() > 0)
{
const qreal scaleX = qAbs(delta.x()) / naturalSize.width();
const qreal scaleY = qAbs(delta.y()) / naturalSize.height();
// The larger of the two, not a per-axis stretch: images
// only support a single uniform scale today (see
// boundingRect()/paint(), which never touch aspect
// ratio), so this is a diagonal-drag size, not a
// free-form one -- breaking aspect ratio on purpose is
// its own, separate, larger piece of work.
const qreal newScale = qBound(0.01, qMax(scaleX, scaleY), 50.0);
m_image->setScale(newScale);
}
m_image->setPos(qMin(m_press_pos.x(), pos.x()), qMin(m_press_pos.y(), pos.y()));
}
}
else
{
m_image->setPos(pos - m_image->boundingRect().center());
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddImage::mouseReleaseEvent
Left button release commits whatever mouseMoveEvent last set --
original size and centered if the press never turned into a real
drag, or the dragged-out size and position otherwise. Either way,
this is the only place placement is actually finalized now; a plain
click no longer finishes inside mousePressEvent itself, since it has
to wait and see whether a drag follows.
@param event : event of mouse release
*/
void DiagramEventAddImage::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (m_image && m_pressed && event->button() == Qt::LeftButton)
{
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_image, m_diagram, m_image->pos()));
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
m_running = false;
emit finish();
event->setAccepted(true);
}
}
/**
@brief DiagramEventAddImage::mouseDoubleClickEvent
This method is used only to overwrite double click.
@@ -124,7 +235,14 @@ void DiagramEventAddImage::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
*/
void DiagramEventAddImage::wheelEvent(QGraphicsSceneWheelEvent *event)
{
if (!m_is_added || !m_image || event -> modifiers() != Qt::CTRL) {
// !m_pressed added alongside the modifier fix: without it, wheel
// scaling could fight with an active drag-resize, both trying to
// set scale() from different sources in the same gesture.
// event->modifiers() & Qt::ControlModifier, not != Qt::CTRL: the
// same exact-equality bug already found and fixed several times
// this session elsewhere -- Ctrl held together with any other
// modifier would silently fail to register as Ctrl at all.
if (!m_is_added || !m_image || m_pressed || !(event->modifiers() & Qt::ControlModifier)) {
return;
}
@@ -20,6 +20,8 @@
#include "diagrameventinterface.h"
#include <QPointF>
class Diagram;
class DiagramImageItem;
@@ -37,15 +39,20 @@ class DiagramEventAddImage : public DiagramEventInterface
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
bool isNull () const;
private:
void openDialog();
void showHint() const;
DiagramImageItem *m_image;
bool m_is_added;
bool m_pressed = false; // left button held: dragging out a size, not just positioning
bool m_resize_engaged = false; // latched once the drag threshold is crossed, matching the pen tool's own curve-drag threshold convention -- so dragging out and back near the start point doesn't "snap back" to original size before release
QPointF m_press_pos; // scene position of the left-button press, the resize anchor
};
#endif // DIAGRAMEVENTADDIMAGE_H
@@ -0,0 +1,427 @@
/*
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 "diagrameventaddpath.h"
#include "../diagram.h"
#include "../lastusedstyle.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include <QGraphicsLineItem>
#include <QGraphicsSceneMouseEvent>
#include <QKeyEvent>
#include <QLineF>
#include <QStatusBar>
#include <QTimer>
/**
@brief DiagramEventAddPath::DiagramEventAddPath
@param diagram : the diagram where this event must operate
*/
DiagramEventAddPath::DiagramEventAddPath(Diagram *diagram) :
DiagramEventInterface(diagram),
m_shape_item (nullptr),
m_help_horiz (nullptr),
m_help_verti (nullptr)
{
m_running = true;
init();
// Deferred for the same reason as DiagramEventAddShape's own
// constructor-time hint: Diagram::setEventInterface() destroys
// whatever tool was previously active *after* this constructor
// returns, and that tool's own destructor clears the status bar --
// an immediate show here would just get wiped out moments later.
QTimer::singleShot(0, this, [this]() { showHint(); });
}
DiagramEventAddPath::~DiagramEventAddPath()
{
if ((m_running || m_abort) && m_shape_item)
{
m_diagram->removeItem(m_shape_item);
delete m_shape_item;
}
delete m_help_horiz;
delete m_help_verti;
if (m_diagram && !m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
foreach (QGraphicsView *v, m_diagram->views())
v->setContextMenuPolicy(Qt::DefaultContextMenu);
}
/**
@brief DiagramEventAddPath::showHint
Re-asserted on every move within the canvas (see mouseMoveEvent), not
just once at activation: Qt's own built-in "show an action's
statusTip on hover" has its own internal "restore whatever was there
before" logic for when the hover ends. Since this message is first
shown *during* that same hover session (the user is still over the
toolbar icon when the deferred constructor-time call above fires),
Qt's hover-tracking has no idea this code changed the status bar in
the meantime -- the moment the mouse leaves the icon for the canvas,
it silently restores whatever it remembers being there before its
own tip started, overwriting this one. Re-showing it on every move
within the canvas simply outlasts that one-time restore.
*/
void DiagramEventAddPath::showHint() const
{
if (!m_diagram || m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(tr("Clic: point anguleux. Cliquer-glisser: point courbe. "
"Clic sur le premier point: fermer. Échap/Entrée: terminer. "
"Clic droit: annuler le dernier point."));
}
void DiagramEventAddPath::init()
{
foreach (QGraphicsView *v, m_diagram->views())
v->setContextMenuPolicy(Qt::NoContextMenu);
}
QPointF DiagramEventAddPath::snapped(const QPointF &scenePos, Qt::KeyboardModifiers mods) const
{
return mods == Qt::ControlModifier ? scenePos : Diagram::snapToGrid(scenePos);
}
int DiagramEventAddPath::confirmedNodeCount() const
{
// The trailing element is always the live preview while m_shape_item
// exists; with no shape yet there are no nodes of any kind.
return m_shape_item ? qMax(0, m_nodes.size() - 1) : 0;
}
/**
@brief DiagramEventAddPath::mousePressEvent
Left click: on the very first click, creates the shape with a real
node *and* an immediate preview node at the same spot, so a segment
exists (even if zero-length) from the start rather than requiring a
second click before anything is visible. On later clicks: either
confirms the live preview into a real point and appends a fresh one
for the next segment, or -- if close enough to the first node --
closes the path.
*/
void DiagramEventAddPath::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (Q_UNLIKELY(m_diagram->isReadOnly()))
return;
if (event->button() != Qt::LeftButton)
{
// Accept every button while this tool is running, not just the
// one it actually acts on -- Diagram::mousePressEvent falls
// through to Qt's own default scene handling for anything left
// unaccepted, which is exactly the kind of competing control
// this tool can't afford while it's supposed to have exclusive
// ownership of input.
event->setAccepted(true);
return;
}
const QPointF pos = snapped(event->scenePos(), event->modifiers());
if (!m_shape_item)
{
m_shape_item = new QetShapeItem(pos, pos, QetShapeItem::Path);
if (LastUsedStyle::hasShapePen())
m_shape_item->setPen(LastUsedStyle::shapePen());
if (LastUsedStyle::hasShapeBrush())
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
m_diagram->addItem(m_shape_item);
// Handles only ever get built for a selected item.
m_shape_item->setSelected(true);
QetShapeItem::PathNode node;
node.anchor = pos;
m_nodes << node;
m_nodes << node; // live preview, tracks the mouse from here on
m_shape_item->setPathNodes(m_nodes);
m_shape_item->enableNodeEditMode();
m_dragging_node = 0;
event->setAccepted(true);
return;
}
if (confirmedNodeCount() >= 2 && nearFirstNode(pos))
{
finishPath(true);
event->setAccepted(true);
return;
}
// Confirm the preview node as a real point, then append a fresh
// preview (a plain Corner, not a copy of the just-confirmed node's
// kind/handles) for the segment after it.
m_dragging_node = m_nodes.size() - 1;
m_nodes[m_dragging_node].anchor = pos;
QetShapeItem::PathNode preview;
preview.anchor = pos;
m_nodes << preview;
m_shape_item->setPathNodes(m_nodes);
m_shape_item->enableNodeEditMode();
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseMoveEvent
Two mutually exclusive behaviours, matching whether a button is held:
with the left button down on a just-placed node, dragging shapes that
node's handles (same convention as editing an existing node -- see
QetShapeItem::dragPathControlHandle()). With no button held, the
trailing preview node instead tracks the mouse, giving the live
rubber-band segment.
*/
void DiagramEventAddPath::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
updateHelpCross(event->scenePos());
showHint();
if (m_shape_item)
{
const QPointF pos = snapped(event->scenePos(), event->modifiers());
if (m_dragging_node >= 0 && (event->buttons() & Qt::LeftButton))
{
QetShapeItem::PathNode &node = m_nodes[m_dragging_node];
const QPointF delta = pos - node.anchor;
// A small threshold so an accidental few-pixel wobble on
// what was meant to be a plain click doesn't silently add
// curve handles the user never intended.
if (QLineF(QPointF(), delta).length() > 3.0)
{
node.kind = QetShapeItem::NodeKind::Smooth;
node.outHandle = delta;
node.inHandle = -delta;
}
else
{
node.kind = QetShapeItem::NodeKind::Corner;
node.outHandle.reset();
node.inHandle.reset();
}
m_shape_item->setPathNodes(m_nodes);
}
else if (!(event->buttons() & Qt::LeftButton) && !m_nodes.isEmpty())
{
m_nodes.last().anchor = pos;
m_shape_item->setPathNodes(m_nodes);
}
}
// Ours unconditionally while running: a stray, unaccepted move event
// falling through to Qt's default handling risks it dragging our
// selected, movable in-progress shape out from under the tool.
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseReleaseEvent
Left release just ends the current node's drag phase (the trailing
preview resumes tracking the mouse on the next move). Right release
steps back one *confirmed* point (the preview is left alone), or
cancels outright once only one remains, or exits the tool entirely if
nothing is in progress at all.
*/
void DiagramEventAddPath::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
m_dragging_node = -1;
}
else if (event->button() == Qt::RightButton)
{
if (m_shape_item)
{
if (confirmedNodeCount() > 1)
{
m_nodes.remove(m_nodes.size() - 2); // the last *confirmed* node; keep the trailing preview
m_shape_item->setPathNodes(m_nodes);
}
else
{
cancelPath();
}
}
else
{
m_running = false;
emit finish();
}
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseDoubleClickEvent
A double-click is a press, release, press, release, doubleclick
sequence -- the second press already confirmed the preview into a
duplicate point (mousePressEvent can't distinguish a double-click
from two single clicks in the same place) and appended a fresh
preview after it. Dropping the last node here removes that fresh
preview; finishPath()'s own trailing-preview removal then removes the
duplicate underneath it, leaving only the genuinely-placed points.
*/
void DiagramEventAddPath::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
if (m_shape_item && event->button() == Qt::LeftButton && !m_nodes.isEmpty())
{
m_nodes.removeLast();
finishPath(false);
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::keyPressEvent
Escape or Enter finish the path open once at least two real points
exist; Escape with fewer (or none placed at all) cancels/exits
instead, since there's nothing meaningful to keep.
*/
void DiagramEventAddPath::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Escape)
{
if (m_shape_item && confirmedNodeCount() >= 2)
finishPath(false);
else if (m_shape_item)
cancelPath();
else
{
m_running = false;
emit finish();
}
event->accept();
}
else if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
&& m_shape_item && confirmedNodeCount() >= 2)
{
finishPath(false);
event->accept();
}
}
/**
@brief DiagramEventAddPath::finishPath
Strips the trailing live-preview node, commits the in-progress path
onto the undo stack, and resets so the tool is ready to draw another
one immediately -- matching every other shape tool's own behaviour
after finishing a shape.
*/
void DiagramEventAddPath::finishPath(bool closed)
{
if (!m_shape_item)
return;
if (!m_nodes.isEmpty())
m_nodes.removeLast();
if (m_nodes.size() < 2)
{
cancelPath();
return;
}
if (closed)
m_shape_item->setClosed(true);
m_shape_item->setPathNodes(m_nodes);
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_shape_item, m_diagram));
m_shape_item = nullptr;
m_nodes.clear();
m_dragging_node = -1;
}
/**
@brief DiagramEventAddPath::cancelPath
Discards the in-progress path entirely -- nothing worth keeping (an
empty or single-point path isn't a usable shape).
*/
void DiagramEventAddPath::cancelPath()
{
if (m_shape_item)
{
m_diagram->removeItem(m_shape_item);
delete m_shape_item;
m_shape_item = nullptr;
}
m_nodes.clear();
m_dragging_node = -1;
}
/**
@brief DiagramEventAddPath::nearFirstNode
m_shape_item's pos()/transform() stay at their identity defaults for
its entire construction here -- nothing during drawing ever touches
them -- so the first node's anchor, stored in local coordinates, is
directly comparable to a scene position without any mapping.
*/
bool DiagramEventAddPath::nearFirstNode(const QPointF &scenePos) const
{
if (m_nodes.isEmpty())
return false;
return QLineF(m_nodes.first().anchor, scenePos).length() <= CLOSE_THRESHOLD;
}
/**
@brief DiagramEventAddPath::updateHelpCross
Same crosshair guide as every other shape tool (see
DiagramEventAddShape::updateHelpCross) -- duplicated rather than
shared, since the two classes don't otherwise share a common base
beyond DiagramEventInterface.
*/
void DiagramEventAddPath::updateHelpCross(const QPointF &p)
{
if (!m_help_horiz || !m_help_verti)
{
QPen pen;
pen.setWidthF(0.4);
pen.setCosmetic(true);
pen.setColor(Diagram::background_color == Qt::darkGray ? Qt::lightGray : Qt::darkGray);
QRectF rect = m_diagram->border_and_titleblock.insideBorderRect();
if (!m_help_horiz)
{
m_help_horiz = new QGraphicsLineItem(rect.topLeft().x(), 0, rect.topRight().x(), 0);
m_help_horiz->setPen(pen);
m_diagram->addItem(m_help_horiz);
}
if (!m_help_verti)
{
m_help_verti = new QGraphicsLineItem(0, rect.topLeft().y(), 0, rect.bottomLeft().y());
m_help_verti->setPen(pen);
m_diagram->addItem(m_help_verti);
}
}
QPointF point = Diagram::snapToGrid(p);
m_help_horiz->setY(point.y());
m_help_verti->setX(point.x());
}
@@ -0,0 +1,81 @@
/*
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 DIAGRAMEVENTADDPATH_H
#define DIAGRAMEVENTADDPATH_H
#include "../qetgraphicsitem/qetshapeitem.h"
#include "diagrameventinterface.h"
class QGraphicsLineItem;
/**
@brief The DiagramEventAddPath class
Pen tool: interactively draw a new Path (Bezier) shape, following the
same vocabulary every vector editor's pen tool uses:
- click places a Corner node;
- press-drag-release places a Smooth node, with the drag defining a
pair of mirrored handles (same convention as
QetShapeItem::dragPathControlHandle's "Smooth" mirroring);
- clicking back on the first node closes the path;
- double-click, Enter, or Escape (once 2+ nodes exist) finishes it
open;
- right-click steps back one node;
- right-click or Escape with nothing placed yet cancels the tool.
While running, m_nodes always carries one extra "preview" node at the
end, tracking the mouse so a live rubber-band segment is always
visible -- confirmedNodeCount() excludes it; every public gesture
handler is responsible for stripping it before treating the list as
"the path so far" (see finishPath(), which does this once for every
finishing gesture).
*/
class DiagramEventAddPath : public DiagramEventInterface
{
Q_OBJECT
public:
DiagramEventAddPath(Diagram *diagram);
~DiagramEventAddPath() override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void init() override;
private:
void updateHelpCross (const QPointF &p);
void showHint () const;
void finishPath (bool closed);
void cancelPath ();
bool nearFirstNode (const QPointF &scenePos) const;
int confirmedNodeCount () const; // m_nodes always carries one trailing "preview" node while running; this excludes it
QPointF snapped (const QPointF &scenePos, Qt::KeyboardModifiers mods) const;
QetShapeItem *m_shape_item;
QVector<QetShapeItem::PathNode> m_nodes;
int m_dragging_node = -1;
QGraphicsLineItem *m_help_horiz, *m_help_verti;
// Scene units within which a click on an existing path is
// treated as "on the first node" and closes the shape, rather
// than adding yet another node right next to it.
static constexpr qreal CLOSE_THRESHOLD = 12.0;
};
#endif // DIAGRAMEVENTADDPATH_H
+271 -5
View File
@@ -19,8 +19,16 @@
#include "../diagram.h"
#include "../lastusedstyle.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include <QGraphicsEllipseItem>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QStatusBar>
#include <QTimer>
/**
@brief DiagramEventAddShape::DiagramEventAddShape
Default constructor
@@ -36,6 +44,18 @@ DiagramEventAddShape::DiagramEventAddShape(Diagram *diagram, QetShapeItem::Shape
{
m_running = true;
init();
// Deferred to the next event-loop iteration, not shown immediately:
// Diagram::setEventInterface() destroys whatever tool was
// previously active *after* this constructor returns, and that
// previous tool's own destructor clears the status bar (see
// ~DiagramEventAddShape() below) -- an immediate show here would
// just get wiped out moments later by that cleanup. Letting the old
// tool's teardown finish first, then showing this one's message, is
// the same fix already used for the tooltip-flicker issue in
// QetShapeItem::refreshInteractionHints(), applied to the same
// class of "something later in the same call chain undoes what I
// just did" ordering problem.
QTimer::singleShot(0, this, [this]() { updateCreationHint(); });
}
/**
@@ -50,11 +70,123 @@ DiagramEventAddShape::~DiagramEventAddShape()
}
delete m_help_horiz;
delete m_help_verti;
delete m_center_marker;
if (m_diagram && !m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
foreach (QGraphicsView *v, m_diagram->views())
v->setContextMenuPolicy(Qt::DefaultContextMenu);
}
/**
@brief DiagramEventAddShape::applyPosition
Applies a drag/click position to the in-progress shape, honouring two
modifiers that mirror how the very same shape can already be edited
afterward, once placed:
- Ctrl, for Rectangle/Ellipse only: the first click becomes the
shape's *center* rather than a corner, growing symmetrically as
the cursor moves away from it -- the same meaning Ctrl already
has on a Resize handle (anchor at center). Deliberately not
offered for Line: unlike the Rectangle/Ellipse case, there's no
established convention for "a line grows symmetrically from its
middle" to justify it by, so Ctrl for Line means only free
positioning (see the plain grid-snap check above), nothing more.
- Shift, for Rectangle/Ellipse only: forces the bounding box square
(so Ellipse becomes a true circle), using whichever of the two
dragged dimensions is currently larger and mirroring that onto
the other, preserving the direction the user is actually
dragging in.
Both can combine (Ctrl+Shift: a centered square/circle). Whether or
not Ctrl is currently held, the non-anchored branch always rebuilds
from m_anchor_point rather than nudging the existing rect/line --
otherwise, if Ctrl had been held earlier in the same drag (moving the
shape's own first point to a mirrored position), releasing it would
leave that point stuck there instead of actually restoring it.
*/
void DiagramEventAddShape::applyPosition(const QPointF &pos, Qt::KeyboardModifiers mods)
{
if (!m_shape_item)
return;
if (m_shape_type == QetShapeItem::Polygon)
{
// setP2() has its own dedicated Polygon handling: it moves the
// *last* vertex in place rather than setting a second point,
// which is exactly the live "next segment follows the mouse"
// preview -- the same idea DiagramEventAddPath's trailing
// preview node gives the pen tool. No Ctrl/Shift modifiers apply
// here (those are Rectangle/Ellipse/Line-specific below), so
// this is a direct, unconditional call.
m_shape_item->setP2(pos);
return;
}
// m_center_anchored is decided once, in mousePressEvent, not
// re-checked here on every call -- re-checking it live meant
// releasing Ctrl mid-drag (something you'd naturally do the moment
// your hand gets tired holding it, long before you're done resizing)
// silently snapped the shape back to corner-anchored, discarding
// what felt like an already-made decision. Deciding it once at the
// first click matches "I held Ctrl when I clicked, so this shape is
// centered" -- a single, predictable rule instead of a live toggle.
QPointF target = pos;
if ((mods & Qt::ShiftModifier)
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse))
{
const QPointF ref = m_anchor_point;
const qreal dx = target.x() - ref.x();
const qreal dy = target.y() - ref.y();
const qreal size = qMax(qAbs(dx), qAbs(dy));
target.setX(ref.x() + (dx < 0 ? -size : size));
target.setY(ref.y() + (dy < 0 ? -size : size));
}
if (m_center_anchored)
{
const QPointF mirrored = 2 * m_anchor_point - target;
m_shape_item->setRect(QRectF(mirrored, target));
}
else
{
if (m_shape_type == QetShapeItem::Line)
m_shape_item->setLine(QLineF(m_anchor_point, target));
else
m_shape_item->setRect(QRectF(m_anchor_point, target));
}
}
/**
@brief DiagramEventAddShape::showCenterMarker
Small, filled marker at the anchor point, shown only while Ctrl-
anchoring is actually in effect right now (see applyPosition()) --
doubling as live confirmation that it is, rather than leaving the
user to infer it purely from how the shape happens to be growing.
*/
void DiagramEventAddShape::showCenterMarker(const QPointF &scenePos)
{
if (!m_center_marker)
{
m_center_marker = new QGraphicsEllipseItem(-4, -4, 8, 8);
QPen pen(Qt::red);
pen.setCosmetic(true);
m_center_marker->setPen(pen);
m_center_marker->setBrush(Qt::red);
m_diagram->addItem(m_center_marker);
}
m_center_marker->setPos(scenePos);
}
void DiagramEventAddShape::hideCenterMarker()
{
delete m_center_marker;
m_center_marker = nullptr;
}
/**
@brief DiagramEventAddShape::mousePressEvent
Action when mouse is pressed
@@ -67,7 +199,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
}
QPointF pos = event->scenePos();
if (event->modifiers() != Qt::ControlModifier) {
// A bitwise flag check, not exact equality: modifiers() == Ctrl
// alone fails the moment any other key (Shift for the square/circle
// lock, or an incidental platform flag) is also held, silently
// falling through to snapToGrid() even though Ctrl is held --
// exactly what made Ctrl+Shift together feel "frozen" (both grid-
// snapped *and* square-locked, quantizing to whichever is coarser)
// and made "Ctrl = free positioning" not actually hold up.
if (!(event->modifiers() & Qt::ControlModifier)) {
pos = Diagram::snapToGrid(pos);
}
@@ -78,6 +217,15 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
if (!m_shape_item)
{
m_shape_item = new QetShapeItem(pos, pos, m_shape_type);
m_anchor_point = pos;
// Decided once, here, rather than re-checked on every mouse
// move for the rest of the drag -- see applyPosition()'s doc
// comment for why continuous re-checking made releasing Ctrl
// mid-drag feel like a bug rather than a deliberate choice.
m_center_anchored = (event->modifiers() & Qt::ControlModifier)
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse);
if (m_center_anchored)
showCenterMarker(m_anchor_point);
//Start from whatever pen/brush was last applied this
//session, rather than always the hardcoded default.
if (LastUsedStyle::hasShapePen()) {
@@ -87,6 +235,7 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
}
m_diagram->addItem (m_shape_item);
updateCreationHint();
event->setAccepted(true);
return;
}
@@ -94,12 +243,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
//If current item isn't a polyline, add it with an undo command
if (m_shape_type != QetShapeItem::Polygon)
{
m_shape_item->setP2 (pos);
applyPosition(pos, event->modifiers());
if (m_shape_item->shapeType() == QetShapeItem::Rectangle || m_shape_item->shapeType() == QetShapeItem::Ellipse) {
m_shape_item->setRect(m_shape_item->rect().normalized());
}
m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram));
m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic
hideCenterMarker();
updateCreationHint();
}
//Else add a new point to polyline
else
@@ -125,18 +276,76 @@ void DiagramEventAddShape::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
updateHelpCross(event->scenePos());
// Re-asserted on every move, not just once at activation: Qt's own
// built-in "show an action's statusTip on hover" has its own
// internal "restore whatever was there before" logic for when the
// hover ends. Since our message is shown *during* that same hover
// session (the user is still over the toolbar icon when the
// deferred constructor-time call fires), Qt's hover-tracking has no
// idea we changed the status bar in the meantime -- the moment the
// mouse leaves the icon for the canvas, it "restores" to whatever it
// remembers being there before its own tip started, which is stale
// and empty, silently overwriting ours. Re-showing it here, on every
// move within the canvas, simply outlasts that one-time restore.
updateCreationHint();
if (m_shape_item && event->buttons() == Qt::NoButton)
{
m_last_mouse_scene_pos = event->scenePos(); // raw, before snapping -- see reapplyLastPosition()
QPointF pos = event->scenePos();
if (event->modifiers() != Qt::ControlModifier) {
if (!(event->modifiers() & Qt::ControlModifier)) {
pos = Diagram::snapToGrid(pos);
}
m_shape_item->setP2 (pos);
applyPosition(pos, event->modifiers());
event->setAccepted(true);
}
}
/**
@brief DiagramEventAddShape::keyPressEvent / keyReleaseEvent
Pressing or releasing Shift (the square/circle lock) does nothing
visible on its own -- applyPosition() only ever runs from
mouseMoveEvent, so without this, a keyboard-only change just sits
there until the next, often incidental, pixel of mouse movement
brings the shape in line with it. That's exactly what looked like a
freeze: holding Shift while the mouse is genuinely still produces no
visible change (correctly -- nothing has moved), and it only
"unsticks" once the mouse moves again, which released keys tend to
coincide with purely by hand tremor, not because releasing itself
did anything. Re-running the last known mouse position through
applyPosition() here makes the key press or release itself the
trigger, giving immediate feedback instead of waiting on chance.
*/
void DiagramEventAddShape::keyPressEvent(QKeyEvent *event)
{
reapplyLastPosition(event);
}
void DiagramEventAddShape::keyReleaseEvent(QKeyEvent *event)
{
reapplyLastPosition(event);
}
void DiagramEventAddShape::reapplyLastPosition(QKeyEvent *event)
{
if (!m_shape_item || (event->key() != Qt::Key_Shift && event->key() != Qt::Key_Control))
return;
// A fresh, global query, not event->modifiers(): for a press/release
// of a modifier key itself, whether that key is already reflected in
// the key event's own modifiers() is ambiguous and platform-
// dependent -- the same reason Diagram::snapToGrid() queries this
// directly rather than trusting a passed-in modifiers() value.
const Qt::KeyboardModifiers mods = QGuiApplication::keyboardModifiers();
QPointF pos = m_last_mouse_scene_pos;
if (!(mods & Qt::ControlModifier))
pos = Diagram::snapToGrid(pos);
applyPosition(pos, mods);
}
/**
@brief DiagramEventAddShape::mouseReleaseEvent
Action when mouse button is released
@@ -155,7 +364,7 @@ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
m_shape_item->removePoints();
QPointF pos = event->scenePos();
if (event->modifiers() != Qt::ControlModifier)
if (!(event->modifiers() & Qt::ControlModifier))
pos = Diagram::snapToGrid(pos);
m_shape_item->setP2(pos); //Set the new last point under the cursor
@@ -167,6 +376,8 @@ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
m_diagram->removeItem(m_shape_item);
delete m_shape_item;
m_shape_item = nullptr;
hideCenterMarker();
updateCreationHint();
event->setAccepted(true);
return;
}
@@ -203,6 +414,8 @@ void DiagramEventAddShape::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
}
m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram));
m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic
hideCenterMarker();
updateCreationHint();
event->setAccepted(true);
}
}
@@ -213,6 +426,59 @@ void DiagramEventAddShape::init()
v->setContextMenuPolicy(Qt::NoContextMenu);
}
/**
@brief DiagramEventAddShape::updateCreationHint
Shows whichever of beforeClickHint()/afterClickHint() matches the
current phase -- there was previously either no message at all
(Line/Rectangle/Ellipse) or a single static one that never changed
regardless of progress (Polygon, set externally in
QETDiagramEditor::addItemGroupTriggered()); this replaces both with
one phase-aware message per shape type, managed by the tool itself.
*/
void DiagramEventAddShape::updateCreationHint() const
{
if (!m_diagram || m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(m_shape_item ? afterClickHint() : beforeClickHint());
}
QString DiagramEventAddShape::beforeClickHint() const
{
switch (m_shape_type)
{
case QetShapeItem::Line:
return tr("Clic gauche : positionner le point de départ (Ctrl = position libre)");
case QetShapeItem::Rectangle:
case QetShapeItem::Ellipse:
return tr("Clic gauche : positionner le premier coin (Ctrl = point central, position libre)");
case QetShapeItem::Polygon:
return tr("Clic gauche : positionner le premier point (Ctrl = position libre)");
default:
return QString();
}
}
QString DiagramEventAddShape::afterClickHint() const
{
switch (m_shape_type)
{
case QetShapeItem::Line:
return tr("Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler");
case QetShapeItem::Rectangle:
return tr("Clic gauche : positionner le coin opposé (Maj = carré, "
"Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler");
case QetShapeItem::Ellipse:
return tr("Clic gauche : positionner le coin opposé (Maj = cercle, "
"Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler");
case QetShapeItem::Polygon:
return tr("Clic gauche : point suivant ; double-clic ou Entrée : terminer ; "
"clic droit : annuler le dernier point");
default:
return QString();
}
}
/**
@brief DiagramEventAddShape::updateHelpCross
Create and update the position of the cross to help user for draw new shape
@@ -21,6 +21,8 @@
#include "../qetgraphicsitem/qetshapeitem.h"
#include "diagrameventinterface.h"
class QGraphicsEllipseItem;
/**
@brief The DiagramEventAddShape class
This event manage the creation of a shape.
@@ -37,15 +39,28 @@ class DiagramEventAddShape : public DiagramEventInterface
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void keyReleaseEvent (QKeyEvent *event) override;
void init() override;
private:
void updateHelpCross (const QPointF &p);
void applyPosition (const QPointF &pos, Qt::KeyboardModifiers mods);
void updateCreationHint () const;
QString beforeClickHint () const;
QString afterClickHint () const;
void showCenterMarker (const QPointF &scenePos);
void hideCenterMarker ();
void reapplyLastPosition (QKeyEvent *event);
protected:
QetShapeItem::ShapeType m_shape_type;
QetShapeItem *m_shape_item;
QGraphicsLineItem *m_help_horiz, *m_help_verti;
QPointF m_anchor_point; // the shape's first-click point -- meaningful once m_shape_item exists
QGraphicsEllipseItem *m_center_marker = nullptr; // shown only while Ctrl-anchoring is actually in effect, so it doubles as confirmation that it is
bool m_center_anchored = false; // decided once, at the first click -- see applyPosition()'s doc comment for why
QPointF m_last_mouse_scene_pos; // raw, unsnapped -- lets a modifier-only change re-snap correctly when reapplied
};
#endif // DIAGRAMEVENTADDSHAPE_H
+53
View File
@@ -0,0 +1,53 @@
/*
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 DIAGRAMSORTKEYS_H
#define DIAGRAMSORTKEYS_H
#include <QPointF>
#include <QString>
#include <QtGlobal>
/// Sort-key helpers used by Diagram::toXml() to serialize elements and
/// conductors in a deterministic order. Pulled into their own header
/// (instead of an anonymous namespace in diagram.cpp) so they can be
/// unit-tested directly -- see tests/qttest/tst_diagramsortkeys.cpp.
namespace DiagramSortKeys {
/// Format a coordinate as a string that sorts the same way the number
/// does. Plain fixed-precision formatting ("%.4f") does NOT do this --
/// e.g. "15.0000" sorts before "5.0000" as text even though 15 > 5 --
/// so shift into a non-negative range and zero-pad to a fixed width
/// before comparing.
inline QString coordinateKey(double v)
{
//Diagram coordinates are nowhere near this range; the offset and
//width just need to be big enough that shifted values are always
//non-negative and always the same digit count.
constexpr double offset = 1e9;
qint64 scaled = qint64(qRound64((v + offset) * 10000.0));
return QStringLiteral("%1").arg(scaled, 20, 10, QLatin1Char('0'));
}
inline QString positionKey(const QPointF &pos)
{
return coordinateKey(pos.x()) + QLatin1Char('|') + coordinateKey(pos.y());
}
}
#endif
+7
View File
@@ -334,6 +334,13 @@ void DiagramView::setSelectionMode()
*/
void DiagramView::zoom(const qreal zoom_factor)
{
// clamp the resulting scale so a repeated wheel-zoom cannot drive the view
// transform to floating-point overflow and crash the editor (issue #798)
const qreal target = transform().m11() * zoom_factor;
if (target < m_min_zoom || target > m_max_zoom) {
return;
}
if (zoom_factor >= 1){
scale(zoom_factor, zoom_factor);
}
+6
View File
@@ -103,6 +103,12 @@ class DiagramView : public QGraphicsView
bool mustIntegrateTitleBlockTemplate(const TitleBlockTemplateLocation &) const;
bool gestures() const;
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents wheel-zoom from driving the transform to overflow, which
/// crashes the editor (see GitHub issue #798, same class of bug).
static constexpr qreal m_min_zoom = 0.01;
static constexpr qreal m_max_zoom = 200.0;
signals:
/// Signal emitted after the selection mode changed
void modeChanged();
+38 -8
View File
@@ -200,18 +200,35 @@ void DxfPaintEngine::drawTextItem(const QPointF &p, const QTextItem &textItem)
/**
@brief DxfPaintEngine::drawPixmap
Not implemented in v1 - out of scope per the design note in the
header (CrossRefItem, the only item exported through this engine so
far, never draws a pixmap). qWarning() rather than a hard failure, so
an item that does call this in the future degrades to "one entity
missing" instead of crashing the whole export.
No DXF dialect this old (AC1006, AutoCAD R10 from 1988) has any
raster image representation at all -- IMAGE/IMAGEDEF wasn't
introduced until R2000, over a decade later, and even there the
pixels are never embedded, only referenced by external file path.
Actually supporting images means upgrading the DXF version target
and managing a second file alongside the DXF; until then, this
draws a placeholder rectangle outline (the item's own destination
rect, mapped through m_world_transform exactly like drawRects()
does) instead of silently dropping the item -- so its position,
size, rotation, and skew all survive the export even though the
picture itself can't yet. A fixed, plain pen, not m_pen: an image
item has no meaningful pen of its own for this engine to have
picked up from a prior updateState().
*/
void DxfPaintEngine::drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr)
{
Q_UNUSED(r)
Q_UNUSED(pm)
Q_UNUSED(sr)
qWarning() << "DxfPaintEngine::drawPixmap: not supported, entity skipped";
QPolygonF corners;
corners << r.topLeft() << r.topRight() << r.bottomRight() << r.bottomLeft();
QPolygonF mapped;
mapped.reserve(corners.size() + 1);
for (const QPointF &p : corners)
mapped << toDxf(p);
mapped << mapped.first();
Createdxf::drawPolyline(m_filepath, mapped, Createdxf::dxfColor(QPen(Qt::black)));
}
/**
@@ -261,8 +278,21 @@ int DxfPaintDevice::metric(PaintDeviceMetric metric) const
case PdmDepth:
return 24;
case PdmDevicePixelRatio:
case PdmDevicePixelRatioScaled:
return 1;
case PdmDevicePixelRatioScaled:
// Not just 1: Qt's own convention (see QPaintDevice's docs)
// is that this metric equals PdmDevicePixelRatio scaled by
// devicePixelRatioFScale() -- a large constant (65536)
// used internally for sub-integer precision. Returning the
// plain, unscaled ratio here (as this used to) meant
// QPainter's own transform bookkeeping divided by it
// expecting the scaled value, silently shrinking every
// drawPixmap() destination rect by a factor of 65536 --
// this is what was actually behind the placeholder
// rectangle rendering at a barely-visible fraction of its
// real size instead of the size the item's own
// sceneTransform() correctly specified.
return int(1 * QPaintDevice::devicePixelRatioFScale());
default:
return 0;
}
+12 -5
View File
@@ -56,11 +56,18 @@
- fillPath -> same outline-only handling as drawRects; no HATCH
support in v1 (see design note in the PR)
Anything outside this list (images, gradients, etc.) is intentionally
unimplemented and asserts in debug builds rather than silently
producing an incomplete drawing - callers should know immediately if
an item they're exporting uses something this engine doesn't cover
yet, rather than getting a DXF file quietly missing content.
- drawPixmap -> no raster image entity exists in this DXF
dialect at all (see drawPixmap()'s own
comment) -- draws a placeholder rectangle
outline instead, preserving the item's
position/size/rotation/skew even though the
picture itself can't be included yet
Anything else outside this list (gradients, etc.) is intentionally
unimplemented and logs a warning rather than silently producing an
incomplete drawing - callers should know immediately if an item
they're exporting uses something this engine doesn't cover yet,
rather than getting a DXF file quietly missing content.
*/
class DxfPaintEngine : public QPaintEngine
{
+40 -16
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "editorcommands.h"
#include "../diagram.h"
/**
@brief ElementEditionCommand::ElementEditionCommand
@@ -666,10 +667,32 @@ void RotateFineElementsCommand::redo()
}
/**
@brief selectionCenter
@param items
@return the center of the united scene bounding rect of items,
snapped to the nearest half of the diagram grid. Mirroring across a
half-grid line keeps points that were on the grid on the grid.
*/
static QPointF selectionCenter(const QList<QGraphicsItem *> &items)
{
QRectF bounding;
for (auto *item : items) {
bounding = bounding.united(item->sceneBoundingRect());
}
QPointF center = bounding.center();
const qreal half_x = Diagram::xGrid / 2.0;
const qreal half_y = Diagram::yGrid / 2.0;
center.setX(qRound(center.x() / half_x) * half_x);
center.setY(qRound(center.y() / half_y) * half_y);
return center;
}
MirrorElementsCommand::MirrorElementsCommand(ElementScene *scene, QUndoCommand *parent) :
ElementEditionCommand(QObject::tr("Miroir de sélection", "undo caption"), scene, nullptr, parent)
{
m_items = scene->selectedItems();
m_axis_x = selectionCenter(m_items).x();
}
/**
@@ -680,28 +703,28 @@ void MirrorElementsCommand::redo()
foreach (auto *item, m_items) {
if (item->type() == PartText::Type) {
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
staticText->mirror();
staticText->mirror(m_axis_x);
} else if (item->type() == PartDynamicTextField::Type) {
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
dyntext->mirror();
dyntext->mirror(m_axis_x);
} else if (item->type() == PartArc::Type) {
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
arc->mirror();
arc->mirror(m_axis_x);
} else if (item->type() == PartEllipse::Type) {
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
ellipse->mirror();
ellipse->mirror(m_axis_x);
} else if (item->type() == PartLine::Type) {
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
line->mirror();
line->mirror(m_axis_x);
} else if (item->type() == PartPolygon::Type) {
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
poly->mirror();
poly->mirror(m_axis_x);
} else if (item->type() == PartRectangle::Type) {
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
rect->mirror();
rect->mirror(m_axis_x);
} else if (item->type() == PartTerminal::Type) {
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
term->mirror();
term->mirror(m_axis_x);
}
}
}
@@ -718,6 +741,7 @@ FlipElementsCommand::FlipElementsCommand(ElementScene *scene, QUndoCommand *pare
ElementEditionCommand(QObject::tr("Retourner la sélection", "undo caption"), scene, nullptr, parent)
{
m_items = scene->selectedItems();
m_axis_y = selectionCenter(m_items).y();
}
/**
@@ -728,28 +752,28 @@ void FlipElementsCommand::redo()
foreach (auto *item, m_items) {
if (item->type() == PartText::Type) {
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
staticText->flip();
staticText->flip(m_axis_y);
} else if (item->type() == PartDynamicTextField::Type) {
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
dyntext->flip();
dyntext->flip(m_axis_y);
} else if (item->type() == PartArc::Type) {
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
arc->flip();
arc->flip(m_axis_y);
} else if (item->type() == PartEllipse::Type) {
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
ellipse->flip();
ellipse->flip(m_axis_y);
} else if (item->type() == PartLine::Type) {
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
line->flip();
line->flip(m_axis_y);
} else if (item->type() == PartPolygon::Type) {
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
poly->flip();
poly->flip(m_axis_y);
} else if (item->type() == PartRectangle::Type) {
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
rect->flip();
rect->flip(m_axis_y);
} else if (item->type() == PartTerminal::Type) {
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
term->flip();
term->flip(m_axis_y);
}
}
}
+14
View File
@@ -279,6 +279,12 @@ private:
};
/**
@brief The MirrorElementsCommand class
Mirror the selected parts horizontally (left <-> right) across the
vertical line through the center of the selection, so the selection
keeps its place in the scene.
*/
class MirrorElementsCommand : public ElementEditionCommand
{
public:
@@ -288,8 +294,15 @@ public:
private:
ElementScene *m_scene =nullptr;
QList<QGraphicsItem*> m_items;
qreal m_axis_x = 0;
};
/**
@brief The FlipElementsCommand class
Flip the selected parts vertically (top <-> bottom) across the
horizontal line through the center of the selection, so the selection
keeps its place in the scene.
*/
class FlipElementsCommand : public ElementEditionCommand
{
public:
@@ -299,6 +312,7 @@ public:
private:
ElementScene *m_scene =nullptr;
QList<QGraphicsItem*> m_items;
qreal m_axis_y = 0;
};
#endif
+5
View File
@@ -479,8 +479,13 @@ const QDomDocument ElementScene::toXml(bool all_parts)
root.appendChild(m_element_data.kindInfoToXml(xml_document));
}
//Slave is in this list because the element editor offers the
//Informations tab for it, including the PLC-specific rows
//populateTree() adds for a PLC slave. Without it the editor would
//accept that data and silently drop it on save.
if (type_ == ElementData::Simple ||
type_ == ElementData::Master ||
type_ == ElementData::Slave ||
type_ == ElementData::Terminal ||
type_ == ElementData::Thumbnail)
{
+21 -4
View File
@@ -100,13 +100,30 @@ void ElementView::setSelectionMode()
emit(modeChanged());
}
/**
Applique un facteur d'echelle a la vue en bornant l'echelle resultante
entre m_min_zoom et m_max_zoom. Sans cette borne, un zoom repete (molette)
finit par faire deborder la transformation de la vue et fait planter
l'editeur (issue #798).
@param factor facteur d'echelle a appliquer
*/
void ElementView::scaleClamped(qreal factor)
{
const qreal current = transform().m11();
const qreal target = current * factor;
if (target < m_min_zoom || target > m_max_zoom) {
return;
}
scale(factor, factor);
}
/**
Agrandit le schema (+33% = inverse des -25 % de zoomMoins())
*/
void ElementView::zoomIn()
{
adjustSceneRect();
scale(4.0/3.0, 4.0/3.0);
scaleClamped(4.0/3.0);
}
/**
@@ -115,7 +132,7 @@ void ElementView::zoomIn()
void ElementView::zoomOut()
{
adjustSceneRect();
scale(0.75, 0.75);
scaleClamped(0.75);
}
/**
@@ -123,7 +140,7 @@ void ElementView::zoomOut()
*/
void ElementView::zoomInSlowly()
{
scale(1.02, 1.02);
scaleClamped(1.02);
}
/**
@@ -131,7 +148,7 @@ void ElementView::zoomInSlowly()
*/
void ElementView::zoomOutSlowly()
{
scale(0.98, 0.98);
scaleClamped(0.98);
}
/**
+7
View File
@@ -53,6 +53,13 @@ class ElementView : public QGraphicsView {
private:
QRectF applyMovement(const QRectF &, const QPointF &);
void scaleClamped(qreal factor);
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents the wheel-zoom from driving the transform to overflow, which
/// crashes the editor (bugtracker / GitHub issue #798).
static constexpr qreal m_min_zoom = 0.1;
static constexpr qreal m_max_zoom = 200.0;
public slots:
void setVisualisationMode();
+4 -4
View File
@@ -197,13 +197,13 @@ qreal PartArc::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartArc::flip() {
void PartArc::flip(qreal axis_y) {
m_start_angle = (-1) * m_start_angle;
m_span_angle = (-1) * m_span_angle;
while (m_start_angle < 0) { m_start_angle += (360*16); }
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
auto p1 = mapToScene(m_rect.x(),m_rect.y());
p1.setY(((-1.0) * p1.y()) - m_rect.height());
p1.setY(2 * axis_y - p1.y() - m_rect.height());
p1 = mapFromScene(p1.x(),p1.y());
m_rect = QRectF(m_rect.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
@@ -211,13 +211,13 @@ void PartArc::flip() {
emit rectChanged();
}
void PartArc::mirror() {
void PartArc::mirror(qreal axis_x) {
m_start_angle = (180.0 * 16) - m_start_angle;
m_span_angle = (-1) * m_span_angle;
while (m_start_angle < 0) { m_start_angle += (360*16); }
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
auto p1 = mapToScene(m_rect.x(),m_rect.y());
p1.setX(((-1.0) * p1.x()) - m_rect.width());
p1.setX(2 * axis_x - p1.x() - m_rect.width());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), m_rect.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
+2 -2
View File
@@ -62,8 +62,8 @@ class PartArc : public AbstractPartEllipse
QRectF sceneGeometricRect() const override;
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
@@ -74,19 +74,19 @@ void PartDynamicTextField::setRotation(qreal angle) {
setPos(QTransform().rotate(diffAngle).map(pos()));
}
void PartDynamicTextField::mirror() {
void PartDynamicTextField::mirror(qreal axis_x) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
// then see, where we need to re-position depending on the angle!
qreal rot = qRound(QET::correctAngle(rotation(), true));
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
qreal x = (-1) * pos().x() - c * boundingRect().width();
qreal x = 2 * axis_x - pos().x() - c * boundingRect().width();
qreal y = pos().y() - s * boundingRect().width();
setPos(x, y);
}
void PartDynamicTextField::flip() {
void PartDynamicTextField::flip(qreal axis_y) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
// then see, where we need to re-position depending on the angle!
@@ -94,7 +94,7 @@ void PartDynamicTextField::flip() {
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
qreal x = pos().x() + s * boundingRect().height();
qreal y = (-1) * pos().y() - c * boundingRect().height();
qreal y = 2 * axis_y - pos().y() - c * boundingRect().height();
setPos(x, y);
}
@@ -106,8 +106,8 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
bool rotationPointCenter() const;
void setRotation(qreal angle);
void mirror();
void flip();
void mirror(qreal axis_x = 0);
void flip(qreal axis_y = 0);
protected:
+4 -4
View File
@@ -256,9 +256,9 @@ qreal PartEllipse::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartEllipse::flip() {
void PartEllipse::flip(qreal axis_y) {
auto p1 = mapToScene(m_rect.x(), m_rect.y());
p1.setY(((-1.0) * p1.y()) - m_rect.height());
p1.setY(2 * axis_y - p1.y() - m_rect.height());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
@@ -266,9 +266,9 @@ void PartEllipse::flip() {
emit rectChanged();
}
void PartEllipse::mirror() {
void PartEllipse::mirror(qreal axis_x) {
auto p1 = mapToScene(m_rect.x(), m_rect.y());
p1.setX(((-1.0) * p1.x()) - m_rect.width());
p1.setX(2 * axis_x - p1.x() - m_rect.width());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
+2 -2
View File
@@ -62,8 +62,8 @@ class PartEllipse : public AbstractPartEllipse
void setRect(const QRectF &rect) override {AbstractPartEllipse::setRect(rect); adjustHandlerPos();}
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
+6 -6
View File
@@ -590,11 +590,11 @@ qreal PartLine::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartLine::flip() {
void PartLine::flip(qreal axis_y) {
auto p1 = mapToScene(m_line.p1());
auto p2 = mapToScene(m_line.p2());
p1 = QPointF(p1.x(), (-1) * p1.y());
p2 = QPointF(p2.x(), (-1) * p2.y());
p1 = QPointF(p1.x(), 2 * axis_y - p1.y());
p2 = QPointF(p2.x(), 2 * axis_y - p2.y());
m_line.setP1(mapFromScene(p1));
m_line.setP2(mapFromScene(p2));
setLine(m_line);
@@ -603,11 +603,11 @@ void PartLine::flip() {
emit lineChanged();
}
void PartLine::mirror() {
void PartLine::mirror(qreal axis_x) {
auto p1 = mapToScene(m_line.p1());
auto p2 = mapToScene(m_line.p2());
p1 = QPointF((-1) * p1.x(), p1.y());
p2 = QPointF((-1) * p2.x(), p2.y());
p1 = QPointF(2 * axis_x - p1.x(), p1.y());
p2 = QPointF(2 * axis_x - p2.x(), p2.y());
m_line.setP1(mapFromScene(p1));
m_line.setP2(mapFromScene(p2));
setLine(m_line);
+2 -2
View File
@@ -96,8 +96,8 @@ class PartLine : public CustomElementGraphicPart
void setSecondEndLength(const qreal &l);
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
+4 -4
View File
@@ -307,10 +307,10 @@ qreal PartPolygon::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartPolygon::flip() {
void PartPolygon::flip(qreal axis_y) {
for (auto &pt : m_polygon) {
pt = mapToScene(pt.x(), pt.y());
pt = QPointF(pt.x(), (-1) * pt.y());
pt = QPointF(pt.x(), 2 * axis_y - pt.y());
pt = mapFromScene(pt.x(), pt.y());
}
setPolygon(m_polygon);
@@ -319,10 +319,10 @@ void PartPolygon::flip() {
emit polygonChanged();
}
void PartPolygon::mirror() {
void PartPolygon::mirror(qreal axis_x) {
for (auto &pt : m_polygon) {
pt = mapToScene(pt.x(), pt.y());
pt = QPointF((-1) * pt.x(), pt.y());
pt = QPointF(2 * axis_x - pt.x(), pt.y());
pt = mapFromScene(pt.x(), pt.y());
}
setPolygon(m_polygon);
+2 -2
View File
@@ -89,8 +89,8 @@ class PartPolygon : public CustomElementGraphicPart
void setRotation (qreal angle);
qreal rotation () const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
@@ -187,11 +187,11 @@ qreal PartRectangle::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartRectangle::flip() {
void PartRectangle::flip(qreal axis_y) {
auto height = m_rect.height();
auto p1 = mapToScene(m_rect.x(),m_rect.y());
qreal x = p1.x();
qreal y = ((-1.0) * p1.y()) - height;
qreal y = 2 * axis_y - p1.y() - height;
p1 = mapFromScene(x, y);
m_rect.setX(p1.x());
m_rect.setY(p1.y());
@@ -201,10 +201,10 @@ void PartRectangle::flip() {
emit rectChanged();
}
void PartRectangle::mirror() {
void PartRectangle::mirror(qreal axis_x) {
auto width = m_rect.width();
auto p1 = mapToScene(m_rect.x(),m_rect.y());
qreal x = ((-1.0) * p1.x()) - width;
qreal x = 2 * axis_x - p1.x() - width;
qreal y = p1.y();
p1 = mapFromScene(x, y);
m_rect.setX(p1.x());
+2 -2
View File
@@ -73,8 +73,8 @@ class PartRectangle : public CustomElementGraphicPart
void setYRadius(qreal Y);
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
QRectF sceneGeometricRect() const override;
virtual QPointF sceneTopLeft() const;
+4 -4
View File
@@ -293,9 +293,9 @@ qreal PartTerminal::rotation() const {
@brief PartTerminal::flip
turn part upside down
*/
void PartTerminal::flip() {
void PartTerminal::flip(qreal axis_y) {
d->m_pos.setX( pos().x());
d->m_pos.setY((-1.0) * pos().y());
d->m_pos.setY(2 * axis_y - pos().y());
switch (d->m_orientation) {
case Qet::North : setOrientation(Qet::South);
break;
@@ -314,8 +314,8 @@ void PartTerminal::flip() {
@brief PartTerminal::mirror
turn part from left to right
*/
void PartTerminal::mirror() {
d->m_pos.setX((-1.0) * pos().x());
void PartTerminal::mirror(qreal axis_x) {
d->m_pos.setX(2 * axis_x - pos().x());
d->m_pos.setY( pos().y());
switch (d->m_orientation) {
case Qet::North : break;
+2 -2
View File
@@ -96,8 +96,8 @@ class PartTerminal : public CustomElementGraphicPart
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void nextOrientation();
void previousOrientation();
+4 -4
View File
@@ -75,7 +75,7 @@ void PartText::setRotation(qreal angle) {
setPos(QTransform().rotate(diffAngle).map(pos()));
}
void PartText::mirror() {
void PartText::mirror(qreal axis_x) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
// then see, where we need to re-position depending on text, font ...
@@ -86,12 +86,12 @@ void PartText::mirror() {
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
// Now: Move!
qreal x = (-1) * pos().x() - c * (textwidth);
qreal x = 2 * axis_x - pos().x() - c * (textwidth);
qreal y = pos().y() - s * (textwidth);
setPos(x, y);
}
void PartText::flip() {
void PartText::flip(qreal axis_y) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
// then see, where we need to re-position depending on text, font ...
@@ -103,7 +103,7 @@ void PartText::flip() {
qreal s = qSin(qDegreesToRadians(rot));
// Now: Move!
qreal x = pos().x() - s * (textheight);
qreal y = (-1) * pos().y() + c * (textheight);
qreal y = 2 * axis_y - pos().y() + c * (textheight);
setPos(x, y);
}
+2 -2
View File
@@ -64,8 +64,8 @@ class PartText : public QGraphicsTextItem, public CustomElementPart {
void fromXml(const QDomElement &) override;
const QDomElement toXml(QDomDocument &) const override;
void setRotation(qreal angle);
void mirror();
void flip();
void mirror(qreal axis_x = 0);
void flip(qreal axis_y = 0);
bool isUseless() const override;
QRectF sceneGeometricRect() const override;
void startUserTransformation(const QRectF &) override;
+6 -1
View File
@@ -245,7 +245,8 @@ void DynamicTextFieldEditor::fillInfoComboBox()
else {
strl = QETInformation::elementInfoKeys();
bool is_plc_slave = (type == ElementData::Slave
bool is_slave = (type == ElementData::Slave);
bool is_plc_slave = (is_slave
&& ed.m_slave_type == ElementData::PLCSlave);
if (is_plc_slave) {
@@ -274,6 +275,10 @@ void DynamicTextFieldEditor::fillInfoComboBox()
strl.removeAll(QETInformation::ELMT_PLC_T3);
strl.removeAll(QETInformation::ELMT_PLC_T4);
}
if (is_slave) {
strl.prepend(QETInformation::ELMT_XREF);
}
}
for (int i=0; i<strl.size();++i) {
@@ -292,7 +292,7 @@ void ElementPropertiesEditorWidget::updateTree()
ui->m_tree->setEnabled(true);
break;
case ElementData::Slave:
ui->m_tree->setDisabled(true);
ui->m_tree->setEnabled(true);
break;
case ElementData::Terminal:
ui->m_tree->setEnabled(true);
@@ -371,7 +371,7 @@ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted()
m_data.m_master_type = ui->m_master_type_cb->currentData().value<ElementData::MasterType>();
//If the checkbox is checked, save the number; otherwise, -1 (infinity)
if (ui->max_slaves_checkbox->isVisible() && ui->max_slaves_checkbox->isChecked()) {
if ((m_data.m_master_type == ElementData::Coil || m_data.m_master_type == ElementData::Protection || m_data.m_master_type == ElementData::Commutator) && ui->max_slaves_checkbox->isChecked()) {
m_data.m_max_slaves = ui->max_slaves_spinbox->value();
} else {
m_data.m_max_slaves = -1;
@@ -425,7 +425,9 @@ void ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged(int in
ui->tabWidget->setTabVisible(1,
(type_ == ElementData::Simple ||
type_ == ElementData::Master));
type_ == ElementData::Master ||
type_ == ElementData::Slave ||
type_ == ElementData::Terminal));
updateTree();
}
+36 -13
View File
@@ -78,8 +78,9 @@ QETElementEditor::QETElementEditor(QWidget *parent) :
//ui->m_display_menu->insertMenu(ui->m_zoom_in_action, menu);
setWindowState(Qt::WindowMaximized);
readSettings();
readSettings(); // restoreGeometry before show()
show();
readSettingsState(); // restoreState() must be called after show() in Qt6
}
/**
@@ -951,16 +952,28 @@ void QETElementEditor::readSettings()
restoreGeometry(geometry.toByteArray());
}
QVariant state = settings.value("elementeditor/state");
if (state.isValid()) {
restoreState(state.toByteArray());
}
auto data = m_elmt_scene->elementData();
data.m_drawing_information = settings.value("elementeditor/default-informations", "").toString();
m_elmt_scene->setElementData(data);
}
/**
* @brief QETElementEditor::readSettingsState
* Restore the window state (docks, toolbars).
* Must be called AFTER show() in Qt6 for restoreState() to work correctly.
*/
void QETElementEditor::readSettingsState()
{
QSettings settings;
QVariant state = settings.value("elementeditor/state");
if (state.isValid()) {
if (!restoreState(state.toByteArray())) {
settings.remove("elementeditor/state");
}
}
}
/**
* @brief QETElementEditor::writeSettings
* Write some setting of this widget in the
@@ -1089,29 +1102,39 @@ void QETElementEditor::updateAction()
{
//Action disabled if read only
auto ro_list = m_add_part_action_grp->actions();
ro_list << ui->m_select_all_act
<< ui->m_revert_selection_action
<< ui->m_paste_from_file_action
ro_list << ui->m_paste_from_file_action
<< ui->m_paste_from_element_action;
for (auto action : std::as_const(ro_list)) {
action->setDisabled(m_read_only);
}
//Changing what is selected does not change the element, so these stay
//available when it is read only -- otherwise there is no way to pick
//out the primitive you want to copy out of it.
ui->m_select_all_act->setEnabled(true);
ui->m_revert_selection_action->setEnabled(true);
//Action enabled if a primitive is selected
auto select_list = m_depth_action_group->actions();
select_list << ui->m_deselect_all_action
<< ui->m_cut_action
<< ui->m_copy_action
select_list << ui->m_cut_action
<< ui->m_delete_action
<< ui->m_rotate_action
<< ui->m_rotateFine_action
<< ui->m_flip_action
<< ui->m_mirror_action;
auto items_selected = !m_read_only && m_elmt_scene->selectedItems().count();
const bool has_selection = m_elmt_scene->selectedItems().count() > 0;
auto items_selected = !m_read_only && has_selection;
for (auto action : std::as_const(select_list)) {
action->setEnabled(items_selected);
}
//Copying only reads the element -- ElementScene::copy() serialises the
//selection to the clipboard and touches nothing else -- so it is
//allowed on a read-only element too. Cut, paste and delete above stay
//disabled, so the element itself is still protected.
ui->m_copy_action->setEnabled(has_selection);
ui->m_deselect_all_action->setEnabled(has_selection);
//Action about clipboard
auto clipboard_contain_elmt = !m_read_only && ElementScene::clipboardMayContainElement();
ui->m_paste_action->setEnabled(clipboard_contain_elmt);
+1
View File
@@ -116,6 +116,7 @@ class QETElementEditor : public QMainWindow
private:
bool canClose();
void readSettings();
void readSettingsState();
void writeSettings() const;
void setupActions();
void updateAction();
+55
View File
@@ -744,6 +744,25 @@ void ExportDialog::generateDxf(
painter.end();
}
//Draw images -- collected above (list_images) but never actually
//drawn until now, an existing gap this reuses the same paint()
//-replay approach to fix: DiagramImageItem::paint() has no
//viewport-dependent logic (unlike CrossRefItem, which needs its own
//paintForExport() for that reason), so it's called directly with a
//default QStyleOptionGraphicsItem rather than needing an export-
//specific variant of its own. DxfPaintEngine::drawPixmap() is what
//actually turns the drawPixmap() call inside paint() into a
//placeholder outline, since this DXF dialect has no raster image
//entity to draw instead.
for (DiagramImageItem *image : std::as_const(list_images))
{
DxfPaintDevice dxf_device(file_path);
QPainter painter(&dxf_device);
painter.setWorldTransform(image->sceneTransform());
image->paintForExport(&painter);
painter.end();
}
Createdxf::dxfEnd(file_path);
saveReloadDiagramParameters(diagram, false);
@@ -818,6 +837,42 @@ void ExportDialog::slot_export()
return;
}
// Warn once, up front, rather than per-diagram: this DXF dialect
// (AC1006, AutoCAD R10) has no raster image representation at all
// (IMAGE/IMAGEDEF wasn't introduced until R2000, over a decade
// later, and even there the picture is never embedded, only
// referenced by external file path) -- so any image ends up as a
// placeholder rectangle outline instead (see DxfPaintEngine::
// drawPixmap()), with its position/size/rotation/skew preserved but
// not its actual content.
if (epw -> exportProperties().format.compare(QLatin1String("dxf"), Qt::CaseInsensitive) == 0)
{
bool any_images = false;
for (ExportDiagramLine *diagram_line : std::as_const(diagrams_to_export))
{
for (QGraphicsItem *item : diagram_line->diagram->items())
{
if (qgraphicsitem_cast<DiagramImageItem *>(item)) {
any_images = true;
break;
}
}
if (any_images) break;
}
if (any_images)
{
QET::QetMessageBox::warning(
this,
tr("Images non incluses dans l'export DXF", "message box title"),
tr("Le format DXF utilisé ici (AC1006) ne permet pas d'inclure d'image. "
"Les images seront représentées uniquement par un rectangle de contour "
"(position, taille, rotation et inclinaison conservées), sans le contenu de l'image.",
"message box content")
);
}
}
// exporte chaque schema a exporter
foreach(ExportDiagramLine *diagram_line, diagrams_to_export) {
exportDiagram(diagram_line);
+39 -14
View File
@@ -36,6 +36,33 @@
ElementPictureFactory* ElementPictureFactory::m_factory = nullptr;
/**
@brief ElementPictureFactory::cacheKey
@param location
@return the key under which the drawing of the element at location is
cached.
An element definition normally carries its own uuid, and that is used
directly. Definitions saved before uuids were written do not have one --
a good share of the shipped example projects are still in that state --
and they all presented the same null uuid as a key. The drawing of such
an element was therefore either rebuilt for every instance placed, or
stored under a key it shared with every other element lacking a uuid.
Derive a stable key from the location for those instead:
ElementsLocation::toString() qualifies an embedded path with the id of
the project that owns it, and project ids come from an ever-increasing
counter and are never reused, so the derived key cannot collide with an
element of another project.
*/
QUuid ElementPictureFactory::cacheKey(const ElementsLocation &location)
{
const QUuid uuid = location.uuid();
if (!uuid.isNull()) {
return uuid;
}
return QUuid::createUuidV5(QUuid(), location.toString().toUtf8());
}
/**
@brief ElementPictureFactory::getPictures
Set the picture of the element at location.
@@ -50,12 +77,7 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
return;
}
QUuid uuid = location.uuid();
if(Q_UNLIKELY(uuid.isNull()))
{
build(location, &picture, &low_picture);
return;
}
const QUuid uuid = cacheKey(location);
if(m_pictures_H.contains(uuid))
{
@@ -80,7 +102,7 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
*/
QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
{
QUuid uuid = location.uuid();
const QUuid uuid = cacheKey(location);
if (m_pixmap_H.contains(uuid)) {
return m_pixmap_H.value(uuid);
@@ -114,9 +136,7 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
painter.translate(hsx, hsy);
painter.drawPicture(0, 0, m_pictures_H.value(uuid));
if (!uuid.isNull()) {
m_pixmap_H.insert(uuid, pix);
}
m_pixmap_H.insert(uuid, pix);
return pix;
}
@@ -132,10 +152,11 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
ElementPictureFactory::primitives ElementPictureFactory::getPrimitives(
const ElementsLocation &location)
{
if(!m_primitives_H.contains(location.uuid()))
const QUuid uuid = cacheKey(location);
if(!m_primitives_H.contains(uuid))
build(location);
return m_primitives_H.value(location.uuid());
return m_primitives_H.value(uuid);
}
ElementPictureFactory::~ElementPictureFactory()
@@ -270,7 +291,7 @@ bool ElementPictureFactory::build(const ElementsLocation &location,
painter.end();
low_painter.end();
const auto uuid_ = location.uuid();
const auto uuid_ = cacheKey(location);
if (!picture) {
m_pictures_H.insert(uuid_, pic);
m_primitives_H.insert(uuid_, primitives_);
@@ -621,7 +642,11 @@ void ElementPictureFactory::setPainterStyle(const QDomElement &dom, QPainter &pa
//Get the couples style/value
const QStringList styles = dom.attribute("style").split(";", Qt::SkipEmptyParts);
QRegularExpression rx("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$");
//Built once : this runs for every primitive of every element
//instance a project places, and recompiling the pattern each time
//was the single largest cost of opening a project.
static const QRegularExpression rx(
QStringLiteral("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$"));
if (!rx.isValid())
{
qWarning() <<QObject::tr("this is an error in the code")
+1
View File
@@ -92,6 +92,7 @@ class ElementPictureFactory
ElementPictureFactory operator= (const ElementPictureFactory &);
~ElementPictureFactory();
static QUuid cacheKey(const ElementsLocation &location);
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr);
void parseElement(const QDomElement &dom, QPainter &painter, primitives &prim) const;
void parseLine (const QDomElement &dom, QPainter &painter, primitives &prim) const;
+16 -2
View File
@@ -730,11 +730,25 @@ void GenericPanel::projectInformationsChanged(QETProject *project) {
/**
@brief GenericPanel::diagramAdded
@param project
@param diagram (unused)
@param diagram the newly added diagram to select in the tree
*/
void GenericPanel::diagramAdded(QETProject *project, Diagram *diagram) {
Q_UNUSED(diagram)
addProject(project, nullptr, GenericPanel::AddChildDiagrams);
if (diagram) {
QTreeWidgetItem *projectItem = itemForProject(project);
if (projectItem) {
for (int i = 0; i < projectItem->childCount(); ++i) {
QTreeWidgetItem *child = projectItem->child(i);
if (child && child->type() == QET::Diagram
&& valueForItem<Diagram *>(child) == diagram) {
clearSelection();
setCurrentItem(child);
child->setSelected(true);
break;
}
}
}
}
emit(panelContentChanged());
}
+2
View File
@@ -159,6 +159,8 @@ void MachineInfo::send_info_to_debug()
qInfo()<< "";
qInfo()<< " System language defined in QET configuration:"<< QString(QETApp::langFromSetting().toLatin1());
qInfo()<< " language Path:"<< QString(QETApp::languagesPath().toLatin1());
qInfo()<< " Loaded QET translation:"<< QETApp::loadedQetTranslationFile();
qInfo()<< " Loaded Qt translation:"<< QETApp::loadedQtTranslationFile();
qInfo()<< " Common Elements Dir:"<< QString(QETApp::commonElementsDir().toLatin1());
qInfo()<< " Common TitleBlock Templates Dir:"<< QString(QETApp::commonTitleBlockTemplatesDir().toLatin1());
qInfo()<< " Custom Elements Dir:"<< QString(QETApp::customElementsDir().toLatin1());
+6
View File
@@ -21,6 +21,7 @@
#include "machine_info.h"
#include "qet.h"
#include "qetapp.h"
#include "qetmessagebox.h"
#include "qetproject.h"
#include "singleapplication.h"
#include "utils/qetsettings.h"
@@ -127,6 +128,11 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
// runs on a background thread referencing the project and races the
// process exit (intermittent segfault in QET::writeToFile).
QETProject::setBackupEnabled(false);
// Answer message boxes instead of showing them: opening a project
// saved by an older QElectroTech raises a warning from
// QETProject::readProjectXml(), and with nobody able to dismiss it
// QDialog::exec() would spin its event loop forever.
QET::QetMessageBox::setNonInteractive(true);
return CLIExport::run(export_app.arguments());
}
}
+1 -1
View File
@@ -228,7 +228,7 @@ void convertUriToGoTo(const QString &pdfPath)
// Find closing ')' of the URI value
int uriStart = found + sUri.size();
int closeParen = data.indexOf(')', uriStart);
int closeParen = data.indexOf(")\n", uriStart);
if (closeParen == -1) {
// Malformed — copy rest verbatim
out.append(data.mid(found));
+5 -1
View File
@@ -210,10 +210,14 @@ void ProjectView::changeFirstTab()
}
/**
@return first folio of current project
@return first folio of current project, or nullptr if the project
has no diagram.
*/
DiagramView *ProjectView::firstDiagram()
{
if (m_diagram_ids.isEmpty()) {
return(nullptr);
}
return(m_diagram_ids.first());
}
+65
View File
@@ -0,0 +1,65 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "deviceinformation.h"
#include "../qetinformation.h"
namespace {
struct DeviceField
{
QString m_key;
QString DeviceInformation::*m_member;
};
const DeviceField fields[] = {
{QETInformation::ELMT_LABEL, &DeviceInformation::m_device_tag},
{QETInformation::ELMT_MANUFACTURER, &DeviceInformation::m_manufacturer},
{QETInformation::ELMT_MANUFACTURER_REF, &DeviceInformation::m_part_number},
{QETInformation::ELMT_MODEL, &DeviceInformation::m_model},
{QETInformation::ELMT_DESCRIPTION, &DeviceInformation::m_description},
{QETInformation::ELMT_CATEGORY, &DeviceInformation::m_category},
{QETInformation::ELMT_QUANTITY, &DeviceInformation::m_quantity},
{QETInformation::ELMT_VOLTAGE_RATING, &DeviceInformation::m_voltage_rating},
{QETInformation::ELMT_CURRENT_RATING, &DeviceInformation::m_current_rating},
{QETInformation::ELMT_NOTES, &DeviceInformation::m_notes}
};
}
DeviceInformation DeviceInformation::fromContext(const DiagramContext &context)
{
DeviceInformation info;
for (const auto &field : fields) {
info.*(field.m_member) = context.value(field.m_key).toString();
}
return info;
}
DiagramContext DeviceInformation::toContext(DiagramContext context) const
{
for (const auto &field : fields)
{
const auto &value = this->*(field.m_member);
if (value.isEmpty()) {
context.remove(field.m_key);
} else {
const bool show = !context.contains(field.m_key) || context.keyMustShow(field.m_key);
context.addValue(field.m_key, value, show);
}
}
return context;
}
+47
View File
@@ -0,0 +1,47 @@
/*
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 DEVICEINFORMATION_H
#define DEVICEINFORMATION_H
#include "../diagramcontext.h"
#include <QString>
/**
* Device properties backed by ElementData::m_informations, not a second store.
* Missing properties remain empty. Quantity and ratings retain the existing
* free-text representation (including decimal quantities and units).
*/
struct DeviceInformation
{
QString m_device_tag;
QString m_manufacturer;
QString m_part_number;
QString m_model;
QString m_description;
QString m_category;
QString m_quantity;
QString m_voltage_rating;
QString m_current_rating;
QString m_notes;
static DeviceInformation fromContext(const DiagramContext &context);
DiagramContext toContext(DiagramContext context = DiagramContext()) const;
};
#endif // DEVICEINFORMATION_H
+7
View File
@@ -36,6 +36,7 @@ XRefProperties::XRefProperties()
m_master_label = "%f-%l%c";
m_slave_label = "(%f-%l%c)";
m_offset = 0;
m_slave_offset = 0;
m_xref_pos = Qt::AlignBottom;
}
@@ -56,6 +57,8 @@ void XRefProperties::toSettings(QSettings &settings,
settings.setValue(prefix % "snapto", snap);
int offset = m_offset;
settings.setValue(prefix % "offset", offset);
int slave_offset = m_slave_offset;
settings.setValue(prefix % "slave_offset", slave_offset);
QString master_label = m_master_label;
settings.setValue(prefix % "master_label", master_label);
QString slave_label = m_slave_label;
@@ -86,6 +89,7 @@ void XRefProperties::fromSettings(const QSettings &settings,
QString snap = settings.value(prefix % "snapto", "label").toString();
snap == "bottom"? m_snap_to = Bottom : m_snap_to = Label;
m_offset = settings.value(prefix % "offset", "0").toInt();
m_slave_offset = settings.value(prefix % "slave_offset", "0").toInt();
m_master_label = settings.value(prefix % "master_label", "%f-%l%c").toString();
m_slave_label = settings.value(prefix % "slave_label", "(%f-%l%c)").toString();
@@ -123,6 +127,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
int offset = m_offset;
xml_element.setAttribute("offset", QString::number(offset));
xml_element.setAttribute("slave_offset", QString::number(m_slave_offset));
QString master_label = m_master_label;
xml_element.setAttribute("master_label", master_label);
QString slave_label = m_slave_label;
@@ -157,6 +162,7 @@ bool XRefProperties::fromXml(const QDomElement &xml_element) {
m_xref_pos = Qt::AlignBottom;
m_offset = xml_element.attribute("offset", "0").toInt();
m_slave_offset = xml_element.attribute("slave_offset", "0").toInt();
m_master_label = xml_element.attribute("master_label", "%f-%l%c");
m_slave_label = xml_element.attribute("slave_label","(%f-%l%c)");
foreach (QString key, m_prefix_keys) {
@@ -199,6 +205,7 @@ bool XRefProperties::operator ==(const XRefProperties &xrp) const{
&& m_prefix == xrp.m_prefix
&& m_master_label== xrp.m_master_label
&& m_offset == xrp.m_offset
&& m_slave_offset== xrp.m_slave_offset
&& m_xref_pos == xrp.m_xref_pos
&& m_slave_label == xrp.m_slave_label);
}
+4
View File
@@ -80,6 +80,9 @@ class XRefProperties : public PropertiesInterface
void setOffset(const int offset) {m_offset = offset;}
int offset() const {return m_offset;}
void setSlaveOffset(const int offset) {m_slave_offset = offset;}
int slaveOffset() const {return m_slave_offset;}
void setKey(QString& key) {m_key = key;}
private:
@@ -93,6 +96,7 @@ class XRefProperties : public PropertiesInterface
QString m_master_label;
QString m_slave_label;
int m_offset;
int m_slave_offset;
QString m_key;
};
+13 -5
View File
@@ -297,11 +297,19 @@ QString QET::ElementsAndConductorsSentence(
if (images_count) {
if (!text.isEmpty()) text += ", ";
text += QObject::tr(
"%n image(s)",
"part of a sentence listing the content of a diagram",
images_count
);
// Qt's %n only selects a grammatical singular/plural form (the
// "(s)" convention used by every other count here) -- it never
// spells the number out as a word, so getting "une image"
// instead of the literal "1 image" for the single-item case
// means handling that count outside %n entirely, with its own
// fixed string.
text += images_count == 1
? QObject::tr("une image", "part of a sentence listing the content of a diagram")
: QObject::tr(
"%n images",
"part of a sentence listing the content of a diagram",
images_count
);
}
if (shapes_count) {
+34 -5
View File
@@ -197,6 +197,26 @@ QETApp *QETApp::instance()
return m_qetapp;
}
/**
@brief QETApp::loadedQetTranslationFile
@return path of the QET .qm file actually loaded, empty if none
(diagnostic helper for the startup log, see MachineInfo)
*/
QString QETApp::loadedQetTranslationFile()
{
return m_qetapp ? m_qetapp->qetTranslator.filePath() : QString();
}
/**
@brief QETApp::loadedQtTranslationFile
@return path of the Qt .qm file actually loaded, empty if none
(diagnostic helper for the startup log, see MachineInfo)
*/
QString QETApp::loadedQtTranslationFile()
{
return m_qetapp ? m_qetapp->qtTranslator.filePath() : QString();
}
/**
@brief QETApp::setLanguage
Change the language used by the application.
@@ -234,14 +254,21 @@ void QETApp::setLanguage(const QString &desired_language) {
// desired_language may be a full locale such as "pt_BR": try that exact
// translation, then the base language ("pt"), then fall back to English.
// French is the application's source language and needs no translation.
// A .qm compiled from an untranslated .ts (0% done) loads "successfully"
// but is empty: treat it as missing, so the user falls back to English
// instead of silently getting the French source strings.
const QString base_language = desired_language.section('_', 0, 0);
bool loaded = qetTranslator.load("qet_" + desired_language, languages_path);
auto loadQet = [this, &languages_path](const QString &name) {
return qetTranslator.load(name, languages_path)
&& !qetTranslator.isEmpty();
};
bool loaded = loadQet("qet_" + desired_language);
if (!loaded && base_language != desired_language)
loaded = qetTranslator.load("qet_" + base_language, languages_path);
loaded = loadQet("qet_" + base_language);
if (!loaded && base_language != "fr") {
// use of the English version by default
// utilisation de la version anglaise par defaut
if(!qetTranslator.load("qet_en", languages_path))
if(!loadQet("qet_en"))
qWarning() << "failed to load"
<< "qet_en" << languages_path << "(" << __FILE__
<< __LINE__ << __FUNCTION__ << ")";
@@ -267,7 +294,7 @@ QString QETApp::langFromSetting()
{
QSettings settings;
system_language = settings.value("lang", "system").toString();
if(system_language == "system") {
if ((system_language == "system") || (system_language == QString())) {
// Keep the full locale (e.g. "pt_BR"), not just the base language
// ("pt"): QET ships regional translations (pt_BR, nl_BE, nl_NL) and
// truncating here loaded the wrong one. setLanguage() falls back to
@@ -1423,7 +1450,7 @@ QFont QETApp::diagramTextsItemFont(qreal size)
@param size
@return dynamic text font with PointSizeF(size)
*/
QFont QETApp::dynamicTextsItemFont(qreal size)
QFont QETApp::dynamicTextsItemFont(qreal size)
{
QSettings settings;
//Font to use
@@ -1975,6 +2002,7 @@ void QETApp::openTitleBlockTemplate(const TitleBlockTemplateLocation &location,
qet_template_editor -> setOpenForDuplication(duplicate);
qet_template_editor -> edit(location);
qet_template_editor -> show();
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
}
/**
@@ -1986,6 +2014,7 @@ void QETApp::openTitleBlockTemplate(const QString &filepath) {
QETTitleBlockTemplateEditor *qet_template_editor = new QETTitleBlockTemplateEditor();
qet_template_editor -> edit(filepath);
qet_template_editor -> show();
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
}
/**
+2
View File
@@ -102,6 +102,8 @@ class QETApp : public QObject
static QString documentDir();
static QString pictureDir();
static QString languagesPath();
static QString loadedQetTranslationFile();
static QString loadedQtTranslationFile();
static QString realPath(const QString &);
static QString symbolicPath(const QString &);
static QStringList handledFileExtensions();
+94 -10
View File
@@ -27,6 +27,7 @@
#include "diagramevent/diagrameventaddpdf.h"
#endif
#include "diagramevent/diagrameventaddshape.h"
#include "diagramevent/diagrameventaddpath.h"
#include "diagramevent/diagrameventaddtext.h"
#include "diagramview.h"
#include "elementspanelwidget.h"
@@ -56,10 +57,12 @@
#include "ui/diagrameditorhandlersizewidget.h"
#include "TerminalStrip/ui/addterminalstripitemdialog.h"
#include "wiringlistexport.h"
#include "ui/wiringlistdialog.h"
#include "ui/terminalnumberingdialog.h"
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QTimer>
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
#else
@@ -135,8 +138,9 @@ QETDiagramEditor::QETDiagramEditor(const QStringList &files, QWidget *parent) :
connect(&m_workspace, &QMdiArea::subWindowActivated, this, &QETDiagramEditor::subWindowActivated);
connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &QETDiagramEditor::slot_updatePasteAction);
readSettings();
readSettings(); // restoreGeometry before show()
show();
readSettingsState(); // restoreState() must be called after show() in Qt6
//If valid file path is given as arguments
uint opened_projects = 0;
@@ -516,6 +520,17 @@ void QETDiagramEditor::setUpActions()
}
});
// Show the wiring list read from the project database
m_project_wiring_list_view = new QAction(QET::Icons::DocumentSpreadsheet, tr("Liste de câblage (base de données)"), this);
connect(m_project_wiring_list_view, &QAction::triggered, [this]() {
QETProject *project = this->currentProject();
if (project)
{
WiringListDialog dialog(project, this);
dialog.exec();
}
});
// Terminal Numbering
m_terminal_numbering = new QAction(QET::Icons::TerminalStrip, tr("Numérotation automatique des bornes"), this);
connect(m_terminal_numbering, &QAction::triggered, this, &QETDiagramEditor::slot_terminalNumbering);
@@ -717,6 +732,19 @@ void QETDiagramEditor::setUpActions()
connect(&m_zoom_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::zoomGroupTriggered);
//Adding action (add text, image, shape...)
// Exclusive (the default) prevents the active action from ever being
// unchecked by clicking it again -- Qt only lets you switch to a
// different one. That's exactly why clicking an already-active
// tool's own icon never actually deactivated it: confirmed by
// logging every addItemGroupTriggered() call and finding
// isChecked()==true on *every* click, including the one meant to
// turn the tool off -- the click-handling code's own "was this an
// uncheck?" check was correct, its precondition just could never
// occur. ExclusiveOptional allows exactly one more transition:
// clicking the currently-checked action unchecks it, leaving none
// checked, which is required for "click the active tool to turn it
// off" to mean anything at the QAction level at all.
m_add_item_actions_group.setExclusionPolicy(QActionGroup::ExclusionPolicy::ExclusiveOptional);
QAction *add_text = m_add_item_actions_group.addAction(QET::Icons::PartTextField, tr("Ajouter un champ de texte"));
QAction *add_image = m_add_item_actions_group.addAction(QET::Icons::adding_image, tr("Ajouter une image"));
#ifdef QET_HAS_QTPDF
@@ -726,6 +754,7 @@ void QETDiagramEditor::setUpActions()
QAction *add_rectangle = m_add_item_actions_group.addAction(QET::Icons::PartRectangle, tr("Ajouter un rectangle"));
QAction *add_ellipse = m_add_item_actions_group.addAction(QET::Icons::PartEllipse, tr("Ajouter une ellipse"));
QAction *add_polyline = m_add_item_actions_group.addAction(QET::Icons::PartPolygon, tr("Ajouter une polyligne"));
QAction *add_path = m_add_item_actions_group.addAction(QET::Icons::PartBezier, tr("Ajouter une courbe"));
QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Ajouter un plan de bornes"));
add_text ->setStatusTip(tr("Ajoute un champ de texte sur le folio actuel"));
@@ -737,6 +766,7 @@ void QETDiagramEditor::setUpActions()
add_rectangle->setStatusTip(tr("Ajoute un rectangle sur le folio actuel"));
add_ellipse ->setStatusTip(tr("Ajoute une ellipse sur le folio actuel"));
add_polyline ->setStatusTip(tr("Ajoute une polyligne sur le folio actuel"));
add_path ->setStatusTip(tr("Ajoute une courbe de Bézier sur le folio actuel"));
add_terminal_strip->setStatusTip(tr("Ajoute un plan de bornier sur le folio actuel"));
add_text ->setData(QStringLiteral("text"));
@@ -748,6 +778,7 @@ void QETDiagramEditor::setUpActions()
add_rectangle->setData(QStringLiteral("rectangle"));
add_ellipse ->setData(QStringLiteral("ellipse"));
add_polyline ->setData(QStringLiteral("polyline"));
add_path ->setData(QStringLiteral("path"));
add_terminal_strip->setData(QStringLiteral("terminal_strip"));
add_text->setCheckable(true);
@@ -755,6 +786,7 @@ void QETDiagramEditor::setUpActions()
add_rectangle->setCheckable(true);
add_ellipse->setCheckable(true);
add_polyline->setCheckable(true);
add_path->setCheckable(true);
connect(&m_add_item_actions_group, &QActionGroup::triggered, this, &QETDiagramEditor::addItemGroupTriggered);
@@ -920,6 +952,7 @@ void QETDiagramEditor::setUpMenu()
menu_project -> addAction(m_terminal_strip_dialog);
menu_project -> addAction(m_project_terminalBloc);
menu_project -> addAction(m_project_export_wiring_list);
menu_project -> addAction(m_project_wiring_list_view);
menu_project -> addAction(m_terminal_numbering);
#ifdef QET_EXPORT_PROJECT_DB
menu_project -> addSeparator();
@@ -1554,6 +1587,27 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
if (Q_UNLIKELY (!currentDiagramView() || !currentDiagramView()->diagram() || value.isEmpty())) return;
Diagram *d = currentDiagramView()->diagram();
// This action group allows deselecting the currently-active tool by
// clicking its own icon again, not just switching between tools --
// that click still fires this slot (QActionGroup::triggered fires on
// every click in the group, checked-state-changing or not), so
// without this check it would unconditionally construct *another*
// instance of the same tool and reactivate it, leaving the tool
// fully active in the canvas while its own toolbar button shows
// unchecked -- exactly backwards from what the click was for.
// Scoped to checkable actions specifically: image/pdf/terminal_strip
// are one-shot actions (pick a file, open a dialog) with no
// persistent "active tool" state at all, and aren't even checkable
// -- isChecked() on them is always false, so without this guard
// they'd hit the branch above on every single click and never
// reach their own handling below.
if (action->isCheckable() && !action->isChecked())
{
d->clearEventInterface();
return;
}
DiagramEventInterface *diagram_event = nullptr;
if (value == "line")
@@ -1563,12 +1617,10 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
else if (value == "ellipse")
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Ellipse);
else if (value == "polyline")
{
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Polygon);
statusBar()-> showMessage(tr("Double-click pour terminer la forme, Click droit pour annuler le dernier point"));
connect(diagram_event, &DiagramEventInterface::destroyed, [this]() {
statusBar()->clearMessage();
});
else if (value == "path")
{
diagram_event = new DiagramEventAddPath (d);
}
else if (value == "image")
{
@@ -1611,6 +1663,22 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
{
d->setEventInterface(diagram_event);
connect(diagram_event, &DiagramEventInterface::destroyed, [action]() {action->setChecked(false);});
// Defensive: on this style/theme, the toolbar button bound to an
// exclusive-group action doesn't reliably repaint its checked
// appearance after the group briefly had *no* action checked at
// all (the gap between unchecking one tool and checking one
// again, even the same one) -- confirmed by tracing a real
// recording frame by frame: the status bar correctly showed this
// tool's "before first click" hint (which can only appear from
// inside a freshly-constructed tool's own constructor, so the
// action's checked state and the tool's activation were both
// genuinely correct) while the button itself stayed visually
// unchecked for a sustained period. Forcing an explicit repaint
// here makes the button's appearance match its actual state
// regardless of whether Qt's own change notification fired
// correctly.
if (QWidget *button = m_add_item_tool_bar->widgetForAction(action))
button->update();
}
}
@@ -1734,6 +1802,7 @@ void QETDiagramEditor::slot_updateActions()
m_project_export_conductor_num-> setEnabled(opened_project);
m_terminal_strip_dialog -> setEnabled(editable_project);
m_project_export_wiring_list -> setEnabled(opened_project);
m_project_wiring_list_view -> setEnabled(opened_project);
m_terminal_numbering -> setEnabled(editable_project);
#ifdef QET_EXPORT_PROJECT_DB
m_export_project_db -> setEnabled(editable_project);
@@ -2122,6 +2191,7 @@ void QETDiagramEditor::openBackupFiles(QList<KAutoSaveFile *> backup_files)
}
delete project;
DialogWaiting::dropInstance();
continue;
}
addProject(project);
DialogWaiting::dropInstance();
@@ -2227,10 +2297,6 @@ void QETDiagramEditor::readSettings()
QVariant geometry = settings.value("diagrameditor/geometry");
if (geometry.isValid()) restoreGeometry(geometry.toByteArray());
// etat de la fenetre (barres d'outils, docks...)
QVariant state = settings.value("diagrameditor/state");
if (state.isValid()) restoreState(state.toByteArray());
// gestion des projets (onglets ou fenetres)
bool tabbed = settings.value("diagrameditor/viewmode", "tabbed") == "tabbed";
if (tabbed) {
@@ -2240,6 +2306,24 @@ void QETDiagramEditor::readSettings()
}
}
/**
@brief QETDiagramEditor::readSettingsState
Restore the window state (docks, toolbars).
Must be called AFTER show() in Qt6 for restoreState() to work correctly.
*/
void QETDiagramEditor::readSettingsState()
{
QSettings settings;
// etat de la fenetre (barres d'outils, docks...)
QVariant state = settings.value("diagrameditor/state");
if (state.isValid()) {
if (!restoreState(state.toByteArray())) {
settings.remove("diagrameditor/state");
}
}
}
/**
@brief QETDiagramEditor::writeSettings
Write the settings
+2
View File
@@ -127,6 +127,7 @@ class QETDiagramEditor : public QETMainWindow
void setWindowedMode();
void setTabbedMode();
void readSettings();
void readSettingsState();
void writeSettings();
void activateProject(QETProject *);
void activateProject(ProjectView *);
@@ -208,6 +209,7 @@ class QETDiagramEditor : public QETMainWindow
*m_project_terminalBloc, ///< generate terminal block
*m_project_export_conductor_num,///<Export the wire num to csv
*m_project_export_wiring_list, ///< Action to export the wiring list
*m_project_wiring_list_view, ///< Action to show the wiring list read from the project database
*m_terminal_numbering, ///< Action to launch terminal numbering
*m_export_project_db, ///Export to file the internal database of the current project
*m_tile_window, ///< Show MDI subwindows as tile
@@ -21,6 +21,7 @@
#include "../../createdxf.h"
#include "../../diagram.h"
#include "../../elementprovider.h"
#include "../../qetmessagebox.h"
#include "../../qetxml.h"
#include "../../utils/qetutils.h"
#include "projectdbmodel.h"
@@ -119,7 +120,7 @@ void QetGraphicsTableItem::checkInsufficientRowsCount(
text = tr("Les information à afficher sont supérieurs à la quantité maximal pouvant être affiché par le tableau.\n"
"Veuillez ajouter un nouveau tableau ou regler le tableau existant afin d'afficher l'integralité des informations.");
}
QMessageBox::information(parent, tr("Limitation de tableau"), text);
QET::QetMessageBox::information(parent, tr("Limitation de tableau"), text);
}
}
+7 -2
View File
@@ -1011,10 +1011,14 @@ bool Conductor::fromXml(QDomElement &dom_element)
//that field was introduced (see terminal1/terminal2 handling in
//toXml() below).
m_uuid = QUuid(dom_element.attribute(QStringLiteral("uuid")));
m_persist_uuid = !m_uuid.isNull();
if (m_uuid.isNull()) {
//Absent, empty or malformed: mint one. A null uuid is not a usable
//identity -- every conductor carrying one would collide with every
//other on the conductor table's primary key.
//other on the conductor table's primary key. It's runtime-only,
//though: toXml() must not write it back out, or a legacy file
//with no conductor uuids gets a freshly different one on every
//single load-and-resave (see #754).
m_uuid = QUuid::createUuid();
}
@@ -1056,7 +1060,8 @@ QDomElement Conductor::toXml(QDomDocument &dom_document,
{
QDomElement dom_element = dom_document.createElement("conductor");
dom_element.setAttribute("uuid", m_uuid.toString());
if (m_persist_uuid)
dom_element.setAttribute("uuid", m_uuid.toString());
dom_element.setAttribute("x", QString::number(pos().x()));
dom_element.setAttribute("y", QString::number(pos().y()));
+5 -1
View File
@@ -79,7 +79,7 @@ class Conductor : public QGraphicsObject
Diagram *diagram() const;
ConductorTextItem *textItem() const;
QUuid uuid() const {return m_uuid;}
void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this conductor
void newUuid() {m_uuid = QUuid::createUuid(); m_persist_uuid = true;} //create new uuid for this conductor
void updatePath(const QRectF & = QRectF());
//This method do nothing, it's only made to be used with Q_PROPERTY
@@ -209,6 +209,10 @@ class Conductor : public QGraphicsObject
bool m_valid;
bool m_freeze_label = false;
QUuid m_uuid;
/// false when m_uuid was synthesized by fromXml() because the
/// file had none -- toXml() must not persist that value, or
/// every reload mints and saves a new random one (see #754).
bool m_persist_uuid = true;
/// QPen et QBrush objects used to draw conductors
static QPen conductor_pen;
+48 -8
View File
@@ -117,6 +117,10 @@ void CrossRefItem::setUpConnection()
m_update_connection << connect(m_element, &Element::rotationChanged, this, &CrossRefItem::autoPos);
set=true;
}
// For PLC masters, always set up connections for update notifications
// (page reorder, diagram removal, etc.)
if (!set && m_element->elementData().m_master_type == ElementData::PLC && !m_text && !m_group)
set = true;
if(set)
{
@@ -234,9 +238,24 @@ void CrossRefItem::updateLabel()
qp.setPen(pen_);
qp.setFont(QETApp::diagramTextsFont(5));
// PLC table is drawn by Element::drawPlcTable(), not by CrossRefItem
// PLC table is drawn and managed entirely by CrossRefItem
if (m_element->elementData().m_master_type == ElementData::PLC)
{
// Position at the PLC table position from the .elmt definition
QList<QPointF> positions = m_element->plcTablePositions();
QPointF pos = positions.isEmpty() ? QPointF(0, 0) : positions.first();
setPos(pos);
// Populate m_hovered_contacts_map using drawAsPlcTable on a
// dummy painter (m_update_map=true).
m_update_map = true;
drawAsPlcTable(qp);
m_update_map = false;
update();
QTimer::singleShot(0, this, [this]{ update(); });
return;
}
//Draw cross or contact, only if master element is linked.
else if (! m_element->linkedElements().isEmpty())
{
@@ -263,6 +282,11 @@ void CrossRefItem::updateLabel()
*/
void CrossRefItem::autoPos()
{
// For PLC masters, position is set by updateLabel() based on
// m_plc_table_positions - don't override it here.
if (m_element->elementData().m_master_type == ElementData::PLC)
return;
//We calculate the position according to the snapTo of the xrefproperties
if (m_properties.snapTo() == XRefProperties::Bottom)
QGIUtility::centerToBottomDiagram(this,
@@ -328,7 +352,9 @@ void CrossRefItem::paint(
// confirmed by analysis of 19+ coredumps.
// m_update_map=false: draw functions do not overwrite m_hovered_contacts_map.
// PLC table is drawn by Element::drawPlcTable(), not by CrossRefItem
// PLC: do not draw here (Element::drawPlcTable handles visual rendering).
// The m_hovered_contacts_map was populated in updateLabel() for
// click navigation and PDF hyperlink injection.
if (m_element->elementData().m_master_type == ElementData::PLC)
return;
@@ -1378,11 +1404,24 @@ void CrossRefItem::drawAsPlcTable(QPainter &painter)
headers[COL_COMMENT] = QObject::tr("Commentaire");
headers[COL_CROSSREF] = QObject::tr("Réf. croisée");
// Build list of visible columns
// Build visible columns (must match Element::drawPlcTable logic)
QList<int> visible_cols;
for (int i = 0; i < COL_COUNT; ++i) {
if (plc_data.colVisible.value(i, true))
visible_cols.append(i);
if (!plc_data.columnOrder.isEmpty()) {
for (int logical : plc_data.columnOrder) {
if (logical >= 0 && logical < COL_COUNT
&& plc_data.colVisible.value(logical, true)
&& !visible_cols.contains(logical))
visible_cols.append(logical);
}
for (int i = 0; i < COL_COUNT; ++i) {
if (plc_data.colVisible.value(i, true) && !visible_cols.contains(i))
visible_cols.append(i);
}
} else {
for (int i = 0; i < COL_COUNT; ++i) {
if (plc_data.colVisible.value(i, true))
visible_cols.append(i);
}
}
if (visible_cols.isEmpty())
return;
@@ -1405,7 +1444,7 @@ void CrossRefItem::drawAsPlcTable(QPainter &painter)
}
qreal row_h = plc_data.rowHeight > 0 ? plc_data.rowHeight : 8.0;
qreal header_h = row_h + 2.0;
qreal header_h = plc_data.showHeaders ? (row_h + 2.0) : 0;
// Calculate total width
qreal total_width = 0;
@@ -1431,6 +1470,7 @@ void CrossRefItem::drawAsPlcTable(QPainter &painter)
qreal total_height;
int block_count = block_starts.size();
qreal block_total_width = total_width; // width of one block, before multi-block scaling
if (block_count > 1) {
int max_rows = 0;
@@ -1455,7 +1495,7 @@ void CrossRefItem::drawAsPlcTable(QPainter &painter)
// Draw header row
for (int block = 0; block < block_count; ++block) {
qreal block_x = block * (col_widths.value(visible_cols.first(), 30) * visible_cols.size() + 3);
qreal block_x = block * (block_total_width + 3);
qreal cx = block_x;
// Draw column headers
File diff suppressed because it is too large Load Diff
+153 -2
View File
@@ -19,9 +19,18 @@
#define DIAGRAM_IMAGE_ITEM_H
#include "qetgraphicsitem.h"
#include "shapetransform.h"
#include "../ui/imagetransparentcolordialog.h"
#include <QColor>
#include <QList>
#include <QStyleOptionGraphicsItem>
#include <QVector>
class QDomElement;
class QDomDocument;
class QGraphicsSceneContextMenuEvent;
class QetGraphicsHandlerItem;
/**
This class represents a selectable, movable and editable image on a
@@ -30,6 +39,23 @@ class QDomDocument;
*/
class DiagramImageItem : public QetGraphicsItem {
Q_OBJECT
Q_PROPERTY(QPixmap pixmap READ pixmap WRITE setPixmap NOTIFY pixmapChanged)
Q_PROPERTY(qreal scaleFactorX READ scaleFactorX WRITE setScaleFactorX NOTIFY transformChanged)
Q_PROPERTY(qreal scaleFactorY READ scaleFactorY WRITE setScaleFactorY NOTIFY transformChanged)
Q_PROPERTY(qreal rotationAngle READ rotationAngle WRITE setRotationAngle NOTIFY transformChanged)
Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged)
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
// A second, deliberately non-compensating property on the SAME
// underlying value -- setPivot() (above) intentionally adjusts
// pos() to keep the image visually in place, which is exactly
// wrong for crop()'s own undo chain: crop() already computes the
// correct final pos() itself (accounting for the crop, not just
// the pivot move) and pushes it as its own separate "pos" command,
// so a chained "pivot" step going through the compensating setter
// would silently overwrite that already-correct pos() a second
// time. rawPivot exists solely for that one caller.
Q_PROPERTY(QPointF rawPivot READ pivot WRITE setPivotRaw NOTIFY transformChanged)
// constructors, destructor
public:
@@ -40,6 +66,19 @@ class DiagramImageItem : public QetGraphicsItem {
// attributes
public:
enum { Type = UserType + 1007 };
// A deliberately smaller, image-specific set than QetShapeItem's own
// HandleMode/HandleRole: images have no path/polygon/arc concepts at
// all, so reusing those enums directly would pull in a great deal of
// irrelevant baggage for no benefit. RotateSkew's math (rotate,
// skew, and their shared scaleAndShearOffset()/scaleOnlyOffset()
// helpers) mirrors QetShapeItem's own identical formulas -- adapted
// to this class's own, simpler 4-corner/4-edge/1-pivot handle set,
// not shared code, since the two classes' broader handle roles
// differ too much (no path/polygon/arc concepts here) for sharing
// the dispatcher itself to be worth an indirect abstraction.
enum class HandleMode { Size, RotateSkew };
enum class HandleRole { Resize, Rotate, SkewEdge, Pivot };
// methods
public:
@@ -54,13 +93,125 @@ class DiagramImageItem : public QetGraphicsItem {
virtual QDomElement toXml(QDomDocument &) const;
void editProperty() override;
void setPixmap(const QPixmap &pixmap);
QPixmap pixmap() const { return pixmap_; }
QRectF boundingRect() const override;
QString name() const override;
/// DXF export: replay this item's paint() on an arbitrary QPainter
/// (e.g. one targeting DxfPaintDevice). paint() itself stays
/// protected, as it should for the normal
/// QGraphicsScene/QGraphicsView paint contract - this is a
/// deliberate, narrow escape hatch for exporters, not a general
/// relaxation of that contract. Matches CrossRefItem::paintForExport()
/// exactly, for the identical reason.
void paintForExport(QPainter *painter)
{
QStyleOptionGraphicsItem option;
paint(painter, &option, nullptr);
}
qreal scaleFactorX() const { return m_transform.scaleX; }
qreal scaleFactorY() const { return m_transform.scaleY; }
void setScaleFactorX(qreal factor);
void setScaleFactorY(qreal factor);
qreal rotationAngle() const { return m_transform.rotation; }
void setRotationAngle(qreal angle);
qreal skewX() const { return m_transform.skewX; }
void setSkewX(qreal skew);
qreal skewY() const { return m_transform.skewY; }
void setSkewY(qreal skew);
QPointF pivot() const { return m_transform.pivot; }
void setPivot(const QPointF &pivot);
void setPivotRaw(const QPointF &pivot);
signals:
void pixmapChanged();
void transformChanged();
protected:
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
void replaceImage();
void mirror(bool horizontal);
void setTransparentColor();
void crop();
void restoreAspectRatio();
void saveImageAs();
void saveOriginalImageAs();
void saveImagePixmapAs(const QPixmap &pixmap, const QString &dialogTitle, bool hasTransparency);
static bool writeRasterAsSvg(const QPixmap &pixmap, const QString &path);
static QPixmap computeDisplayPixmap(const QPixmap &base, const QRect &cropRect, const QList<ImageTransparentColorDialog::PickedColor> &colors);
void toggleHandleMode();
HandleMode nextHandleMode() const;
static QString handleModeLabel(HandleMode mode);
void updateModeHint();
void refreshInteractionHints();
QString currentModeStatusHint() const;
void rebuildHandles();
void repositionHandles();
void clearHandles();
void resetPivotToBoundingRectCenter();
void handlerMousePressEvent(int index, Qt::KeyboardModifiers mods);
void handlerMouseMoveEvent(int index, QGraphicsSceneMouseEvent *event);
void handlerMouseReleaseEvent(int index);
void dragResize(int index, const QPointF &localPos, Qt::KeyboardModifiers mods);
void dragRotateHandle(int cornerIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
void dragSkewHandle(int edgeIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
void dragPivot(const QPointF &localPos);
QPointF scaleOnlyOffset(const QPointF &localPoint) const;
QPointF scaleAndShearOffset(const QPointF &localPoint) const;
QPointF handlePosition(int index) const;
static QPointF cornerPosition(int cornerIndex, qreal w, qreal h);
static QPointF edgeMidpointPosition(int edgeIndex, qreal w, qreal h);
static QColor colorForHandleRole(HandleRole role);
static QString hintForHandleRole(HandleRole role);
void showStatusHint(const QString &text) const;
void clearStatusHint() const;
protected:
QPixmap pixmap_;
// The true, pristine original -- never itself cropped or colour-
// keyed. pixmap_ (the displayed result) is always re-derived from
// this plus m_crop_rect and m_transparent_colors (each colour
// carrying its own tolerance), via computeDisplayPixmap(). Without
// keeping this separate, re-opening either the crop or
// transparency dialog after using the other would show an
// already-modified image as if it were the source -- areas already
// cropped away or coloured out would be gone for good, with no way
// to recover or adjust them, only start over. Updated by whatever
// genuinely replaces or reorients the image's actual content
// (construction, replaceImage(), and mirror(), which also mirrors
// m_crop_rect to keep referring to the same region of the
// now-flipped base) -- never by crop() or setTransparentColor()
// themselves, which only ever change which subset of this base is
// shown.
QPixmap m_base_pixmap;
QRect m_crop_rect; // relative to m_base_pixmap; equals m_base_pixmap.rect() when nothing has been cropped
QList<ImageTransparentColorDialog::PickedColor> m_transparent_colors;
// Independent scaleX/scaleY here is the actual point of this whole
// member: QGraphicsItem::scale() is a single, uniform float, which
// is exactly why an image could never break its own aspect ratio
// before this. skewX/skewY exist in the struct but are never
// written by anything below -- deliberately deferred, not an
// oversight (see the .cpp for why).
ShapeTransform m_transform;
bool m_pivotIsCustom = false;
HandleMode m_handleMode = HandleMode::Size;
QVector<QetGraphicsHandlerItem *> m_handler_vector;
QVector<HandleRole> m_handleRoles;
int m_vector_index = -1;
QPointF m_original_pos; // scene position at the start of a resize/rotate/pivot drag, for Escape-to-cancel
ShapeTransform m_original_transform;
bool m_deferHandleReposition = false; // see setPivot()'s comment
bool m_resizeCenterAnchored = false; // decided once, at press time -- see handlerMousePressEvent()'s comment for why, mirroring the identical fix already made for shape creation
};
#endif
@@ -261,6 +261,29 @@ ElementTextItemGroup *DynamicElementTextItem::parentGroup() const
return nullptr;
}
/**
@brief DynamicElementTextItem::masterElement
@return the master element for slave elements, or the parent element for others.
For PLC slaves, returns the actual master from linkedElements() instead of
m_master_element (which points to self because elementUseForInfo() returns
self for PLC slaves to resolve %{plc_*} variables).
*/
Element *DynamicElementTextItem::masterElement() const
{
Element *elmt = parentElement();
if (!elmt)
return m_master_element.data();
if (elmt->linkType() == Element::Slave &&
elmt->elementData().m_slave_type == ElementData::PLCSlave &&
!elmt->linkedElements().isEmpty())
{
return elmt->linkedElements().first();
}
return m_master_element.data();
}
/**
@brief DynamicElementTextItem::elementUseForInfo
@return a pointer to the element we must use for the variable information.
@@ -562,7 +585,7 @@ void DynamicElementTextItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
setDefaultTextColor(m_user_color);
m_user_color = QColor(); //m_user_color is now invalid
if(m_slave_Xref_item)
m_slave_Xref_item->setDefaultTextColor(Qt::black);
m_slave_Xref_item->setDefaultTextColor(color());
}
// Shift or no parent initiates movement of dynamic text, otherwise movement of parent element
@@ -801,7 +824,7 @@ bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *ev
return true;
}
else if(event->type() == QEvent::GraphicsSceneHoverLeave) {
m_slave_Xref_item->setDefaultTextColor(Qt::black);
m_slave_Xref_item->setDefaultTextColor(color());
return true;
}
else if(event->type() == QEvent::GraphicsSceneMouseDoubleClick) {
@@ -1368,36 +1391,72 @@ void DynamicElementTextItem::updateXref()
!m_parent_element.data()->linkedElements().isEmpty())
{
Element *master_elmt = m_parent_element.data()->linkedElements().first();
if(master_elmt && !parentGroup() &&
(
if(master_elmt && !parentGroup())
{
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
//Champ de texte: store xref in element informations
if(xrp.getXrefPos() == Qt::AlignHCenter)
{
if(m_text_from == DynamicElementTextItem::ElementInfo && m_info_name == "xref")
{
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
DiagramContext dc = m_parent_element->elementInformations();
if(dc.value("xref").toString() != xref_label)
{
dc.addValue("xref", xref_label);
m_parent_element->setElementInformations(dc);
}
//Set up connections for future updates
if(m_update_slave_Xref_connection.isEmpty())
{
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
}
return;
}
//For label/composite text: fall through to cleanup (delete m_slave_Xref_item)
}
else if(
(m_text_from == DynamicElementTextItem::ElementInfo && m_info_name == "label") ||
(m_text_from == DynamicElementTextItem::CompositeText && m_composite_text.contains("%{label}"))
)
)
{
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
if(!m_slave_Xref_item)
{
m_slave_Xref_item = new QGraphicsTextItem(xref_label, this);
m_slave_Xref_item->setFont(QETApp::diagramTextsFont(5));
m_slave_Xref_item->setDefaultTextColor(Qt::black);
m_slave_Xref_item->installSceneEventFilter(this);
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
if(!m_slave_Xref_item)
{
m_slave_Xref_item = new QGraphicsTextItem(xref_label, this);
m_slave_Xref_item->setFont(QETApp::diagramTextsFont(5));
// Match the parent text's user-configurable color instead of
// hardcoding black, which renders invisible under dark themes
// or stylesheets that do not affect this sub-item the same way
// they affect the parent (bugtracker #247).
m_slave_Xref_item->setDefaultTextColor(color());
m_slave_Xref_item->installSceneEventFilter(this);
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref);
m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref);
}
else
m_slave_Xref_item->setPlainText(xref_label);
setXref_item(xrp.getXrefPos(), xrp.slaveOffset());
return;
}
else
m_slave_Xref_item->setPlainText(xref_label);
setXref_item(xrp.getXrefPos());
return;
}
}
}
@@ -1414,6 +1473,29 @@ void DynamicElementTextItem::updateXref()
delete m_slave_Xref_item;
m_slave_Xref_item = nullptr;
m_update_slave_Xref_connection.clear();
//If position changed to Champ de texte, store xref in element info
if(m_parent_element->linkType() == Element::Slave &&
!m_parent_element->linkedElements().isEmpty())
{
Element *master_elmt = m_parent_element->linkedElements().first();
if(master_elmt && diagram())
{
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
if(xrp.getXrefPos() == Qt::AlignHCenter)
{
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
DiagramContext dc = m_parent_element->elementInformations();
if(dc.value("xref").toString() != xref_label)
{
dc.addValue("xref", xref_label);
m_parent_element->setElementInformations(dc);
}
}
}
}
}
}
@@ -1462,7 +1544,7 @@ void DynamicElementTextItem::setPlainText(const QString &text)
? nullptr : m_parent_element.data()->linkedElements().first();
if (master_elmt) {
XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
setXref_item(xrp.getXrefPos());
setXref_item(xrp.getXrefPos(), xrp.slaveOffset());
}
}
}
@@ -1474,44 +1556,44 @@ void DynamicElementTextItem::setTextWidth(qreal width)
emit textWidthChanged(width);
}
void DynamicElementTextItem::setXref_item(Qt::AlignmentFlag m_exHrefPos)
void DynamicElementTextItem::setXref_item(Qt::AlignmentFlag m_exHrefPos, int slave_offset)
{
QRectF r = boundingRect();
QPointF pos;
//QPointF pos(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top());
if (m_exHrefPos == Qt::AlignBottom)
{
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom());
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom() + slave_offset);
}
else if (m_exHrefPos == Qt::AlignTop)
{
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height());
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height() - slave_offset);
}
else if (m_exHrefPos == Qt::AlignLeft) //
{
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width(),r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width() - slave_offset,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
}
else if (m_exHrefPos == Qt::AlignRight) //
{
pos = QPointF(r.right() ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
pos = QPointF(r.right() + slave_offset ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
}
else if (m_exHrefPos == Qt::AlignBaseline) //
{
if(this->alignment() &Qt::AlignBottom)
{
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom());
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.bottom() + slave_offset);
}
else if(this->alignment() &Qt::AlignTop)
{
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height());
pos = QPointF(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,r.top() - m_slave_Xref_item->boundingRect().height() - slave_offset);
}
else if(this->alignment() &Qt::AlignLeft)
{
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width(),r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
pos = QPointF(r.left() - m_slave_Xref_item->boundingRect().width() - slave_offset,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
}
else if(this->alignment() &Qt::AlignRight)
{
pos = QPointF(r.right() ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
pos = QPointF(r.right() + slave_offset ,r.center().y() - m_slave_Xref_item->boundingRect().height()/2);
}
}
m_slave_Xref_item->setPos(pos);
@@ -92,7 +92,7 @@ class DynamicElementTextItem : public DiagramTextItem
/// DXF export: the master-side cross-reference item (the table/cross
/// drawn next to a report/master element), if this text item has one.
CrossRefItem *masterXrefItem() const { return m_Xref_item; }
Element *masterElement() const { return m_master_element.data(); }
Element *masterElement() const;
ElementTextItemGroup *parentGroup() const;
Element *elementUseForInfo() const;
void refreshLabelConnection();
@@ -112,7 +112,7 @@ class DynamicElementTextItem : public DiagramTextItem
void updateXref();
void setPlainText(const QString &text);
void setTextWidth(qreal width);
void setXref_item(Qt::AlignmentFlag m_exHrefPos);
void setXref_item(Qt::AlignmentFlag m_exHrefPos, int slave_offset = 0);
void setKeepVisualRotation(bool set);
bool keepVisualRotation() const;
+2
View File
@@ -812,6 +812,7 @@ bool Element::fromXml(QDomElement &e,
setZValue(e.attribute(QStringLiteral("z"), QString::number(this->zValue())).toDouble());
setFlags(QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemIsSelectable);
is_movable_ = e.attribute(QStringLiteral("is_movable"), QStringLiteral("1")).toInt();
// orientation
bool conv_ok;
@@ -943,6 +944,7 @@ QDomElement Element::toXml(
element.setAttribute(QStringLiteral("y"), QString::number(pos().y()));
element.setAttribute(QStringLiteral("z"), QString::number(this->zValue()));
element.setAttribute(QStringLiteral("orientation"), QString::number(orientation()));
element.setAttribute(QStringLiteral("is_movable"), bool(is_movable_));
/* get the first id to use for the bounds of this element
* recupere le premier id a utiliser pour les bornes de cet element */
+4
View File
@@ -266,6 +266,10 @@ class Element : public QetGraphicsItem
void drawPlcTable(QPainter *painter);
public:
/// Positions where the PLC IO table is drawn (from the .elmt file).
QList<QPointF> plcTablePositions() const { return m_plc_table_positions; }
private:
bool m_must_highlight = false;
QSize dimensions;
@@ -263,7 +263,16 @@ void ElementTextItemGroup::updateAlignment()
if(m_Xref_item)
m_Xref_item->autoPos();
if(m_slave_Xref_item)
adjustSlaveXrefPos();
{
int slave_offset = 0;
Element *master_elmt = m_parent_element->linkedElements().isEmpty()
? nullptr : m_parent_element->linkedElements().first();
if (master_elmt && m_parent_element->diagram()) {
XRefProperties xrp = m_parent_element->diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
slave_offset = xrp.slaveOffset();
}
adjustSlaveXrefPos(slave_offset);
}
if(m_hold_to_bottom_of_page)
autoPos();
}
@@ -786,12 +795,46 @@ void ElementTextItemGroup::updateXref()
!m_parent_element->linkedElements().isEmpty())
{
Element *master_elmt = m_parent_element->linkedElements().first();
XRefProperties xrp = project->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
//Champ de texte: store xref in element informations
if(xrp.getXrefPos() == Qt::AlignHCenter)
{
for(DynamicElementTextItem *deti : texts())
{
if(deti->textFrom() == DynamicElementTextItem::ElementInfo && deti->infoName() == "xref")
{
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
DiagramContext dc = m_parent_element->elementInformations();
if(dc.value("xref").toString() != xref_label)
{
dc.addValue("xref", xref_label);
m_parent_element->setElementInformations(dc);
}
//Set up connections for future updates
if(m_update_slave_Xref_connection.isEmpty())
{
m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(project, &QETProject::projectDiagramsOrderChanged, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(project, &QETProject::diagramRemoved, this, &ElementTextItemGroup::updateXref);
m_update_slave_Xref_connection << connect(project, &QETProject::XRefPropertiesChanged, this, &ElementTextItemGroup::updateXref);
}
return;
}
}
//No "xref" text found: fall through to cleanup
}
for(DynamicElementTextItem *deti : texts())
{
if((deti->textFrom() == DynamicElementTextItem::ElementInfo && deti->infoName() == "label") ||
(deti->textFrom() == DynamicElementTextItem::CompositeText && deti->compositeText().contains("%{label")))
{
XRefProperties xrp = project->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
@@ -810,7 +853,7 @@ void ElementTextItemGroup::updateXref()
else
m_slave_Xref_item->setPlainText(xref_label);
adjustSlaveXrefPos();
adjustSlaveXrefPos(xrp.slaveOffset());
return;
}
}
@@ -830,14 +873,38 @@ void ElementTextItemGroup::updateXref()
delete m_slave_Xref_item;
m_slave_Xref_item = nullptr;
m_update_slave_Xref_connection.clear();
//If position changed to Champ de texte, store xref in element info
if(m_parent_element->linkType() == Element::Slave &&
!m_parent_element->linkedElements().isEmpty() &&
m_parent_element->diagram())
{
Element *master_elmt = m_parent_element->linkedElements().first();
if(master_elmt)
{
XRefProperties xrp = m_parent_element->diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString());
if(xrp.getXrefPos() == Qt::AlignHCenter)
{
QString xref_label = xrp.slaveLabel();
xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt);
DiagramContext dc = m_parent_element->elementInformations();
if(dc.value("xref").toString() != xref_label)
{
dc.addValue("xref", xref_label);
m_parent_element->setElementInformations(dc);
}
}
}
}
}
}
void ElementTextItemGroup::adjustSlaveXrefPos()
void ElementTextItemGroup::adjustSlaveXrefPos(int slave_offset)
{
QRectF r = boundingRect();
QPointF pos(r.center().x() - m_slave_Xref_item->boundingRect().width()/2,
r.bottom());
r.bottom() + slave_offset);
m_slave_Xref_item->setPos(pos);
}
@@ -104,7 +104,7 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup
private:
void updateXref();
void adjustSlaveXrefPos();
void adjustSlaveXrefPos(int slave_offset = 0);
void autoPos();
private:
+79 -1
View File
@@ -158,6 +158,14 @@ QVariant MasterElement::itemChange(QGraphicsItem::GraphicsItemChange change, con
{
m_first_scene_change = false;
connect(diagram()->project(), &QETProject::XRefPropertiesChanged, this, &MasterElement::xrefPropertiesChanged);
// For PLC masters, create the CrossRefItem immediately so the
// IO table is visible even without linked slaves.
if (m_data.m_master_type == ElementData::PLC && !m_Xref_item)
{
m_Xref_item = new CrossRefItem(this);
m_Xref_item->updateLabel();
}
}
return Element::itemChange(change, value);
}
@@ -219,6 +227,73 @@ void MasterElement::aboutDeleteXref()
}
}
/**
* @brief MasterElement::contactUsage
* Count the slave contacts currently linked to this master, by type.
* This is the single place where that count is worked out: the cross ref
* item, the properties dialog and the link widgets all read it from here,
* so they cannot disagree with each other.
* @return the per type usage
*/
namespace {
/**
Map the element data's contact type onto the tally's own, so that
the used count and the declared capacity cannot classify the same
contact type differently.
*/
ContactUsage::Type contactType(ElementData::SlaveState state)
{
switch (state)
{
case ElementData::NO: return ContactUsage::NO;
case ElementData::NC: return ContactUsage::NC;
case ElementData::SW: return ContactUsage::SW;
case ElementData::Other: break;
}
return ContactUsage::Other;
}
}
ContactUsage MasterElement::contactUsage() const
{
ContactUsage usage;
for (Element *elmt : connected_elements)
{
if (!elmt) {
continue;
}
const ElementData &data = elmt->elementData();
usage.addSlave(contactType(data.m_slave_state), data.m_contact_count);
}
return usage;
}
/**
* @brief MasterElement::contactCapacity
* The contacts this master declares it provides, by type, summed over its
* contact groups. A group stands for contactCount contacts of its type.
* Returns an empty tally when the element declares no groups, which is the
* case for every element in the standard collection today -- callers use
* that to decide whether a capacity is worth showing at all.
* @return the per type capacity
*/
ContactUsage MasterElement::contactCapacity() const
{
ContactUsage capacity;
for (const auto &group : m_data.m_slave_contact_groups) {
capacity.addSlave(contactType(group.type), group.contactCount);
}
return capacity;
}
/**
* @brief MasterElement::isFull
* @return true if the master has reached its maximum number of slaves
@@ -239,7 +314,10 @@ bool MasterElement::isFull() const
return false;
}
// Return true if current connected elements reached or exceeded the limit
// max_slaves is a number of slots, not of contacts: it sizes the
// element's contact group table, and a slave occupies exactly one
// group however many contacts that group stands for. So the slots
// in use are the linked elements, not the contacts they carry.
return connected_elements.size() >= max_slaves;
}
+3
View File
@@ -19,6 +19,7 @@
#define MASTERELEMENT_H
#include "element.h"
#include "../contactusage.h"
#include <QHash>
#include <QMetaObject>
@@ -47,6 +48,8 @@ class MasterElement : public Element
void initLink (QETProject *project) override;
QRectF XrefBoundingRect() const;
ContactUsage contactUsage() const;
ContactUsage contactCapacity() const;
bool isFull() const; // Check Slave-Limit
protected:
File diff suppressed because it is too large Load Diff
+192 -15
View File
@@ -20,8 +20,11 @@
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "qetgraphicsitem.h"
#include "shapetransform.h"
#include <QPen>
#include <optional>
#include <utility>
class QDomElement;
class QDomDocument;
@@ -30,8 +33,10 @@ class QAction;
/**
@brief The QetShapeItem class
this class is used to draw a basic shape (line, rectangle, ellipse)
into a diagram, that can be saved to .qet file.
this class is used to draw a basic shape (line, rectangle, ellipse,
polygon or free-form path) into a diagram, that can be saved to a
.qet file. Beyond its local geometry, a shape may also carry a
rotation/skew/scale (see ShapeTransform) around an arbitrary pivot.
*/
class QetShapeItem : public QetGraphicsItem
{
@@ -46,21 +51,86 @@ class QetShapeItem : public QetGraphicsItem
Q_PROPERTY(qreal xRadius READ XRadius WRITE setXRadius NOTIFY XRadiusChanged)
Q_PROPERTY(qreal yRadius READ YRadius WRITE setYRadius NOTIFY YRadiusChanged)
// One property per ShapeTransform scalar -- this is what lets a plain
// QPropertyUndoCommand(this, "rotation", oldValue, newValue) work for
// every handle, exactly like xRadius/yRadius already do for corner
// rounding.
Q_PROPERTY(qreal rotation READ rotation WRITE setRotation NOTIFY transformChanged)
Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged)
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
Q_PROPERTY(qreal scaleFactorX READ scaleFactorX WRITE setScaleFactorX NOTIFY transformChanged)
Q_PROPERTY(qreal scaleFactorY READ scaleFactorY WRITE setScaleFactorY NOTIFY transformChanged)
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY arcChanged)
Q_PROPERTY(qreal endAngle READ endAngle WRITE setEndAngle NOTIFY arcChanged)
signals:
void penChanged();
void brushChanged();
void closeChanged();
void XRadiusChanged();
void YRadiusChanged();
void transformChanged();
void arcChanged();
void geometryChanged(); // P1/P2, polygon points, or path nodes changed -- lets the properties panel stay in sync while a handle is dragged, not just when it's typed into
public:
enum ShapeType {Line =1,
Rectangle =2,
Ellipse =4,
Polygon =8 };
Polygon =8,
Path =16 };
Q_ENUM (ShapeType)
enum ArcClosure {NoClosure = 0, Chord = 1, Pie = 2};
Q_ENUM (ArcClosure)
// Point of a Path shape. Anchors are in the same local coordinate
// frame as an ordinary Polygon's points; handle offsets are stored
// *relative to the anchor*, so moving a node never has to rewrite
// its own handle coordinates.
enum class NodeKind {Corner, Smooth, Symmetric};
struct PathNode {
QPointF anchor;
NodeKind kind = NodeKind::Corner;
std::optional<QPointF> inHandle;
std::optional<QPointF> outHandle;
bool operator==(const PathNode &other) const {
return anchor == other.anchor && kind == other.kind
&& inHandle == other.inHandle && outHandle == other.outHandle;
}
};
// Orthogonal to ShapeType: which handle set is currently shown.
// Cycled by clicking an already-selected shape without dragging:
// Size -> Corner -> RotateSkew -> Size for Rectangle (the only
// type with a corner-radius concept); Size -> NodeEdit ->
// RotateSkew -> Size for Path (reveals control handles for
// every node that has any); Size -> RotateSkew -> Size for
// everything else. One unified click-cycle for every shape
// type, rather than a separate, less discoverable gesture
// (e.g. double-click) for any one shape's extra mode.
enum class HandleMode {Size, Corner, NodeEdit, RotateSkew};
// index conventions, deliberately matched to what already exists
// rather than invented fresh:
// Resize 0..7, same order as QetGraphicsHandlerUtility::pointsForRect
// (this is exactly today's Rectangle/Ellipse handle set --
// only the Ctrl/Shift dispatch around it is new)
// Rotate 0..3, corners: NW, NE, SE, SW
// SkewEdge 0..3, edges: N, E, S, W
// CornerRadius 0..1, same order as QetGraphicsHandlerUtility::pointForRadiusRect
// ArcEndpoint 0 = start angle, 1 = end angle
enum class HandleRole {
Resize, // Size mode
Rotate, SkewEdge, Pivot, // RotateSkew mode
CornerRadius, // Rectangle, always shown alongside Size handles
ArcEndpoint, // Ellipse, always shown
PathAnchor, PathControlIn, PathControlOut // Polygon/Path, node-edit mode (see setPathNodes())
};
enum { Type = UserType + 1008 };
QetShapeItem(
@@ -102,6 +172,41 @@ class QetShapeItem : public QetGraphicsItem
qreal YRadius() const {return m_yRadius;}
void setYRadius(qreal Y);
//Transform: one accessor pair per ShapeTransform scalar (see
//the Q_PROPERTY block above for why they are not grouped into
//a single property).
const ShapeTransform &shapeTransform() const {return m_transform;}
qreal rotation() const {return m_transform.rotation;}
void setRotation(qreal degrees);
qreal skewX() const {return m_transform.skewX;}
void setSkewX(qreal degrees);
qreal skewY() const {return m_transform.skewY;}
void setSkewY(qreal degrees);
qreal scaleFactorX() const {return m_transform.scaleX;}
void setScaleFactorX(qreal factor);
qreal scaleFactorY() const {return m_transform.scaleY;}
void setScaleFactorY(qreal factor);
QPointF pivot() const {return m_transform.pivot;}
void setPivot(const QPointF &pivot); // moves the pivot handle: compensates pos() so the shape does not jump
void resetPivotToBoundingRectCenter();
void enableNodeEditMode(); // Path only: switches to NodeEdit mode, so every node's control handles become visible
//Arc: only meaningful when shapeType() == Ellipse. A full
//ellipse is just an arc with span 360 -- there is no separate
//Arc shape type.
qreal startAngle() const {return m_startAngle;}
void setStartAngle(qreal degrees);
qreal endAngle() const {return m_endAngle;}
void setEndAngle(qreal degrees);
qreal spanAngle() const {return m_endAngle - m_startAngle;}
bool isFullEllipse() const {return qFuzzyCompare(qAbs(spanAngle()), qreal(360));}
ArcClosure arcClosure() const {return m_arcClosure;}
void setArcClosure(ArcClosure closure);
//Path (Bezier): only meaningful when shapeType() == Path.
const QVector<PathNode> &pathNodes() const {return m_nodes;}
void setPathNodes(const QVector<PathNode> &nodes);
//Methods available for polygon shape
int pointsCount () const;
void setNextPoint (QPointF P);
@@ -118,6 +223,8 @@ class QetShapeItem : public QetGraphicsItem
void hoverEnterEvent (QGraphicsSceneHoverEvent *event) override;
void hoverLeaveEvent (QGraphicsSceneHoverEvent *event) override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
QVariant itemChange(
GraphicsItemChange change,
const QVariant &value) override;
@@ -128,17 +235,65 @@ class QetShapeItem : public QetGraphicsItem
QGraphicsSceneContextMenuEvent *event) override;
private:
void switchResizeMode();
void addHandler();
void adjustHandlerPos();
void toggleHandleMode();
HandleMode nextHandleMode() const; // what a click would switch to from here -- shared by toggleHandleMode() and the tooltip
void updateModeHint(); // keeps the tooltip in sync with nextHandleMode()
void refreshInteractionHints(); // updateModeHint(), plus an immediate re-show of tooltip/status bar if currently hovered
void showStatusHint(const QString &text) const;
void clearStatusHint() const;
QString currentModeStatusHint() const; // richer, one-line gesture/modifier reference for the status bar, shown when hovering the shape body
QString handleRoleTooltip(HandleRole role, int slot) const; // shown natively by Qt when hovering that specific handle, and pushed to the status bar too
static QString handleModeLabel(HandleMode mode);
static bool isResizeCornerSlot(int slot); // true for the 4 corner slots (of 8) in QetGraphicsHandlerUtility::pointsForRect's ordering
void rebuildHandles(); // (re)creates handler items -- only when the *set* of handles changes
void repositionHandles(); // moves existing handler items -- safe to call every frame of a live drag
void insertPoint();
void removePoint();
void handlerMousePressEvent();
void handlerMouseMoveEvent(QGraphicsSceneMouseEvent *event);
void handlerMouseReleaseEvent();
void convertToPathOrPolygon(); // context-menu action; see promoteRectangleOrEllipseToPolygon() for the separate Alt+drag mechanism
static QVector<PathNode> bezierNodesForArc(const QRectF &rect, qreal startAngleDeg, qreal spanAngleDeg);
static QVector<PathNode> bezierNodesForRoundedRect(const QRectF &rect, qreal xRadius, qreal yRadius);
void mirror(bool horizontal); // context-menu action: flips scaleFactorX (horizontal) or scaleFactorY (vertical) around the current pivot
void setNodeKind(int nodeIndex, NodeKind kind); // context-menu action on a Path node
///ATTRIBUTES
void handlerMousePressEvent(int handlerIndex);
void handlerMouseMoveEvent(int handlerIndex, QGraphicsSceneMouseEvent *event);
void handlerMouseReleaseEvent(int handlerIndex);
// One dispatch function per handle role -- called from
// handlerMouseMoveEvent() with the mouse position already
// mapped to local coordinates and any grid snap applied.
// dragResize() covers both the plain and Ctrl/Shift/Alt-modified
// interpretations of the whole Size handle set.
void dragResize (int index, const QPointF &localPos, Qt::KeyboardModifiers mods);
void dragRotateHandle(int cornerIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
void dragSkewHandle (int edgeIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
void dragPivotHandle (const QPointF &localPos);
void dragArcEndpoint (int which, const QPointF &localPos, Qt::KeyboardModifiers mods);
void dragCornerRadius(int which, const QPointF &localPos);
void dragPathAnchor (int which, const QPointF &localPos, Qt::KeyboardModifiers mods);
void dragPathControlHandle(bool isOutHandle, int nodeIndex, const QPointF &localPos, Qt::KeyboardModifiers mods);
void mirrorOppositeHandle(PathNode &node, bool justChangedIsOut); // shared by dragPathControlHandle() and dragCurveSegment()
void dragCurveSegment(int segmentIndex, qreal t, const QPointF &localPos); // Inkscape-style "grab the curve itself"
void promoteRectangleOrEllipseToPolygon(int detachedResizeIndex, const QPointF &newLocalPos);
std::pair<int, qreal> nearestPathSegment(const QPointF &localPos) const; // {segmentIndex, t}, -1 if fewer than 2 nodes
void insertPathPoint(int segmentIndex, qreal t); // De Casteljau split -- see .cpp for why
void removePathPoint(int nodeIndex);
QDomElement snapshotXml() const; // helper for PromoteShapeCommand: toXml() into a throwaway document
QRectF localRect() const;
QPainterPath outline() const; // raw, unstroked path for the current type; shared by shape() and paint()
QVector<QPointF> currentHandlePositions() const; // in m_handleRoles/m_handleSlot order, local coordinates
QPointF handlePositionFor(HandleRole role, int slot) const;
QPointF rotateHandleReference(int slot) const; // the point a Rotate handle tracks, before rotation is applied
QPointF scaleOnlyOffset(const QPointF &localPoint) const; // (localPoint - pivot), scaled, in the pre-shear frame
QPointF scaleAndShearOffset(const QPointF &localPoint) const; // same, with current shear also applied -- the pre-rotation frame
static QColor colorForHandleRole(HandleRole role);
static QRectF lockAspectRatio(const QRectF &oldRect, QRectF newRect, int resizeIndex, bool mirrored);
static QPointF cornerPoint(const QRectF &rect, int cornerIndex); // 0=NW,1=NE,2=SE,3=SW
static QPointF edgeMidpoint(const QRectF &rect, int edgeIndex); // 0=N, 1=E, 2=S, 3=W
///ATTRIBUTES
private:
ShapeType m_shapeType;
QPen m_pen;
@@ -153,13 +308,35 @@ class QetShapeItem : public QetGraphicsItem
int m_vector_index;
bool m_closed = false,
m_modifie_radius_equaly = false;
int m_resize_mode = 1;
QVector<QetGraphicsHandlerItem *> m_handler_vector;
QAction *m_insert_point,
*m_remove_point;
qreal m_xRadius = 0,
m_yRadius = 0,
m_old_xRadius,
m_old_yRadius;
m_old_yRadius,
m_old_startAngle,
m_old_endAngle;
ShapeTransform m_transform;
ShapeTransform m_old_transform;
HandleMode m_handleMode = HandleMode::Size;
QVector<HandleRole> m_handleRoles; // parallel to m_handler_vector, one role per handle
QVector<int> m_handleSlot; // parallel to m_handler_vector, meaning depends on role (see HandleRole)
QPointF m_old_pos;
qreal m_startAngle = 0;
qreal m_endAngle = 360;
ArcClosure m_arcClosure = NoClosure;
bool m_pivotIsCustom = false; // false: pivot auto-follows the bounding-rect center on every geometry edit
bool m_deferHandleReposition = false; // true while setPivot() is applying its two related updates together
QVector<PathNode> m_nodes;
QVector<PathNode> m_old_nodes; // saved at handle press, for undo
int m_curveDragSegment = -1; // >=0 while dragging the curve itself (not a handle) between two nodes
qreal m_curveDragT = 0; // parameter along that segment where the drag started
QPointF m_curveDragOriginalP1, m_curveDragOriginalP2; // absolute control points at drag start, fixed for the whole drag
QPointF m_curveDragPressPos; // local position at press, to tell a plain click from a real drag
bool m_curveDragEngaged = false; // false until the drag actually exceeds a small threshold
};
#endif // QETSHAPEITEM_H
@@ -0,0 +1,88 @@
/*
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 "shapetransform.h"
#include <QtCore/qmath.h>
bool ShapeTransform::isIdentity() const
{
return qFuzzyIsNull(rotation) && qFuzzyIsNull(skewX) && qFuzzyIsNull(skewY)
&& qFuzzyCompare(scaleX, qreal(1)) && qFuzzyCompare(scaleY, qreal(1));
}
QTransform ShapeTransform::linearPart() const
{
QTransform t;
t.rotate(rotation);
t.shear(qTan(qDegreesToRadians(skewX)), qTan(qDegreesToRadians(skewY)));
t.scale(scaleX, scaleY);
return t;
}
QTransform ShapeTransform::toMatrix() const
{
QTransform t;
t.translate(pivot.x(), pivot.y());
t.rotate(rotation);
t.shear(qTan(qDegreesToRadians(skewX)), qTan(qDegreesToRadians(skewY)));
t.scale(scaleX, scaleY);
t.translate(-pivot.x(), -pivot.y());
return t;
}
bool ShapeTransform::operator==(const ShapeTransform &other) const
{
return qFuzzyCompare(rotation, other.rotation)
&& qFuzzyCompare(skewX, other.skewX)
&& qFuzzyCompare(skewY, other.skewY)
&& qFuzzyCompare(scaleX, other.scaleX)
&& qFuzzyCompare(scaleY, other.scaleY)
&& pivot == other.pivot;
}
ShapeTransform decomposeLinear(const QTransform &m)
{
const qreal scaleX = qSqrt(m.m11() * m.m11() + m.m12() * m.m12());
const qreal rotation = qRadiansToDegrees(qAtan2(m.m12(), m.m11()));
const qreal rad = qDegreesToRadians(rotation);
const qreal c = qCos(rad), s = qSin(rad);
// v * R(-rotation): undo the rotation, leaving the pure shear+scale
// contribution the local y-axis picked up from Shear(sh,0)*Scale(sx,sy).
const qreal vx = m.m21() * c + m.m22() * s;
const qreal scaleY = -m.m21() * s + m.m22() * c;
const qreal skewX = qFuzzyIsNull(scaleY) ? 0.0 : qRadiansToDegrees(qAtan(vx / scaleY));
ShapeTransform result;
result.rotation = rotation;
result.skewX = skewX;
result.skewY = 0;
result.scaleX = scaleX;
result.scaleY = scaleY;
return result;
}
QPointF compensatedPositionForNewPivot(
const QPointF &position,
const QPointF &oldPivot,
const QPointF &newPivot,
const QTransform &linearPart)
{
return position + (oldPivot - linearPart.map(oldPivot))
- (newPivot - linearPart.map(newPivot));
}
+90
View File
@@ -0,0 +1,90 @@
/*
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 SHAPETRANSFORM_H
#define SHAPETRANSFORM_H
#include <QPointF>
#include <QTransform>
/**
@brief The ShapeTransform struct
Rotation, skew and scale applied to a shape's local geometry around an
arbitrary local pivot point.
These five scalars plus a pivot are a convenient, directly-editable
basis for 2D affine transforms: every handle in the UI changes exactly
one field, which is what makes per-handle drag math simple. Note that
a plain 2x2 linear map only has 4 true degrees of freedom, one fewer
than (rotation, skewX, skewY, scaleX, scaleY) -- so this is not a
*unique* representation of a matrix, only a convenient one. That
redundancy is harmless for editing (nothing here ever needs to invert
the forward direction) and only matters to decomposeLinear() below,
which exists for later consumers (flattening a group transform onto
its children, importing a foreign matrix) rather than everyday use.
The resulting matrix is meant to be handed directly to
QGraphicsItem::setTransform(). QGraphicsItem::transformOriginPoint()
is deliberately NOT used anywhere in this design: it only centers
QGraphicsItem's own rotation()/scale() convenience properties, and has
no effect on a custom transform() matrix -- the pivot has to be baked
into the matrix itself, as toMatrix() does. QGraphicsItem::pos()
supplies the translation on top, unchanged from how shapes are
already positioned and moved today.
*/
struct ShapeTransform
{
qreal rotation = 0; // degrees
qreal skewX = 0; // degrees
qreal skewY = 0; // degrees
qreal scaleX = 1;
qreal scaleY = 1;
QPointF pivot; // local coordinates; caller decides the default (usually local bbox center)
bool isIdentity() const;
// Rotate+shear+scale about the origin, ignoring pivot -- this is the
// "linear part" used by compensatedPositionForNewPivot() and by
// anything that needs to map a direction/offset rather than a point.
QTransform linearPart() const;
// The pivot-centered matrix to pass to QGraphicsItem::setTransform().
QTransform toMatrix() const;
bool operator==(const ShapeTransform &other) const;
bool operator!=(const ShapeTransform &other) const { return !(*this == other); }
};
// Canonical decomposition of an arbitrary 2x2 linear map into the five
// scalars above, always returning skewY == 0 (all shear folded into
// skewX -- matching how most authoring tools represent shear on export).
// Rebuilding via ShapeTransform::linearPart() from the result reproduces
// the input matrix exactly; the individual scalar values are not
// guaranteed to match whatever scalars (if any) originally produced that
// matrix, only the matrix itself is guaranteed to match.
ShapeTransform decomposeLinear(const QTransform &linear);
// Position adjustment needed when the pivot moves, so the shape does not
// visibly jump on screen: call once, at the end of a pivot-handle drag,
// alongside setting the new pivot.
QPointF compensatedPositionForNewPivot(
const QPointF &position,
const QPointF &oldPivot,
const QPointF &newPivot,
const QTransform &linearPart);
#endif // SHAPETRANSFORM_H
+16
View File
@@ -65,6 +65,7 @@ namespace QET {
QIcon EditCopy;
QIcon EditCut;
QIcon EditDelete;
QIcon EditOpacity;
QIcon EditPaste;
QIcon EditRedo;
QIcon EditRename;
@@ -86,6 +87,7 @@ namespace QET {
QIcon ElementEdit;
QIcon ElementNew;
QIcon ElementSmall;
QIcon EllipseToBezier;
QIcon EndLineCircle;
QIcon EndLineDiamond;
QIcon EndLineNone;
@@ -115,6 +117,8 @@ namespace QET {
QIcon Hide;
QIcon Home;
QIcon HotSpot;
QIcon ImageFlipHorizontal;
QIcon ImageFlipVertical;
QIcon InsertImage;
QIcon Lower;
QIcon IC_MoveFile;
@@ -128,6 +132,7 @@ namespace QET {
QIcon ObjectUnlocked;
QIcon Orientations;
QIcon PartArc;
QIcon PartBezier;
QIcon PartCircle;
QIcon PartEllipse;
QIcon PartLine;
@@ -155,6 +160,8 @@ namespace QET {
QIcon QETOxygenLogo;
QIcon QtLogo;
QIcon Raise;
QIcon RectToBezier;
QIcon RectToPolyline;
QIcon Remove;
QIcon Restore;
QIcon RunDxf;
@@ -168,6 +175,7 @@ namespace QET {
QIcon TitleBlock;
QIcon TitleBlockBottom;
QIcon TitleBlockRight;
QIcon TransformCrop;
QIcon TransformRotate;
QIcon UserInformations;
QIcon ViewFitWidth;
@@ -469,6 +477,7 @@ void QET::Icons::initIcons()
EditCut .addFile(":/ico/22x22/edit-cut.png");
EditDelete .addFile(":/ico/16x16/edit-delete.png");
EditDelete .addFile(":/ico/22x22/edit-delete.png");
EditOpacity .addFile(":/ico/breeze-icons/scalable/apps/hidef/edit-opacity.svg");
EditPaste .addFile(":/ico/22x22/edit-paste.png");
EditPaste .addFile(":/ico/16x16/edit-paste.png");
if (rtl) {
@@ -517,6 +526,7 @@ void QET::Icons::initIcons()
ElementNew .addFile(":/ico/16x16/element-new.png");
ElementNew .addFile(":/ico/22x22/element-new.png");
ElementSmall .addFile(":/ico/16x16/element.png");
EllipseToBezier .addFile(":/ico/generated/ellipse-to-bezier.svg");
EndLineCircle .addFile(":/ico/16x16/endline-circle.png");
EndLineDiamond .addFile(":/ico/16x16/endline-diamond.png");
EndLineNone .addFile(":/ico/16x16/endline-none.png");
@@ -556,6 +566,8 @@ void QET::Icons::initIcons()
Home .addFile(":/ico/16x16/go-home.png");
Home .addFile(":/ico/22x22/go-home.png");
HotSpot .addFile(":/ico/22x22/hotspot.png");
ImageFlipHorizontal .addFile(":/ico/breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg");
ImageFlipVertical .addFile(":/ico/breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg");
InsertImage .addFile(":/ico/22x22/insert-image.png");
Lower .addFile(":/ico/22x22/lower.png");
IC_MoveFile .addFile(":/ico/16x16/item-move.png");
@@ -570,6 +582,7 @@ void QET::Icons::initIcons()
ObjectUnlocked .addFile(":/ico/22x22/object-unlocked.png");
Orientations .addFile(":/ico/16x16/orientations.png");
PartArc .addFile(":/ico/22x22/arc.png");
PartBezier .addFile(":/ico/breeze-icons/scalable/apps/hidef/draw-bezier-curves.svg");
PartCircle .addFile(":/ico/16x16/circle.png");
PartEllipse .addFile(":/ico/22x22/ellipse.png");
PartLine .addFile(":/ico/22x22/line.png");
@@ -613,6 +626,8 @@ void QET::Icons::initIcons()
QETVideo .addFile(":/ico/16x16/kdenlive-show-video.png");
QtLogo .addFile(":/ico/16x16/qt.png");
Raise .addFile(":/ico/22x22/raise.png");
RectToBezier .addFile(":/ico/generated/rect-to-bezier.svg");
RectToPolyline .addFile(":/ico/generated/rect-to-polyline.svg");
Remove .addFile(":/ico/16x16/list-remove.png");
Remove .addFile(":/ico/22x22/list-remove.png");
resize_image .addFile(":/ico/22x22/transform-scale.png");
@@ -631,6 +646,7 @@ void QET::Icons::initIcons()
TitleBlock .addFile(":/ico/22x22/label.png");
TitleBlockBottom .addFile(":/ico/22x22/titleblock-bottom.png");
TitleBlockRight .addFile(":/ico/22x22/titleblock-right.png");
TransformCrop .addFile(":/ico/breeze-icons/scalable/apps/hidef/transform-crop.svg");
TransformRotate .addFile(":/ico/16x16/transform-rotate.png");
UserInformations .addFile(":/ico/16x16/preferences-desktop-user.png");
UserInformations .addFile(":/ico/22x22/preferences-desktop-user.png");
+8
View File
@@ -72,6 +72,7 @@ namespace QET {
extern QIcon EditCopy;
extern QIcon EditCut;
extern QIcon EditDelete;
extern QIcon EditOpacity;
extern QIcon EditPaste;
extern QIcon EditRedo;
extern QIcon EditRename;
@@ -92,6 +93,7 @@ namespace QET {
extern QIcon ElementDelete;
extern QIcon ElementEdit;
extern QIcon ElementNew;
extern QIcon EllipseToBezier;
extern QIcon EndLineCircle;
extern QIcon EndLineDiamond;
extern QIcon EndLineNone;
@@ -121,6 +123,8 @@ namespace QET {
extern QIcon Hide;
extern QIcon Home;
extern QIcon HotSpot;
extern QIcon ImageFlipHorizontal;
extern QIcon ImageFlipVertical;
extern QIcon InsertImage;
extern QIcon Lower;
extern QIcon IC_MoveFile;
@@ -136,6 +140,7 @@ namespace QET {
extern QIcon ObjectUnlocked;
extern QIcon Orientations;
extern QIcon PartArc;
extern QIcon PartBezier;
extern QIcon PartCircle;
extern QIcon PartEllipse;
extern QIcon PartLine;
@@ -163,6 +168,8 @@ namespace QET {
extern QIcon QETOxygenLogo;
extern QIcon QtLogo;
extern QIcon Raise;
extern QIcon RectToBezier;
extern QIcon RectToPolyline;
extern QIcon Remove;
extern QIcon Restore;
extern QIcon RunDxf;
@@ -176,6 +183,7 @@ namespace QET {
extern QIcon TitleBlock;
extern QIcon TitleBlockBottom;
extern QIcon TitleBlockRight;
extern QIcon TransformCrop;
extern QIcon TransformRotate;
extern QIcon UserInformations;
extern QIcon ViewFitWidth;
+20 -3
View File
@@ -150,6 +150,11 @@ QStringList QETInformation::elementInfoKeys()
ELMT_DESIGNATION,
ELMT_MANUFACTURER,
ELMT_MANUFACTURER_REF,
ELMT_MODEL,
ELMT_CATEGORY,
ELMT_VOLTAGE_RATING,
ELMT_CURRENT_RATING,
ELMT_NOTES,
ELMT_MACHINE_MANUFACTURER_REF,
ELMT_SUPPLIER,
ELMT_QUANTITY,
@@ -194,8 +199,9 @@ QStringList QETInformation::elementInfoKeys()
ELMT_PLC_ADDRESS,
ELMT_PLC_FUNCTION,
ELMT_PLC_COMMENT,
ELMT_PLC_CROSSREF,
"exclude_from_bom" };
ELMT_PLC_CROSSREF,
ELMT_XREF,
"exclude_from_bom" };
return list;
}
@@ -233,7 +239,12 @@ QString QETInformation::infoToVar(const QString &info)
*/
QString QETInformation::translatedInfoKey(const QString &info)
{
if (info == DIA_AUTHOR) return QObject::tr("Auteur");
if (info == ELMT_MODEL) return QObject::tr("Modèle");
else if (info == ELMT_CATEGORY) return QObject::tr("Catégorie");
else if (info == ELMT_VOLTAGE_RATING) return QObject::tr("Tension nominale");
else if (info == ELMT_CURRENT_RATING) return QObject::tr("Courant nominal");
else if (info == ELMT_NOTES) return QObject::tr("Notes");
else if (info == DIA_AUTHOR) return QObject::tr("Auteur");
else if (info == DIA_DATE) return QObject::tr("Date");
else if (info == DIA_TITLE) return QObject::tr("Titre");
else if (info == DIA_FILENAME) return QObject::tr("Fichier");
@@ -316,6 +327,7 @@ QString QETInformation::translatedInfoKey(const QString &info)
else if (info == ELMT_PLC_FUNCTION) return QObject::tr("Fonction PLC");
else if (info == ELMT_PLC_COMMENT) return QObject::tr("Commentaire PLC");
else if (info == ELMT_PLC_CROSSREF) return QObject::tr("Réf. croisée PLC");
else if (info == ELMT_XREF) return QObject::tr("Réf. croisée");
else return QString();
}
@@ -330,6 +342,11 @@ QStringList QETInformation::elementEditorElementInfoKeys()
ELMT_DESIGNATION,
ELMT_MANUFACTURER,
ELMT_MANUFACTURER_REF,
ELMT_MODEL,
ELMT_CATEGORY,
ELMT_VOLTAGE_RATING,
ELMT_CURRENT_RATING,
ELMT_NOTES,
ELMT_MACHINE_MANUFACTURER_REF,
ELMT_SUPPLIER,
ELMT_QUANTITY,
+6
View File
@@ -44,6 +44,11 @@ namespace QETInformation
static QString ELMT_MACHINE_MANUFACTURER_REF = "machine_manufacturer_reference";
static QString ELMT_SUPPLIER = "supplier";
static QString ELMT_QUANTITY = "quantity";
static QString ELMT_MODEL = "model";
static QString ELMT_CATEGORY = "category";
static QString ELMT_VOLTAGE_RATING = "voltage_rating";
static QString ELMT_CURRENT_RATING = "current_rating";
static QString ELMT_NOTES = "notes";
static QString ELMT_UNITY = "unity";
static QString ELMT_PLANT = "plant";
static QString ELMT_LOCATION = "location";
@@ -83,6 +88,7 @@ namespace QETInformation
static QString ELMT_SUPPLIER_AUX4 = "supplier_auxiliary4";
static QString ELMT_QUANTITY_AUX4 = "quantity_auxiliary4";
static QString ELMT_UNITY_AUX4 = "unity_auxiliary4";
static QString ELMT_XREF = "xref";
/** Default information related to conductor **/
+90
View File
@@ -17,6 +17,84 @@
*/
#include "qetmessagebox.h"
#include <QTextStream>
namespace {
bool g_non_interactive = false;
/**
@brief autoAnswer
Report a message box on stderr and pick an answer, for use when there
is no user to click anything. @see QET::QetMessageBox::setNonInteractive
@param severity : short word naming the kind of box, for the log line
@param title
@param text
@param buttons : the buttons the caller offered
@param defaultButton : the caller's preferred answer, may be NoButton
@return the button to report as pressed
*/
QMessageBox::StandardButton autoAnswer(
const char *severity,
const QString &title,
const QString &text,
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton)
{
//Honour the caller's own default when it named one.
if (defaultButton != QMessageBox::NoButton
&& (buttons & defaultButton)) {
QTextStream(stderr) << severity << ": " << title << " -- " << text
<< "\n(no display: answered with the caller's default button)\n";
return defaultButton;
}
//Otherwise prefer a "carry on" answer over one that cancels, so a
//batch run completes rather than silently doing nothing.
static const QMessageBox::StandardButton preference[] = {
QMessageBox::Ok, QMessageBox::Open, QMessageBox::Yes,
QMessageBox::Save, QMessageBox::Apply, QMessageBox::YesToAll,
QMessageBox::Retry, QMessageBox::Ignore, QMessageBox::Close
};
for (auto candidate : preference) {
if (buttons & candidate) {
QTextStream(stderr) << severity << ": " << title << " -- " << text
<< "\n(no display: continuing)\n";
return candidate;
}
}
//Nothing affirmative on offer -- fall back to whatever is set.
for (int bit = QMessageBox::Ok; bit <= QMessageBox::RestoreDefaults; bit <<= 1) {
auto candidate = static_cast<QMessageBox::StandardButton>(bit);
if (buttons & candidate) {
QTextStream(stderr) << severity << ": " << title << " -- " << text
<< "\n(no display: answered automatically)\n";
return candidate;
}
}
QTextStream(stderr) << severity << ": " << title << " -- " << text
<< "\n(no display: no button offered)\n";
return QMessageBox::NoButton;
}
}
/**
@brief QET::QetMessageBox::setNonInteractive
@param non_interactive
*/
void QET::QetMessageBox::setNonInteractive(bool non_interactive) {
g_non_interactive = non_interactive;
}
/**
@brief QET::QetMessageBox::isNonInteractive
@return true when message boxes are answered without a user
*/
bool QET::QetMessageBox::isNonInteractive() {
return g_non_interactive;
}
/**
@see Documentation Qt pour QMessageBox::critical
*/
@@ -27,6 +105,9 @@ QMessageBox::StandardButton QET::QetMessageBox::critical (
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton)
{
if (g_non_interactive) {
return autoAnswer("Critical", title, text, buttons, defaultButton);
}
#ifdef Q_OS_MACOS
QMessageBox message_box(
QMessageBox::Critical,
@@ -59,6 +140,9 @@ QMessageBox::StandardButton QET::QetMessageBox::information(
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton)
{
if (g_non_interactive) {
return autoAnswer("Information", title, text, buttons, defaultButton);
}
#ifdef Q_OS_MACOS
QMessageBox message_box(
QMessageBox::Information,
@@ -91,6 +175,9 @@ QMessageBox::StandardButton QET::QetMessageBox::question (
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton)
{
if (g_non_interactive) {
return autoAnswer("Question", title, text, buttons, defaultButton);
}
#ifdef Q_OS_MACOS
QMessageBox message_box(
QMessageBox::Question,
@@ -123,6 +210,9 @@ QMessageBox::StandardButton QET::QetMessageBox::warning (
QMessageBox::StandardButtons buttons,
QMessageBox::StandardButton defaultButton)
{
if (g_non_interactive) {
return autoAnswer("Warning", title, text, buttons, defaultButton);
}
#ifdef Q_OS_MACOS
QMessageBox message_box(
QMessageBox::Warning,
+22
View File
@@ -27,6 +27,28 @@ namespace QET {
Qt:Sheet flag, thus enabling a better MacOS integration.
*/
namespace QetMessageBox {
/**
Enable non-interactive mode.
In non-interactive mode the functions below never construct a
dialog. They write the message to stderr and return an answer
immediately, so a headless run cannot block on a modal box that
nobody is there to dismiss.
This is needed because these are reachable from the command-line
tools: opening a project written by an older QElectroTech raises
a warning from QETProject::readProjectXml(), and with no display
to click it, QDialog::exec() spins its event loop forever.
The answer is chosen as: the caller's defaultButton when it gave
one, otherwise the first "carry on" button among those offered
(Ok, Open, Yes, Save, Apply...), otherwise the first button set.
So the two warnings above resolve to Open and the project loads,
which is what a batch invocation wants.
*/
void setNonInteractive(bool non_interactive);
bool isNonInteractive();
QMessageBox::StandardButton critical (
QWidget *,
const QString &,
+36 -1
View File
@@ -160,6 +160,11 @@ QETProject::~QETProject()
//We block database signal to avoid hundreds of unnecessary emitted signal
//due to deletion (diagram, item, etc...) and as much update made in the not yet deleted things.
m_data_base.blockSignals(true);
//Same reasoning for the rebuild itself : destroying a table relinks
//the tables that were chained to it, which re-queries the database,
//which rebuilds it completely -- for a project that is on its way out.
//Nothing can observe the result : the database is destroyed with it.
m_data_base.setUpdateBlocked(true);
//Each time a diagram is deleted we also remove it from m_diagram_list
//because a lot of thing append during the destructor of a diagram class
@@ -174,6 +179,30 @@ QETProject::~QETProject()
delete diagram;
m_diagrams_list.removeOne(diagram);
}
//A diagram can be detached from this project (detachDiagram(), used by
//both removeDiagram() and RemoveDiagramCommand::redo()) and scheduled
//for deferred deletion via deleteLater(), without that deletion having
//actually run yet -- deleteLater() only fires on the next event-loop
//iteration, and nothing guarantees one runs before this destructor
//does. Such a diagram is no longer in m_diagrams_list (so the loop
//above never touches it) but is still a QObject child of this project
//(Diagram's constructor passes `project` straight to QGraphicsScene's
//parent argument). Left alone, it is destroyed later by QObject's own
//automatic child cleanup in ~QObject(), which runs AFTER m_data_base
//(a plain value member, destroyed by ordinary C++ member teardown)
//has already been destroyed -- and Diagram's destructor calls back
//into dataBase()->removeElement() for each of its elements, so that
//ordering is a use-after-free (confirmed by crash: SIGSEGV in
//QSqlResult::exec(), called from Diagram::~Diagram() by way of
//Diagram::removeItem(), by way of QObjectPrivate::deleteChildren()).
//Delete any such stragglers now, synchronously, while m_data_base is
//still alive. The deleteLater() event, if it is ever processed
//afterward, is a safe no-op on an already-deleted QObject.
const auto orphaned_diagrams = findChildren<Diagram *>(QString(), Qt::FindDirectChildrenOnly);
for (Diagram *diagram : orphaned_diagrams) {
delete diagram;
}
}
/**
@@ -1228,7 +1257,7 @@ ElementsLocation QETProject::importElement(ElementsLocation &location)
// Warn if the new element introduces slave contact groups
QDomElement new_kind = location.xml().firstChildElement("kindInformations");
if (!new_kind.firstChildElement("slaveContactGroups").isNull()) {
QMessageBox::StandardButton answer = QMessageBox::warning(nullptr,
QMessageBox::StandardButton answer = QET::QetMessageBox::warning(nullptr,
tr("Système de contacts modifié"),
tr("Le nouvel élément définit des groupes de contacts esclaves.\n"
"Les éléments esclaves existants ne seront pas automatiquement "
@@ -1523,6 +1552,11 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
}
m_data_base.blockSignals(true);
//Blocking the signals is not enough : every table model built below
//re-queries the database, and each of those queries used to trigger a
//complete rebuild of it. The content being loaded is the same for all
//of them, so a single rebuild once everything is in place is enough.
m_data_base.setUpdateBlocked(true);
//Load the project-wide properties
readProjectPropertiesXml(xml_project);
@@ -1562,6 +1596,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
const qint64 refresh_ms = phase_timer.restart();
m_data_base.blockSignals(false);
m_data_base.setUpdateBlocked(false);
m_data_base.updateDB();
const qint64 database_ms = phase_timer.elapsed();
+18 -2
View File
@@ -358,6 +358,7 @@ void QETTitleBlockTemplateEditor::newTemplate()
QETTitleBlockTemplateEditor *qet_template_editor = new QETTitleBlockTemplateEditor();
qet_template_editor -> edit(TitleBlockTemplateLocation());
qet_template_editor -> show();
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
}
/**
@@ -645,10 +646,25 @@ void QETTitleBlockTemplateEditor::readSettings()
// window size and position
QVariant geometry = settings.value("titleblocktemplateeditor/geometry");
if (geometry.isValid()) restoreGeometry(geometry.toByteArray());
}
/**
@brief QETTitleBlockTemplateEditor::readSettingsState
Restore the window state (docks, toolbars).
Must be called AFTER show() in Qt6 for restoreState() to work correctly
-- callers are responsible for calling this after show(), since this
window isn't shown by its own constructor.
*/
void QETTitleBlockTemplateEditor::readSettingsState()
{
QSettings settings;
// window state (toolbars, docks...)
QVariant state = settings.value("titleblocktemplateeditor/state");
if (state.isValid()) restoreState(state.toByteArray());
if (state.isValid()) {
if (!restoreState(state.toByteArray())) {
settings.remove("titleblocktemplateeditor/state");
}
}
}
/**
+1
View File
@@ -108,6 +108,7 @@ class QETTitleBlockTemplateEditor : public QETMainWindow {
public slots:
void readSettings();
void readSettingsState();
void writeSettings();
void selectedCellsChanged(const QList<TitleBlockCell *>&);
void duplicateCurrentLocation();
+10 -5
View File
@@ -1830,10 +1830,13 @@ QString TitleBlockTemplate::interpreteVariables(
QStringList TitleBlockTemplate::listOfVariables()
{
QStringList list;
// Match every "%{name}" placeholder. The bare "%name" form can't be
// extracted reliably without the variable list, and templates use the
// braced form, so only that is collected here.
static const QRegularExpression rx(QStringLiteral("%\\{([^}]+)\\}"));
// Match both the braced "%{name}" form and the bare "%name" form
// (bugtracker #245): a bare name is taken as the longest run of
// identifier characters right after the '%', which correctly stops at
// whitespace -- so "%name2 " and "%name2 %name3" are both detected,
// matching what previously only worked for the braced form.
static const QRegularExpression rx(
QStringLiteral("%\\{([^}]+)\\}|%([A-Za-z0-9_-]+)"));
// run through each individual cell
for (int j = 0 ; j < rows_heights_.count() ; ++ j) {
for (int i = 0 ; i < columns_width_.count() ; ++ i) {
@@ -1844,7 +1847,9 @@ QStringList TitleBlockTemplate::listOfVariables()
const QString cell_value = cells_[i][j] -> value.name();
auto it = rx.globalMatch(cell_value);
while (it.hasNext()) {
const QString name = it.next().captured(1);
const QRegularExpressionMatch m = it.next();
const QString name = m.captured(1).isEmpty()
? m.captured(2) : m.captured(1);
if (!name.isEmpty() && !list.contains(name))
list << name;
}
+35 -66
View File
@@ -18,6 +18,7 @@
#include "bomexportdialog.h"
#include "../dataBase/ui/elementquerywidget.h"
#include "../bomexport.h"
#include "../qetapp.h"
#include "../qetinformation.h"
#include "../qetproject.h"
@@ -41,8 +42,8 @@ BOMExportDialog::BOMExportDialog(QETProject *project, QWidget *parent) :
m_query_widget = new ElementQueryWidget(this);
ui->m_main_layout->insertWidget(0, m_query_widget);
//By default format as bom is clicked
on_m_format_as_bom_clicked(true);
m_query_widget->setQuery(BomExport::defaultQuery());
on_m_format_as_bom_clicked(false);
}
/**
@@ -67,90 +68,58 @@ int BOMExportDialog::exec()
if (dir.isEmpty()) dir = QETApp::documentDir();
QString file_name = dir % "/" % tr("nomenclature_") % QString(m_project ->title() % ".csv");
QString file_path = QFileDialog::getSaveFileName(this, tr("Enregister sous... "), file_name, tr("Fichiers csv (*.csv)"));
QFile file(file_path);
if (!file_path.isEmpty())
{
if (QFile::exists(file_path ))
{
// if file already exist -> delete it
if (!QFile::remove(file_path) )
{
QMessageBox::critical(this, tr("Erreur"),
tr("Impossible de remplacer le fichier!\n\n")+
"Destination : "+file_path+"\n");
}
}
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream stream(&file);
stream << getBom() << &Qt::endl(stream);
QString error;
const auto csv = getBom(&error);
if (!error.isEmpty() || !BomExport::writeCsv(file_path, csv, &error)) {
QMessageBox::critical(
this, tr("Erreur"),
tr("Impossible d'enregistrer la nomenclature dans %1.\n%2")
.arg(file_path, error));
}
}
}
return r;
}
QString BOMExportDialog::getBom()
QByteArray BOMExportDialog::getBom(QString *error)
{
if (error) {
error->clear();
}
m_project->dataBase()->updateDB();
auto query_ = m_project->dataBase()->newQuery(m_query_widget->queryStr());
QString return_string;
if (!query_.exec()) {
qDebug() << "BOMExportDialog::getBom : query errir : " << query_.lastError();
if (error) {
*error = query_.lastError().text();
}
return {};
}
else
QStringList header_names;
if (ui->m_include_headers->isChecked())
{
//HEADERS
if (ui->m_include_headers)
const auto record_ = query_.record();
for (int i = 0; i < record_.count(); ++i)
{
auto record_ = query_.record();
QStringList header_name;
for (auto i=0 ; i<record_.count() ; ++i)
{
auto field_name = record_.fieldName(i);
qDebug() << "field name = " << field_name;
if (field_name == "position") {
header_name << tr("Position");
} else if (field_name == "diagram_position") {
header_name << tr("Position du folio");
} else if (field_name == "designation_qty") {
header_name << tr("Quantité numéro d'article", "Special field with name : designation quantity");
} else {
header_name << QETInformation::translatedInfoKey(field_name);
if (header_name.isEmpty()) {
header_name << field_name;
}
}
const auto field_name = record_.fieldName(i);
if (field_name == QLatin1String("position")) {
header_names << tr("Position");
} else if (field_name == QLatin1String("diagram_position")) {
header_names << tr("Position du folio");
} else if (field_name == QLatin1String("designation_qty")) {
header_names << tr("Quantité numéro d'article", "Special field with name : designation quantity");
} else {
const auto translated = QETInformation::translatedInfoKey(field_name);
header_names << (translated.isEmpty() ? field_name : translated);
}
return_string = header_name.join(";") % "\n";
}
//ROWS
while (query_.next())
{
auto i=0;
QStringList values;
while (query_.value(i).isValid())
{
auto date = query_.value(i).toDate();
if (!date.isNull()) {
values << QLocale::system().toString(query_.value(i).toDate(), QLocale::ShortFormat);
} else {
values << query_.value(i).toString();
}
++i;
}
return_string += values.join(";") % "\n";
values.clear();
}
}
qDebug() << return_string;
return return_string;
return BomExport::toCsv(
query_, header_names, ui->m_include_headers->isChecked());
}
/**
+2 -1
View File
@@ -19,6 +19,7 @@
#define BOMEXPORTDIALOG_H
#include <QDialog>
#include <QByteArray>
class QETProject;
class ElementQueryWidget;
@@ -39,7 +40,7 @@ class BOMExportDialog : public QDialog
~BOMExportDialog() override;
virtual int exec() override;
QString getBom();
QByteArray getBom(QString *error = nullptr);
private slots:
void on_m_format_as_bom_clicked(bool checked);

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