Merge branch 'master' into fix/titleblock-bare-variable-detection-bug245

This commit is contained in:
ispyisail
2026-09-12 23:05:41 +12:00
288 changed files with 80744 additions and 35497 deletions
@@ -38,6 +38,20 @@ ElementsCollectionModel::ElementsCollectionModel(QObject *parent) :
{
}
/**
@brief ElementsCollectionModel::~ElementsCollectionModel
Destructor. loadCollections() may still have background threads
(via QtConcurrent::map()) running setUpData() on this model's items
when the model is destroyed (e.g. the user cancels the dialog before
loading finishes). Wait for them here so QStandardItemModel's
destructor doesn't free items out from under them, which used to
crash the whole application (bugtracker #291).
*/
ElementsCollectionModel::~ElementsCollectionModel()
{
m_future.waitForFinished();
}
/**
@brief ElementsCollectionModel::data
Reimplemented from QStandardItemModel
@@ -49,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();
@@ -316,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);
}
@@ -35,6 +35,7 @@ class ElementsCollectionModel : public QStandardItemModel
public:
ElementsCollectionModel(QObject *parent = Q_NULLPTR);
~ElementsCollectionModel() override;
QVariant data(const QModelIndex &index, int role) const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
@@ -949,14 +949,7 @@ void ElementsCollectionWidget::search()
}
hideCollection(true);
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
const QStringList text_list = text.split("+", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
#endif
QModelIndexList match_index;
for (QString txt : text_list) {
match_index << m_model->match(m_showed_index.isValid()
@@ -143,11 +143,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
if (!dir_path.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
#else
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
#endif
QString current_path = "";
for (const QString &part : parts) {
QString parent_path = current_path;
@@ -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
@@ -46,6 +46,18 @@ class PropertiesEditorDialog : public QDialog
PropertiesEditorDialog(T editor, QWidget *parent = nullptr) :
QDialog (parent)
{
// Without this, a QWidget-modal QDialog with a parent falls
// back to macOS's automatic sheet presentation, which some
// Qt/Cocoa versions render stuck centered on screen and
// non-draggable rather than as a proper attached sheet
// (bugtracker #275). Other dialogs in this codebase already
// opt in explicitly (see ElementDialog, DiagramPropertiesDialog)
// -- this one was missing it.
setWindowModality(Qt::WindowModal);
#ifdef Q_OS_MACOS
setWindowFlags(Qt::Sheet);
#endif
//Set dialog title
setWindowTitle(editor->title());
// Reparent the editor,
@@ -72,6 +72,7 @@ QPropertyUndoCommand::QPropertyUndoCommand(const QPropertyUndoCommand *other)
m_new_value = other->m_new_value;
m_animate = other->m_animate;
m_first_time = other->m_first_time;
m_undo_first_time = other->m_undo_first_time;
setText(other->text());
}
@@ -99,11 +100,20 @@ void QPropertyUndoCommand::enableAnimation (bool animate) {
@param first_time = if true,
the first animation is done at the first call of redo if false,
the first animation is done at the second call of redo.
The same rule applies to undo, tracked independently: redo() always
runs before the first undo() (QUndoStack::push() calls redo()
immediately), so by the time undo() can run at all, redo()'s own
m_first_time has already flipped true. Sharing that flag would leave
undo() with no instant path ever reachable in practice -- reusing it
is not actually symmetric, it just looks like it is. m_undo_first_time
gives undo() the same one-time grace period redo() has, on its own
first call instead of redo's.
*/
void QPropertyUndoCommand::setAnimated(bool animate, bool first_time)
{
m_animate = animate;
m_first_time = first_time;
m_undo_first_time = first_time;
}
/**
@@ -118,6 +128,18 @@ bool QPropertyUndoCommand::mergeWith(const QUndoCommand *other)
QPropertyUndoCommand const *undo = static_cast<const QPropertyUndoCommand *>(other);
if (m_object != undo->m_object
|| m_property_name != undo->m_property_name) return false;
// Same object and property name alone isn't enough: two entirely
// separate, deliberate actions (say, cropping an image and then
// mirroring it) both go through the same "pixmap" property and
// would otherwise silently coalesce into one undo entry, carrying
// only the first action's label -- the second vanishes from the
// undo list with no way to undo just it. Legitimate merging (a
// slider or spinbox pushing one command per tick while being
// dragged, e.g. ArcEditor's angle editors) always reuses the exact
// same text() across the whole sequence, so requiring a match here
// keeps that working unchanged while refusing to merge anything
// that isn't actually a continuation of the same action.
if (text() != other->text()) return false;
m_new_value = undo->m_new_value;
return true;
}
@@ -155,7 +177,7 @@ void QPropertyUndoCommand::undo()
{
if (m_object->property(m_property_name) != m_old_value)
{
if (m_animate)
if (m_animate && m_undo_first_time)
{
QPropertyAnimation *animation = new QPropertyAnimation(m_object, m_property_name);
animation->setStartValue(m_new_value);
@@ -163,7 +185,10 @@ void QPropertyUndoCommand::undo()
animation->start(QAbstractAnimation::DeleteWhenStopped);
}
else
{
m_object->setProperty(m_property_name, m_old_value);
m_undo_first_time = true;
}
}
QUndoCommand::undo();
@@ -58,7 +58,8 @@ class QPropertyUndoCommand : public QUndoCommand
const char *m_property_name;
QVariant m_old_value, m_new_value;
bool m_animate = false,
m_first_time = true;
m_first_time = true,
m_undo_first_time = true;
};
#endif // QPROPERTYUNDOCOMMAND_H
@@ -19,6 +19,7 @@
#define TERMINALSTRIPDRAWER_H
#include <QPointer>
#include <QUuid>
#include "properties/terminalstriplayoutpattern.h"
@@ -130,12 +130,7 @@ bool PhysicalTerminal::setLevelOf(const QSharedPointer<RealTerminal> &terminal,
const int i = m_real_terminal.indexOf(terminal);
if (i >= 0)
{
#if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
m_real_terminal.swapItemsAt(i, std::min<int>(level, static_cast<int>(m_real_terminal.size())-1));
#else
auto j = std::min(level, m_real_terminal.size()-1);
std::swap(m_real_terminal.begin()[i], m_real_terminal.begin()[j]);
#endif
return true;
}
return false;
+1 -4
View File
@@ -64,11 +64,8 @@ bool TerminalStripData::fromXml(const QDomElement &xml_element)
"due to wrong tag name. Expected " << this->xmlTagName() << " used " << xml_element.tagName();
return false;
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
m_uuid = QUuid::fromString(xml_element.attribute(QStringLiteral("uuid")));
#else
m_uuid = QUuid(xml_element.attribute(QStringLiteral("uuid")));
#endif
for (auto &xml_info :
QETXML::findInDomElement(xml_element.firstChildElement(QStringLiteral("informations")),
@@ -28,6 +28,8 @@
#include "../terminalstrip.h"
#include "../../qetinformation.h"
#include <QUuid>
TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QWidget *parent) :
QDockWidget(parent),
ui(new Ui::TerminalStripTreeDockWidget)
@@ -35,11 +37,7 @@ TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QW
ui->setupUi(this);
setProject(project);
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
#else
ui->m_tree_view->expandAll();
#endif
}
TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget()
@@ -93,11 +91,7 @@ void TerminalStripTreeDockWidget::reload()
buildTree();
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
#else
ui->m_tree_view->expandAll();
#endif
//Reselect the tree widget item of the current edited strip
auto item = m_item_strip_H.key(current_);
@@ -20,6 +20,7 @@
#include <QDockWidget>
#include <QPointer>
#include <QHash>
class QETProject;
class QTreeWidgetItem;
+139 -98
View File
@@ -24,6 +24,8 @@
#include "../qetgraphicsitem/element.h"
#include "../qetxml.h"
#include "../qetproject.h"
#include <QDir>
#include <QDomDocument>
#include <QStringList>
#include <QVariant>
#include <utility>
@@ -703,6 +705,74 @@ namespace autonum
return formula;
}
/**
@brief prefixFromLabelFile
Look up a prefix for @a path (path[dirLevel] outermost, path[1] the
deepest directory; path[0], the element's own file name, is never
matched) in the qet_labels.xml at @a filepath.
Descends through nested \<category name="..."\> elements matching
path[dirLevel], path[dirLevel-1], ..., path[1] in turn, considering
only *direct* children at each step -- unlike a flat token scan,
this cannot be fooled by a same-named category living elsewhere in
the document at the wrong nesting depth (bugtracker #671 item 5).
At each matched level, that category's own \<prefix\> child -- even
an empty one -- overrides whatever a shallower ancestor already
provided, so an explicit empty \<prefix/\> cancels inheritance
rather than silently falling back to it (the behaviour requested in
PR #686 review). A category with no \<prefix\> child at all leaves
the inherited value untouched, which is how a directory with no
prefix of its own comes to inherit its parent's, as the file's own
header comment documents.
@return the prefix that applies, or a null QString if the file
cannot be read, is not well-formed, or does not describe this
path at all (as opposed to describing it with no prefix
anywhere along it, which is a non-null empty string).
*/
static QString prefixFromLabelFile(const QString &filepath, const QStringList &path, int dirLevel)
{
QFile file(filepath);
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
QDomDocument document;
if (!document.setContent(&file))
return QString();
QDomElement node = document.documentElement();
if (node.isNull())
return QString();
QString prefix;
for (int i = dirLevel ; i >= 1 ; --i) {
QDomElement child = node.firstChildElement(QStringLiteral("category"));
while (!child.isNull()
&& child.attribute(QStringLiteral("name")) != path[i]) {
child = child.nextSiblingElement(QStringLiteral("category"));
}
if (child.isNull())
return QString();
node = child;
const QDomElement own = node.firstChildElement(QStringLiteral("prefix"));
if (!own.isNull()) {
//readElementText()'s null-vs-empty distinction that PR
//#686 needed for the old QXmlStreamReader-based lookup
//has a QDomElement equivalent: text() on an empty
//element can itself come back null depending on how the
//XML was written, so the same explicit fallback applies
//-- an empty QString here means "found, deliberately
//blank", not "not found".
prefix = own.text();
if (prefix.isNull())
prefix = QString("");
}
}
return prefix;
}
/**
@brief elementPrefixForLocation
@param location
@@ -716,114 +786,85 @@ namespace autonum
if (!location.isProject())
return QString();
QXmlStreamReader rxml;
QString path[10];
int i = -1;
//Directory names from the element up to (not including) the
//collection root, outermost last -- path[dirLevel] is the
//top-level category, path[1] the element's immediate parent
//directory, path[0] the element's own file name (never matched
//against a category: the search stops descending once it has
//matched path[1], the deepest real directory). An unbounded
//QStringList rather than a fixed-size array, because a custom
//collection can nest deeper than the shipped one -- see
//bugtracker #671 item 3.
QStringList path;
ElementsLocation current_location = location;
int dirLevel = -1;
//Add location name to path array
while((current_location.parent() != current_location) && (current_location.parent().fileName() != "import"))
while ((current_location.parent() != current_location)
&& (current_location.parent().fileName() != "import"))
{
i++;
path[i]=current_location.fileName();
path << current_location.fileName();
current_location = current_location.parent();
dirLevel++;
}
//User Element without folder treatment
if (i == -1)
{
i = 0;
path[i]=current_location.fileName();
//User element without folder treatment
if (path.isEmpty()) {
path << current_location.fileName();
current_location = current_location.parent();
dirLevel = 0;
}
const int dirLevel = path.size() - 1;
//Name of the top-level tree the element's path was found
//under, e.g. "10_electric" -- or, for a custom/company
//collection not organised that way, whatever its top-level
//folder happens to be called.
const QString collection_root = current_location.fileName();
//Every top-level common-collection tree (10_electric,
//20_logic, 30_hydraulic, ...) may carry its own
//qet_labels.xml, with categories relative to that tree, the
//same way 10_electric/qet_labels.xml already does -- not just
//10_electric, which is all the hardcoded check this replaces
//used to allow (bugtracker #671 item 2). commonElementsDir()
//-- unlike customElementsDir(), which normalises this itself
//-- returns whatever path the user configured verbatim, with
//no guaranteed trailing separator; concatenating a suffix onto
//it directly used to silently mangle the path (and so the
//prefix lookup) for any install relocated to a directory
//without a trailing slash (#671 item 1). QDir::filePath()
//joins correctly either way.
{
const QString common_file = QDir(QETApp::commonElementsDir())
.filePath(collection_root + QStringLiteral("/qet_labels.xml"));
const QString prefix = prefixFromLabelFile(common_file, path, dirLevel);
if (!prefix.isNull()) {
return prefix;
}
}
// Create Custom labels if qet_labels.xml exits in customElementsDir
if (current_location.fileName() != "10_electric"){
QString custom_labels = "qet_labels.xml";
QString customfilepath = QETApp::customElementsDir().append(custom_labels);
QFile file(customfilepath);
file.isReadable();
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
rxml.setDevice(&file);
rxml.readNext();
/* Which collection an element actually came from is not
* recoverable post-import (addElement() strips the protocol),
* so custom and company labels files are tried against two
* possible layouts: with the collection's top-level tree name
* folded into the path (a custom/company file organised as a
* mirror of the common collection, tree name included) and
* without it (a file scoped to just this one tree, matching
* how the common collection's own files are written). Custom
* is tried before company, so a user override wins over a
* shared one.
*/
QStringList path_from_root = path;
path_from_root << collection_root;
while(!rxml.atEnd())
{
if (rxml.attributes().value("name").toString() == path[i])
{
rxml.readNext();
i=i-1;
//reached element directory
if (i==0)
{
for (int j=i; j<= dirLevel; j = j +1)
{
//if there is a prefix available apply prefix
if(rxml.name().toString()=="prefix")
{
return rxml.readElementText();
}
//if there isn't a prefix available, find parent prefix in parent folder
else
{
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
{
rxml.skipCurrentElement();
rxml.readNext();
}
}
}
const QStringList candidate_dirs = {
QETApp::customElementsDir(),
QETApp::companyElementsDir()
};
for (const QString &dir : candidate_dirs) {
const QString candidate =
QDir(dir).filePath(QStringLiteral("qet_labels.xml"));
for (const QStringList &segments : {path_from_root, path}) {
const QString prefix = prefixFromLabelFile(
candidate, segments, segments.size() - 1);
if (!prefix.isNull()) {
return prefix;
}
}
rxml.readNext();
}
}
else
{
QString qet_labels = "10_electric/qet_labels.xml";
QString filepath = QETApp::commonElementsDir().append(qet_labels);
QFile file(filepath);
file.isReadable();
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
rxml.setDevice(&file);
rxml.readNext();
while(!rxml.atEnd())
{
if (rxml.attributes().value("name").toString() == path[i])
{
rxml.readNext();
i=i-1;
//reached element directory
if (i==0)
{
for (int j=i; j<= dirLevel; j = j +1)
{
//if there is a prefix available apply prefix
if(rxml.name().toString()=="prefix")
{
return rxml.readElementText();
}
//if there isn't a prefix available, find parent prefix in parent folder
else
{
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
{
rxml.skipCurrentElement();
rxml.readNext();
}
}
}
}
}
rxml.readNext();
}
}
return QString();
}
@@ -141,7 +141,6 @@
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -163,7 +162,6 @@
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -238,7 +236,6 @@
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -328,7 +325,6 @@
<widget class="QWidget" name="folioWidget" native="true">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
<kerning>true</kerning>
</font>
@@ -338,7 +334,6 @@
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
+114
View File
@@ -0,0 +1,114 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "bomexport.h"
#include <QSaveFile>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QVariant>
namespace {
QString csvField(QString value)
{
value.replace(QLatin1Char('"'), QStringLiteral("\"\""));
return QLatin1Char('"') + value + QLatin1Char('"');
}
QByteArray csvRecord(const QStringList &values)
{
QStringList escaped;
for (const auto &value : values) {
escaped.append(csvField(value));
}
return (escaped.join(QLatin1Char(';')) + QLatin1Char('\n')).toUtf8();
}
}
QStringList BomExport::defaultColumns()
{
return {
QStringLiteral("label"),
QStringLiteral("designation"),
QStringLiteral("manufacturer"),
QStringLiteral("manufacturer_reference"),
QStringLiteral("model"),
QStringLiteral("description"),
QStringLiteral("category"),
QStringLiteral("quantity"),
QStringLiteral("voltage_rating"),
QStringLiteral("current_rating"),
QStringLiteral("folio"),
QStringLiteral("notes")
};
}
QString BomExport::defaultQuery()
{
return QStringLiteral("SELECT %1 FROM element_nomenclature_view "
"WHERE ( element_type = 'simple' OR element_type = 'master') "
"ORDER BY diagram_position, position, label")
.arg(defaultColumns().join(QStringLiteral(", ")));
}
QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers, int *row_count)
{
QByteArray csv("\xEF\xBB\xBF");
if (include_headers) {
csv += csvRecord(headers);
}
int rows = 0;
while (query.next())
{
QStringList values;
for (int i = 0; i < query.record().count(); ++i) {
values.append(query.value(i).toString());
}
csv += csvRecord(values);
++rows;
}
if (row_count) {
*row_count = rows;
}
return csv;
}
bool BomExport::writeCsv(const QString &file_path, const QByteArray &csv,
QString *error)
{
if (error) {
error->clear();
}
QSaveFile file(file_path);
if (!file.open(QIODevice::WriteOnly) || file.write(csv) != csv.size())
{
if (error) {
*error = file.errorString();
}
file.cancelWriting();
return false;
}
if (!file.commit())
{
if (error) {
*error = file.errorString();
}
return false;
}
return true;
}
+37
View File
@@ -0,0 +1,37 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BOMEXPORT_H
#define BOMEXPORT_H
#include <QByteArray>
#include <QString>
#include <QStringList>
class QSqlQuery;
namespace BomExport
{
QStringList defaultColumns();
QString defaultQuery();
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers = true, int *row_count = nullptr);
bool writeCsv(const QString &file_path, const QByteArray &csv,
QString *error = nullptr);
}
#endif // BOMEXPORT_H
+2
View File
@@ -73,6 +73,8 @@ class BorderTitleBlock : public QObject
return(rows_count_ * rows_height_); }
/// @return la rows header width, in pixels
qreal rowsHeaderWidth() const { return(rows_header_width_); }
/// @return the edge where title block is docked
Qt::Edge titleBlockEdge() const { return(m_edge); }
// border - title block = diagram
/**
+126 -54
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cli_export.h"
#include "bomexport.h"
#include "bordertitleblock.h"
#include "conductornumexport.h"
@@ -39,6 +40,7 @@
#include <QDomDocument>
#include <QDate>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
@@ -70,6 +72,7 @@ const QHash<QString, QString> &exportFlags()
{"--export-cables", "cables"},
{"--export-wires", "wires"},
{"--export-bom", "bom"},
{"--export-wiring", "wiring"},
{"--export-nets", "nets"},
{"--export-links", "links"},
{"--info", "info"},
@@ -109,7 +112,16 @@ QString diagramStem(Diagram *diagram, int index)
}
/// Render @p diagram into @p painter, fitting @p target to the page rect.
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
/// @p showTerminals: paint terminal markers (red stroke + blue docking
/// dot) and terminal names, as the interactive editor does. Off by
/// default — Terminal::paint() draws them whenever the diagram's
/// drawTerminals()/drawTerminalNames() flags are set (default true, so
/// headless export used to ship them as editor UI); the GUI export
/// dialog already defaults to clearing them via
/// Diagram::applyProperties(). Pass true (--show-terminals) to keep
/// them, e.g. to visually debug an unconnected pin.
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target,
bool showTerminals = false)
{
const QRect source = diagramRect(diagram);
// Export without the editor grid: drawBackground() only paints it when
@@ -117,14 +129,21 @@ void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
// and restore it afterwards.
const bool was_drawing_grid = diagram->displayGrid();
const bool was_drawing_guides = diagram->displayGuides();
const bool was_drawing_terminals = diagram->drawTerminals();
const bool was_drawing_terminal_names = diagram->drawTerminalNames();
diagram->setDisplayGrid(false);
diagram->setDisplayGuides(false);
diagram->setDrawTerminals(showTerminals);
diagram->setDrawTerminalNames(showTerminals);
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
diagram->setDisplayGrid(was_drawing_grid);
diagram->setDisplayGuides(was_drawing_guides);
diagram->setDrawTerminals(was_drawing_terminals);
diagram->setDrawTerminalNames(was_drawing_terminal_names);
}
int exportPdf(QETProject &project, const QString &output)
int exportPdf(QETProject &project, const QString &output,
bool showTerminals = false)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
@@ -164,7 +183,7 @@ int exportPdf(QETProject &project, const QString &output)
}
const QRectF target(0, 0,
writer.width(), writer.height());
renderDiagram(diagram, painter, target);
renderDiagram(diagram, painter, target, showTerminals);
// Inject clickable cross-reference / folio-report hyperlinks for this
// page. The geometry is rebuilt from the QPdfWriter (not a QPrinter):
@@ -214,7 +233,7 @@ int exportPdf(QETProject &project, const QString &output)
}
int exportImages(QETProject &project, const QString &format,
const QString &out_dir)
const QString &out_dir, bool showTerminals = false)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
@@ -237,14 +256,16 @@ int exportImages(QETProject &project, const QString &format,
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
gen.setTitle(diagram->title());
QPainter painter(&gen);
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
showTerminals);
painter.end();
} else { // png
QImage image(r.size(), QImage::Format_ARGB32);
image.fill(Qt::white);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
showTerminals);
painter.end();
if (!image.save(path)) {
err << "Failed to write '" << path << "'.\n";
@@ -284,61 +305,40 @@ int exportCsv(QETProject &project, const QString &format, const QString &output)
return 0;
}
/// Quote a field for CSV output (RFC-4180 style, ';' delimiter).
QString csvField(const QString &value)
{
if (value.contains(';') || value.contains('"')
|| value.contains('\n') || value.contains('\r')) {
QString v = value;
v.replace('"', "\"\"");
return '"' % v % '"';
}
return value;
}
/// Bill of materials: one row per element, key component-data fields.
/// Pulls from QET's own project database (the same source as the GUI BOM
/// export), so the output matches what the editor produces.
/// Bill of materials from the same project database and default query as the
/// GUI nomenclature export.
int exportBom(QETProject &project, const QString &output)
{
// The project database is built lazily; force a (re)build before querying.
project.dataBase()->updateDB();
static const QStringList columns {
"label", "designation", "manufacturer", "manufacturer_reference",
"quantity", "location", "function", "title", "folio"
};
QSqlQuery query = project.dataBase()->newQuery(
"SELECT " % columns.join(", ") %
" FROM element_nomenclature_view ORDER BY label");
QSqlQuery query = project.dataBase()->newQuery(BomExport::defaultQuery());
if (!query.exec()) {
err << "BOM query failed: " << query.lastError().text() << "\n";
return 1;
}
QString csv = columns.join(";") % "\n";
int rows = 0;
while (query.next()) {
QStringList values;
for (int i = 0; i < columns.size(); ++i)
values << csvField(query.value(i).toString());
csv += values.join(";") % "\n";
++rows;
}
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
const auto csv = BomExport::toCsv(
query, BomExport::defaultColumns(), true, &rows);
QString error;
if (!BomExport::writeCsv(output, csv, &error)) {
err << "Cannot write '" << output << "': " << error << "\n";
return 1;
}
QTextStream fout(&file);
fout << csv;
file.close();
out << "Exported " << rows << " component(s) -> " << output << "\n";
return 0;
}
QString csvField(const QString &value)
{
if (value.contains(QLatin1Char(';')) || value.contains(QLatin1Char('"'))
|| value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r')))
{
QString escaped = value;
escaped.replace(QLatin1Char('"'), QStringLiteral("\"\""));
return QLatin1Char('"') % escaped % QLatin1Char('"');
}
return value;
}
/// Count terminals on @p element that no conductor connects to.
int freeTerminals(Element *element)
{
@@ -525,6 +525,70 @@ QHash<Element *, int> folioIndex(QETProject &project)
return folio;
}
/// From-to wiring list: one row per conductor, each endpoint resolved to its
/// element label and terminal name.
///
/// Reads wiring_list_view out of the project database. --export-cables produces
/// the same logical list from the document XML instead, and the two are meant
/// to agree: running both and diffing them is a direct check that the database
/// still describes the project, which is otherwise only observable through the
/// GUI.
int exportWiring(QETProject &project, const QString &output)
{
// The project database is built lazily; force a (re)build before querying.
project.dataBase()->updateDB();
static const QStringList columns {
"wire_number", "from_element_label", "from_terminal",
"to_element_label", "to_terminal", "diagram_position", "conductor_uuid"
};
QSqlQuery query = project.dataBase()->newQuery(
"SELECT " % columns.join(", ") %
" FROM wiring_list_view"
//Wire numbers are text, so a plain sort puts "10" before "9".
//Numeric ones first, ordered by value; anything non-numeric after,
//ordered as text. The trailing wire_number keeps ties stable.
" ORDER BY diagram_position,"
" CASE WHEN wire_number GLOB '[0-9]*' THEN 0 ELSE 1 END,"
" CAST(wire_number AS INTEGER),"
" wire_number");
if (!query.exec()) {
err << "Wiring list query failed: " << query.lastError().text() << "\n";
return 1;
}
QString csv = columns.join(";") % "\n";
int rows = 0;
while (query.next()) {
QStringList values;
for (int i = 0; i < columns.size(); ++i)
values << csvField(query.value(i).toString());
csv += values.join(";") % "\n";
++rows;
}
//Written through QSaveFile so a failure part-way leaves the previous
//file intact rather than a truncated one, and with a UTF-8 byte order
//mark: without it Excel opens a .csv as the local 8-bit codepage and
//mangles any accented element label. Qt writes UTF-8 by default, so
//the bytes were already right -- the mark is what tells Excel so.
QSaveFile file(output);
if (!file.open(QIODevice::WriteOnly)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
static const char utf8_bom[] = "\xEF\xBB\xBF";
file.write(utf8_bom, 3);
file.write(csv.toUtf8());
if (!file.commit()) {
err << "Cannot write '" << output << "': " << file.errorString() << "\n";
return 1;
}
out << "Exported " << rows << " conductor(s) -> " << output << "\n";
return 0;
}
/// Electrical nets: groups of terminals joined into one potential.
/// Walks QET's own potential graph, so each net is a connected component
/// of terminals across all folios. The ground truth for connectivity.
@@ -768,13 +832,19 @@ bool isExportRequest(const QStringList &args)
int run(const QStringList &args)
{
// --show-terminals is a standalone switch (not tied to a position),
// so pull it out before the positional project/output arguments are
// collected below.
QStringList filtered = args;
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
QString flag;
QStringList rest;
for (int i = 0; i < args.size(); ++i) {
if (exportFlags().contains(args.at(i))) {
flag = args.at(i);
for (int j = i + 1; j < args.size(); ++j)
rest << args.at(j);
for (int i = 0; i < filtered.size(); ++i) {
if (exportFlags().contains(filtered.at(i))) {
flag = filtered.at(i);
for (int j = i + 1; j < filtered.size(); ++j)
rest << filtered.at(j);
break;
}
}
@@ -818,11 +888,13 @@ int run(const QStringList &args)
return 2;
}
if (format == "pdf")
return exportPdf(project, output);
return exportPdf(project, output, showTerminals);
if (format == "cables" || format == "wires")
return exportCsv(project, format, output);
if (format == "bom")
return exportBom(project, output);
if (format == "wiring")
return exportWiring(project, output);
if (format == "nets")
return exportNets(project, output);
if (format == "links")
@@ -831,7 +903,7 @@ int run(const QStringList &args)
return resaveProject(project, output);
if (format == "settb")
return setTitleBlock(project, output, rest.mid(2));
return exportImages(project, format, output);
return exportImages(project, format, output, showTerminals);
}
} // namespace CLIExport
+13 -3
View File
@@ -42,12 +42,13 @@ namespace CLIExport {
@return process exit code (0 on success).
Usage:
qelectrotech --export-pdf <project.qet> <output.pdf>
qelectrotech --export-png <project.qet> <output_dir>
qelectrotech --export-svg <project.qet> <output_dir>
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --export-wiring <project.qet> <output.csv>
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
qelectrotech --info <project.qet> [output.json]
@@ -57,9 +58,18 @@ namespace CLIExport {
PDF: one multi-page document (one diagram per page).
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
--show-terminals: also paint terminal markers (red stroke + blue
docking dot) and terminal names, as the interactive editor
does; off by default, matching the GUI export dialog's
default. Has no effect on the non-image export modes.
cables: wiring list (one row per conductor) as CSV.
wires: list of distinct wire numbers as CSV.
bom: bill of materials (one row per element) as CSV.
wiring: from-to wiring list (one row per conductor) as CSV, read from
the project database. Same logical list as `cables`, which
reads the document XML instead; the two are meant to agree,
so diffing them checks that the database still describes the
project.
nets: electrical nets (connected-terminal groups) as JSON.
links: element cross-references (coil/contact) as CSV, with
unresolved links flagged.
+1 -7
View File
@@ -72,14 +72,8 @@ bool ConductorNumExport::toCsv()
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream stream(&file);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
stream << wiresNum() << endl;
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
stream << wiresNum() << &Qt::endl(stream);
#endif
}
else {
return false;
-7
View File
@@ -811,14 +811,7 @@ void ConductorProperties::readStyle(const QString &style_string) {
if (style_string.isEmpty()) return;
// recupere la liste des couples style / valeur
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QStringList styles = style_string.split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code QString::SkipEmptyParts for QT 5.14 or later")
#endif
QStringList styles = style_string.split(";", Qt::SkipEmptyParts);
#endif
QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$");
if (!Rx.isValid())
+91
View File
@@ -0,0 +1,91 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTUSAGE_H
#define CONTACTUSAGE_H
#include <algorithm>
/**
@brief The ContactUsage struct
How many slave contacts a master element currently uses, broken down
by contact type.
Header-only and free of any graphics dependency so that the counting
rules can be unit tested on their own. MasterElement::contactUsage()
is the thin wrapper that feeds it the linked elements.
This counts contacts, which is what tells you how many contacts an
auxiliary block must provide. It is deliberately not the count that
MasterElement::isFull() uses: a master's max_slaves is a number of
slots, and a slave fills exactly one slot however many contacts it
carries.
Two rules are easy to get wrong, and both live here so that every
caller gets them right:
- a slave stands for as many contacts as its "number" kind
information says, so a 4 pole contact counts as 4, not as 1
- a changeover contact is counted once, as sw. CrossRefItem's
NOElements() and NCElements() both return it, so adding those two
lists together would count it twice.
*/
struct ContactUsage
{
/**
Contact types a slave can declare. Mirrors
ElementData::SlaveState, which is not used directly so that this
header stays free of the element data dependencies and can be
unit tested on its own. MasterElement::contactUsage() maps
between the two.
*/
enum Type
{
NO, ///< Normally open
NC, ///< Normally closed
SW, ///< Changeover
Other ///< Neither of the above
};
int no = 0; ///< Normally open
int nc = 0; ///< Normally closed
int sw = 0; ///< Changeover
int other = 0; ///< Neither of the above
int total() const { return no + nc + sw + other; }
/**
Add one slave element to the tally.
@param type the contact type the slave declares
@param contacts how many contacts it stands for. Values below 1
are treated as 1: an element which declares no
contact count is still one contact.
*/
void addSlave(Type type, int contacts)
{
const int n = std::max(1, contacts);
switch (type)
{
case NO: no += n; break;
case NC: nc += n; break;
case SW: sw += n; break;
case Other: other += n; break;
}
}
};
#endif // CONTACTUSAGE_H
+548 -36
View File
@@ -21,7 +21,9 @@
#include "../diagramposition.h"
#include "../elementprovider.h"
#include "../qetapp.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/terminal.h"
#include "../qetinformation.h"
#include "../qetproject.h"
@@ -51,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);
@@ -83,13 +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
@@ -108,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 {
@@ -150,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();
@@ -164,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));
@@ -178,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)
@@ -194,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()) {
@@ -222,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()) {
@@ -243,6 +356,137 @@ void projectDataBase::diagramInfoChanged(Diagram *diagram)
void projectDataBase::diagramOrderChanged()
{
m_content_changed = true;
}
/**
@brief projectDataBase::addConductor
@param conductor
*/
void projectDataBase::addConductor(Conductor *conductor)
{
m_content_changed = true;
if (!conductor || !conductor->diagram()) {
qDebug() << "projectDataBase::addConductor: null conductor or diagram";
return;
}
//Both endpoints must belong to an element: the terminal table is keyed
//on (terminal, element) and a terminal with no parent has no identity
//to key on. Terminals whose *definition* predates terminal uuids are
//fine -- Terminal::stableUuid() derives one from the terminal's local
//position, which is what the project format itself matches on.
if (!conductor->terminal1->parentElement()
|| !conductor->terminal2->parentElement()) {
return;
}
insertTerminal(conductor->terminal1);
insertTerminal(conductor->terminal2);
watchConductor(conductor);
bindConductorValues(m_insert_conductor_query, conductor, conductor->diagram());
if (!m_insert_conductor_query.exec()) {
qDebug() << "projectDataBase::addConductor insert error : " << m_insert_conductor_query.lastError();
} else {
emit dataBaseUpdated();
}
}
/**
@brief projectDataBase::removeConductor
@param 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();
} else {
emit dataBaseUpdated();
}
}
/**
@brief projectDataBase::updateConductor
Refresh the mutable columns of an already-inserted conductor.
Only the text (the wire number) can change without the conductor being
removed and re-added: its endpoints are fixed for its lifetime. Without
this, renaming a wire left the database holding the old number and the
wiring list showed a stale value until the next full repopulate.
@param conductor
*/
void projectDataBase::updateConductor(Conductor *conductor)
{
m_content_changed = true;
if (!conductor) {
return;
}
m_update_conductor_query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString());
m_update_conductor_query.bindValue(QStringLiteral(":text"), conductor->properties().text);
if (!m_update_conductor_query.exec()) {
qDebug() << "projectDataBase::updateConductor update error : " << m_update_conductor_query.lastError();
}
//Deliberately no dataBaseUpdated() here, unlike add/remove. The only
//column this touches is the wire text, which no view watched by
//ProjectDBModel displays -- the nomenclature shows elements, and its
//wire_count changes when a conductor appears or disappears, not when
//it is renamed. Emitting would make every ProjectDBModel re-run its
//query, and auto-numbering renames every conductor in the project in
//one pass.
}
/**
@brief projectDataBase::watchConductor
Keep this conductor's row in step with its properties.
Conductor::setProperties() has a dozen call sites (auto-numbering, the
properties dialog, element moves, deletion re-links...), so listening to
the signal it already emits is the only way to catch them all -- and the
only way to catch the ones added later. Qt::UniqueConnection makes a
repeated insert or a full repopulate harmless.
@param conductor
*/
void projectDataBase::watchConductor(Conductor *conductor)
{
connect(conductor, &Conductor::propertiesChange,
this, &projectDataBase::conductorPropertiesChanged,
Qt::UniqueConnection);
}
/**
@brief projectDataBase::conductorPropertiesChanged
*/
void projectDataBase::conductorPropertiesChanged()
{
m_content_changed = true;
if (auto *conductor = qobject_cast<Conductor *>(sender())) {
updateConductor(conductor);
}
}
/**
@brief projectDataBase::bindConductorValues
One binder for both insert paths, so a conductor added to a live diagram
and one read from a file can never drift apart -- the same reason
bindElementValues() exists for elements.
@param query
@param conductor
@param diagram : the diagram the conductor belongs to
*/
void projectDataBase::bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram)
{
query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString());
query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString());
query.bindValue(QStringLiteral(":terminal1_uuid"), conductor->terminal1->stableUuid().toString());
query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor->terminal1->parentElement()->uuid().toString());
query.bindValue(QStringLiteral(":terminal2_uuid"), conductor->terminal2->stableUuid().toString());
query.bindValue(QStringLiteral(":terminal2_element_uuid"), conductor->terminal2->parentElement()->uuid().toString());
query.bindValue(QStringLiteral(":text"), conductor->properties().text);
}
/**
@@ -323,8 +567,60 @@ bool projectDataBase::createDataBase()
qDebug() << " element_info_table query : " << query_.lastError();
}
//Create the terminal table.
//Terminal::uuid() is the terminal-position id baked into the catalog
//.elmt definition (e.g. "the top terminal") -- identical across every
//placed instance of that catalog element, not a per-instance id. A
//terminal instance is only uniquely identified by (uuid, element_uuid)
//together, so that pair is the primary key here, not uuid alone.
QString terminal_table("CREATE TABLE terminal"
"( "
"uuid VARCHAR(50) NOT NULL, "
"element_uuid VARCHAR(50) NOT NULL,"
"name VARCHAR(50),"
"PRIMARY KEY (uuid, element_uuid),"
"FOREIGN KEY (element_uuid) REFERENCES element (uuid)"
")");
if (!query_.exec(terminal_table)) {
qDebug() << "terminal_table query : "<< query_.lastError();
}
//Create the conductor table
QString conductor_table("CREATE TABLE conductor"
"( "
"uuid VARCHAR(50) PRIMARY KEY NOT NULL, "
"diagram_uuid VARCHAR(50) NOT NULL,"
"terminal1_uuid VARCHAR(50) NOT NULL,"
"terminal1_element_uuid VARCHAR(50) NOT NULL,"
"terminal2_uuid VARCHAR(50) NOT NULL,"
"terminal2_element_uuid VARCHAR(50) NOT NULL,"
"text VARCHAR(100),"
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid),"
"FOREIGN KEY (terminal1_uuid, terminal1_element_uuid) REFERENCES terminal (uuid, element_uuid),"
"FOREIGN KEY (terminal2_uuid, terminal2_element_uuid) REFERENCES terminal (uuid, element_uuid)"
")");
if (!query_.exec(conductor_table)) {
qDebug() << "conductor_table query : "<< query_.lastError();
}
//The element-facing columns are looked up per element row, not per
//conductor row: element_nomenclature_view carries a correlated
//subquery counting the wires touching each element. Without these
//indexes each element row full-scans the conductor table, which grows
//as elements x conductors.
for (const QString &index_ : {
QStringLiteral("CREATE INDEX idx_conductor_terminal1_element ON conductor (terminal1_element_uuid)"),
QStringLiteral("CREATE INDEX idx_conductor_terminal2_element ON conductor (terminal2_element_uuid)"),
QStringLiteral("CREATE INDEX idx_conductor_diagram ON conductor (diagram_uuid)") })
{
if (!query_.exec(index_)) {
qDebug() << "conductor index query : " << query_.lastError();
}
}
createElementNomenclatureView();
createSummaryView();
createWiringListView();
prepareQuery();
updateDB();
return true;
@@ -345,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,"
@@ -403,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)) {
@@ -442,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);
@@ -457,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
@@ -469,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();
}
@@ -498,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();
}
@@ -534,13 +911,85 @@ void projectDataBase::populateDiagramInfoTable()
}
}
/**
@brief projectDataBase::populateConductorTable
Populate the terminal and conductor tables. Terminals only matter here
in the context of a conductor referencing them, so their population is
folded into this method rather than tracked independently.
*/
void projectDataBase::populateConductorTable()
{
QSqlQuery query(m_data_base);
query.exec(QStringLiteral("DELETE FROM conductor"));
query.exec(QStringLiteral("DELETE FROM terminal"));
for (auto *diagram : m_project->diagrams())
{
const auto conductor_list = diagram->conductors();
for (auto *conductor : conductor_list)
{
//See addConductor(): only a terminal with no parent element is
//skipped. A missing terminal uuid is handled by stableUuid().
if (!conductor->terminal1->parentElement()
|| !conductor->terminal2->parentElement()) {
continue;
}
insertTerminal(conductor->terminal1);
insertTerminal(conductor->terminal2);
watchConductor(conductor);
bindConductorValues(m_insert_conductor_query, conductor, diagram);
if (!m_insert_conductor_query.exec()) {
qDebug() << "projectDataBase::populateConductorTable insert error : " << m_insert_conductor_query.lastError();
}
}
}
}
/**
@brief projectDataBase::insertTerminal
Insert (or, if already present -- e.g. a junction shared by several
conductors -- silently keep) @terminal in the terminal table.
@param terminal
*/
void projectDataBase::insertTerminal(Terminal *terminal)
{
m_insert_terminal_query.bindValue(":uuid", terminal->stableUuid().toString());
m_insert_terminal_query.bindValue(":element_uuid", terminal->parentElement()->uuid().toString());
m_insert_terminal_query.bindValue(":name", terminal->name());
if (!m_insert_terminal_query.exec()) {
qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError();
}
}
void projectDataBase::prepareQuery()
{
//INSERT DIAGRAM
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");
@@ -606,6 +1055,23 @@ void projectDataBase::prepareQuery()
update_str.append(" WHERE element_uuid = :uuid");
m_update_element_query = QSqlQuery(m_data_base);
m_update_element_query.prepare(update_str);
//INSERT TERMINAL
m_insert_terminal_query = QSqlQuery(m_data_base);
m_insert_terminal_query.prepare("INSERT OR IGNORE INTO terminal (uuid, element_uuid, name) VALUES (:uuid, :element_uuid, :name)");
//INSERT CONDUCTOR
m_insert_conductor_query = QSqlQuery(m_data_base);
m_insert_conductor_query.prepare("INSERT INTO conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid, terminal2_uuid, terminal2_element_uuid, text) "
"VALUES (:uuid, :diagram_uuid, :terminal1_uuid, :terminal1_element_uuid, :terminal2_uuid, :terminal2_element_uuid, :text)");
//UPDATE CONDUCTOR
m_update_conductor_query = QSqlQuery(m_data_base);
m_update_conductor_query.prepare(QStringLiteral("UPDATE conductor SET text = :text WHERE uuid = :uuid"));
//REMOVE CONDUCTOR
m_remove_conductor_query = QSqlQuery(m_data_base);
m_remove_conductor_query.prepare("DELETE FROM conductor WHERE uuid=:uuid");
}
/**
@@ -629,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 -1
View File
@@ -27,6 +27,8 @@
class Element;
class QETProject;
class Diagram;
class Conductor;
class Terminal;
class sqlite3;
/**
@@ -45,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);
@@ -58,6 +73,16 @@ class projectDataBase : public QObject
void diagramInfoChanged (Diagram *diagram);
void diagramOrderChanged();
void addConductor (Conductor *conductor);
void removeConductor (Conductor *conductor);
void updateConductor (Conductor *conductor);
private slots:
//Refresh the sender()'s row after Conductor::setProperties().
void conductorPropertiesChanged();
public:
signals:
void dataBaseUpdated();
@@ -65,17 +90,30 @@ class projectDataBase : public QObject
bool createDataBase();
void createElementNomenclatureView();
void createSummaryView();
void createWiringListView();
void populateDiagramTable();
void populateElementTable();
void populateElementInfoTable();
void populateDiagramInfoTable();
void populateConductorTable();
void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram);
void watchConductor(Conductor *conductor);
void insertTerminal(Terminal *terminal);
void prepareQuery();
static QHash<QString, QString> elementInfoToString(
Element *elmt);
void bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram);
static void bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram);
static void bindElementInfoValues(QSqlQuery &query, Element *element);
private:
QPointer<QETProject> m_project;
bool m_update_blocked = false;
//Starts true : the database is empty until the first rebuild.
//Set by every method of this class that writes rows, cleared by
//updateDB(). Callers reach the database from outside only through
//newQuery(), and every such call site reads.
bool m_content_changed = true;
QSqlDatabase m_data_base;
QSqlQuery m_insert_elements_query,
m_insert_element_info_query,
@@ -86,7 +124,15 @@ class projectDataBase : public QObject
m_insert_diagram_info_query,
m_update_diagram_info_query,
m_diagram_order_changed,
m_diagram_info_order_changed;
m_diagram_info_order_changed,
m_insert_terminal_query,
m_insert_conductor_query,
m_update_conductor_query,
m_remove_conductor_query,
m_cascade_remove_element_info_query,
m_cascade_remove_terminal_query,
m_cascade_remove_conductor_query,
m_cascade_remove_element_query;
#ifdef QET_EXPORT_PROJECT_DB
public:
+26 -20
View File
@@ -48,14 +48,8 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
m_button_group.addButton(ui->m_protection_cb, 5);
m_button_group.addButton(ui->m_thumbnail_cb, 6);
m_button_group.addButton(ui->m_plc_cb, 7);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), [this](int id)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
#endif
{
auto check_box = static_cast<QCheckBox *>(m_button_group.button(0));
if (id == 0)
@@ -166,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);
@@ -176,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;
}
@@ -358,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;
@@ -384,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.
+60 -9
View File
@@ -40,9 +40,53 @@
#include "undocommand/addelementtextcommand.h"
#include "qetinformation.h"
#include "qetproject.h"
#include "diagramsortkeys.h"
#include <algorithm>
#include <cassert>
#include <math.h>
namespace {
using DiagramSortKeys::positionKey;
/// Sort key for Diagram::toXml()'s <elements> block: the element's own
/// diagram-local position, exactly what it's already saved as (x/y),
/// never invented or regenerated. uuid() is deliberately NOT used here:
/// for an element with no persisted uuid attribute, fromXml() invents a
/// fresh random one on every load, so sorting by uuid would still be
/// non-deterministic across process runs for any legacy file.
QString elementSortKey(Element *elmt)
{
return positionKey(elmt->pos());
}
/// Sort key for a terminal: its parent element's position, then the
/// terminal's own position local to that element (from the .elmt
/// definition, fixed regardless of where the element is placed).
QString terminalSortKey(Terminal *terminal)
{
if (!terminal)
return QString();
Element *parent = terminal->parentElement();
return (parent ? positionKey(parent->pos()) : QStringLiteral("?"))
+ QLatin1Char(':') + positionKey(terminal->pos());
}
/// Sort key for Diagram::toXml()'s <conductors> block. Built from both
/// endpoints' terminalSortKey(), not Conductor::uuid(): in every example
/// project checked, conductors have no persisted uuid attribute at all,
/// so uuid() is a freshly-minted random value on every load -- exactly
/// as unusable for cross-run determinism as the element case above, just
/// with no persisted fallback to reach for instead. Canonicalised
/// (smaller key first) since a conductor's two ends are unordered for
/// this purpose.
QString conductorSortKey(Conductor *cond)
{
QString a = terminalSortKey(cond->terminal1);
QString b = terminalSortKey(cond->terminal2);
return (a <= b) ? (a + QLatin1Char('>') + b) : (b + QLatin1Char('>') + a);
}
}
int Diagram::xGrid = 10;
int Diagram::yGrid = 10;
int Diagram::xKeyGrid = 10;
@@ -1014,6 +1058,20 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
}
}
// items() returns items in stacking order, which is not guaranteed
// reproducible across processes (ties between same-Z items follow
// the scene's internal index, not any content-derived order) -- so
// without this, saving an unmodified project produces a different
// byte stream on every run. Elements and conductors are the two
// blocks observed to actually churn across the example corpus;
// sort them into a deterministic, content-derived order before
// serializing. This also fixes the legacy terminal-id churn below,
// since those ids are assigned sequentially in element order.
std::stable_sort(list_elements.begin(), list_elements.end(),
[](Element *a, Element *b) { return elementSortKey(a) < elementSortKey(b); });
std::stable_sort(list_conductors.begin(), list_conductors.end(),
[](Conductor *a, Conductor *b) { return conductorSortKey(a) < conductorSortKey(b); });
// correspondence table between the addresses of the terminals and their ids
// table de correspondance entre les adresses des bornes et leurs ids
QHash<Terminal *, int> table_adr_id;
@@ -1556,14 +1614,6 @@ bool Diagram::fromXml(QDomElement &document,
if (content_ptr) {
content_ptr -> m_elements = added_elements;
content_ptr -> m_conductors_to_move = added_conductors;
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
content_ptr -> m_text_fields = added_texts.toSet();
content_ptr -> m_images = added_images.toSet();
content_ptr -> m_shapes = added_shapes.toSet();
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
content_ptr -> m_text_fields = QSet<IndependentTextItem *>(
added_texts.begin(),
added_texts.end());
@@ -1574,7 +1624,6 @@ bool Diagram::fromXml(QDomElement &document,
added_shapes.begin(),
added_shapes.end());
content_ptr->m_terminal_strip.swap(added_strips);
#endif
content_ptr->m_tables.swap(added_tables);
}
@@ -1675,6 +1724,7 @@ void Diagram::addItem(QGraphicsItem *item)
conductor->terminal1->addConductor(conductor);
conductor->terminal2->addConductor(conductor);
conductor->calculateTextItemPosition();
m_project->dataBase()->addConductor(conductor);
break;
}
default: {break;}
@@ -1705,6 +1755,7 @@ void Diagram::removeItem(QGraphicsItem *item)
Conductor *conductor = static_cast<Conductor *>(item);
conductor->terminal1->removeConductor(conductor);
conductor->terminal2->removeConductor(conductor);
m_project->dataBase()->removeConductor(conductor);
break;
}
default: {break;}
+15 -34
View File
@@ -17,9 +17,7 @@
*/
#include "diagramcommands.h"
#include "autobreakconductor.h"
#include "diagram.h"
#include "qetproject.h"
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/elementtextitemgroup.h"
@@ -50,7 +48,6 @@ PasteDiagramCommand::PasteDiagramCommand( Diagram *dia, const DiagramContent &c,
PasteDiagramCommand::~PasteDiagramCommand()
{
diagram -> qgiManager().release(content.items(filter));
delete m_break_cmd;
}
/**
@@ -61,10 +58,6 @@ void PasteDiagramCommand::undo()
{
diagram -> showMe();
//Undo auto-break before removing items, so terminals are still on scene
if (m_break_cmd)
m_break_cmd->undo();
foreach(QGraphicsItem *item, content.items(filter))
diagram->removeItem(item);
}
@@ -82,6 +75,13 @@ void PasteDiagramCommand::redo()
{
first_redo = false;
//make new uuid for every pasted conductor, because old uuid are
//the uuid of the copied conductor
const QList <Conductor *> all_pasted_conductors = content.conductors();
for (Conductor *c : all_pasted_conductors) {
c -> newUuid();
}
//this is the first paste, we do some actions for the new element
const QList <Element *> elmts_list = content.m_elements;
for (Element *e : elmts_list)
@@ -99,37 +99,22 @@ void PasteDiagramCommand::redo()
dc.addValue("location", "");
e->setElementInformations(dc);
//Reset the text of conductors
//Reset the text of conductors, the same way the label/comment/
//location above are reset to "" rather than to some other
//value - "erase on copy" means erase, not "replace with the
//project's default new-conductor text" (which happens to
//default to a literal "_" character, unrelated to whether the
//user wanted this copy's old label kept or cleared; see
//issue #413).
const QList <Conductor *> conductors_list = content.m_conductors_to_move;
for (Conductor *c : conductors_list)
{
ConductorProperties cp = c -> properties();
cp.text = c->diagram() ? c -> diagram() -> defaultConductorProperties.text : "_";
cp.text = "";
c -> setProperties(cp);
}
}
}
//Auto-break conductors on first paste. Items are already on the
//scene at this point (added before this command was created).
if (diagram->project()->autoBreakConductor())
{
m_break_cmd = new QUndoCommand();
QList<Conductor *> conductors_handled;
QSet<Terminal *> used_terminals;
for (Element *e : content.m_elements) {
autoBreakConductors(diagram, e, m_break_cmd,
conductors_handled, used_terminals);
}
if (m_break_cmd->childCount() == 0) {
delete m_break_cmd;
m_break_cmd = nullptr;
}
else
{
m_break_cmd->redo();
}
}
}
else
{
@@ -137,10 +122,6 @@ void PasteDiagramCommand::redo()
for (QGraphicsItem *item : qgis_list) {
diagram->addItem(item);
}
//Re-execute the stored break commands (items are back on scene)
if (m_break_cmd)
m_break_cmd->redo();
}
const QList<QGraphicsItem *> qgis_list = content.items();
-2
View File
@@ -53,8 +53,6 @@ class PasteDiagramCommand : public QUndoCommand {
int filter;
/// prevent the first call to redo()
bool first_redo;
/// auto-break conductor sub-commands (created on first redo)
QUndoCommand *m_break_cmd = nullptr;
};
/**
+139 -21
View File
@@ -19,10 +19,14 @@
#include "diagrameventaddimage.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../diagram.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../qetgraphicsitem/diagramimageitem.h"
#include <QStatusBar>
#include <QTimer>
/**
@brief DiagramEventAddImage::DiagramEventAddImage
Default constructor
@@ -34,6 +38,16 @@ DiagramEventAddImage::DiagramEventAddImage(Diagram *diagram) :
m_is_added (false)
{
openDialog();
if (m_running)
{
// Deferred for the same reason as the shape tools' own
// constructor-time hint: Diagram::setEventInterface() destroys
// whatever tool was previously active *after* this constructor
// returns, and that tool's own destructor clears the status bar
// -- an immediate show here would just get wiped out moments
// later by that cleanup.
QTimer::singleShot(0, this, [this]() { showHint(); });
}
}
/**
@@ -47,33 +61,61 @@ DiagramEventAddImage::~DiagramEventAddImage()
delete m_image;
}
if (!m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
foreach (QGraphicsView *view, m_diagram->views())
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
/**
@brief DiagramEventAddImage::showHint
Re-asserted on every move (see mouseMoveEvent), not just once at
activation: Qt's own built-in "show an action's statusTip on hover"
has its own internal "restore whatever was there before" logic for
when the hover ends. Since this message is first shown *during* that
same hover session (the user is still over the toolbar icon when the
deferred constructor-time call above fires), Qt's hover-tracking has
no idea this code changed the status bar in the meantime -- the
moment the mouse leaves the icon for the canvas, it silently
restores whatever it remembers being there before its own tip
started, overwriting this one. Re-showing it on every move within
the canvas simply outlasts that one-time restore -- the exact same
issue already found and fixed for the shape tools.
*/
void DiagramEventAddImage::showHint() const
{
if (m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(tr("Clic : positionner à la taille d'origine. "
"Cliquer-glisser : positionner et redimensionner. "
"Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille."));
}
/**
@brief DiagramEventAddImage::mousePressEvent
Action when mouse is pressed
Left button: starts a potential drag-to-resize, anchored here -- but
doesn't commit to anything yet. A quick click-release (see
mouseMoveEvent's threshold check) still places the image at its
original size, matching the previous behavior exactly; only an
actual drag switches to resizing. Right button still rotates in 90
degree steps, unchanged, and only while not already left-dragging.
@param event : event of mouse pressed
*/
void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (m_image && event -> button() == Qt::LeftButton)
if (m_image && event->button() == Qt::LeftButton)
{
QPointF pos = event->scenePos();
pos.rx() -= m_image->boundingRect().width()/2;
pos.ry() -= m_image->boundingRect().height()/2;
m_diagram -> undoStack().push (new AddGraphicsObjectCommand(m_image, m_diagram, pos));
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
m_running = false;
emit finish();
m_pressed = true;
m_resize_engaged = false;
m_press_pos = event->scenePos();
event->setAccepted(true);
}
else if (m_image && event -> button() == Qt::RightButton)
else if (m_image && !m_pressed && event->button() == Qt::RightButton)
{
m_image->setRotation(m_image->rotation() + 90);
event->setAccepted(true);
@@ -87,26 +129,95 @@ void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
*/
void DiagramEventAddImage::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_image || event->buttons() != Qt::NoButton) {
if (!m_image) {
return;
}
showHint();
QPointF pos = event->scenePos();
if (!m_is_added)
{
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::NoContextMenu));
}
m_diagram->addItem(m_image);
m_is_added = true;
}
m_image->setPos(pos - m_image->boundingRect().center());
if (m_pressed)
{
// Anchored on m_press_pos, not the item's own current position:
// dragging in any direction has to visibly grow the image from
// where the click started, not from wherever the "no button
// held" preview phase happened to leave it centered.
const QPointF delta = pos - m_press_pos;
if (!m_resize_engaged && QLineF(m_press_pos, pos).length() >= 4.0)
m_resize_engaged = true; // latched: crossing back within the threshold afterward must not un-engage it
if (!m_resize_engaged)
{
// Still just a (so far) plain click -- keep behaving like
// the pre-drag preview: original size, centered here, so
// releasing right now reproduces the old click-to-place
// behavior exactly.
m_image->setPos(m_press_pos - m_image->boundingRect().center());
}
else
{
const QSizeF naturalSize = m_image->boundingRect().size();
if (naturalSize.width() > 0 && naturalSize.height() > 0)
{
const qreal scaleX = qAbs(delta.x()) / naturalSize.width();
const qreal scaleY = qAbs(delta.y()) / naturalSize.height();
// The larger of the two, not a per-axis stretch: images
// only support a single uniform scale today (see
// boundingRect()/paint(), which never touch aspect
// ratio), so this is a diagonal-drag size, not a
// free-form one -- breaking aspect ratio on purpose is
// its own, separate, larger piece of work.
const qreal newScale = qBound(0.01, qMax(scaleX, scaleY), 50.0);
m_image->setScale(newScale);
}
m_image->setPos(qMin(m_press_pos.x(), pos.x()), qMin(m_press_pos.y(), pos.y()));
}
}
else
{
m_image->setPos(pos - m_image->boundingRect().center());
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddImage::mouseReleaseEvent
Left button release commits whatever mouseMoveEvent last set --
original size and centered if the press never turned into a real
drag, or the dragged-out size and position otherwise. Either way,
this is the only place placement is actually finalized now; a plain
click no longer finishes inside mousePressEvent itself, since it has
to wait and see whether a drag follows.
@param event : event of mouse release
*/
void DiagramEventAddImage::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (m_image && m_pressed && event->button() == Qt::LeftButton)
{
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_image, m_diagram, m_image->pos()));
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
m_running = false;
emit finish();
event->setAccepted(true);
}
}
/**
@brief DiagramEventAddImage::mouseDoubleClickEvent
This method is used only to overwrite double click.
@@ -124,7 +235,14 @@ void DiagramEventAddImage::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
*/
void DiagramEventAddImage::wheelEvent(QGraphicsSceneWheelEvent *event)
{
if (!m_is_added || !m_image || event -> modifiers() != Qt::CTRL) {
// !m_pressed added alongside the modifier fix: without it, wheel
// scaling could fight with an active drag-resize, both trying to
// set scale() from different sources in the same gesture.
// event->modifiers() & Qt::ControlModifier, not != Qt::CTRL: the
// same exact-equality bug already found and fixed several times
// this session elsewhere -- Ctrl held together with any other
// modifier would silently fail to register as Ctrl at all.
if (!m_is_added || !m_image || m_pressed || !(event->modifiers() & Qt::ControlModifier)) {
return;
}
@@ -20,6 +20,8 @@
#include "diagrameventinterface.h"
#include <QPointF>
class Diagram;
class DiagramImageItem;
@@ -37,15 +39,20 @@ class DiagramEventAddImage : public DiagramEventInterface
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
bool isNull () const;
private:
void openDialog();
void showHint() const;
DiagramImageItem *m_image;
bool m_is_added;
bool m_pressed = false; // left button held: dragging out a size, not just positioning
bool m_resize_engaged = false; // latched once the drag threshold is crossed, matching the pen tool's own curve-drag threshold convention -- so dragging out and back near the start point doesn't "snap back" to original size before release
QPointF m_press_pos; // scene position of the left-button press, the resize anchor
};
#endif // DIAGRAMEVENTADDIMAGE_H
@@ -43,11 +43,7 @@ m_preview_item(nullptr)
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
if (!dir_path.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
#else
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
#endif
QString current_path = "";
for (const QString &part : parts) {
QString parent_path = current_path;
@@ -202,11 +198,7 @@ bool DiagramEventAddMacro::loadMacro()
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
if (!dir_path.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
#else
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
#endif
QString current_path = "";
for (const QString &part : parts) {
QString parent_path = current_path;
@@ -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
+250
View File
@@ -0,0 +1,250 @@
/*
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 "diagrameventaddpdf.h"
// Whole file is a no-op unless QtPdf is available (see diagrameventaddpdf.h).
#ifdef QET_HAS_QTPDF
#include "../qetapp.h"
#include "../diagram.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../qetgraphicsitem/diagramimageitem.h"
#include "../ui/pdfpagesdialog.h"
#include <QPdfDocument>
#include <QFileDialog>
#include <QMessageBox>
#include <QPainter>
/**
@brief DiagramEventAddPdf::DiagramEventAddPdf
Constructor
@param diagram the diagram where this event operates
*/
DiagramEventAddPdf::DiagramEventAddPdf(Diagram *diagram) :
DiagramEventInterface(diagram),
m_image(nullptr),
m_is_added(false)
{
openDialog();
}
/**
@brief DiagramEventAddPdf::~DiagramEventAddPdf
Destructor
*/
DiagramEventAddPdf::~DiagramEventAddPdf()
{
if (m_running || m_abort)
{
if (m_is_added) m_diagram->removeItem(m_image);
delete m_image;
}
foreach (QGraphicsView *view, m_diagram->views())
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
/**
@brief DiagramEventAddPdf::mousePressEvent
Action when mouse is pressed
@param event event of mouse pressed
*/
void DiagramEventAddPdf::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
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();
event->setAccepted(true);
}
else if (m_image && event->button() == Qt::RightButton)
{
m_image->setRotation(m_image->rotation() + 90);
event->setAccepted(true);
}
}
/**
@brief DiagramEventAddPdf::mouseMoveEvent
Action when mouse moves
@param event event of mouse move
*/
void DiagramEventAddPdf::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_image || event->buttons() != Qt::NoButton) {
return;
}
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());
event->setAccepted(true);
}
/**
@brief DiagramEventAddPdf::mouseDoubleClickEvent
Overwrite double click to prevent opening properties dialog.
@param event event of mouse double click
*/
void DiagramEventAddPdf::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
event->setAccepted(true);
}
/**
@brief DiagramEventAddPdf::wheelEvent
Action when mouse wheel is rotated (CTRL+wheel to scale)
@param event event of mouse wheel
*/
void DiagramEventAddPdf::wheelEvent(QGraphicsSceneWheelEvent *event)
{
if (!m_is_added || !m_image || event->modifiers() != Qt::CTRL) {
return;
}
qreal scaling = m_image->scale();
event->delta() > 1 ? scaling += 0.01 : scaling -= 0.01;
if (scaling > 0.01 && scaling <= 2) {
m_image->setScale(scaling);
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddPdf::isNull
@return true if the PDF image couldn't be loaded, false otherwise
*/
bool DiagramEventAddPdf::isNull() const
{
if (!m_image) return true;
return false;
}
/**
@brief DiagramEventAddPdf::openDialog
Opens a file dialog to select a PDF file, then opens a page selection
dialog with DPI options. The selected page is rendered to a QImage at
the chosen DPI and converted to a DiagramImageItem.
*/
void DiagramEventAddPdf::openDialog()
{
if (m_diagram->isReadOnly()) return;
// Open file dialog to select a PDF file
QString pathPDFs = QETApp::documentDir();
QString fileName = QFileDialog::getOpenFileName(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Sélectionner un fichier PDF..."),
pathPDFs,
QObject::tr("Fichiers PDF (*.pdf)")
);
if (fileName.isEmpty()) return;
// Load the PDF document
QPdfDocument document;
document.load(fileName);
if (document.status() != QPdfDocument::Status::Ready)
{
QMessageBox::critical(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Erreur"),
QObject::tr("Impossible de charger le fichier PDF.")
);
return;
}
int pageCount = document.pageCount();
if (pageCount <= 0)
{
QMessageBox::critical(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Erreur"),
QObject::tr("Le fichier PDF ne contient aucune page.")
);
return;
}
// Always show the dialog so the user can choose page and DPI
PdfPagesDialog dialog(document,
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first());
if (dialog.exec() != QDialog::Accepted) return;
int pageIndex = dialog.selectedPage() - 1; // Convert to 0-based index
int dpi = dialog.selectedDpi();
// Calculate pixel size from PDF points at the user-selected DPI
// PDF point = 1/72 inch
QSizeF pageSize = document.pagePointSize(pageIndex);
int pixelWidth = qRound((pageSize.width() / 72.0) * dpi);
int pixelHeight = qRound((pageSize.height() / 72.0) * dpi);
if (pixelWidth <= 0 || pixelHeight <= 0)
{
QMessageBox::critical(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Erreur"),
QObject::tr("Impossible de déterminer la taille de la page PDF.")
);
return;
}
// Render PDF page to QImage
QImage image = document.render(pageIndex, QSize(pixelWidth, pixelHeight));
if (image.isNull())
{
QMessageBox::critical(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Erreur"),
QObject::tr("Impossible de rendre la page PDF.")
);
return;
}
// Fill white background to handle transparent PDFs
QImage background(image.size(), QImage::Format_ARGB32_Premultiplied);
background.fill(Qt::white);
QPainter painter(&background);
painter.drawImage(0, 0, image);
painter.end();
m_image = new DiagramImageItem(QPixmap::fromImage(background));
m_running = true;
}
#endif // QET_HAS_QTPDF
+61
View File
@@ -0,0 +1,61 @@
/*
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 DIAGRAMEVENTADDPDF_H
#define DIAGRAMEVENTADDPDF_H
// PDF page import needs the QtPdf module (QET_HAS_QTPDF, set by CMake
// only when QtPdf is present AND Qt >= 6.4, for QPdfDocument::pagePointSize()).
#ifdef QET_HAS_QTPDF
#include "diagrameventinterface.h"
class Diagram;
class DiagramImageItem;
/**
@brief The DiagramEventAddPdf class
This diagram event handles the adding of a PDF page as an image in a diagram.
The selected PDF page is rendered to a QImage at 150 DPI and added as a
DiagramImageItem. The placement, rotation and scaling behavior is identical
to the existing image import.
*/
class DiagramEventAddPdf : public DiagramEventInterface
{
Q_OBJECT
public:
DiagramEventAddPdf(Diagram *diagram);
~DiagramEventAddPdf() override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
bool isNull() const;
private:
void openDialog();
DiagramImageItem *m_image;
bool m_is_added;
};
#endif // QET_HAS_QTPDF
#endif // DIAGRAMEVENTADDPDF_H
+271 -5
View File
@@ -19,8 +19,16 @@
#include "../diagram.h"
#include "../lastusedstyle.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include <QGraphicsEllipseItem>
#include <QGuiApplication>
#include <QKeyEvent>
#include <QStatusBar>
#include <QTimer>
/**
@brief DiagramEventAddShape::DiagramEventAddShape
Default constructor
@@ -36,6 +44,18 @@ DiagramEventAddShape::DiagramEventAddShape(Diagram *diagram, QetShapeItem::Shape
{
m_running = true;
init();
// Deferred to the next event-loop iteration, not shown immediately:
// Diagram::setEventInterface() destroys whatever tool was
// previously active *after* this constructor returns, and that
// previous tool's own destructor clears the status bar (see
// ~DiagramEventAddShape() below) -- an immediate show here would
// just get wiped out moments later by that cleanup. Letting the old
// tool's teardown finish first, then showing this one's message, is
// the same fix already used for the tooltip-flicker issue in
// QetShapeItem::refreshInteractionHints(), applied to the same
// class of "something later in the same call chain undoes what I
// just did" ordering problem.
QTimer::singleShot(0, this, [this]() { updateCreationHint(); });
}
/**
@@ -50,11 +70,123 @@ DiagramEventAddShape::~DiagramEventAddShape()
}
delete m_help_horiz;
delete m_help_verti;
delete m_center_marker;
if (m_diagram && !m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
foreach (QGraphicsView *v, m_diagram->views())
v->setContextMenuPolicy(Qt::DefaultContextMenu);
}
/**
@brief DiagramEventAddShape::applyPosition
Applies a drag/click position to the in-progress shape, honouring two
modifiers that mirror how the very same shape can already be edited
afterward, once placed:
- Ctrl, for Rectangle/Ellipse only: the first click becomes the
shape's *center* rather than a corner, growing symmetrically as
the cursor moves away from it -- the same meaning Ctrl already
has on a Resize handle (anchor at center). Deliberately not
offered for Line: unlike the Rectangle/Ellipse case, there's no
established convention for "a line grows symmetrically from its
middle" to justify it by, so Ctrl for Line means only free
positioning (see the plain grid-snap check above), nothing more.
- Shift, for Rectangle/Ellipse only: forces the bounding box square
(so Ellipse becomes a true circle), using whichever of the two
dragged dimensions is currently larger and mirroring that onto
the other, preserving the direction the user is actually
dragging in.
Both can combine (Ctrl+Shift: a centered square/circle). Whether or
not Ctrl is currently held, the non-anchored branch always rebuilds
from m_anchor_point rather than nudging the existing rect/line --
otherwise, if Ctrl had been held earlier in the same drag (moving the
shape's own first point to a mirrored position), releasing it would
leave that point stuck there instead of actually restoring it.
*/
void DiagramEventAddShape::applyPosition(const QPointF &pos, Qt::KeyboardModifiers mods)
{
if (!m_shape_item)
return;
if (m_shape_type == QetShapeItem::Polygon)
{
// setP2() has its own dedicated Polygon handling: it moves the
// *last* vertex in place rather than setting a second point,
// which is exactly the live "next segment follows the mouse"
// preview -- the same idea DiagramEventAddPath's trailing
// preview node gives the pen tool. No Ctrl/Shift modifiers apply
// here (those are Rectangle/Ellipse/Line-specific below), so
// this is a direct, unconditional call.
m_shape_item->setP2(pos);
return;
}
// m_center_anchored is decided once, in mousePressEvent, not
// re-checked here on every call -- re-checking it live meant
// releasing Ctrl mid-drag (something you'd naturally do the moment
// your hand gets tired holding it, long before you're done resizing)
// silently snapped the shape back to corner-anchored, discarding
// what felt like an already-made decision. Deciding it once at the
// first click matches "I held Ctrl when I clicked, so this shape is
// centered" -- a single, predictable rule instead of a live toggle.
QPointF target = pos;
if ((mods & Qt::ShiftModifier)
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse))
{
const QPointF ref = m_anchor_point;
const qreal dx = target.x() - ref.x();
const qreal dy = target.y() - ref.y();
const qreal size = qMax(qAbs(dx), qAbs(dy));
target.setX(ref.x() + (dx < 0 ? -size : size));
target.setY(ref.y() + (dy < 0 ? -size : size));
}
if (m_center_anchored)
{
const QPointF mirrored = 2 * m_anchor_point - target;
m_shape_item->setRect(QRectF(mirrored, target));
}
else
{
if (m_shape_type == QetShapeItem::Line)
m_shape_item->setLine(QLineF(m_anchor_point, target));
else
m_shape_item->setRect(QRectF(m_anchor_point, target));
}
}
/**
@brief DiagramEventAddShape::showCenterMarker
Small, filled marker at the anchor point, shown only while Ctrl-
anchoring is actually in effect right now (see applyPosition()) --
doubling as live confirmation that it is, rather than leaving the
user to infer it purely from how the shape happens to be growing.
*/
void DiagramEventAddShape::showCenterMarker(const QPointF &scenePos)
{
if (!m_center_marker)
{
m_center_marker = new QGraphicsEllipseItem(-4, -4, 8, 8);
QPen pen(Qt::red);
pen.setCosmetic(true);
m_center_marker->setPen(pen);
m_center_marker->setBrush(Qt::red);
m_diagram->addItem(m_center_marker);
}
m_center_marker->setPos(scenePos);
}
void DiagramEventAddShape::hideCenterMarker()
{
delete m_center_marker;
m_center_marker = nullptr;
}
/**
@brief DiagramEventAddShape::mousePressEvent
Action when mouse is pressed
@@ -67,7 +199,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
}
QPointF pos = event->scenePos();
if (event->modifiers() != Qt::ControlModifier) {
// A bitwise flag check, not exact equality: modifiers() == Ctrl
// alone fails the moment any other key (Shift for the square/circle
// lock, or an incidental platform flag) is also held, silently
// falling through to snapToGrid() even though Ctrl is held --
// exactly what made Ctrl+Shift together feel "frozen" (both grid-
// snapped *and* square-locked, quantizing to whichever is coarser)
// and made "Ctrl = free positioning" not actually hold up.
if (!(event->modifiers() & Qt::ControlModifier)) {
pos = Diagram::snapToGrid(pos);
}
@@ -78,6 +217,15 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
if (!m_shape_item)
{
m_shape_item = new QetShapeItem(pos, pos, m_shape_type);
m_anchor_point = pos;
// Decided once, here, rather than re-checked on every mouse
// move for the rest of the drag -- see applyPosition()'s doc
// comment for why continuous re-checking made releasing Ctrl
// mid-drag feel like a bug rather than a deliberate choice.
m_center_anchored = (event->modifiers() & Qt::ControlModifier)
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse);
if (m_center_anchored)
showCenterMarker(m_anchor_point);
//Start from whatever pen/brush was last applied this
//session, rather than always the hardcoded default.
if (LastUsedStyle::hasShapePen()) {
@@ -87,6 +235,7 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
}
m_diagram->addItem (m_shape_item);
updateCreationHint();
event->setAccepted(true);
return;
}
@@ -94,12 +243,14 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
//If current item isn't a polyline, add it with an undo command
if (m_shape_type != QetShapeItem::Polygon)
{
m_shape_item->setP2 (pos);
applyPosition(pos, event->modifiers());
if (m_shape_item->shapeType() == QetShapeItem::Rectangle || m_shape_item->shapeType() == QetShapeItem::Ellipse) {
m_shape_item->setRect(m_shape_item->rect().normalized());
}
m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram));
m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic
hideCenterMarker();
updateCreationHint();
}
//Else add a new point to polyline
else
@@ -125,18 +276,76 @@ void DiagramEventAddShape::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
updateHelpCross(event->scenePos());
// Re-asserted on every move, not just once at activation: Qt's own
// built-in "show an action's statusTip on hover" has its own
// internal "restore whatever was there before" logic for when the
// hover ends. Since our message is shown *during* that same hover
// session (the user is still over the toolbar icon when the
// deferred constructor-time call fires), Qt's hover-tracking has no
// idea we changed the status bar in the meantime -- the moment the
// mouse leaves the icon for the canvas, it "restores" to whatever it
// remembers being there before its own tip started, which is stale
// and empty, silently overwriting ours. Re-showing it here, on every
// move within the canvas, simply outlasts that one-time restore.
updateCreationHint();
if (m_shape_item && event->buttons() == Qt::NoButton)
{
m_last_mouse_scene_pos = event->scenePos(); // raw, before snapping -- see reapplyLastPosition()
QPointF pos = event->scenePos();
if (event->modifiers() != Qt::ControlModifier) {
if (!(event->modifiers() & Qt::ControlModifier)) {
pos = Diagram::snapToGrid(pos);
}
m_shape_item->setP2 (pos);
applyPosition(pos, event->modifiers());
event->setAccepted(true);
}
}
/**
@brief DiagramEventAddShape::keyPressEvent / keyReleaseEvent
Pressing or releasing Shift (the square/circle lock) does nothing
visible on its own -- applyPosition() only ever runs from
mouseMoveEvent, so without this, a keyboard-only change just sits
there until the next, often incidental, pixel of mouse movement
brings the shape in line with it. That's exactly what looked like a
freeze: holding Shift while the mouse is genuinely still produces no
visible change (correctly -- nothing has moved), and it only
"unsticks" once the mouse moves again, which released keys tend to
coincide with purely by hand tremor, not because releasing itself
did anything. Re-running the last known mouse position through
applyPosition() here makes the key press or release itself the
trigger, giving immediate feedback instead of waiting on chance.
*/
void DiagramEventAddShape::keyPressEvent(QKeyEvent *event)
{
reapplyLastPosition(event);
}
void DiagramEventAddShape::keyReleaseEvent(QKeyEvent *event)
{
reapplyLastPosition(event);
}
void DiagramEventAddShape::reapplyLastPosition(QKeyEvent *event)
{
if (!m_shape_item || (event->key() != Qt::Key_Shift && event->key() != Qt::Key_Control))
return;
// A fresh, global query, not event->modifiers(): for a press/release
// of a modifier key itself, whether that key is already reflected in
// the key event's own modifiers() is ambiguous and platform-
// dependent -- the same reason Diagram::snapToGrid() queries this
// directly rather than trusting a passed-in modifiers() value.
const Qt::KeyboardModifiers mods = QGuiApplication::keyboardModifiers();
QPointF pos = m_last_mouse_scene_pos;
if (!(mods & Qt::ControlModifier))
pos = Diagram::snapToGrid(pos);
applyPosition(pos, mods);
}
/**
@brief DiagramEventAddShape::mouseReleaseEvent
Action when mouse button is released
@@ -155,7 +364,7 @@ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
m_shape_item->removePoints();
QPointF pos = event->scenePos();
if (event->modifiers() != Qt::ControlModifier)
if (!(event->modifiers() & Qt::ControlModifier))
pos = Diagram::snapToGrid(pos);
m_shape_item->setP2(pos); //Set the new last point under the cursor
@@ -167,6 +376,8 @@ void DiagramEventAddShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
m_diagram->removeItem(m_shape_item);
delete m_shape_item;
m_shape_item = nullptr;
hideCenterMarker();
updateCreationHint();
event->setAccepted(true);
return;
}
@@ -203,6 +414,8 @@ void DiagramEventAddShape::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
}
m_diagram->undoStack().push (new AddGraphicsObjectCommand(m_shape_item, m_diagram));
m_shape_item = nullptr; //< set to nullptr for create new shape at next left clic
hideCenterMarker();
updateCreationHint();
event->setAccepted(true);
}
}
@@ -213,6 +426,59 @@ void DiagramEventAddShape::init()
v->setContextMenuPolicy(Qt::NoContextMenu);
}
/**
@brief DiagramEventAddShape::updateCreationHint
Shows whichever of beforeClickHint()/afterClickHint() matches the
current phase -- there was previously either no message at all
(Line/Rectangle/Ellipse) or a single static one that never changed
regardless of progress (Polygon, set externally in
QETDiagramEditor::addItemGroupTriggered()); this replaces both with
one phase-aware message per shape type, managed by the tool itself.
*/
void DiagramEventAddShape::updateCreationHint() const
{
if (!m_diagram || m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(m_shape_item ? afterClickHint() : beforeClickHint());
}
QString DiagramEventAddShape::beforeClickHint() const
{
switch (m_shape_type)
{
case QetShapeItem::Line:
return tr("Clic gauche : positionner le point de départ (Ctrl = position libre)");
case QetShapeItem::Rectangle:
case QetShapeItem::Ellipse:
return tr("Clic gauche : positionner le premier coin (Ctrl = point central, position libre)");
case QetShapeItem::Polygon:
return tr("Clic gauche : positionner le premier point (Ctrl = position libre)");
default:
return QString();
}
}
QString DiagramEventAddShape::afterClickHint() const
{
switch (m_shape_type)
{
case QetShapeItem::Line:
return tr("Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler");
case QetShapeItem::Rectangle:
return tr("Clic gauche : positionner le coin opposé (Maj = carré, "
"Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler");
case QetShapeItem::Ellipse:
return tr("Clic gauche : positionner le coin opposé (Maj = cercle, "
"Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler");
case QetShapeItem::Polygon:
return tr("Clic gauche : point suivant ; double-clic ou Entrée : terminer ; "
"clic droit : annuler le dernier point");
default:
return QString();
}
}
/**
@brief DiagramEventAddShape::updateHelpCross
Create and update the position of the cross to help user for draw new shape
@@ -21,6 +21,8 @@
#include "../qetgraphicsitem/qetshapeitem.h"
#include "diagrameventinterface.h"
class QGraphicsEllipseItem;
/**
@brief The DiagramEventAddShape class
This event manage the creation of a shape.
@@ -37,15 +39,28 @@ class DiagramEventAddShape : public DiagramEventInterface
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void keyReleaseEvent (QKeyEvent *event) override;
void init() override;
private:
void updateHelpCross (const QPointF &p);
void applyPosition (const QPointF &pos, Qt::KeyboardModifiers mods);
void updateCreationHint () const;
QString beforeClickHint () const;
QString afterClickHint () const;
void showCenterMarker (const QPointF &scenePos);
void hideCenterMarker ();
void reapplyLastPosition (QKeyEvent *event);
protected:
QetShapeItem::ShapeType m_shape_type;
QetShapeItem *m_shape_item;
QGraphicsLineItem *m_help_horiz, *m_help_verti;
QPointF m_anchor_point; // the shape's first-click point -- meaningful once m_shape_item exists
QGraphicsEllipseItem *m_center_marker = nullptr; // shown only while Ctrl-anchoring is actually in effect, so it doubles as confirmation that it is
bool m_center_anchored = false; // decided once, at the first click -- see applyPosition()'s doc comment for why
QPointF m_last_mouse_scene_pos; // raw, unsnapped -- lets a modifier-only change re-snap correctly when reapplied
};
#endif // DIAGRAMEVENTADDSHAPE_H
+53
View File
@@ -0,0 +1,53 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DIAGRAMSORTKEYS_H
#define DIAGRAMSORTKEYS_H
#include <QPointF>
#include <QString>
#include <QtGlobal>
/// Sort-key helpers used by Diagram::toXml() to serialize elements and
/// conductors in a deterministic order. Pulled into their own header
/// (instead of an anonymous namespace in diagram.cpp) so they can be
/// unit-tested directly -- see tests/qttest/tst_diagramsortkeys.cpp.
namespace DiagramSortKeys {
/// Format a coordinate as a string that sorts the same way the number
/// does. Plain fixed-precision formatting ("%.4f") does NOT do this --
/// e.g. "15.0000" sorts before "5.0000" as text even though 15 > 5 --
/// so shift into a non-negative range and zero-pad to a fixed width
/// before comparing.
inline QString coordinateKey(double v)
{
//Diagram coordinates are nowhere near this range; the offset and
//width just need to be big enough that shifted values are always
//non-negative and always the same digit count.
constexpr double offset = 1e9;
qint64 scaled = qint64(qRound64((v + offset) * 10000.0));
return QStringLiteral("%1").arg(scaled, 20, 10, QLatin1Char('0'));
}
inline QString positionKey(const QPointF &pos)
{
return coordinateKey(pos.x()) + QLatin1Char('|') + coordinateKey(pos.y());
}
}
#endif
+7 -21
View File
@@ -334,6 +334,13 @@ void DiagramView::setSelectionMode()
*/
void DiagramView::zoom(const qreal zoom_factor)
{
// clamp the resulting scale so a repeated wheel-zoom cannot drive the view
// transform to floating-point overflow and crash the editor (issue #798)
const qreal target = transform().m11() * zoom_factor;
if (target < m_min_zoom || target > m_max_zoom) {
return;
}
if (zoom_factor >= 1){
scale(zoom_factor, zoom_factor);
}
@@ -458,14 +465,7 @@ void DiagramView::mousePressEvent(QMouseEvent *e)
if (m_event_interface && m_event_interface->mousePressEvent(e)) return;
//Start drag view when hold the middle button
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->button() == Qt::MiddleButton)
#endif
{
m_drag_last_pos = e->pos();
viewport()->setCursor(Qt::ClosedHandCursor);
@@ -515,14 +515,7 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
// Drag the view
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->buttons() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->buttons() == Qt::MiddleButton)
#endif
{
QScrollBar *h = horizontalScrollBar();
QScrollBar *v = verticalScrollBar();
@@ -583,14 +576,7 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
// Stop drag view
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->button() == Qt::MiddleButton)
#endif
{
viewport()->setCursor(Qt::ArrowCursor);
}
+6
View File
@@ -103,6 +103,12 @@ class DiagramView : public QGraphicsView
bool mustIntegrateTitleBlockTemplate(const TitleBlockTemplateLocation &) const;
bool gestures() const;
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents wheel-zoom from driving the transform to overflow, which
/// crashes the editor (see GitHub issue #798, same class of bug).
static constexpr qreal m_min_zoom = 0.01;
static constexpr qreal m_max_zoom = 200.0;
signals:
/// Signal emitted after the selection mode changed
void modeChanged();
+299
View File
@@ -0,0 +1,299 @@
/*
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 "dxfpaintdevice.h"
#include "createdxf.h"
#include <QPainterPath>
#include <QTextItem>
#include <QtMath>
#include <QDebug>
/**
@brief DxfPaintEngine::DxfPaintEngine
@param filepath path of the already-open DXF file entities are
appended to (see Createdxf::dxfBegin()/dxfEnd()).
*/
DxfPaintEngine::DxfPaintEngine(const QString &filepath) :
QPaintEngine(QPaintEngine::AllFeatures),
m_filepath(filepath)
{
}
bool DxfPaintEngine::begin(QPaintDevice *pdev)
{
Q_UNUSED(pdev)
setActive(true);
return true;
}
bool DxfPaintEngine::end()
{
setActive(false);
return true;
}
/**
@brief DxfPaintEngine::updateState
Track the properties this engine cares about. Everything drawn is
mapped through the transform in effect at draw time - this is what
lets an item's paint() be replayed unmodified at whatever scene
position/rotation it actually has, exactly as QPainter does for
on-screen painting.
*/
void DxfPaintEngine::updateState(const QPaintEngineState &state)
{
QPaintEngine::DirtyFlags flags = state.state();
if (flags & QPaintEngine::DirtyTransform)
m_world_transform = state.transform();
if (flags & QPaintEngine::DirtyPen)
m_pen = state.pen();
if (flags & QPaintEngine::DirtyBrush)
m_brush = state.brush();
if (flags & QPaintEngine::DirtyFont)
m_font = state.font();
}
QPointF DxfPaintEngine::toDxf(const QPointF &local_point) const
{
// Createdxf's QRectF/QLineF/QPolygonF/QPointF convenience overloads
// each apply xScale/yScale and the sheetHeight Y-flip internally, so
// this engine's whole job is composing the world transform - not
// touching DXF units at all.
return m_world_transform.map(local_point);
}
void DxfPaintEngine::strokePolygon(const QPolygonF &poly, bool close)
{
if (poly.size() < 2)
return;
if (m_pen.style() == Qt::NoPen)
return;
QPolygonF mapped;
mapped.reserve(poly.size());
for (const QPointF &p : poly)
mapped << toDxf(p);
if (close && mapped.first() != mapped.last())
mapped << mapped.first();
Createdxf::drawPolyline(m_filepath, mapped, Createdxf::dxfColor(m_pen));
}
void DxfPaintEngine::drawLines(const QLineF *lines, int lineCount)
{
if (m_pen.style() == Qt::NoPen)
return;
for (int i = 0; i < lineCount; ++i)
{
QLineF mapped(toDxf(lines[i].p1()), toDxf(lines[i].p2()));
Createdxf::drawLine(m_filepath, mapped, Createdxf::dxfColor(m_pen));
}
}
void DxfPaintEngine::drawRects(const QRectF *rects, int rectCount)
{
for (int i = 0; i < rectCount; ++i)
{
QPolygonF corners;
corners << rects[i].topLeft() << rects[i].topRight()
<< rects[i].bottomRight() << rects[i].bottomLeft();
// No HATCH support in v1 (see the design note in the header) - a
// filled rect is emitted as its outline only, same as an unfilled
// one. Still emitted (not skipped) when there's no pen but there
// is a brush, so a filled-only shape isn't dropped entirely.
if (m_pen.style() != Qt::NoPen || m_brush.style() != Qt::NoBrush)
strokePolygon(corners, true);
}
}
void DxfPaintEngine::drawEllipse(const QRectF &rect)
{
if (m_pen.style() == Qt::NoPen && m_brush.style() == Qt::NoBrush)
return;
// A non axis-aligned ellipse (rotation baked into m_world_transform)
// can't be expressed as Createdxf::drawEllipse(), which only accepts
// an axis-aligned QRectF. Approximate with a flattened polygon in
// that case; use the exact CIRCLE/ELLIPSE entity when the transform
// is axis-aligned, matching what the existing hand-written exporters
// already produce for unrotated shapes.
const bool axis_aligned = qFuzzyIsNull(m_world_transform.m12())
&& qFuzzyIsNull(m_world_transform.m21());
if (axis_aligned)
{
QRectF mapped = m_world_transform.mapRect(rect);
Createdxf::drawEllipse(m_filepath, mapped, Createdxf::dxfColor(m_pen));
return;
}
QPainterPath path;
path.addEllipse(rect);
const auto polygons = path.toSubpathPolygons();
for (const QPolygonF &poly : polygons)
strokePolygon(poly, true);
}
/**
@brief DxfPaintEngine::drawPath
Handles drawArc()/drawPie() (QPainterPath::Arc elements - emitted as
chord segments, since Createdxf's ARC-equivalent
(drawArcEllipse()) only takes an axis-aligned ellipse + angle pair,
which doesn't compose with an arbitrary path transform) and
fillPath()/generic paths (flattened to polylines).
*/
void DxfPaintEngine::drawPath(const QPainterPath &path)
{
if (m_pen.style() == Qt::NoPen && m_brush.style() == Qt::NoBrush)
return;
const auto polygons = path.toSubpathPolygons();
for (const QPolygonF &poly : polygons)
strokePolygon(poly, path.isEmpty() ? false : true);
}
void DxfPaintEngine::drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode)
{
if (pointCount < 2)
return;
QPolygonF poly;
poly.reserve(pointCount);
for (int i = 0; i < pointCount; ++i)
poly << points[i];
strokePolygon(poly, mode != PolylineMode);
}
void DxfPaintEngine::drawTextItem(const QPointF &p, const QTextItem &textItem)
{
qreal font_size = textItem.font().pointSizeF();
if (font_size < 0)
font_size = textItem.font().pixelSize();
// Rotation: extract the angle baked into the world transform by
// mapping a unit vector, the same approach used to compute rotation
// for the existing element-text DXF export in exportdialog.cpp.
QPointF origin = m_world_transform.map(p);
QPointF right = m_world_transform.map(p + QPointF(1, 0));
qreal angle = qRadiansToDegrees(qAtan2(-(right.y() - origin.y()), right.x() - origin.x()));
Createdxf::drawText(m_filepath, textItem.text(), toDxf(p), font_size,
angle, Createdxf::dxfColor(m_pen), 0.72);
}
/**
@brief DxfPaintEngine::drawPixmap
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(pm)
Q_UNUSED(sr)
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)));
}
/**
@brief DxfPaintDevice::DxfPaintDevice
@param filepath path of the already-open DXF file (see
Createdxf::dxfBegin()/dxfEnd() - this device only appends entities in
between, it does not open/close the file itself).
*/
DxfPaintDevice::DxfPaintDevice(const QString &filepath) :
QPaintDevice(),
m_filepath(filepath),
m_engine(new DxfPaintEngine(filepath))
{
}
DxfPaintDevice::~DxfPaintDevice()
{
delete m_engine;
}
QPaintEngine *DxfPaintDevice::paintEngine() const
{
return m_engine;
}
int DxfPaintDevice::metric(PaintDeviceMetric metric) const
{
// A DXF document has no pixel grid or physical size in the sense
// QPaintDevice::metric() expects - large-but-finite values here are
// enough to keep QPainter's own bookkeeping (e.g. clip region setup)
// from misbehaving; nothing in this engine actually consults them.
switch (metric)
{
case PdmWidth:
case PdmHeight:
return 1000000;
case PdmDpiX:
case PdmDpiY:
case PdmPhysicalDpiX:
case PdmPhysicalDpiY:
return 96;
case PdmWidthMM:
case PdmHeightMM:
return 1000000;
case PdmNumColors:
return 16777216;
case PdmDepth:
return 24;
case PdmDevicePixelRatio:
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;
}
}
+124
View File
@@ -0,0 +1,124 @@
/*
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 DXFPAINTDEVICE_H
#define DXFPAINTDEVICE_H
#include <QPaintDevice>
#include <QPaintEngine>
#include <QPen>
#include <QBrush>
#include <QFont>
#include <QString>
/**
@brief The DxfPaintEngine class
A QPaintEngine that translates the small set of QPainter calls made by
QGraphicsItem::paint() implementations (drawLines, drawRects,
drawEllipse, drawPolygon/drawPolyline, drawTextItem) into DXF entities
written through Createdxf, instead of pixels.
This exists so that an item's existing, already-correct paint() code
can be reused unmodified to produce a DXF export: construct a QPainter
on a DxfPaintDevice targeting the item, call painter.begin()/end()
around item->paint(&painter, ...), and every primitive the item draws
becomes a DXF entity at the item's scene position instead of a pixel
on screen.
Scope (deliberately not the full QPainter surface - only what
QElectroTech's own paint() implementations are observed to call):
- drawLines -> LINE
- drawRects -> LWPOLYLINE (closed 4-point outline; DXF has no
filled-rect primitive in the AC1006 dialect this
exporter targets, so brush fill is not emitted -
see fillPath below)
- drawEllipse -> Createdxf::drawArcEllipse (full sweep) or CIRCLE
- drawPath -> arcs (from QPainterPath::Arc elements, used by
drawArc/drawPie) become a sequence of LINE chords;
anything else in the path is flattened to
polyline segments via QPainterPath::toSubpathPolygons
- drawPolygon -> LWPOLYLINE
- drawTextItem -> TEXT (drawText() calls route through this)
- fillPath -> same outline-only handling as drawRects; no HATCH
support in v1 (see design note in the PR)
- 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
{
public:
explicit DxfPaintEngine(const QString &filepath);
bool begin(QPaintDevice *pdev) override;
bool end() override;
void updateState(const QPaintEngineState &state) override;
void drawLines(const QLineF *lines, int lineCount) override;
void drawRects(const QRectF *rects, int rectCount) override;
void drawEllipse(const QRectF &rect) override;
void drawPath(const QPainterPath &path) override;
void drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode) override;
void drawTextItem(const QPointF &p, const QTextItem &textItem) override;
void drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr) override;
Type type() const override { return QPaintEngine::User; }
private:
QPointF toDxf(const QPointF &scene_point) const;
void strokePolygon(const QPolygonF &poly, bool close);
QString m_filepath;
QTransform m_world_transform;
QPen m_pen;
QBrush m_brush;
QFont m_font;
};
/**
@brief The DxfPaintDevice class
Pairs with DxfPaintEngine. One instance targets one already-open DXF
file (Createdxf::dxfBegin()/dxfEnd() bracket the whole export, same as
today - this device only ever appends entities in between).
*/
class DxfPaintDevice : public QPaintDevice
{
public:
explicit DxfPaintDevice(const QString &filepath);
~DxfPaintDevice() override;
QPaintEngine *paintEngine() const override;
protected:
int metric(PaintDeviceMetric metric) const override;
private:
QString m_filepath;
DxfPaintEngine *m_engine;
};
#endif // DXFPAINTDEVICE_H
+40 -30
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "editorcommands.h"
#include "../diagram.h"
/**
@brief ElementEditionCommand::ElementEditionCommand
@@ -276,14 +277,7 @@ void ChangeZValueCommand::applyRaise(const QList<QGraphicsItem *> &items_list) {
for (int i = my_items_list.count() - 2 ; i >= 0 ; -- i) {
if (my_items_list[i] -> isSelected()) {
if (!my_items_list[i +1] -> isSelected()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) // ### Qt 6: remove
my_items_list.swap(i, i + 1);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.13 or later")
#endif
my_items_list.swapItemsAt(i, i + 1);
#endif
}
}
}
@@ -301,14 +295,7 @@ void ChangeZValueCommand::applyLower(const QList<QGraphicsItem *> &items_list) {
for (int i = 1 ; i < my_items_list.count() ; ++ i) {
if (my_items_list[i] -> isSelected()) {
if (!my_items_list[i - 1] -> isSelected()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) // ### Qt 6: remove
my_items_list.swap(i, i - 1);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.13 or later")
#endif
my_items_list.swapItemsAt(i, i - 1);
#endif
}
}
}
@@ -680,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();
}
/**
@@ -694,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);
}
}
}
@@ -732,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();
}
/**
@@ -742,28 +752,28 @@ void FlipElementsCommand::redo()
foreach (auto *item, m_items) {
if (item->type() == PartText::Type) {
PartText* staticText = qgraphicsitem_cast<PartText*>(item);
staticText->flip();
staticText->flip(m_axis_y);
} else if (item->type() == PartDynamicTextField::Type) {
PartDynamicTextField* dyntext = qgraphicsitem_cast<PartDynamicTextField*>(item);
dyntext->flip();
dyntext->flip(m_axis_y);
} else if (item->type() == PartArc::Type) {
PartArc* arc = qgraphicsitem_cast<PartArc*>(item);
arc->flip();
arc->flip(m_axis_y);
} else if (item->type() == PartEllipse::Type) {
PartEllipse* ellipse = qgraphicsitem_cast<PartEllipse*>(item);
ellipse->flip();
ellipse->flip(m_axis_y);
} else if (item->type() == PartLine::Type) {
PartLine* line = qgraphicsitem_cast<PartLine*>(item);
line->flip();
line->flip(m_axis_y);
} else if (item->type() == PartPolygon::Type) {
PartPolygon* poly = qgraphicsitem_cast<PartPolygon*>(item);
poly->flip();
poly->flip(m_axis_y);
} else if (item->type() == PartRectangle::Type) {
PartRectangle* rect = qgraphicsitem_cast<PartRectangle*>(item);
rect->flip();
rect->flip(m_axis_y);
} else if (item->type() == PartTerminal::Type) {
PartTerminal* term = qgraphicsitem_cast<PartTerminal*>(item);
term->flip();
term->flip(m_axis_y);
}
}
}
+14
View File
@@ -279,6 +279,12 @@ private:
};
/**
@brief The MirrorElementsCommand class
Mirror the selected parts horizontally (left <-> right) across the
vertical line through the center of the selection, so the selection
keeps its place in the scene.
*/
class MirrorElementsCommand : public ElementEditionCommand
{
public:
@@ -288,8 +294,15 @@ public:
private:
ElementScene *m_scene =nullptr;
QList<QGraphicsItem*> m_items;
qreal m_axis_x = 0;
};
/**
@brief The FlipElementsCommand class
Flip the selected parts vertically (top <-> bottom) across the
horizontal line through the center of the selection, so the selection
keeps its place in the scene.
*/
class FlipElementsCommand : public ElementEditionCommand
{
public:
@@ -299,6 +312,7 @@ public:
private:
ElementScene *m_scene =nullptr;
QList<QGraphicsItem*> m_items;
qreal m_axis_y = 0;
};
#endif
+6 -1
View File
@@ -121,7 +121,7 @@ void ElementScene::mouseMoveEvent(QGraphicsSceneMouseEvent *e)
{
if (m_event_interface) {
if (m_event_interface -> mouseMoveEvent(e)) {
emit mouseMoved(e -> scenePos());
emit mouseMoved(snapToGrid(e->scenePos()));
if (m_event_interface->isFinish()) {
delete m_event_interface;
m_event_interface = nullptr;
@@ -479,8 +479,13 @@ const QDomDocument ElementScene::toXml(bool all_parts)
root.appendChild(m_element_data.kindInfoToXml(xml_document));
}
//Slave is in this list because the element editor offers the
//Informations tab for it, including the PLC-specific rows
//populateTree() adds for a PLC slave. Without it the editor would
//accept that data and silently drop it on save.
if (type_ == ElementData::Simple ||
type_ == ElementData::Master ||
type_ == ElementData::Slave ||
type_ == ElementData::Terminal ||
type_ == ElementData::Thumbnail)
{
+21 -25
View File
@@ -100,13 +100,30 @@ void ElementView::setSelectionMode()
emit(modeChanged());
}
/**
Applique un facteur d'echelle a la vue en bornant l'echelle resultante
entre m_min_zoom et m_max_zoom. Sans cette borne, un zoom repete (molette)
finit par faire deborder la transformation de la vue et fait planter
l'editeur (issue #798).
@param factor facteur d'echelle a appliquer
*/
void ElementView::scaleClamped(qreal factor)
{
const qreal current = transform().m11();
const qreal target = current * factor;
if (target < m_min_zoom || target > m_max_zoom) {
return;
}
scale(factor, factor);
}
/**
Agrandit le schema (+33% = inverse des -25 % de zoomMoins())
*/
void ElementView::zoomIn()
{
adjustSceneRect();
scale(4.0/3.0, 4.0/3.0);
scaleClamped(4.0/3.0);
}
/**
@@ -115,7 +132,7 @@ void ElementView::zoomIn()
void ElementView::zoomOut()
{
adjustSceneRect();
scale(0.75, 0.75);
scaleClamped(0.75);
}
/**
@@ -123,7 +140,7 @@ void ElementView::zoomOut()
*/
void ElementView::zoomInSlowly()
{
scale(1.02, 1.02);
scaleClamped(1.02);
}
/**
@@ -131,7 +148,7 @@ void ElementView::zoomInSlowly()
*/
void ElementView::zoomOutSlowly()
{
scale(0.98, 0.98);
scaleClamped(0.98);
}
/**
@@ -372,14 +389,7 @@ ElementContent ElementView::pasteWithOffset(const QDomDocument &xml_document) {
*/
void ElementView::mousePressEvent(QMouseEvent* e)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->button() == Qt::MiddleButton)
#endif
{
setCursor( (Qt::ClosedHandCursor));
reference_view_ = e->pos();
@@ -394,14 +404,7 @@ void ElementView::mousePressEvent(QMouseEvent* e)
*/
void ElementView::mouseMoveEvent(QMouseEvent* e)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->buttons() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->buttons() == Qt::MiddleButton)
#endif
{
QScrollBar *h = horizontalScrollBar();
QScrollBar *v = verticalScrollBar();
@@ -420,14 +423,7 @@ void ElementView::mouseMoveEvent(QMouseEvent* e)
*/
void ElementView::mouseReleaseEvent(QMouseEvent* e)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
if (e->button() == Qt::MiddleButton)
#endif
{
setCursor(Qt::ArrowCursor);
adjustSceneRect();
+7
View File
@@ -53,6 +53,13 @@ class ElementView : public QGraphicsView {
private:
QRectF applyMovement(const QRectF &, const QPointF &);
void scaleClamped(qreal factor);
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents the wheel-zoom from driving the transform to overflow, which
/// crashes the editor (bugtracker / GitHub issue #798).
static constexpr qreal m_min_zoom = 0.1;
static constexpr qreal m_max_zoom = 200.0;
public slots:
void setVisualisationMode();
@@ -521,14 +521,7 @@ void CustomElementGraphicPart::stylesFromXml(const QDomElement &qde)
resetStyles();
//Get the list of pair style/value
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QStringList styles = qde.attribute("style").split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
QStringList styles = qde.attribute("style").split(";", Qt::SkipEmptyParts);
#endif
//Check each pair of style
QRegularExpression rx("^\\s*([a-z-]+)\\s*:\\s*([a-zA-Z-]+)\\s*$");
+4 -4
View File
@@ -197,13 +197,13 @@ qreal PartArc::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartArc::flip() {
void PartArc::flip(qreal axis_y) {
m_start_angle = (-1) * m_start_angle;
m_span_angle = (-1) * m_span_angle;
while (m_start_angle < 0) { m_start_angle += (360*16); }
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
auto p1 = mapToScene(m_rect.x(),m_rect.y());
p1.setY(((-1.0) * p1.y()) - m_rect.height());
p1.setY(2 * axis_y - p1.y() - m_rect.height());
p1 = mapFromScene(p1.x(),p1.y());
m_rect = QRectF(m_rect.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
@@ -211,13 +211,13 @@ void PartArc::flip() {
emit rectChanged();
}
void PartArc::mirror() {
void PartArc::mirror(qreal axis_x) {
m_start_angle = (180.0 * 16) - m_start_angle;
m_span_angle = (-1) * m_span_angle;
while (m_start_angle < 0) { m_start_angle += (360*16); }
while (m_start_angle >= (360*16)) { m_start_angle -= (360*16); }
auto p1 = mapToScene(m_rect.x(),m_rect.y());
p1.setX(((-1.0) * p1.x()) - m_rect.width());
p1.setX(2 * axis_x - p1.x() - m_rect.width());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), m_rect.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
+2 -2
View File
@@ -62,8 +62,8 @@ class PartArc : public AbstractPartEllipse
QRectF sceneGeometricRect() const override;
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
@@ -74,19 +74,19 @@ void PartDynamicTextField::setRotation(qreal angle) {
setPos(QTransform().rotate(diffAngle).map(pos()));
}
void PartDynamicTextField::mirror() {
void PartDynamicTextField::mirror(qreal axis_x) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
// then see, where we need to re-position depending on the angle!
qreal rot = qRound(QET::correctAngle(rotation(), true));
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
qreal x = (-1) * pos().x() - c * boundingRect().width();
qreal x = 2 * axis_x - pos().x() - c * boundingRect().width();
qreal y = pos().y() - s * boundingRect().width();
setPos(x, y);
}
void PartDynamicTextField::flip() {
void PartDynamicTextField::flip(qreal axis_y) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle(360-rotation(), true));
// then see, where we need to re-position depending on the angle!
@@ -94,7 +94,7 @@ void PartDynamicTextField::flip() {
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
qreal x = pos().x() + s * boundingRect().height();
qreal y = (-1) * pos().y() - c * boundingRect().height();
qreal y = 2 * axis_y - pos().y() - c * boundingRect().height();
setPos(x, y);
}
@@ -106,8 +106,8 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
bool rotationPointCenter() const;
void setRotation(qreal angle);
void mirror();
void flip();
void mirror(qreal axis_x = 0);
void flip(qreal axis_y = 0);
protected:
+4 -4
View File
@@ -256,9 +256,9 @@ qreal PartEllipse::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartEllipse::flip() {
void PartEllipse::flip(qreal axis_y) {
auto p1 = mapToScene(m_rect.x(), m_rect.y());
p1.setY(((-1.0) * p1.y()) - m_rect.height());
p1.setY(2 * axis_y - p1.y() - m_rect.height());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
@@ -266,9 +266,9 @@ void PartEllipse::flip() {
emit rectChanged();
}
void PartEllipse::mirror() {
void PartEllipse::mirror(qreal axis_x) {
auto p1 = mapToScene(m_rect.x(), m_rect.y());
p1.setX(((-1.0) * p1.x()) - m_rect.width());
p1.setX(2 * axis_x - p1.x() - m_rect.width());
p1 = mapFromScene(p1.x(), p1.y());
m_rect = QRectF(p1.x(), p1.y(), m_rect.width(), m_rect.height());
prepareGeometryChange();
+2 -2
View File
@@ -62,8 +62,8 @@ class PartEllipse : public AbstractPartEllipse
void setRect(const QRectF &rect) override {AbstractPartEllipse::setRect(rect); adjustHandlerPos();}
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
+6 -6
View File
@@ -590,11 +590,11 @@ qreal PartLine::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartLine::flip() {
void PartLine::flip(qreal axis_y) {
auto p1 = mapToScene(m_line.p1());
auto p2 = mapToScene(m_line.p2());
p1 = QPointF(p1.x(), (-1) * p1.y());
p2 = QPointF(p2.x(), (-1) * p2.y());
p1 = QPointF(p1.x(), 2 * axis_y - p1.y());
p2 = QPointF(p2.x(), 2 * axis_y - p2.y());
m_line.setP1(mapFromScene(p1));
m_line.setP2(mapFromScene(p2));
setLine(m_line);
@@ -603,11 +603,11 @@ void PartLine::flip() {
emit lineChanged();
}
void PartLine::mirror() {
void PartLine::mirror(qreal axis_x) {
auto p1 = mapToScene(m_line.p1());
auto p2 = mapToScene(m_line.p2());
p1 = QPointF((-1) * p1.x(), p1.y());
p2 = QPointF((-1) * p2.x(), p2.y());
p1 = QPointF(2 * axis_x - p1.x(), p1.y());
p2 = QPointF(2 * axis_x - p2.x(), p2.y());
m_line.setP1(mapFromScene(p1));
m_line.setP2(mapFromScene(p2));
setLine(m_line);
+2 -2
View File
@@ -96,8 +96,8 @@ class PartLine : public CustomElementGraphicPart
void setSecondEndLength(const qreal &l);
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
+4 -4
View File
@@ -307,10 +307,10 @@ qreal PartPolygon::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartPolygon::flip() {
void PartPolygon::flip(qreal axis_y) {
for (auto &pt : m_polygon) {
pt = mapToScene(pt.x(), pt.y());
pt = QPointF(pt.x(), (-1) * pt.y());
pt = QPointF(pt.x(), 2 * axis_y - pt.y());
pt = mapFromScene(pt.x(), pt.y());
}
setPolygon(m_polygon);
@@ -319,10 +319,10 @@ void PartPolygon::flip() {
emit polygonChanged();
}
void PartPolygon::mirror() {
void PartPolygon::mirror(qreal axis_x) {
for (auto &pt : m_polygon) {
pt = mapToScene(pt.x(), pt.y());
pt = QPointF((-1) * pt.x(), pt.y());
pt = QPointF(2 * axis_x - pt.x(), pt.y());
pt = mapFromScene(pt.x(), pt.y());
}
setPolygon(m_polygon);
+2 -2
View File
@@ -89,8 +89,8 @@ class PartPolygon : public CustomElementGraphicPart
void setRotation (qreal angle);
qreal rotation () const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void addHandler() override;
void removeHandler() override;
@@ -187,11 +187,11 @@ qreal PartRectangle::rotation() const {
return qRound(m_rot * 100.0) / 100.0;
}
void PartRectangle::flip() {
void PartRectangle::flip(qreal axis_y) {
auto height = m_rect.height();
auto p1 = mapToScene(m_rect.x(),m_rect.y());
qreal x = p1.x();
qreal y = ((-1.0) * p1.y()) - height;
qreal y = 2 * axis_y - p1.y() - height;
p1 = mapFromScene(x, y);
m_rect.setX(p1.x());
m_rect.setY(p1.y());
@@ -201,10 +201,10 @@ void PartRectangle::flip() {
emit rectChanged();
}
void PartRectangle::mirror() {
void PartRectangle::mirror(qreal axis_x) {
auto width = m_rect.width();
auto p1 = mapToScene(m_rect.x(),m_rect.y());
qreal x = ((-1.0) * p1.x()) - width;
qreal x = 2 * axis_x - p1.x() - width;
qreal y = p1.y();
p1 = mapFromScene(x, y);
m_rect.setX(p1.x());
+2 -2
View File
@@ -73,8 +73,8 @@ class PartRectangle : public CustomElementGraphicPart
void setYRadius(qreal Y);
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
QRectF sceneGeometricRect() const override;
virtual QPointF sceneTopLeft() const;
+4 -4
View File
@@ -293,9 +293,9 @@ qreal PartTerminal::rotation() const {
@brief PartTerminal::flip
turn part upside down
*/
void PartTerminal::flip() {
void PartTerminal::flip(qreal axis_y) {
d->m_pos.setX( pos().x());
d->m_pos.setY((-1.0) * pos().y());
d->m_pos.setY(2 * axis_y - pos().y());
switch (d->m_orientation) {
case Qet::North : setOrientation(Qet::South);
break;
@@ -314,8 +314,8 @@ void PartTerminal::flip() {
@brief PartTerminal::mirror
turn part from left to right
*/
void PartTerminal::mirror() {
d->m_pos.setX((-1.0) * pos().x());
void PartTerminal::mirror(qreal axis_x) {
d->m_pos.setX(2 * axis_x - pos().x());
d->m_pos.setY( pos().y());
switch (d->m_orientation) {
case Qet::North : break;
+2 -2
View File
@@ -96,8 +96,8 @@ class PartTerminal : public CustomElementGraphicPart
void setRotation(qreal angle);
qreal rotation() const;
void flip();
void mirror();
void flip(qreal axis_y = 0);
void mirror(qreal axis_x = 0);
void nextOrientation();
void previousOrientation();
+12 -4
View File
@@ -75,7 +75,7 @@ void PartText::setRotation(qreal angle) {
setPos(QTransform().rotate(diffAngle).map(pos()));
}
void PartText::mirror() {
void PartText::mirror(qreal axis_x) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
// then see, where we need to re-position depending on text, font ...
@@ -86,12 +86,12 @@ void PartText::mirror() {
qreal c = qCos(qDegreesToRadians(rot));
qreal s = qSin(qDegreesToRadians(rot));
// Now: Move!
qreal x = (-1) * pos().x() - c * (textwidth);
qreal x = 2 * axis_x - pos().x() - c * (textwidth);
qreal y = pos().y() - s * (textwidth);
setPos(x, y);
}
void PartText::flip() {
void PartText::flip(qreal axis_y) {
// at first: rotate the text:
QGraphicsObject::setRotation(QET::correctAngle((360-rotation()), true));
// then see, where we need to re-position depending on text, font ...
@@ -103,7 +103,7 @@ void PartText::flip() {
qreal s = qSin(qDegreesToRadians(rot));
// Now: Move!
qreal x = pos().x() - s * (textheight);
qreal y = (-1) * pos().y() + c * (textheight);
qreal y = 2 * axis_y - pos().y() + c * (textheight);
setPos(x, y);
}
@@ -440,6 +440,14 @@ void PartText::setFont(const QFont &font) {
// at a different spot after save/reopen (the position recomputes from
// the saved font on load). See #158.
adjustItemPosition();
// Keep real_font_size_ in sync with the actual font. It's the base
// size startUserTransformation()/handleUserTransformation() scale
// from when the user drags a resize handle, and flip() also reads it
// to reposition the item. Left stale here, either one computes from
// whatever size the item had when it was first created, ignoring any
// size set since (toolbar, property editor, or loaded from XML) -
// found investigating #158.
real_font_size_ = font.pointSize();
emit fontChanged(font);
}
}
+2 -2
View File
@@ -64,8 +64,8 @@ class PartText : public QGraphicsTextItem, public CustomElementPart {
void fromXml(const QDomElement &) override;
const QDomElement toXml(QDomDocument &) const override;
void setRotation(qreal angle);
void mirror();
void flip();
void mirror(qreal axis_x = 0);
void flip(qreal axis_y = 0);
bool isUseless() const override;
QRectF sceneGeometricRect() const override;
void startUserTransformation(const QRectF &) override;
+21 -4
View File
@@ -31,6 +31,7 @@
#include <QColorDialog>
#include <QGraphicsItem>
#include <QPointer>
#include <QSignalBlocker>
DynamicTextFieldEditor::DynamicTextFieldEditor(QETElementEditor *editor,
PartDynamicTextField *text_field,
@@ -141,9 +142,20 @@ void DynamicTextFieldEditor::updateForm()
ui -> m_size_sb -> setValue(m_text_field.data() -> font().pointSize());
ui->m_keep_visual_rotation_cb->setChecked(m_text_field.data()->keepVisualRotation());
ui->m_rotation_point_center_cb->setChecked(m_text_field.data()->rotationPointCenter());
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#else
m_color_kpb -> setColor(m_text_field.data() -> color());
//Block signals while loading the colour into the button.
//KColorButton::changed fires on a programmatic setColor() as well
//as on user interaction, and m_color_kpb_changed() applies the new
//colour to *every* selected part -- so merely showing the first
//part's colour would overwrite the colour of all the others.
//The other widgets above are immune because they are wired to
//user-only signals (editingFinished, clicked), which setValue()
//and setChecked() do not emit.
{
const QSignalBlocker blocker(m_color_kpb);
m_color_kpb -> setColor(m_text_field.data() -> color());
}
#endif
ui -> m_width_sb -> setValue(m_text_field.data() -> textWidth());
ui -> m_font_pb -> setText(m_text_field -> font().family());
@@ -170,7 +182,7 @@ void DynamicTextFieldEditor::updateForm()
void DynamicTextFieldEditor::setupWidget()
{
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#else
m_color_kpb = new KColorButton(this);
m_color_kpb->setObjectName(QString::fromUtf8("m_color_kpb"));
@@ -233,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) {
@@ -262,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) {
+2 -2
View File
@@ -21,7 +21,7 @@
#include "../elementitemeditor.h"
#include "../graphicspart/partdynamictextfield.h"
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#else
# include <KColorButton>
#endif
@@ -79,7 +79,7 @@ class DynamicTextFieldEditor : public ElementItemEditor {
QList<PartDynamicTextField*> m_parts;
QList<QMetaObject::Connection> m_connection_list;
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#else
KColorButton* m_color_kpb = nullptr;
#endif
@@ -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;
@@ -423,11 +423,11 @@ void ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged(int in
ui->m_master_gb->setVisible(master);
ui->m_terminal_gb->setVisible(terminal);
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
ui->tabWidget->setTabVisible(1,
(type_ == ElementData::Simple ||
type_ == ElementData::Master));
#endif
type_ == ElementData::Master ||
type_ == ElementData::Slave ||
type_ == ElementData::Terminal));
updateTree();
}
@@ -454,6 +454,9 @@ void ElementPropertiesEditorWidget::on_m_slave_groups_checkbox_toggled(bool chec
if (checked && !ui->max_slaves_checkbox->isChecked()) {
ui->max_slaves_checkbox->setChecked(true);
if (!m_data.m_slave_contact_groups.isEmpty()) {
ui->max_slaves_spinbox->setValue(m_data.m_slave_contact_groups.size());
}
}
if (checked) {
@@ -689,7 +692,7 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_table->horizontalHeader()->resizeSection(2, 150);
m_plc_table->horizontalHeader()->resizeSection(3, 150);
m_plc_table->horizontalHeader()->resizeSection(4, 100);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+32 -7
View File
@@ -55,6 +55,7 @@
#include <QActionGroup>
#include <QFileDialog>
#include <QSvgGenerator>
#include <QHBoxLayout>
/**
* @brief QETElementEditor::QETElementEditor
@@ -77,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
}
/**
@@ -950,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
@@ -1200,7 +1214,18 @@ void QETElementEditor::initGui()
//Live cursor position readout, in the same scene coordinates as the parts' X/Y properties
m_position_label = new QLabel(this);
m_position_label->setMinimumWidth(120);
statusBar()->addPermanentWidget(m_position_label);
// Layout
QHBoxLayout *coordDisplayLayout = new QHBoxLayout();
coordDisplayLayout->setContentsMargins(0, 0, 0, 0);
coordDisplayLayout->addWidget(m_position_label);
coordDisplayLayout->addStretch();
// Widget
QWidget *coordDisplay = new QWidget;
coordDisplay->setLayout(coordDisplayLayout);
statusBar()->addPermanentWidget(coordDisplay);
connect(m_elmt_scene, &ElementScene::mouseMoved, this, [this](const QPointF &pos) {
m_position_label->setText(tr("X: %1 Y: %2")
.arg(pos.x(), 0, 'f', 1)
+1
View File
@@ -116,6 +116,7 @@ class QETElementEditor : public QMainWindow
private:
bool canClose();
void readSettings();
void readSettingsState();
void writeSettings() const;
void setupActions();
void updateAction();
+3 -3
View File
@@ -40,7 +40,7 @@ TerminalEditor::TerminalEditor(QETElementEditor *editor, QWidget *parent) :
{
ui->setupUi(this);
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
m_color_pb = new QPushButton(this);
m_color_pb->setMinimumSize(40, 24);
connect(m_color_pb, &QPushButton::clicked, this, &TerminalEditor::labelColorClicked);
@@ -93,7 +93,7 @@ void TerminalEditor::updateForm()
ui->m_label_rotation_sb->setValue(static_cast<int>(m_part->labelRotation()));
ui->m_label_frame_cb->setChecked(m_part->labelFrame());
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
QPixmap px(16, 16);
px.fill(m_part->labelColor());
m_color_pb->setIcon(QIcon(px));
@@ -410,7 +410,7 @@ void TerminalEditor::labelColorClicked()
if (m_locked) return;
m_locked = true;
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
QColor new_color = QColorDialog::getColor(m_part->labelColor(), this);
if (new_color.isValid() && m_part->labelColor() != new_color) {
auto undo = new QPropertyUndoCommand(m_part, "label_color", m_part->labelColor(), new_color);
+2 -2
View File
@@ -21,7 +21,7 @@
#include <QWidget>
#include "../elementitemeditor.h"
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#include <QPushButton>
#else
#include <KColorButton>
@@ -78,7 +78,7 @@ class TerminalEditor : public ElementItemEditor
m_change_connections;
PartTerminal *m_part = nullptr;
bool m_locked = false;
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
QPushButton *m_color_pb;
#else
KColorButton *m_color_pb;
+2 -2
View File
@@ -63,7 +63,7 @@ void TextEditor::updateForm()
m_rotation_sb -> setValue(m_text -> rotation());
m_size_sb -> setValue(m_text -> font().pointSize());
m_font_pb -> setText(m_text -> font().family());
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#else
m_color_pb -> setColor(m_text -> defaultTextColor());
#endif
@@ -346,7 +346,7 @@ void TextEditor::setUpWidget(QWidget *parent)
m_line_edit->setPlaceholderText(tr("Entrer votre texte ici"));
gridLayout->addWidget(m_line_edit, 0, 0, 1, 6);
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#else
m_color_pb = new KColorButton(parent);
m_color_pb->setObjectName(QString::fromUtf8("m_color_pb"));
+2 -2
View File
@@ -25,7 +25,7 @@
#include <QSpinBox>
#include <QLineEdit>
#include <QPushButton>
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#else
#include <KColorButton>
#endif
@@ -65,7 +65,7 @@ class TextEditor : public ElementItemEditor {
QSpinBox *m_size_sb;
QLineEdit *m_line_edit;
QPushButton *m_font_pb;
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
#else
KColorButton *m_color_pb;
#endif
+8
View File
@@ -17,6 +17,7 @@
*/
#include "elementspanelwidget.h"
#include "diagram.h"
#include "qetgraphicsitem/conductor.h"
#include "editor/ui/qetelementeditor.h"
#include "elementscategoryeditor.h"
#include "qetapp.h"
@@ -684,6 +685,13 @@ void ElementsPanelWidget::duplicateDiagram()
elmt->newUuid();
new_diagram->restoreText(elmt);
}
else if (Conductor *cond = dynamic_cast<Conductor *>(item)) {
// Same reasoning for conductors: conductor.uuid is the PRIMARY
// KEY of the conductor table, and its insert is a plain INSERT,
// so a duplicated uuid fails and the wire silently disappears
// from the wiring list and the per-element wire count.
cond->newUuid();
}
}
}
+120 -9
View File
@@ -22,8 +22,10 @@
#include "exportpropertieswidget.h"
#include "factory/elementpicturefactory.h"
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "dxfpaintdevice.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/crossrefitem.h"
#include "qetgraphicsitem/diagramimageitem.h"
#include "qetgraphicsitem/diagramtextitem.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
@@ -140,21 +142,12 @@ QWidget *ExportDialog::initDiagramsListPart()
reset_mapper_ = new QSignalMapper(this);
clipboard_mapper_ = new QSignalMapper(this);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // TODO Qt6 only: remove, mappedInt() always available
connect(preview_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_previewDiagram(int)));
connect(width_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_correctHeight(int)));
connect(height_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_correctWidth(int)));
connect(ratio_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_keepRatioChanged(int)));
connect(reset_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_resetSize(int)));
connect(clipboard_mapper_, SIGNAL(mapped(int)), this, SLOT(slot_exportToClipBoard(int)));
#else
connect(preview_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_previewDiagram);
connect(width_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_correctHeight);
connect(height_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_correctWidth);
connect(ratio_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_keepRatioChanged);
connect(reset_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_resetSize);
connect(clipboard_mapper_, &QSignalMapper::mappedInt, this, &ExportDialog::slot_exportToClipBoard);
#endif
diagrams_list_layout_ = new QGridLayout();
@@ -472,6 +465,16 @@ void ExportDialog::generateDxf(
QList<Conductor *> list_conductors;
QList<DiagramTextItem *> list_texts;
QList<DiagramImageItem *> list_images;
//Slave cross-reference labels. They hang off a DynamicElementTextItem
//as plain QGraphicsTextItem children, so neither cast below picks them
//up and they were missing from the DXF entirely.
QList<QGraphicsTextItem *> list_xref_texts;
//Master-side cross-reference item (the table/cross drawn next to a
//report/master element). It paints itself with hand-written
//QPainter code across three modes (drawAsCross/drawAsContacts/
//drawAsPlcTable), so instead of hand-porting each one it's replayed
//through DxfPaintEngine, which reuses paint() unmodified.
QList<CrossRefItem *> list_master_xrefs;
QList<QLineF *> list_lines;
QList<QRectF *> list_rectangles;
//QList<QRectF *> list_ellipses;
@@ -493,8 +496,13 @@ void ExportDialog::generateDxf(
list_shapes << dii;
} else if (DynamicElementTextItem *deti = qgraphicsitem_cast<DynamicElementTextItem *>(qgi)) {
list_texts << deti;
if (QGraphicsTextItem *xref = deti->slaveXrefItem()) {
list_xref_texts << xref;
}
} else if (QetGraphicsTableItem *gti = qgraphicsitem_cast<QetGraphicsTableItem *>(qgi)) {
list_tables << gti;
} else if (CrossRefItem *xref = qgraphicsitem_cast<CrossRefItem *>(qgi)) {
list_master_xrefs << xref;
}
}
@@ -688,6 +696,73 @@ void ExportDialog::generateDxf(
}
}
//Draw the slave cross-reference labels
for (QGraphicsTextItem *xref : std::as_const(list_xref_texts))
{
qreal fontSize = xref->font().pointSizeF();
if (fontSize < 0)
fontSize = xref->font().pixelSize();
qreal angle = xref->rotation();
QGraphicsItem *parent = xref->parentItem();
while (parent) {
angle += parent->rotation();
parent = parent->parentItem();
}
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
qreal x = xref->scenePos().x()
+ xdir * fontSize * 1.8
- ydir * fontSize;
qreal y = xref->scenePos().y()
- ydir * fontSize * 1.8
- xdir * fontSize * 0.9;
const QStringList lines = xref->toPlainText().split('\n');
const qreal offset = fontSize * 1.6;
for (const QString &line : lines) {
if (line.size() > 0 && line != QLatin1String("_")) {
Createdxf::drawText(file_path, line, QPointF(x, y), fontSize,
360-angle, Createdxf::dxfColor(xref->defaultTextColor()), 0.72);
}
x += offset * xdir;
y -= offset * ydir;
}
}
//Draw the master-side cross-reference items (table/cross), replaying
//their existing paint() unmodified through DxfPaintEngine instead of
//hand-porting drawAsCross()/drawAsContacts()/drawAsPlcTable().
for (CrossRefItem *xref : std::as_const(list_master_xrefs))
{
DxfPaintDevice dxf_device(file_path);
QPainter painter(&dxf_device);
painter.setWorldTransform(xref->sceneTransform());
xref->paintForExport(&painter);
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);
@@ -762,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);
+47 -22
View File
@@ -36,6 +36,33 @@
ElementPictureFactory* ElementPictureFactory::m_factory = nullptr;
/**
@brief ElementPictureFactory::cacheKey
@param location
@return the key under which the drawing of the element at location is
cached.
An element definition normally carries its own uuid, and that is used
directly. Definitions saved before uuids were written do not have one --
a good share of the shipped example projects are still in that state --
and they all presented the same null uuid as a key. The drawing of such
an element was therefore either rebuilt for every instance placed, or
stored under a key it shared with every other element lacking a uuid.
Derive a stable key from the location for those instead:
ElementsLocation::toString() qualifies an embedded path with the id of
the project that owns it, and project ids come from an ever-increasing
counter and are never reused, so the derived key cannot collide with an
element of another project.
*/
QUuid ElementPictureFactory::cacheKey(const ElementsLocation &location)
{
const QUuid uuid = location.uuid();
if (!uuid.isNull()) {
return uuid;
}
return QUuid::createUuidV5(QUuid(), location.toString().toUtf8());
}
/**
@brief ElementPictureFactory::getPictures
Set the picture of the element at location.
@@ -50,12 +77,7 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
return;
}
QUuid uuid = location.uuid();
if(Q_UNLIKELY(uuid.isNull()))
{
build(location, &picture, &low_picture);
return;
}
const QUuid uuid = cacheKey(location);
if(m_pictures_H.contains(uuid))
{
@@ -80,7 +102,7 @@ void ElementPictureFactory::getPictures(const ElementsLocation &location, QPictu
*/
QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
{
QUuid uuid = location.uuid();
const QUuid uuid = cacheKey(location);
if (m_pixmap_H.contains(uuid)) {
return m_pixmap_H.value(uuid);
@@ -99,7 +121,14 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
int hsy = qMin(doc.document_element().attribute("hotspot_y").as_int(), h);
QPixmap pix(w, h);
pix.fill(QColor(255, 255, 255, 0));
//Element definitions almost always draw with a hardcoded black
//stroke color, on the assumption of the white diagram sheet they
//are normally placed on. A transparent background here makes
//that stroke disappear against a dark widget/tree-view background
//(bugtracker #335). Give it an opaque white background instead -
//exactly what the element already assumes visually, in every
//context this pixmap is used (tree icons, drag icon, previews).
pix.fill(Qt::white);
QPainter painter(&pix);
painter.setRenderHint(QPainter::Antialiasing, true);
@@ -107,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;
}
@@ -125,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()
@@ -263,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_);
@@ -612,16 +640,13 @@ void ElementPictureFactory::setPainterStyle(const QDomElement &dom, QPainter &pa
pen.setCapStyle(Qt::SquareCap);
//Get the couples style/value
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
const QStringList styles = dom.attribute("style").split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
const QStringList styles = dom.attribute("style").split(";", Qt::SkipEmptyParts);
#endif
QRegularExpression rx("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$");
//Built once : this runs for every primitive of every element
//instance a project places, and recompiling the pattern each time
//was the single largest cost of opening a project.
static const QRegularExpression rx(
QStringLiteral("^(?<name>[a-z-]+):(?<value>[a-zA-Z-]+)$"));
if (!rx.isValid())
{
qWarning() <<QObject::tr("this is an error in the code")
+1
View File
@@ -92,6 +92,7 @@ class ElementPictureFactory
ElementPictureFactory operator= (const ElementPictureFactory &);
~ElementPictureFactory();
static QUuid cacheKey(const ElementsLocation &location);
bool build(const ElementsLocation &location, QPicture *picture=nullptr, QPicture *low_picture=nullptr);
void parseElement(const QDomElement &dom, QPainter &painter, primitives &prim) const;
void parseLine (const QDomElement &dom, QPainter &painter, primitives &prim) const;
+16 -2
View File
@@ -730,11 +730,25 @@ void GenericPanel::projectInformationsChanged(QETProject *project) {
/**
@brief GenericPanel::diagramAdded
@param project
@param diagram (unused)
@param diagram the newly added diagram to select in the tree
*/
void GenericPanel::diagramAdded(QETProject *project, Diagram *diagram) {
Q_UNUSED(diagram)
addProject(project, nullptr, GenericPanel::AddChildDiagrams);
if (diagram) {
QTreeWidgetItem *projectItem = itemForProject(project);
if (projectItem) {
for (int i = 0; i < projectItem->childCount(); ++i) {
QTreeWidgetItem *child = projectItem->child(i);
if (child && child->type() == QET::Diagram
&& valueForItem<Diagram *>(child) == diagram) {
clearSelection();
setCurrentItem(child);
child->setSelected(true);
break;
}
}
}
}
emit(panelContentChanged());
}
+13 -4
View File
@@ -45,11 +45,11 @@ QList<QPair<QString, QString>> langDict(const QString &value)
for (int i = 0; i < marks.size(); ++i) {
const QRegularExpressionMatch &m = marks.at(i);
const QString code = value.mid(m.capturedStart(),
m.capturedLength() - 1); // drop '@'
m.capturedLength() - 1); // drop '@'
const int text_start = m.capturedEnd();
const int text_end = (i + 1 < marks.size())
? marks.at(i + 1).capturedStart()
: value.size();
? marks.at(i + 1).capturedStart()
: value.size();
QString text = value.mid(text_start, text_end - text_start);
while (!text.isEmpty() && text.back().isSpace()) {
text.chop(1);
@@ -77,7 +77,7 @@ QString pickLang(const QString &value)
return value.trimmed();
}
for (const QString &code : {QStringLiteral("en_EN"), QStringLiteral("en_US"),
QStringLiteral("en_GB")}) {
QStringLiteral("en_GB")}) {
for (const auto &p : d) {
if (p.first == code && !p.second.isEmpty()) {
return p.second;
@@ -158,11 +158,20 @@ bool EdzPart::parse(const QString &part_xml_path)
return false;
}
QDomDocument doc;
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) // TODO Qt 6.5: remove
QString parse_err;
int line = 0, col = 0;
if (!doc.setContent(&file, &parse_err, &line, &col)) {
m_error = QStringLiteral("Malformed part.xml (line %1): %2")
.arg(line).arg(parse_err);
#else
const auto result = doc.setContent(&file);
if (!result) {
m_error = QStringLiteral("Malformed part.xml (line %1, column %2): %3")
.arg(result.errorLine)
.arg(result.errorColumn)
.arg(result.errorMessage);
#endif
return false;
}
file.close();
+2
View File
@@ -159,6 +159,8 @@ void MachineInfo::send_info_to_debug()
qInfo()<< "";
qInfo()<< " System language defined in QET configuration:"<< QString(QETApp::langFromSetting().toLatin1());
qInfo()<< " language Path:"<< QString(QETApp::languagesPath().toLatin1());
qInfo()<< " Loaded QET translation:"<< QETApp::loadedQetTranslationFile();
qInfo()<< " Loaded Qt translation:"<< QETApp::loadedQtTranslationFile();
qInfo()<< " Common Elements Dir:"<< QString(QETApp::commonElementsDir().toLatin1());
qInfo()<< " Common TitleBlock Templates Dir:"<< QString(QETApp::commonTitleBlockTemplatesDir().toLatin1());
qInfo()<< " Custom Elements Dir:"<< QString(QETApp::customElementsDir().toLatin1());
+7 -10
View File
@@ -21,6 +21,7 @@
#include "machine_info.h"
#include "qet.h"
#include "qetapp.h"
#include "qetmessagebox.h"
#include "qetproject.h"
#include "singleapplication.h"
#include "utils/qetsettings.h"
@@ -103,24 +104,15 @@ int main(int argc, char **argv)
QDomImplementation::ReturnNullNode);
//Creation and execution of the application
//HighDPI
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
#endif
#if QT_VERSION > QT_VERSION_CHECK(5, 7, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
qputenv("QT_ENABLE_HIGHDPI_SCALING", "1");
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
#endif
// Headless command-line export: render a project to PDF/PNG/SVG without
@@ -136,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());
}
}
+6
View File
@@ -252,4 +252,10 @@ void NewElementWizard::createNewElement()
loc_.addToPath(m_chosen_file);
edit_new_element -> setLocation(loc_);
edit_new_element -> show();
// Without this, the just-closed wizard can leave the main window
// as the active/key window (bugtracker #281, reported on macOS):
// the new editor is created and shown, but stays behind the main
// window and doesn't surface in the Dock/Windows menu.
edit_new_element -> raise();
edit_new_element -> activateWindow();
}
+277 -3
View File
@@ -23,8 +23,8 @@
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/elementtextitemgroup.h"
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4.
// Requires QT += gui-private in qelectrotech.pro / gui-private in CMake.
// Private Qt PDF engine for drawHyperlink() — not public API.
// Availability of Qt::GuiPrivate is verified at configure time in CMakeLists.txt.
#include <private/qpdf_p.h>
#include <QByteArray>
@@ -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));
@@ -238,6 +238,15 @@ void convertUriToGoTo(const QString &pdfPath)
QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
// Skip component-info annotations — handled by
// convertComponentInfoAnnotations() in a separate pass.
if (uriVal.startsWith("componentinfo://")
|| uriVal.startsWith("http://componentinfo.local/")) {
out.append(data.mid(found, closeParen + 1 - found));
pos = closeParen + 1;
continue;
}
// Extract page number: look for #page=N or bare page=N
int pageNum = -1;
int hashPos = uriVal.lastIndexOf("#page=");
@@ -379,4 +388,269 @@ void convertUriToGoTo(const QString &pdfPath)
out.close();
}
void convertComponentInfoAnnotations(const QString &pdfPath,
const QList<ComponentInfo> &annotations)
{
if (annotations.isEmpty()) return;
QFile f(pdfPath);
if (!f.open(QIODevice::ReadOnly)) return;
QByteArray data = f.readAll();
f.close();
const QByteArray markerPrefix("http://componentinfo.local/");
if (!data.contains(markerPrefix)) return;
int xrefStart = data.lastIndexOf("\nxref\n");
if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
if (xrefStart == -1) return;
++xrefStart;
QByteArray body = data.left(xrefStart);
// Pre-scan body for highest object number
int maxObjNum = 0;
{
const QByteArray objMarker(" 0 obj");
int p = 0;
while ((p = body.indexOf(objMarker, p)) != -1) {
int numStart = p - 1;
while (numStart > 0 && body[numStart - 1] != '\n' && body[numStart - 1] != '\r')
--numStart;
QByteArray numStr = body.mid(numStart, p - numStart).trimmed();
bool ok = false;
int num = numStr.toInt(&ok);
if (ok && num > maxObjNum)
maxObjNum = num;
++p;
}
}
// Empty Form XObject used as invisible appearance for all annotations.
// All annotations reference this single object.
int emptyXObjNum = maxObjNum + 1;
QByteArray out;
out.reserve(data.size());
int pos = 0;
bool anyConverted = false;
while (pos < body.size()) {
// Find next indexed marker: http://componentinfo.local/<N>
int markerPos = body.indexOf(markerPrefix, pos);
if (markerPos == -1) {
out.append(body.mid(pos));
break;
}
// Extract index from marker URL
int idxStart = markerPos + markerPrefix.size();
int idxEnd = idxStart;
while (idxEnd < body.size() && body[idxEnd] >= '0' && body[idxEnd] <= '9')
++idxEnd;
if (idxEnd == idxStart) {
// No index — skip malformed marker
out.append(body.mid(pos, markerPos + markerPrefix.size() - pos));
pos = markerPos + markerPrefix.size();
continue;
}
int annotIndex = body.mid(idxStart, idxEnd - idxStart).toInt();
int uriOpen = body.lastIndexOf("/URI (", markerPos);
int closeParen = (uriOpen != -1) ? body.indexOf(')', markerPos) : -1;
if (uriOpen == -1 || closeParen == -1 || uriOpen < pos) {
out.append(body.mid(pos, idxEnd - pos));
pos = idxEnd;
continue;
}
// Find /S /URI before /URI (
int sUriPos = body.lastIndexOf("/S /URI", uriOpen);
// Find the /A << that opens the action dict containing /S /URI.
// Qt always writes: /A <<\n/S /URI\n/URI (...)\n>>\n>>
int aDictOpen = (sUriPos != -1)
? body.lastIndexOf("/A <<", sUriPos)
: -1;
// If /A << is not found, leave this annotation untouched
if (aDictOpen == -1 || aDictOpen < pos) {
out.append(body.mid(pos, closeParen + 1 - pos));
pos = closeParen + 1;
continue;
}
// Validate index
if (annotIndex < 0 || annotIndex >= annotations.size()) {
// Index out of range — keep original annotation intact
out.append(body.mid(pos, closeParen + 1 - pos));
pos = closeParen + 1;
continue;
}
// Copy annotation dict header up to /A <<
out.append(body.mid(pos, aDictOpen - pos));
QByteArray contents = annotations[annotIndex].contents.toUtf8();
// Replace /Subtype /Link with /Subtype /Text, bounded to the enclosing
// PDF object. Anchoring to " 0 obj" prevents hitting /Subtype /Link
// from a cross-ref annotation that sits between the previous marker
// and the current annotation on the same page.
int objStart = body.lastIndexOf(" 0 obj", aDictOpen);
int subTypeInBody = (objStart != -1)
? body.indexOf("/Subtype /Link", objStart) : -1;
int subTypePos = (subTypeInBody != -1 && subTypeInBody < aDictOpen)
? out.size() - (aDictOpen - subTypeInBody) : -1;
if (subTypePos != -1)
// Acrobat draws its own sticky-note icon for /Subtype /Text
// whatever /AP says (verified on Reader 5.0). /Square with no
// /C and no /IC draws nothing anywhere, and is still a markup
// annotation, so the /Contents popup is unaffected.
out.replace(subTypePos, 14, "/Subtype /Square");
// Encode as UTF-16BE hex with BOM for proper Unicode support (Umlauten etc.).
// PDF spec: hex strings starting with FE FF are interpreted as UTF-16BE.
QByteArray utf16be;
utf16be.append('\xfe');
utf16be.append('\xff');
{
for (int i = 0; i < contents.size(); ) {
ushort cp = 0;
uchar c = static_cast<uchar>(contents.at(i));
if (c < 0x80) {
cp = c;
++i;
} else if ((c & 0xE0) == 0xC0 && i + 1 < contents.size()) {
cp = ((c & 0x1F) << 6)
| (static_cast<uchar>(contents.at(i + 1)) & 0x3F);
i += 2;
} else if ((c & 0xF0) == 0xE0 && i + 2 < contents.size()) {
cp = ((c & 0x0F) << 12)
| ((static_cast<uchar>(contents.at(i + 1)) & 0x3F) << 6)
| (static_cast<uchar>(contents.at(i + 2)) & 0x3F);
i += 3;
} else {
cp = 0xFFFD; // replacement character
++i;
}
utf16be.append(static_cast<char>((cp >> 8) & 0xFF));
utf16be.append(static_cast<char>(cp & 0xFF));
}
}
out += "/Contents <";
out += utf16be.toHex();
out += ">\n/AP << /N " + QByteArray::number(emptyXObjNum) + " 0 R >>\n";
// Skip the entire /A << ... >> action dict.
// After closeParen ')' we have: \n>> (closes /A dict) \n>> (closes annot dict)
int actionDictClose = body.indexOf(">>", closeParen + 1);
if (actionDictClose != -1) {
pos = actionDictClose + 2; // skip past >> that closes /A dict
} else {
pos = closeParen + 1;
}
anyConverted = true;
}
if (!anyConverted) return;
// Append empty Form XObject (shared by all annotations for invisible appearance)
QByteArray emptyXObj;
emptyXObj += QByteArray::number(emptyXObjNum) + " 0 obj\n";
emptyXObj += "<< /Type /XObject /Subtype /Form /BBox [0 0 0 0] /Length 0 >>\n";
emptyXObj += "stream\nendstream\n";
emptyXObj += "endobj\n";
out += emptyXObj;
// Rebuild xref table
QMap<int, int> offsets;
{
const QByteArray objMarker(" 0 obj");
int p = 0;
while ((p = out.indexOf(objMarker, p)) != -1) {
int numStart = p - 1;
while (numStart > 0 && out[numStart - 1] != '\n' && out[numStart - 1] != '\r')
--numStart;
QByteArray numStr = out.mid(numStart, p - numStart).trimmed();
bool ok = false;
int objNum = numStr.toInt(&ok);
if (ok && objNum > 0)
offsets[objNum] = numStart;
++p;
}
}
if (offsets.isEmpty()) return;
int maxObj = offsets.lastKey();
QByteArray xref;
xref += "xref\n";
xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
xref += "0000000000 65535 f \n";
for (int i = 1; i <= maxObj; ++i) {
if (offsets.contains(i)) {
xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
+ " 00000 n \n";
} else {
xref += "0000000000 65535 f \n";
}
}
// Copy trailer and bump /Size to account for the new XObject
QByteArray trailer;
{
int tPos = data.indexOf("trailer", xrefStart);
if (tPos != -1) {
int tEnd = data.indexOf("%%EOF", tPos);
if (tEnd != -1) tEnd += 5;
if (tEnd != -1)
trailer = data.mid(tPos, tEnd - tPos);
}
}
if (trailer.isEmpty())
trailer = "trailer\n<<>>\n%%EOF";
// Bump /Size: original was maxObjNum+1, now it's emptyXObjNum+1
{
int sizePos = trailer.indexOf("/Size ");
if (sizePos != -1) {
int numStart = sizePos + 6;
int numEnd = numStart;
while (numEnd < trailer.size() && trailer[numEnd] >= '0' && trailer[numEnd] <= '9')
++numEnd;
if (numEnd > numStart) {
trailer.replace(numStart, numEnd - numStart,
QByteArray::number(emptyXObjNum + 1));
}
}
}
// Remove duplicate startxref if present in copied trailer
{
int stPos = trailer.indexOf("\nstartxref\n");
if (stPos != -1)
trailer = trailer.left(stPos);
// Ensure trailer ends with %%EOF
if (!trailer.endsWith("%%EOF\n"))
trailer += "\n%%EOF\n";
}
QByteArray result;
result.reserve(out.size() + xref.size() + trailer.size() + 64);
result += out;
int newXrefOffset = out.size();
result += xref;
result += trailer;
result += "\nstartxref\n";
result += QByteArray::number(newXrefOffset);
result += "\n%%EOF\n";
QFile outF(pdfPath);
if (!outF.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
outF.write(result);
outF.close();
}
} // namespace PdfLinks
+12
View File
@@ -74,6 +74,18 @@ namespace PdfLinks {
*/
void convertUriToGoTo(const QString &pdfPath);
struct ComponentInfo {
QString contents;
};
/**
Post-process a Qt-generated PDF file: convert component-info placeholder
link annotations (http://componentinfo.local/<N>) into invisible text
annotations with the actual component info as /Contents.
*/
void convertComponentInfoAnnotations(const QString &pdfPath,
const QList<ComponentInfo> &annotations);
}
#endif // PDF_LINKS_H
+84 -109
View File
@@ -20,16 +20,18 @@
#include "../diagram.h"
#include "../pdf_links.h"
#include "../qeticons.h"
#include "../qetinformation.h"
#include "../qetproject.h"
#include "../qetversion.h"
#include "../qetgraphicsitem/crossrefitem.h"
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/elementtextitemgroup.h"
#include "ui_projectprintwindow.h"
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4
// Requires QT += gui-private in qelectrotech.pro
// Private Qt PDF engine for drawHyperlink() is not a public API.
// Availability of Qt::GuiPrivate is verified at configure time in CMakeLists.txt.
#include <private/qpdf_p.h>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
@@ -63,14 +65,7 @@ void ProjectPrintWindow::launchDialog(QETProject *project, QPrinter::OutputForma
auto printer_ = new QPrinter();
QPrinter printer(QPrinter::HighResolution);
printer_->setDocName(ProjectPrintWindow::docName(project));
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
printer_->setOrientation(QPrinter::Landscape);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
# endif
printer_->setPageOrientation(QPageLayout::Landscape);
#endif
if (format == QPrinter::NativeFormat) //To physical printer
{
@@ -79,10 +74,14 @@ void ProjectPrintWindow::launchDialog(QETProject *project, QPrinter::OutputForma
print_dialog.setWindowFlags(Qt::Sheet);
#endif
print_dialog.setWindowTitle(tr("Options d'impression", "window title"));
// setOptions() is the modern spelling of the Qt4-era
// setEnabledOptions() (removed in Qt 6): replace the enabled
// option set with just PrintShowPageSize, on Qt 5 and 6 alike.
print_dialog.setOptions(QAbstractPrintDialog::PrintShowPageSize);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
print_dialog.setEnabledOptions(QAbstractPrintDialog::PrintShowPageSize);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
qDebug()<<"Help code for QT 6 or later";
#endif
if (print_dialog.exec() == QDialog::Rejected) {
delete printer_;
return;
@@ -108,7 +107,7 @@ QString ProjectPrintWindow::docName(QETProject *project)
{
QString doc_name;
if (!project->filePath().isEmpty()) {
doc_name = QFileInfo(project->filePath()).baseName();
doc_name = QFileInfo(project->filePath()).completeBaseName();
} else if (!project->title().isEmpty()) {
doc_name = project->title();
doc_name = QET::stringToFileName(doc_name);
@@ -156,6 +155,7 @@ ProjectPrintWindow::ProjectPrintWindow(QETProject *project, QPrinter *printer, Q
ui->m_button_box->addButton(pdf_button, QDialogButtonBox::ActionRole);
connect(pdf_button, &QPushButton::clicked, this, &ProjectPrintWindow::exportToPDF);
}
ui->m_component_info_cb->setVisible(m_printer->outputFormat() == QPrinter::PdfFormat);
auto exp = ExportProperties::defaultPrintProperties();
ui->m_draw_border_cb->setChecked(exp.draw_border);
@@ -200,7 +200,6 @@ ProjectPrintWindow::~ProjectPrintWindow()
*/
void ProjectPrintWindow::requestPaint()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
#ifdef Q_OS_WIN
#ifdef QT_DEBUG
qDebug() << "--";
@@ -225,7 +224,6 @@ void ProjectPrintWindow::requestPaint()
qDebug() << "--";
#endif
#endif
#endif
if (!m_project->diagrams().count()) {
return;
@@ -253,6 +251,7 @@ void ProjectPrintWindow::requestPaint()
bool first = true;
QPainter painter(m_printer);
int annotIndex = 0;
// A real PDF export uses the QPdfEngine; the on-screen preview uses a
// preview paint engine. We only post-process when actually writing a PDF.
@@ -266,7 +265,7 @@ void ProjectPrintWindow::requestPaint()
for (auto diagram : selectedDiagram())
{
first ? first = false : m_printer->newPage();
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap);
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap, annotIndex);
}
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
@@ -284,7 +283,7 @@ void ProjectPrintWindow::requestPaint()
* @param fit_page
* @param printer
*/
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap)
void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap, int &annotIndex)
{
////Prepare the print////
@@ -313,16 +312,10 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
diagram->render(painter, QRectF(), diagram_rect, Qt::KeepAspectRatio);
} else {
// Print on one or several pages
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
auto printed_rect = full_page ? printer->paperRect() : printer->pageRect();
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
qDebug()<<"Help code for QT 6 or later";
auto printed_rect = full_page ? printer->paperRect(QPrinter::Millimeter) :
printer->pageRect(QPrinter::Millimeter);
#endif
auto used_width = printed_rect.width();
auto used_height = printed_rect.height();
auto h_pages_count = horizontalPagesCount(diagram, option, full_page);
@@ -426,6 +419,53 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
PdfLinks::injectCrossRefLinks(
pdfEngine, diagram, geom, diagramPageMap,
printer->outputFileName());
////Collect component info for popup annotations////
if (ui->m_component_info_cb->isChecked()) {
for (auto *item : diagram->items()) {
auto *el = qgraphicsitem_cast<Element*>(item);
if (!el) continue;
// Skip reports and slaves
auto lt = el->linkType();
if (lt & Element::AllReport) continue;
if (lt == Element::Slave) continue;
auto info = el->elementInformations();
if (info.count() == 0) continue;
// Build info text
QStringList lines;
for (const auto &key : {QStringLiteral("label"),
QStringLiteral("manufacturer"),
QStringLiteral("designation"),
QStringLiteral("description")}) {
if (info.contains(key) && !info.value(key).toString().isEmpty())
lines << QETInformation::translatedInfoKey(key) + ": " + info.value(key).toString();
}
for (const QString &key : info.keys()) {
if (key == "formula") continue;
QString translated = QETInformation::translatedInfoKey(key);
if (lines.contains(translated + ": " + info.value(key).toString())) continue;
if (info.value(key).toString().isEmpty()) continue;
lines << translated + ": " + info.value(key).toString();
}
if (lines.isEmpty()) continue;
// Compute element rect in device pixels
QRectF elemScene = el->mapRectToScene(el->boundingRect());
QRectF devRect = fit.mapRect(elemScene);
PdfLinks::ComponentInfo ci;
ci.contents = lines.join("\n");
m_componentInfoList.append(ci);
// Create a link annotation as placeholder — post-processing
// will convert it to an invisible text annotation with the actual content
pdfEngine->drawHyperlink(devRect, QUrl(QString("http://componentinfo.local/%1").arg(annotIndex++)));
}
}
////Component info end////
}
}
////PDF links end////
@@ -450,8 +490,11 @@ QRect ProjectPrintWindow::diagramRect(Diagram *diagram, const ExportProperties &
{
auto diagram_rect = diagram->border_and_titleblock.borderAndTitleBlockRect();
if (!option.draw_titleblock) {
auto titleblock_height = diagram->border_and_titleblock.titleBlockRect().height();
diagram_rect.setHeight(diagram_rect.height() - titleblock_height);
auto titleblock_rect = diagram->border_and_titleblock.titleBlockRect();
if (diagram->border_and_titleblock.titleBlockEdge() == Qt::BottomEdge)
diagram_rect.setHeight(diagram_rect.height() - titleblock_rect.height());
else
diagram_rect.setWidth(diagram_rect.width() - titleblock_rect.width());
}
//Adjust the border of diagram to 1px (width of the line)
@@ -472,17 +515,12 @@ int ProjectPrintWindow::horizontalPagesCount(
Diagram *diagram, const ExportProperties &option, bool full_page) const
{
QRect printable_area;
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
printable_area = full_page ? m_printer->paperRect() : m_printer->pageRect();
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
# endif
printable_area =
full_page ?
m_printer->pageLayout().fullRectPixels(m_printer->resolution()) :
m_printer->pageLayout().paintRectPixels(m_printer->resolution());
#endif
QRect diagram_rect = diagramRect(diagram, option);
int h_pages_count = int(ceil(qreal(diagram_rect.width()) / qreal(printable_area.width())));
@@ -501,17 +539,12 @@ int ProjectPrintWindow::verticalPagesCount(
Diagram *diagram, const ExportProperties &option, bool full_page) const
{
QRect printable_area;
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
printable_area = full_page ? m_printer->paperRect() : m_printer->pageRect();
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
# endif
printable_area =
full_page ?
m_printer->pageLayout().fullRectPixels(m_printer->resolution()) :
m_printer->pageLayout().paintRectPixels(m_printer->resolution());
#endif
QRect diagram_rect = diagramRect(diagram, option);
int v_pages_count = int(ceil(qreal(diagram_rect.height()) / qreal(printable_area.height())));
@@ -584,43 +617,7 @@ void ProjectPrintWindow::loadPageSetupForCurrentPrinter()
}
settings.beginGroup(printer_section);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
if (settings.contains("orientation")) {
QString value = settings.value("orientation", "landscape").toString();
m_printer -> setOrientation(value == "landscape" ? QPrinter::Landscape : QPrinter::Portrait);
}
if (settings.contains("papersize")) {
int value = settings.value("papersize", QPrinter::A4).toInt();
if (value == QPrinter::Custom) {
bool w_ok, h_ok;
int w = settings.value("customwidthmm", -1).toInt(&w_ok);
int h = settings.value("customheightmm", -1).toInt(&h_ok);
if (w_ok && h_ok && w != -1 && h != -1) {
m_printer -> setPaperSize(QSizeF(w, h), QPrinter::Millimeter);
}
} else if (value < QPrinter::Custom) {
m_printer -> setPaperSize(static_cast<QPrinter::PaperSize>(value));
}
}
qreal margins[4];
m_printer -> getPageMargins(&margins[0], &margins[1], &margins[2], &margins[3], QPrinter::Millimeter);
QStringList margins_names(QStringList() << "left" << "top" << "right" << "bottom");
for (int i = 0 ; i < 4 ; ++ i) {
bool conv_ok;
qreal value = settings.value("margin" + margins_names.at(i), -1.0).toReal(&conv_ok);
if (conv_ok && value != -1.0) margins[i] = value;
}
m_printer->setPageMargins(
margins[0],
margins[1],
margins[2],
margins[3],
QPrinter::Millimeter);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
# endif
if (settings.contains("orientation"))
{
QString value = settings.value("orientation", "landscape").toString();
@@ -673,7 +670,7 @@ void ProjectPrintWindow::loadPageSetupForCurrentPrinter()
m_printer->setPageMargins(
QMarginsF(margins[0], margins[1], margins[2], margins[3]),
QPageLayout::Millimeter);
#endif
m_printer->setFullPage(
settings.value("fullpage", "false").toString() == "true");
@@ -690,35 +687,6 @@ void ProjectPrintWindow::savePageSetupForCurrentPrinter()
settings.beginGroup("printers");
settings.beginGroup(printer_section);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
settings.setValue("orientation", m_printer -> orientation() == QPrinter::Portrait ? "portrait" : "landscape");
settings.setValue("papersize", int(m_printer -> paperSize()));
if (m_printer -> paperSize() == QPrinter::Custom) {
QSizeF size = m_printer -> paperSize(QPrinter::Millimeter);
settings.setValue("customwidthmm", size.width());
settings.setValue("customheightmm", size.height());
} else {
settings.remove("customwidthmm");
settings.remove("customheightmm");
}
qreal left, top, right, bottom;
m_printer
->getPageMargins(&left, &top, &right, &bottom, QPrinter::Millimeter);
settings.setValue("marginleft", left);
settings.setValue("margintop", top);
settings.setValue("marginright", right);
settings.setValue("marginbottom", bottom);
settings.setValue("fullpage", m_printer->fullPage() ? "true" : "false");
settings.endGroup();
settings.endGroup();
settings.sync();
#else
# if TODO_LIST
# pragma message("@TODO remove code for QT 6 or later")
# endif
qDebug() << "Help code for QT 6 or later";
settings.setValue(
"orientation",
m_printer->pageLayout().orientation() == QPageLayout::Portrait ?
@@ -749,7 +717,6 @@ void ProjectPrintWindow::savePageSetupForCurrentPrinter()
settings.endGroup();
settings.endGroup();
settings.sync();
#endif
}
/**
@@ -799,6 +766,7 @@ void ProjectPrintWindow::on_m_draw_titleblock_cb_clicked() { m_preview->upd
void ProjectPrintWindow::on_m_keep_conductor_color_cb_clicked() { m_preview->updatePreview(); }
void ProjectPrintWindow::on_m_draw_terminal_cb_clicked() { m_preview->updatePreview(); }
void ProjectPrintWindow::on_m_draw_terminal_names_cb_clicked() { m_preview->updatePreview(); }
void ProjectPrintWindow::on_m_component_info_cb_clicked() { m_preview->updatePreview(); }
void ProjectPrintWindow::on_m_fit_in_page_cb_clicked() { m_preview->updatePreview(); }
void ProjectPrintWindow::on_m_use_full_page_cb_clicked()
{
@@ -903,9 +871,16 @@ void ProjectPrintWindow::print()
// which prevents the black screen observed on Apple Silicon under
// macOS Sequoia (QPrintPreviewWidget + CALayer timing issue).
QTimer::singleShot(0, this, [this, pdfFile]() {
// Convert component-info link annotations into invisible text annotations
if (ui->m_component_info_cb->isChecked() && !m_componentInfoList.isEmpty()) {
PdfLinks::convertComponentInfoAnnotations(pdfFile, m_componentInfoList);
m_componentInfoList.clear();
}
// Convert URI link annotations into native internal GoTo/FitR
// actions so cross-references jump inside the document.
PdfLinks::convertUriToGoTo(pdfFile);
this->close();
});
} else {
+4 -1
View File
@@ -19,6 +19,7 @@
#define PROJECTPRINTWINDOW_H
#include "../exportproperties.h"
#include "../pdf_links.h"
#include <QMainWindow>
#include <QMap>
@@ -56,6 +57,7 @@ class ProjectPrintWindow : public QMainWindow
void on_m_keep_conductor_color_cb_clicked();
void on_m_draw_terminal_cb_clicked();
void on_m_draw_terminal_names_cb_clicked();
void on_m_component_info_cb_clicked();
void on_m_fit_in_page_cb_clicked();
void on_m_use_full_page_cb_clicked();
void on_m_zoom_out_action_triggered();
@@ -81,7 +83,7 @@ class ProjectPrintWindow : public QMainWindow
private:
void requestPaint();
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap = {});
void printDiagram(Diagram *diagram, bool fit_page, QPainter *painter, QPrinter *printer, const QMap<Diagram*, int> &diagramPageMap, int &annotIndex);
QRect diagramRect(Diagram *diagram, const ExportProperties &option) const;
int horizontalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
int verticalPagesCount(Diagram *diagram, const ExportProperties &option, bool full_page) const;
@@ -102,6 +104,7 @@ class ProjectPrintWindow : public QMainWindow
QPrintPreviewWidget *m_preview=nullptr;
QColor m_backup_diagram_background_color;
QHash<Diagram *, QCheckBox *> m_diagram_list_hash;
QList<PdfLinks::ComponentInfo> m_componentInfoList;
};
#endif // PROJECTPRINTWINDOW_H
+21 -11
View File
@@ -183,17 +183,27 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_draw_terminal_names_cb">
<property name="text">
<string>Dessiner les noms des bornes</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
<item>
<widget class="QCheckBox" name="m_draw_terminal_names_cb">
<property name="text">
<string>Dessiner les noms des bornes</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_component_info_cb">
<property name="text">
<string>Inscrire les informations des composants</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
+18 -6
View File
@@ -210,10 +210,14 @@ void ProjectView::changeFirstTab()
}
/**
@return first folio of current project
@return first folio of current project, or nullptr if the project
has no diagram.
*/
DiagramView *ProjectView::firstDiagram()
{
if (m_diagram_ids.isEmpty()) {
return(nullptr);
}
return(m_diagram_ids.first());
}
@@ -345,11 +349,19 @@ QString ProjectView::askUserForFilePath(bool assign) {
// if no filepath is provided, return an empty string
if (filepath.isEmpty()) return(filepath);
// if the name does not end with the .qet extension and we're _not_ using xdg-desktop-portal, append it
bool usesPortal =
qEnvironmentVariableIsSet("FLATPAK_ID") ||
qEnvironmentVariableIsSet("SNAP_NAME");
if (!filepath.endsWith(".qet", Qt::CaseInsensitive) && !usesPortal) filepath += ".qet";
// Ensure the path ends with exactly one .qet extension, regardless of
// whether the active file dialog already appended one. Whether it does
// depends on which backend actually shows the dialog (Qt's own vs. the
// xdg-desktop-portal used by sandboxed Snap/Flatpak builds), and that
// isn't reliably predictable from environment variables alone -- an
// earlier attempt at that (only appending when *not* Snap/Flatpak,
// assuming the portal always appends it) left Snap saves with no
// extension at all whenever the portal didn't (bugtracker #270).
// Stripping any existing suffix first and re-appending it once is
// correct either way.
if (filepath.endsWith(".qet", Qt::CaseInsensitive))
filepath.chop(4);
filepath += ".qet";
if (assign) {
// assign the provided filepath to the currently edited project
+16 -16
View File
@@ -35,28 +35,28 @@ public:
: QTabWidget(parent)
{}
double temp_index = 0;
void wheelEvent(QWheelEvent *event) override
{
int index = currentIndex();
double delta = 0;
double scale_factor = 0.005; // Decrease or increase speed of mouse wheel (0.04 = decrease)
if (event->modifiers() & Qt::ControlModifier) {
if (index != -1) {
delta = event->delta() * scale_factor; // Read and scale the scroll value
if (delta > 0 && (temp_index > -1)) temp_index = temp_index - abs(delta);
if (delta < 0 && (temp_index < count())) temp_index = temp_index + abs(delta);
// Only intercept if Ctrl is held down
if (event->modifiers() & Qt::ControlModifier) {
int index = currentIndex();
if (index != -1) {
// angleDelta().y() is usually 120 per click.
// Dividing by 120 gives you exactly +1 (scroll up) or -1 (scroll down)
int steps = event->angleDelta().y() / 120;
index = int (temp_index);
qDebug() << "index" << index << "temp_index" << temp_index << " " << event->delta() << delta;
if (steps != 0) {
int newIndex = index - steps;
if (index >= 0 && index < count())
setCurrentIndex(index);
// qDebug() << currentIndex();
if (newIndex >= 0 && newIndex < count())
setCurrentIndex(newIndex);
}
}
event->accept(); // Mark event handled so parent widgets don't process it
} else {
// Allow the default QTabWidget wheel behavior to process if Ctrl isn't held
QTabWidget::wheelEvent(event);
}
}
};
#endif
+65
View File
@@ -0,0 +1,65 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "deviceinformation.h"
#include "../qetinformation.h"
namespace {
struct DeviceField
{
QString m_key;
QString DeviceInformation::*m_member;
};
const DeviceField fields[] = {
{QETInformation::ELMT_LABEL, &DeviceInformation::m_device_tag},
{QETInformation::ELMT_MANUFACTURER, &DeviceInformation::m_manufacturer},
{QETInformation::ELMT_MANUFACTURER_REF, &DeviceInformation::m_part_number},
{QETInformation::ELMT_MODEL, &DeviceInformation::m_model},
{QETInformation::ELMT_DESCRIPTION, &DeviceInformation::m_description},
{QETInformation::ELMT_CATEGORY, &DeviceInformation::m_category},
{QETInformation::ELMT_QUANTITY, &DeviceInformation::m_quantity},
{QETInformation::ELMT_VOLTAGE_RATING, &DeviceInformation::m_voltage_rating},
{QETInformation::ELMT_CURRENT_RATING, &DeviceInformation::m_current_rating},
{QETInformation::ELMT_NOTES, &DeviceInformation::m_notes}
};
}
DeviceInformation DeviceInformation::fromContext(const DiagramContext &context)
{
DeviceInformation info;
for (const auto &field : fields) {
info.*(field.m_member) = context.value(field.m_key).toString();
}
return info;
}
DiagramContext DeviceInformation::toContext(DiagramContext context) const
{
for (const auto &field : fields)
{
const auto &value = this->*(field.m_member);
if (value.isEmpty()) {
context.remove(field.m_key);
} else {
const bool show = !context.contains(field.m_key) || context.keyMustShow(field.m_key);
context.addValue(field.m_key, value, show);
}
}
return context;
}
+47
View File
@@ -0,0 +1,47 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DEVICEINFORMATION_H
#define DEVICEINFORMATION_H
#include "../diagramcontext.h"
#include <QString>
/**
* Device properties backed by ElementData::m_informations, not a second store.
* Missing properties remain empty. Quantity and ratings retain the existing
* free-text representation (including decimal quantities and units).
*/
struct DeviceInformation
{
QString m_device_tag;
QString m_manufacturer;
QString m_part_number;
QString m_model;
QString m_description;
QString m_category;
QString m_quantity;
QString m_voltage_rating;
QString m_current_rating;
QString m_notes;
static DeviceInformation fromContext(const DiagramContext &context);
DiagramContext toContext(DiagramContext context = DiagramContext()) const;
};
#endif // DEVICEINFORMATION_H
+13
View File
@@ -99,6 +99,19 @@ class ElementData : public PropertiesInterface
int terminalCount = 1; ///< Number of terminals for this IO (1-4)
QStringList terminals; ///< Terminal values T1, T2, ... (size = terminalCount)
/**
* @brief Return terminal labels, generating defaults (T1, T2...) if empty
*/
QStringList effectiveTerminals() const {
if (!terminals.isEmpty())
return terminals;
int count = qMax(terminalCount, 1);
QStringList defaults;
for (int i = 0; i < count && i < 4; ++i)
defaults << QStringLiteral("T%1").arg(i + 1);
return defaults;
}
bool operator==(const PlcIO &other) const {
return type == other.type
&& address == other.address
+8 -1
View File
@@ -36,6 +36,7 @@ XRefProperties::XRefProperties()
m_master_label = "%f-%l%c";
m_slave_label = "(%f-%l%c)";
m_offset = 0;
m_slave_offset = 0;
m_xref_pos = Qt::AlignBottom;
}
@@ -56,6 +57,8 @@ void XRefProperties::toSettings(QSettings &settings,
settings.setValue(prefix % "snapto", snap);
int offset = m_offset;
settings.setValue(prefix % "offset", offset);
int slave_offset = m_slave_offset;
settings.setValue(prefix % "slave_offset", slave_offset);
QString master_label = m_master_label;
settings.setValue(prefix % "master_label", master_label);
QString slave_label = m_slave_label;
@@ -86,11 +89,12 @@ 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();
QMetaEnum var = QMetaEnum::fromType<Qt::Alignment>();
m_xref_pos = Qt::AlignmentFlag(var.keyToValue((settings.value(prefix % "xrefpos").toString()).toStdString().data()));
m_xref_pos = Qt::AlignmentFlag(var.keyToValue((settings.value(prefix % "xrefpos", "AlignBottom").toString()).toStdString().data()));
for (QString key : m_prefix_keys) {
m_prefix.insert(key, settings.value(prefix + key % "prefix").toString());
@@ -123,6 +127,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
int offset = m_offset;
xml_element.setAttribute("offset", QString::number(offset));
xml_element.setAttribute("slave_offset", QString::number(m_slave_offset));
QString master_label = m_master_label;
xml_element.setAttribute("master_label", master_label);
QString slave_label = m_slave_label;
@@ -157,6 +162,7 @@ bool XRefProperties::fromXml(const QDomElement &xml_element) {
m_xref_pos = Qt::AlignBottom;
m_offset = xml_element.attribute("offset", "0").toInt();
m_slave_offset = xml_element.attribute("slave_offset", "0").toInt();
m_master_label = xml_element.attribute("master_label", "%f-%l%c");
m_slave_label = xml_element.attribute("slave_label","(%f-%l%c)");
foreach (QString key, m_prefix_keys) {
@@ -199,6 +205,7 @@ bool XRefProperties::operator ==(const XRefProperties &xrp) const{
&& m_prefix == xrp.m_prefix
&& m_master_label== xrp.m_master_label
&& m_offset == xrp.m_offset
&& m_slave_offset== xrp.m_slave_offset
&& m_xref_pos == xrp.m_xref_pos
&& m_slave_label == xrp.m_slave_label);
}
+4
View File
@@ -80,6 +80,9 @@ class XRefProperties : public PropertiesInterface
void setOffset(const int offset) {m_offset = offset;}
int offset() const {return m_offset;}
void setSlaveOffset(const int offset) {m_slave_offset = offset;}
int slaveOffset() const {return m_slave_offset;}
void setKey(QString& key) {m_key = key;}
private:
@@ -93,6 +96,7 @@ class XRefProperties : public PropertiesInterface
QString m_master_label;
QString m_slave_label;
int m_offset;
int m_slave_offset;
QString m_key;
};
+17 -25
View File
@@ -184,16 +184,8 @@ bool QET::orthogonalProjection(
// determine le point d'intersection des deux droites = le projete orthogonal
QPointF intersection_point;
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
QLineF::IntersectType it = line.
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
intersect // ### Qt 6: remove
#else
intersects
#endif
(perpendicular_line, &intersection_point);
QLineF::IntersectType it = line.intersects(perpendicular_line, &intersection_point);
// ne devrait pas arriver (mais bon...)
if (it == QLineF::NoIntersection) return(false);
@@ -305,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) {
@@ -546,16 +546,8 @@ QString QET::joinWithSpaces(const QStringList &string_list) {
QStringList QET::splitWithSpaces(const QString &string) {
// les chaines sont separees par des espaces non echappes
// = avec un nombre nul ou pair de backslashes devant
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QString
#else
Qt
#endif
::SkipEmptyParts);
QStringList escaped_strings = string.split(QRegularExpression("[^\\]?(?:\\\\)* "),Qt::SkipEmptyParts);
QStringList returned_list;
foreach(QString escaped_string, escaped_strings) {
+36 -10
View File
@@ -50,7 +50,7 @@
#include <QFontDatabase>
#include <QProcessEnvironment>
#include <QRegularExpression>
#ifdef BUILD_WITHOUT_KF5
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
#else
# include <KAutoSaveFile>
@@ -124,11 +124,8 @@ QETApp::QETApp() :
initSplashScreen();
initSystemTray();
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // TODO Qt6 only: remove, mappedObject() always available
connect(&signal_map, SIGNAL(mapped(QWidget *)), this, SLOT(invertMainWindowVisibility(QWidget *)));
#else
connect(&signal_map, &QSignalMapper::mappedObject, this, [this](QObject *object) { invertMainWindowVisibility(qobject_cast<QWidget *>(object)); });
#endif
qApp->setQuitOnLastWindowClosed(false);
connect(qApp, &QApplication::lastWindowClosed,
this, &QETApp::checkRemainingWindows);
@@ -200,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.
@@ -237,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__ << ")";
@@ -270,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
@@ -1426,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
@@ -1978,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
}
/**
@@ -1989,6 +2014,7 @@ void QETApp::openTitleBlockTemplate(const QString &filepath) {
QETTitleBlockTemplateEditor *qet_template_editor = new QETTitleBlockTemplateEditor();
qet_template_editor -> edit(filepath);
qet_template_editor -> show();
qet_template_editor -> readSettingsState(); // must run after show() in Qt6
}
/**
+2
View File
@@ -102,6 +102,8 @@ class QETApp : public QObject
static QString documentDir();
static QString pictureDir();
static QString languagesPath();
static QString loadedQetTranslationFile();
static QString loadedQtTranslationFile();
static QString realPath(const QString &);
static QString symbolicPath(const QString &);
static QStringList handledFileExtensions();
+125 -20
View File
@@ -23,7 +23,11 @@
#include "conductornumexport.h"
#include "diagramcommands.h"
#include "diagramevent/diagrameventaddimage.h"
#ifdef QET_HAS_QTPDF
#include "diagramevent/diagrameventaddpdf.h"
#endif
#include "diagramevent/diagrameventaddshape.h"
#include "diagramevent/diagrameventaddpath.h"
#include "diagramevent/diagrameventaddtext.h"
#include "diagramview.h"
#include "elementspanelwidget.h"
@@ -53,11 +57,13 @@
#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>
#ifdef BUILD_WITHOUT_KF5
#include <QTimer>
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
#else
# include <KAutoSaveFile>
@@ -107,11 +113,7 @@ QETDiagramEditor::QETDiagramEditor(const QStringList &files, QWidget *parent) :
m_workspace.setTabsClosable(true);
//Set the signal mapper
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // TODO Qt6 only: remove, mappedObject() always available
connect(&windowMapper, SIGNAL(mapped(QWidget *)), this, SLOT(activateWidget(QWidget *)));
#else
connect(&windowMapper, &QSignalMapper::mappedObject, this, [this](QObject *object) { activateWidget(qobject_cast<QWidget *>(object)); });
#endif
setWindowTitle(tr("QElectroTech", "window title"));
setWindowIcon(QET::Icons::QETLogo);
@@ -136,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;
@@ -517,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);
@@ -718,28 +732,53 @@ 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
QAction *add_pdf = m_add_item_actions_group.addAction(QET::Icons::adding_pdf, tr("Ajouter un PDF"));
#endif
QAction *add_line = m_add_item_actions_group.addAction(QET::Icons::PartLine, tr("Ajouter une ligne", "Draw line"));
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"));
add_image ->setStatusTip(tr("Ajoute une image sur le folio actuel"));
#ifdef QET_HAS_QTPDF
add_pdf ->setStatusTip(tr("Ajoute une page PDF sur le folio actuel"));
#endif
add_line ->setStatusTip(tr("Ajoute une ligne sur le folio actuel"));
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"));
add_image ->setData(QStringLiteral("image"));
#ifdef QET_HAS_QTPDF
add_pdf ->setData(QStringLiteral("pdf"));
#endif
add_line ->setData(QStringLiteral("line"));
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);
@@ -747,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);
@@ -770,7 +810,7 @@ void QETDiagramEditor::setUpActions()
});
m_jump_to_element = new QAction(tr("Atteindre un élément"), this);
m_jump_to_element->setShortcut(Qt::CTRL | Qt::Key_G);
ShortcutManager::instance().registerAction(m_jump_to_element, "diagrameditor.jump_to_element", tr("Éditeur de schémas"), Qt::CTRL | Qt::Key_G);
m_jump_to_element->setStatusTip(tr("Recherche et sélectionne rapidement un élément du folio", "status bar tip"));
connect(m_jump_to_element, &QAction::triggered, [this]()
{
@@ -912,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();
@@ -1546,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")
@@ -1555,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")
{
@@ -1573,6 +1633,19 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
else
diagram_event = deai;
}
#ifdef QET_HAS_QTPDF
else if (value == "pdf")
{
DiagramEventAddPdf *deap = new DiagramEventAddPdf(d);
if (deap->isNull())
{
delete deap;
return;
}
else
diagram_event = deap;
}
#endif
else if (value == "text")
{
diagram_event = new DiagramEventAddText(d);
@@ -1590,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();
}
}
@@ -1713,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);
@@ -2101,6 +2191,7 @@ void QETDiagramEditor::openBackupFiles(QList<KAutoSaveFile *> backup_files)
}
delete project;
DialogWaiting::dropInstance();
continue;
}
addProject(project);
DialogWaiting::dropInstance();
@@ -2206,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) {
@@ -2219,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
@@ -2799,11 +2904,11 @@ void QETDiagramEditor::generateTerminalBlock()
* Opens the dialog for automatic terminal numbering and applies the generated undo command.
*/
void QETDiagramEditor::slot_terminalNumbering() {
TerminalNumberingDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
QETProject *project = currentProject();
if (!project) return;
QETProject *project = currentProject();
if (!project) return;
TerminalNumberingDialog dialog(this, project);
if (dialog.exec() == QDialog::Accepted) {
// Fetch the generated undo command from the dialog logic
QUndoCommand *macro = dialog.getUndoCommand(project);

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