mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-11 14:44:15 +02:00
Merge branch 'master' into fix-nonfinite-coordinate-validation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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());
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -116,6 +116,7 @@ class QETElementEditor : public QMainWindow
|
||||
private:
|
||||
bool canClose();
|
||||
void readSettings();
|
||||
void readSettingsState();
|
||||
void writeSettings() const;
|
||||
void setupActions();
|
||||
void updateAction();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 **/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -108,6 +108,7 @@ class QETTitleBlockTemplateEditor : public QETMainWindow {
|
||||
|
||||
public slots:
|
||||
void readSettings();
|
||||
void readSettingsState();
|
||||
void writeSettings();
|
||||
void selectedCellsChanged(const QList<TitleBlockCell *>&);
|
||||
void duplicateCurrentLocation();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user