Merge remote-tracking branch 'upstream/master' into review-635-fix

# Conflicts:
#	CMakeLists.txt
#	cmake/developer_options.cmake
#	cmake/qet_compilation_vars.cmake
#	sources/qetapp.cpp
This commit is contained in:
ispyisail
2026-09-22 23:10:34 +12:00
676 changed files with 150173 additions and 61935 deletions
@@ -0,0 +1,54 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementpreviewdelegate.h"
#include <QWidget>
#include "../qetpalette.h"
/**
@brief ElementPreviewDelegate::initStyleOption
After the base class has filled the option from the model, replace
the icon with one that reads on a dark palette. The adapted icon is
built from the pixmap the view is about to draw, at the view's
decoration size and device pixel ratio.
*/
void ElementPreviewDelegate::initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const
{
QStyledItemDelegate::initStyleOption(option, index);
if (option->icon.isNull() || !QET::Palette::isDark(option->palette))
return;
const qint64 key = option->icon.cacheKey();
const auto it = m_dark_icons.constFind(key);
if (it != m_dark_icons.constEnd())
{
option->icon = *it;
return;
}
const qreal dpr = option->widget ? option->widget->devicePixelRatio() : 1.0;
const QPixmap source = option->icon.pixmap(option->decorationSize, dpr);
const QPixmap adapted = QET::Palette::forPalette(source, option->palette);
QIcon icon = option->icon;
if (adapted.cacheKey() != source.cacheKey())
icon = QIcon(adapted);
m_dark_icons.insert(key, icon);
option->icon = icon;
}
@@ -0,0 +1,50 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ELEMENTPREVIEWDELEGATE_H
#define ELEMENTPREVIEWDELEGATE_H
#include <QHash>
#include <QIcon>
#include <QStyledItemDelegate>
/**
@brief The ElementPreviewDelegate class
Draws the items of an element collection tree with icons that read on
the current palette. Element previews are black line art drawn for a
white sheet; on a dark palette this delegate hands the view the same
picture with its lightness inverted (QET::Palette::forPalette), so the
ink is light on the dark row. Colored icons, such as folders, are left
alone, and nothing changes on a light palette. Adapted icons are kept
per source icon, so a repaint costs a hash lookup.
*/
class ElementPreviewDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
protected:
void initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const override;
private:
mutable QHash<qint64, QIcon> m_dark_icons;
};
#endif
@@ -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;
@@ -725,7 +725,10 @@ void ElementsCollectionWidget::showThisDir()
ElementCollectionItem *eci =
elementCollectionItemForIndex(m_showed_index);
if (eci)
{
eci->setBackground(QBrush());
eci->setForeground(QBrush());
}
}
m_showed_index = m_index_at_context_menu;
@@ -736,7 +739,11 @@ void ElementsCollectionWidget::showThisDir()
ElementCollectionItem *eci =
elementCollectionItemForIndex(m_showed_index);
if (eci)
{
// Amber under black, whatever the palette's text color.
eci->setBackground(QBrush(QColor(255, 204, 0, 255)));
eci->setForeground(QBrush(Qt::black));
}
search();
}
else
@@ -755,7 +762,10 @@ void ElementsCollectionWidget::resetShowThisDir()
ElementCollectionItem *eci = elementCollectionItemForIndex(
m_showed_index);
if (eci)
{
eci->setBackground(QBrush());
eci->setForeground(QBrush());
}
}
m_showed_index = QModelIndex();
@@ -949,14 +959,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()
@@ -778,30 +778,6 @@ bool ElementsLocation::setXml(const QDomDocument &xml_document) const
//Element doesn't exist, we create the element
else
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
QString path_ = collectionPath(false);
QRegExp rx ("^(.*)/(.*\\.elmt)$");
if (rx.exactMatch(path_)) {
return project()
->embeddedElementCollection()
->addElementDefinition(
rx.cap(1),
rx.cap(2),
xml_document
.documentElement());
}
else {
qDebug() << "ElementsLocation::setXml :"
" rx don't match";
}
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
# pragma message("@TODO ad Core5Compat to Cmake")
#endif
qDebug() << "Help code for QT 6 or later";
QString path_ = collectionPath(false);
QRegularExpression rx("^(.*)/(.*\\.elmt)$");
QRegularExpressionMatch match = rx.match(path_);
@@ -820,7 +796,6 @@ bool ElementsLocation::setXml(const QDomDocument &xml_document) const
qDebug() << "ElementsLocation::setXml :"
" rx don't match";
}
#endif
}
}
@@ -20,7 +20,7 @@
#include "../NameList/nameslist.h"
#include "../diagramcontext.h"
#include "pugixml/src/pugixml.hpp"
#include "pugixml.hpp"
#include <QIcon>
#include <QString>
@@ -16,6 +16,8 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementstreeview.h"
#include "elementpreviewdelegate.h"
#include "../qetpalette.h"
#include "../factory/elementfactory.h"
#include "../qetgraphicsitem/element.h"
@@ -41,24 +43,11 @@ static int MAX_DND_PIXMAP_HEIGHT = 375;
ElementsTreeView::ElementsTreeView(QWidget *parent) :
QTreeView(parent)
{
// force du noir sur une alternance de blanc (comme le schema) et de gris
// clair, avec du blanc sur bleu pas trop fonce pour la selection
//
// Element icons are rendered with colors read directly from each .elmt
// file (almost always black linework, matching printed-schematic
// convention) onto a transparent background -- so this view must keep
// a light background regardless of the OS/desktop theme, or the icons
// become invisible on dark themes. QAbstractItemView paints its rows
// using the viewport's palette, not the view's own, so the palette
// must be applied to both to actually take effect under every style.
QPalette qp = palette();
qp.setColor(QPalette::Text, Qt::black);
qp.setColor(QPalette::Base, Qt::white);
qp.setColor(QPalette::AlternateBase, QColor("#e8e8e8"));
qp.setColor(QPalette::Highlight, QColor("#678db2"));
qp.setColor(QPalette::HighlightedText, Qt::black);
setPalette(qp);
viewport()->setPalette(qp);
// Rows follow the application palette. Element previews are black
// line art drawn for a white sheet; ElementPreviewDelegate adapts them
// to a dark palette, so this view no longer has to force a light one
// (bugtracker #335).
setItemDelegate(new ElementPreviewDelegate(this));
}
/**
@@ -143,11 +132,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
if (!dir_path.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
#else
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
#endif
QString current_path = "";
for (const QString &part : parts) {
QString parent_path = current_path;
@@ -224,7 +209,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
&elmt_creation_state));
if (elmt_creation_state) { return; }
QPixmap elmt_pixmap(temp_elmt->pixmap());
QPixmap elmt_pixmap(QET::Palette::forPalette(temp_elmt->pixmap(), palette()));
QPoint elmt_hotspot(temp_elmt->hotspot());
//Adjust the size of the pixmap if he is too big
@@ -22,7 +22,39 @@
#include "../qeticons.h"
#include "elementslocation.h"
#include <QApplication>
#include <QDir>
#include <QPainter>
#include <QPixmap>
#include <QStyle>
namespace {
/**
@return the folder icon overlaid with a small warning badge in the
bottom-right corner. Used for a directory whose qet_directory could
not be read (@see FileElementCollectionItem::m_qet_directory_unreadable),
so the problem is visible in the tree itself and not only on hover
via the tooltip. Built once: same folder icon, same badge, every time.
*/
const QIcon &unreadableFolderIcon()
{
static const QIcon icon = []() {
QPixmap pixmap = QET::Icons::Folder.pixmap(16, 16);
const QPixmap badge = QApplication::style()
->standardIcon(QStyle::SP_MessageBoxWarning)
.pixmap(9, 9);
QPainter painter(&pixmap);
painter.drawPixmap(pixmap.width() - badge.width(),
pixmap.height() - badge.height(),
badge);
painter.end();
return QIcon(pixmap);
}();
return icon;
}
}
/**
@brief FileElementCollectionItem::FileElementCollectionItem
@@ -136,18 +168,41 @@ QString FileElementCollectionItem::localName()
}
else
{
// Fall back to the raw directory name (m_path) whenever the
// translated name can't be obtained -- qet_directory missing,
// unreadable (e.g. a Windows path-encoding issue with special
// characters, see bugtracker #332), malformed, or present but
// without a usable name entry -- rather than leaving the item
// blank.
QString display_name;
bool readable = false;
QString str(fileSystemPath() % "/qet_directory");
pugi::xml_document docu;
if(docu.load_file(str.toStdWString().c_str()))
if (docu.load_file(str.toStdWString().c_str()))
{
if (QString(docu.document_element().name())
== "qet-directory")
{
readable = true;
NamesList nl;
nl.fromXml(docu.document_element());
setText(nl.name());
// Deliberately no fallback argument: a non-empty one
// is returned *before* NamesList::name() reaches its
// "first available translation" step, so passing
// m_path here would replace a perfectly good name in
// some other language with the raw directory name.
// The fallback belongs after the chain, not inside it.
display_name = nl.name();
}
}
setText(display_name.isEmpty() ? m_path : display_name);
// Only a file-level failure counts: a readable qet-directory
// with no entry for the current language is not an error,
// NamesList::name() resolves that on its own. Recorded here
// and reported by setUpData(), which sets the tooltip after
// this runs.
m_qet_directory_unreadable = !readable;
}
}
else if (isElement()) {
@@ -350,7 +405,21 @@ void FileElementCollectionItem::setUpData()
}
}
setToolTip(collectionPath());
// Falling back to the raw directory name keeps the folder usable, but
// on its own it hides the fact that a file is broken: the user sees a
// plausible name and never learns there is anything to repair. Say so
// above the collection path, which stays as the last line the way the
// element tooltip above builds it.
QStringList tip;
if (isDir() && m_qet_directory_unreadable)
{
tip << QObject::tr("Le fichier « %1 » est absent ou illisible : "
"le nom traduit de ce dossier n'a pas pu être lu, "
"son nom de dossier est affiché à la place.")
.arg(fileSystemPath() % "/qet_directory");
}
tip << collectionPath();
setToolTip(tip.join(QLatin1Char('\n')));
}
/**
@@ -361,26 +430,45 @@ void FileElementCollectionItem::setUpData()
*/
void FileElementCollectionItem::setUpIcon()
{
if (!icon().isNull())
// Must return unconditionally once setUpIcon has run: setIcon() calls
// setData(), which emits dataChanged() regardless of whether the new
// icon differs from the old one (QIcon has no meaningful equality).
// QTreeView responds to dataChanged() by recomputing the row's size
// hint, which re-enters data() for this same index -- so without this
// guard, any repeated setIcon() here recurses until the stack
// overflows. Confirmed by crash report on PR #633.
//
// We use a dedicated bool instead of icon().isNull() because
// some items intentionally keep a null icon (e.g. .qetmak),
// which would bypass a null-based guard and still recurse.
//
// This item's m_qet_directory_unreadable is already final by the time
// this can run at all: ElementsCollectionModel only attaches itself
// to the tree view (making data() reachable) from loadingFinished(),
// which fires after the QtConcurrent::map over every item -- this one
// included -- has completed. So there is no race to work around here.
if (m_icon_initialized)
return;
m_icon_initialized = true;
if (isCollectionRoot()) {
QString macrosPath = QETApp::userMacrosDir();
if (macrosPath.endsWith("/")) macrosPath.remove(macrosPath.length() - 1, 1);
if (m_path == QETApp::commonElementsDirN())
setIcon(QIcon(":/ico/16x16/qet.png"));
setIcon(QIcon::fromTheme("qet"));
else if (m_path == QETApp::companyElementsDirN())
setIcon(QIcon(":/ico/16x16/go-company.png"));
setIcon(QIcon::fromTheme("go-company"));
else if (m_path == macrosPath)
setIcon(QIcon(":/ico/16x16/go-home.png"));
setIcon(QIcon::fromTheme("go-home"));
else
setIcon(QIcon(":/ico/16x16/go-home.png"));
setIcon(QIcon::fromTheme("go-home"));
}
else
{
if (isDir()) {
setIcon(QET::Icons::Folder);
setIcon(m_qet_directory_unreadable ? unreadableFolderIcon()
: QET::Icons::Folder);
} else {
if (m_path.endsWith(".qetmak")) {
setIcon(QIcon());
@@ -64,6 +64,12 @@ class FileElementCollectionItem : public ElementCollectionItem
private:
QString m_path;
/// True when this directory's qet_directory file is missing or
/// unreadable, so setUpData() can say so in the tooltip. Recorded
/// rather than acted on in localName(), because setUpData() resets
/// the tooltip afterwards and would otherwise discard it.
bool m_qet_directory_unreadable = false;
bool m_icon_initialized = false;
};
#endif // FILEELEMENTCOLLECTIONITEM2_H
+1 -1
View File
@@ -17,7 +17,7 @@
*/
#ifndef NAMES_LIST_H
#define NAMES_LIST_H
#include "pugixml/src/pugixml.hpp"
#include "pugixml.hpp"
#include <QtXml>
/**
+2 -2
View File
@@ -36,7 +36,7 @@
<string>Ajouter une ligne</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
</property>
</widget>
@@ -48,7 +48,7 @@
<string>Copier dans le presse papier</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="edit-paste" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-paste.png</normaloff>:/ico/16x16/edit-paste.png</iconset>
</property>
</item>
@@ -46,6 +46,18 @@ class PropertiesEditorDialog : public QDialog
PropertiesEditorDialog(T editor, QWidget *parent = nullptr) :
QDialog (parent)
{
// Without this, a QWidget-modal QDialog with a parent falls
// back to macOS's automatic sheet presentation, which some
// Qt/Cocoa versions render stuck centered on screen and
// non-draggable rather than as a proper attached sheet
// (bugtracker #275). Other dialogs in this codebase already
// opt in explicitly (see ElementDialog, DiagramPropertiesDialog)
// -- this one was missing it.
setWindowModality(Qt::WindowModal);
#ifdef Q_OS_MACOS
setWindowFlags(Qt::Sheet);
#endif
//Set dialog title
setWindowTitle(editor->title());
// Reparent the editor,
@@ -72,6 +72,7 @@ QPropertyUndoCommand::QPropertyUndoCommand(const QPropertyUndoCommand *other)
m_new_value = other->m_new_value;
m_animate = other->m_animate;
m_first_time = other->m_first_time;
m_undo_first_time = other->m_undo_first_time;
setText(other->text());
}
@@ -99,11 +100,20 @@ void QPropertyUndoCommand::enableAnimation (bool animate) {
@param first_time = if true,
the first animation is done at the first call of redo if false,
the first animation is done at the second call of redo.
The same rule applies to undo, tracked independently: redo() always
runs before the first undo() (QUndoStack::push() calls redo()
immediately), so by the time undo() can run at all, redo()'s own
m_first_time has already flipped true. Sharing that flag would leave
undo() with no instant path ever reachable in practice -- reusing it
is not actually symmetric, it just looks like it is. m_undo_first_time
gives undo() the same one-time grace period redo() has, on its own
first call instead of redo's.
*/
void QPropertyUndoCommand::setAnimated(bool animate, bool first_time)
{
m_animate = animate;
m_first_time = first_time;
m_undo_first_time = first_time;
}
/**
@@ -118,6 +128,18 @@ bool QPropertyUndoCommand::mergeWith(const QUndoCommand *other)
QPropertyUndoCommand const *undo = static_cast<const QPropertyUndoCommand *>(other);
if (m_object != undo->m_object
|| m_property_name != undo->m_property_name) return false;
// Same object and property name alone isn't enough: two entirely
// separate, deliberate actions (say, cropping an image and then
// mirroring it) both go through the same "pixmap" property and
// would otherwise silently coalesce into one undo entry, carrying
// only the first action's label -- the second vanishes from the
// undo list with no way to undo just it. Legitimate merging (a
// slider or spinbox pushing one command per tick while being
// dragged, e.g. ArcEditor's angle editors) always reuses the exact
// same text() across the whole sequence, so requiring a match here
// keeps that working unchanged while refusing to merge anything
// that isn't actually a continuation of the same action.
if (text() != other->text()) return false;
m_new_value = undo->m_new_value;
return true;
}
@@ -155,7 +177,7 @@ void QPropertyUndoCommand::undo()
{
if (m_object->property(m_property_name) != m_old_value)
{
if (m_animate)
if (m_animate && m_undo_first_time)
{
QPropertyAnimation *animation = new QPropertyAnimation(m_object, m_property_name);
animation->setStartValue(m_new_value);
@@ -163,7 +185,10 @@ void QPropertyUndoCommand::undo()
animation->start(QAbstractAnimation::DeleteWhenStopped);
}
else
{
m_object->setProperty(m_property_name, m_old_value);
m_undo_first_time = true;
}
}
QUndoCommand::undo();
@@ -58,7 +58,8 @@ class QPropertyUndoCommand : public QUndoCommand
const char *m_property_name;
QVariant m_old_value, m_new_value;
bool m_animate = false,
m_first_time = true;
m_first_time = true,
m_undo_first_time = true;
};
#endif // QPROPERTYUNDOCOMMAND_H
@@ -370,7 +370,7 @@
<string>phase</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="phase" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/phase.png</normaloff>:/ico/16x16/phase.png</iconset>
</property>
</widget>
@@ -397,7 +397,7 @@
<string>neutre</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="neutral" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/neutral.png</normaloff>:/ico/16x16/neutral.png</iconset>
</property>
</widget>
@@ -433,7 +433,7 @@
<string>terre</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="ground" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/ground.png</normaloff>:/ico/16x16/ground.png</iconset>
</property>
</widget>
@@ -180,7 +180,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="start" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/start.png</normaloff>:/ico/22x22/start.png</iconset>
</property>
</widget>
@@ -1020,6 +1020,7 @@ void SearchAndReplaceWidget::on_m_tree_widget_currentItemChanged(
{
m_highlighted_element = elmt;
elmt.data()->setHighlighted(true);
elmt.data()->ensureVisible();
}
}
else if (m_text_hash.contains(current))
@@ -1029,6 +1030,7 @@ void SearchAndReplaceWidget::on_m_tree_widget_currentItemChanged(
{
text.data()->setSelected(true);
m_last_selected = text;
text.data()->ensureVisible();
}
}
else if (m_conductor_hash.contains(current))
@@ -1038,6 +1040,7 @@ void SearchAndReplaceWidget::on_m_tree_widget_currentItemChanged(
{
cond.data()->setSelected(true);
m_last_selected = cond;
cond.data()->ensureVisible();
}
}
@@ -106,7 +106,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-bottom" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-bottom.png</normaloff>:/ico/16x16/go-bottom.png</iconset>
</property>
<property name="flat">
@@ -123,7 +123,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-top" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-top.png</normaloff>:/ico/16x16/go-top.png</iconset>
</property>
<property name="flat">
@@ -140,7 +140,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
<property name="flat">
@@ -157,7 +157,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="configure-toolbars" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/configure-toolbars.png</normaloff>:/ico/16x16/configure-toolbars.png</iconset>
</property>
<property name="checkable">
@@ -19,6 +19,7 @@
#define TERMINALSTRIPDRAWER_H
#include <QPointer>
#include <QUuid>
#include "properties/terminalstriplayoutpattern.h"
@@ -35,13 +35,17 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QSharedPointer<RealTerminal
const auto t_label = terminal->label();
const auto ts_name = strip->name();
const auto str_1 = t_label.isEmpty() ? QObject::tr("Ajouter une borne") :
QObject::tr("Ajouter la borne %1").arg(t_label);
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
QObject::tr("au groupe de bornes %1").arg(ts_name);
setText(str_1 % " " % str_2);
QString text;
if (ts_name.isEmpty()) {
text = t_label.isEmpty()
? QObject::tr("Ajouter une borne à un groupe de bornes")
: QObject::tr("Ajouter la borne %1 à un groupe de bornes").arg(t_label);
} else {
text = t_label.isEmpty()
? QObject::tr("Ajouter une borne au groupe de bornes %1").arg(ts_name)
: QObject::tr("Ajouter la borne %1 au groupe de bornes %2").arg(t_label, ts_name);
}
setText(text);
}
AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<RealTerminal>> terminals, TerminalStrip *strip, QUndoCommand *parent) :
@@ -50,14 +54,11 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<Real
m_new_strip{strip}
{
const auto ts_name = strip->name();
const auto count = m_terminal.size();
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Ajouter %1 bornes").arg(m_terminal.size()) :
QObject::tr("Ajouter une borne");
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
QObject::tr("au groupe de bornes %1").arg(ts_name);
setText(str_1 % " " % str_2);
setText(ts_name.isEmpty()
? QObject::tr("Ajouter %n borne(s) à un groupe de bornes", "", count)
: QObject::tr("Ajouter %n borne(s) au groupe de bornes %1", "", count).arg(ts_name));
}
@@ -137,13 +138,11 @@ void RemoveTerminalFromStripCommand::redo()
void RemoveTerminalFromStripCommand::setCommandTitle()
{
const auto strip_name = m_strip->name();
const auto count = m_terminals.size();
const auto str_1 = m_terminals.size()>1 ? QObject::tr("Enlever %1 bornes").arg(m_terminals.size()):
QObject::tr("Enlever une borne");
const auto str_2 = strip_name.isEmpty() ? QObject::tr("d'un groupe de bornes") :
QObject::tr("du groupe de bornes %1").arg(strip_name);
setText(str_1 % " " % str_2);
setText(strip_name.isEmpty()
? QObject::tr("Enlever %n borne(s) d'un groupe de bornes", "", count)
: QObject::tr("Enlever %n borne(s) du groupe de bornes %1", "", count).arg(strip_name));
}
/**
@@ -166,19 +165,30 @@ MoveTerminalCommand::MoveTerminalCommand(QSharedPointer<PhysicalTerminal> termin
t_label.append(", ");
t_label.append(real_t->label());
}
const auto strip_name = old_strip->name();
const auto new_strip_name = new_strip->name();
auto strip_name = old_strip->name();
auto new_strip_name = new_strip->name();
auto str_1 = t_label.isEmpty() ? QObject::tr("Déplacer une borne") :
QObject::tr("Déplacer la borne %1").arg(t_label);
auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
QObject::tr(" du groupe de bornes %1").arg(strip_name);
auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
setText(str_1 % " " % str_2 % " " % str_3);
QString text;
if (t_label.isEmpty()) {
if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers un autre groupe de bornes");
else if (strip_name.isEmpty())
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers le groupe de bornes %1").arg(new_strip_name);
else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers un autre groupe de bornes").arg(strip_name);
else
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers le groupe de bornes %2").arg(strip_name, new_strip_name);
} else {
if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers un autre groupe de bornes").arg(t_label);
else if (strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers le groupe de bornes %2").arg(t_label, new_strip_name);
else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers un autre groupe de bornes").arg(t_label, strip_name);
else
text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers le groupe de bornes %3").arg(t_label, strip_name, new_strip_name);
}
setText(text);
}
MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal>> terminals, TerminalStrip *old_strip,
@@ -191,17 +201,18 @@ MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal
{
const auto strip_name = old_strip->name();
const auto new_strip_name = new_strip->name();
const auto count = m_terminal.size();
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Déplacer des bornes") :
QObject::tr("Déplacer une borne");
const auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") :
QObject::tr(" du groupe de bornes %1").arg(strip_name);
const auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") :
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name);
setText(str_1 % " " % str_2 % " " % str_3);
QString text;
if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers un autre groupe de bornes", "", count);
else if (strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers le groupe de bornes %1", "", count).arg(new_strip_name);
else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers un autre groupe de bornes", "", count).arg(strip_name);
else
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers le groupe de bornes %2", "", count).arg(strip_name, new_strip_name);
setText(text);
}
void MoveTerminalCommand::undo()
@@ -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")),
@@ -84,7 +84,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="dialog-ok" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/dialog-ok.png</normaloff>:/ico/22x22/dialog-ok.png</iconset>
</property>
</widget>
@@ -63,7 +63,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="dialog-ok" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/dialog-ok.png</normaloff>:/ico/16x16/dialog-ok.png</iconset>
</property>
</widget>
@@ -54,7 +54,7 @@
</widget>
<action name="m_add_terminal_strip">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
</property>
<property name="text">
@@ -66,7 +66,7 @@
</action>
<action name="m_remove_terminal">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
</property>
<property name="text">
@@ -78,7 +78,7 @@
</action>
<action name="m_reload">
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
<property name="text">
@@ -29,6 +29,9 @@ TerminalStripLayoutEditor::TerminalStripLayoutEditor(QSharedPointer<TerminalStri
{
ui->setupUi(this);
ui->m_graphics_view->setScene(new QGraphicsScene{ this });
// The preview is printed output: black ink on a white page, whatever
// the application palette.
ui->m_graphics_view->setBackgroundBrush(Qt::white);
ui->m_graphics_view->scene()->addItem(&m_preview_strip_item);
updateUi();
}
@@ -29,15 +29,9 @@
#include "modelTerminalData.h"
//Code to use QColor as key for QHash
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
inline size_t qHash(const QColor &key, size_t seed = 0) {
return qHash(key.rgba(), seed);
}
#else
inline uint qHash(const QColor &key, uint seed) {
return qHash(key.rgba(), seed);
}
#endif
//needed to use QPointer<Element> as key of QHash
inline uint qHash(const QPointer<Element> &key, uint seed) {
@@ -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;
+185 -106
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>
@@ -39,6 +41,7 @@ namespace autonum
sequentialNumbers::sequentialNumbers(const sequentialNumbers &other)
{
unit = other.unit;
wrap = other.wrap;
unit_folio = other.unit_folio;
ten = other.ten;
ten_folio = other.ten_folio;
@@ -57,6 +60,7 @@ namespace autonum
return (*this);
unit = other.unit;
wrap = other.wrap;
unit_folio = other.unit_folio;
ten = other.ten;
ten_folio = other.ten_folio;
@@ -70,6 +74,7 @@ namespace autonum
bool sequentialNumbers::operator==(const sequentialNumbers &other) const
{
if (unit == other.unit && \
wrap == other.wrap && \
unit_folio == other.unit_folio && \
ten == other.ten && \
ten_folio == other.ten_folio && \
@@ -107,6 +112,11 @@ namespace autonum
document,
"unit",
unit.join(";")));
if (!wrap.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
"wrap",
wrap.join(";")));
if (!unit_folio.isEmpty())
element.appendChild(QETXML::textToDomElement(
document,
@@ -156,6 +166,11 @@ namespace autonum
from = element.firstChildElement("unit");
unit = from.text().split(";");
//Absent from files written before cyclic parts could be
//rendered; an empty list is the correct reading of that.
from = element.firstChildElement("wrap");
wrap = from.text().split(";");
from = element.firstChildElement("unitFolio");
unit_folio = from.text().split(";");
@@ -179,6 +194,7 @@ namespace autonum
void sequentialNumbers::clear()
{
unit.clear();
wrap.clear();
unit_folio.clear();
ten.clear();
ten_folio.clear();
@@ -434,7 +450,8 @@ namespace autonum
qMax(
qMax(m_seq_struct.hundred.size(),
m_seq_struct.ten.size()),
m_seq_struct.alpha.size())
qMax(m_seq_struct.alpha.size(),
m_seq_struct.wrap.size()))
);
for (int i=1; i<=max ; i++)
@@ -442,6 +459,9 @@ namespace autonum
if (m_assigned_label.contains("%sequ_" + QString::number(i)) && m_seq_struct.unit.size() >= i) {
m_assigned_label.replace("%sequ_" + QString::number(i),m_seq_struct.unit.at(i-1));
}
if (m_assigned_label.contains("%seqw_" + QString::number(i)) && m_seq_struct.wrap.size() >= i) {
m_assigned_label.replace("%seqw_" + QString::number(i),m_seq_struct.wrap.at(i-1));
}
if (m_assigned_label.contains("%seqt_" + QString::number(i)) && m_seq_struct.ten.size() >= i) {
m_assigned_label.replace("%seqt_" + QString::number(i),m_seq_struct.ten.at(i-1));
}
@@ -479,15 +499,26 @@ namespace autonum
{
if (context.itemAt(i).at(0) == type)
{
const QStringList item = context.itemAt(i);
//A zero-padding mask, spreadsheet style: its length is the
//minimum number of digits. It overrides the width implied
//by the part type, so "Chiffre 01" with a mask of "0000"
//pads to four. An absent mask -- which is every context
//written before the field existed -- falls through to the
//type's own width, so nothing about existing projects
//changes.
const QString mask = NumerotationContext::formatOf(item);
QString number;
if (type == "ten" || type == "tenfolio")
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 2, 10, QChar('0'));
else if (type == "hundred" || type == "hundredfolio")
number = QString("%1").arg(context.itemAt(i).at(1).toInt(), 3, 10, QChar('0'));
else if (type == "alpha")
if (type == "alpha")
//Alphabetic value, not an integer -- used as-is.
number = context.itemAt(i).at(1);
else number = QString::number(context.itemAt(i).at(1).toInt());
number = item.at(1);
else if (!mask.isEmpty())
number = QString("%1").arg(item.at(1).toInt(), mask.length(), 10, QChar('0'));
else if (type == "ten" || type == "tenfolio")
number = QString("%1").arg(item.at(1).toInt(), 2, 10, QChar('0'));
else if (type == "hundred" || type == "hundredfolio")
number = QString("%1").arg(item.at(1).toInt(), 3, 10, QChar('0'));
else number = QString::number(item.at(1).toInt());
list.append(number);
}
}
@@ -553,6 +584,10 @@ namespace autonum
{
autonum::setSequentialToList(seqStruct.unit, context,"unit");
}
if (label.contains("%seqw_"))
{
autonum::setSequentialToList(seqStruct.wrap, context,"wrap");
}
if (label.contains("%sequf_"))
{
autonum::setSequentialToList(seqStruct.unit_folio, context,"unitfolio");
@@ -594,6 +629,7 @@ namespace autonum
QString value;
QString formula;
int count_unit = 0;
int count_wrap = 0;
int count_unitf = 0;
int count_ten = 0;
int count_tenf = 0;
@@ -636,6 +672,10 @@ namespace autonum
count_unit++;
formula.append("%sequ_" + QString::number(count_unit));
}
else if (type == "wrap") {
count_wrap++;
formula.append("%seqw_" + QString::number(count_wrap));
}
else if (type == "unitfolio") {
count_unitf++;
formula.append("%sequf_" + QString::number(count_unitf));
@@ -665,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
@@ -678,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();
}
+2
View File
@@ -47,6 +47,8 @@ namespace autonum
void clear();
QStringList unit;
/// Values of the cyclic (modulo) parts, referenced by %seqw_N.
QStringList wrap;
QStringList unit_folio;
QStringList ten;
QStringList ten_folio;
+148 -4
View File
@@ -52,13 +52,18 @@ void NumerotationContext::clear ()
@param increase the increase number of value
@param initialvalue
@param modulus wrap-and-carry modulus (0 means "not a wrapping part")
@param format zero-padding mask, spreadsheet style: "00" pads to two
digits, "000" to three. Empty keeps the part type's natural width, so
an absent format reproduces exactly the behaviour of every context
written before this field existed.
@return true if value is append
*/
bool NumerotationContext::addValue(const QString &type,
const QVariant &value,
const int increase,
const int initialvalue,
const int modulus) {
const int modulus,
const QString &format) {
if (!keyIsAcceptable(type) && !value.canConvert<QString>())
return false;
if (keyIsNumber(type) && !value.canConvert<int>())
@@ -74,7 +79,9 @@ bool NumerotationContext::addValue(const QString &type,
+ "|"
+ QString::number(initialvalue)
+ "|"
+ QString::number(modulus);
+ QString::number(modulus)
+ "|"
+ QString(format).remove("|");
return true;
}
@@ -179,6 +186,9 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
if (strl.at(0) == ("wrap") && strl.size() > 4) {
part.setAttribute("modulus", strl.at(4));
}
if (strl.size() > 5 && !strl.at(5).isEmpty()) {
part.setAttribute("format", strl.at(5));
}
num_auto.appendChild(part);
}
return num_auto;
@@ -190,7 +200,7 @@ QDomElement NumerotationContext::toXml(QDomDocument &d, const QString& str) {
*/
void NumerotationContext::fromXml(QDomElement &e) {
clear();
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt(), qde.attribute("modulus").toInt());
foreach(QDomElement qde, QET::findInDomElement(e, "part")) addValue(qde.attribute("type"), qde.attribute("value"), qde.attribute("increase").toInt(), qde.attribute("initialvalue").toInt(), qde.attribute("modulus").toInt(), qde.attribute("format"));
}
/**
@@ -206,5 +216,139 @@ void NumerotationContext::replaceValue(int index, QString content) {
QString increase = strl.at(2);
QString initvalue = strl.at(3);
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus;
QString format = strl.size() > 5 ? strl.at(5) : QString();
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus + "|" + format;
}
/**
@brief NumerotationContext::replaceIncrease
Change how much this part advances per step, leaving its current value,
initial value, modulus and format untouched. Sibling to replaceValue(),
which deliberately never touches this field.
@param index of NC item
@param increase new increase for that item
*/
void NumerotationContext::replaceIncrease(int index, int increase) {
QStringList strl = content_[index].split("|");
QString type = strl.at(0);
QString value = strl.at(1);
QString initvalue = strl.at(3);
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
QString format = strl.size() > 5 ? strl.at(5) : QString();
content_[index] = type + "|" + value + "|" + QString::number(increase) + "|" + initvalue + "|" + modulus + "|" + format;
}
/**
@brief NumerotationContext::formatOf
@param item : a context item as returned by itemAt()
@return the part's zero-padding mask, or an empty string when it has
none -- which every context written before the field existed will be.
*/
QString NumerotationContext::formatOf(const QStringList &item)
{
return item.size() > 5 ? item.at(5) : QString();
}
/**
@brief NumerotationContext::formatValue
@param item : a context item as returned by itemAt()
@return the part's value, zero-padded exactly as
autonum::setSequentialToList() pads it when composing a real label: an
explicit format mask wins, then "ten"/"hundred" parts get their
implicit 2/3-digit width, "alpha" is used as-is, everything else is a
plain number. Kept in step with that function by hand since the two
cannot share code without exposing an assignvariables.cpp-local helper.
*/
QString NumerotationContext::formatValue(const QStringList &item)
{
const QString &type = item.at(0);
const QString &value = item.at(1);
if (type == QLatin1String("alpha"))
return value;
const QString mask = formatOf(item);
if (!mask.isEmpty())
return QString("%1").arg(value.toInt(), mask.length(), 10, QChar('0'));
if (type == QLatin1String("ten") || type == QLatin1String("tenfolio"))
return QString("%1").arg(value.toInt(), 2, 10, QChar('0'));
if (type == QLatin1String("hundred") || type == QLatin1String("hundredfolio"))
return QString("%1").arg(value.toInt(), 3, 10, QChar('0'));
return QString::number(value.toInt());
}
/**
@brief NumerotationContext::saveToSettings
Save a hash of named NumerotationContexts to QSettings.
@param contexts : the named rules to save
@param currentRule : the name of the currently active rule
@param settings : QSettings instance
@param prefix : settings key prefix (e.g. "autonum/conductor")
*/
void NumerotationContext::saveToSettings(
const QHash<QString, NumerotationContext> &contexts,
const QString &currentRule,
QSettings &settings,
const QString &prefix)
{
settings.setValue(prefix + "/current", currentRule);
// Clear stale array entries before writing (beginWriteArray does not
// remove entries beyond the new size).
settings.remove(prefix + "/rules");
QStringList names = contexts.keys();
settings.beginWriteArray(prefix + "/rules", names.size());
for (int i = 0; i < names.size(); ++i) {
settings.setArrayIndex(i);
const QString &name = names.at(i);
const NumerotationContext &nc = contexts.value(name);
settings.setValue("name", name);
// Serialize context to XML string
QDomDocument doc;
NumerotationContext nc_copy = nc;
QDomElement root = nc_copy.toXml(doc, "context");
doc.appendChild(root);
settings.setValue("xml", doc.toString());
}
settings.endArray();
}
/**
@brief NumerotationContext::loadFromSettings
Load named NumerotationContexts from QSettings.
@param settings : QSettings instance
@param prefix : settings key prefix (e.g. "autonum/conductor")
@return pair of (hash of named rules, name of current rule)
*/
QPair<QHash<QString, NumerotationContext>, QString> NumerotationContext::loadFromSettings(
QSettings &settings,
const QString &prefix)
{
QPair<QHash<QString, NumerotationContext>, QString> result;
QHash<QString, NumerotationContext> &contexts = result.first;
QString &currentRule = result.second;
currentRule = settings.value(prefix + "/current").toString();
int size = settings.beginReadArray(prefix + "/rules");
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
QString name = settings.value("name").toString();
QString xmlStr = settings.value("xml").toString();
if (name.isEmpty() || xmlStr.isEmpty()) continue;
QDomDocument doc;
if (!doc.setContent(xmlStr)) continue;
QDomElement root = doc.documentElement();
NumerotationContext nc;
nc.fromXml(root);
contexts.insert(name, nc);
}
settings.endArray();
return result;
}
+20 -1
View File
@@ -21,6 +21,8 @@
#include <QStringList>
#include <QVariant>
#include <QDomElement>
#include <QHash>
#include <QSettings>
/**
This class represents a numerotation context, i.e. the data (type, value, increase)
@@ -37,7 +39,11 @@ class NumerotationContext
const QVariant & = QVariant(1),
const int = 1,
const int = 0,
const int = 0);
const int = 0,
const QString & = QString());
/// Zero-padding mask of a part, e.g. "00"; empty means the type's
/// own natural width. See addValue().
static QString formatOf(const QStringList &item);
QString operator[] (const int &) const;
void operator << (const NumerotationContext &);
int size() const;
@@ -50,6 +56,19 @@ class NumerotationContext
QDomElement toXml(QDomDocument &, const QString&);
void fromXml(QDomElement &);
void replaceValue(int, QString);
void replaceIncrease(int, int);
/// Zero-pad a part's value the same way the real numbering engine
/// does (autonum::setSequentialToList in assignvariables.cpp), so a
/// UI preview of a part's value matches what actually gets rendered.
static QString formatValue(const QStringList &item);
static void saveToSettings(const QHash<QString, NumerotationContext> &contexts,
const QString &currentRule,
QSettings &settings,
const QString &prefix);
static QPair<QHash<QString, NumerotationContext>, QString> loadFromSettings(
QSettings &settings,
const QString &prefix);
private:
QStringList content_;
@@ -259,7 +259,7 @@ NumerotationContext NumStrategy::nextString (const NumerotationContext &nc,
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), strl.at(1), strl.at(2).toInt());
newnc.addValue(strl.at(0), strl.at(1), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -273,7 +273,7 @@ NumerotationContext NumStrategy::nextNumber (const NumerotationContext &nc,
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
QString value = QString::number( (strl.at(1).toInt()) + (strl.at(2).toInt()) );
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt());
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt(), 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -287,7 +287,7 @@ NumerotationContext NumStrategy::previousNumber(const NumerotationContext &nc,
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
QString value = QString::number( (strl.at(1).toInt()) - (strl.at(2).toInt()) );
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt());
newnc.addValue(strl.at(0), value, strl.at(2).toInt(), strl.at(3).toInt(), 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -550,7 +550,7 @@ NumerotationContext WrapNum::next (const NumerotationContext &nc, const int i) c
int new_value = strl.at(1).toInt() + increase;
if (modulus > 0)
new_value %= modulus;
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus);
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -570,7 +570,7 @@ NumerotationContext WrapNum::previous(const NumerotationContext &nc, const int i
if (new_value < 0)
new_value += modulus;
}
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus);
newnc.addValue(strl.at(0), QString::number(new_value), increase, strl.at(3).toInt(), modulus, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -706,7 +706,7 @@ NumerotationContext AlphaNum::next (const NumerotationContext &nc, const int i)
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), incrementAlpha(strl.at(1)), strl.at(2).toInt());
newnc.addValue(strl.at(0), incrementAlpha(strl.at(1)), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
@@ -718,7 +718,7 @@ NumerotationContext AlphaNum::previous(const NumerotationContext &nc, const int
{
QStringList strl = nc.itemAt(i);
NumerotationContext newnc;
newnc.addValue(strl.at(0), decrementAlpha(strl.at(1)), strl.at(2).toInt());
newnc.addValue(strl.at(0), decrementAlpha(strl.at(1)), strl.at(2).toInt(), 0, 0, NumerotationContext::formatOf(strl));
return (newnc);
}
+398 -50
View File
@@ -24,7 +24,14 @@
#include "../../titleblockproperties.h"
#include "../../ui/projectpropertiesdialog.h"
#include "../numerotationcontext.h"
#include "../numerotationcontextcommands.h"
#include "ui_autonumberingdockwidget.h"
#include "../../undocommand/changetitleblockcommand.h"
#include <QComboBox>
#include <QLineEdit>
#include <QSignalBlocker>
#include <QSpinBox>
/**
@brief AutoNumberingDockWidget::AutoNumberingDockWidget
@@ -58,6 +65,32 @@ void AutoNumberingDockWidget::clear()
ui->m_conductor_cb->clear();
ui->m_element_cb->clear();
ui->m_folio_cb->clear();
ui->m_conductor_value_le->clear();
ui->m_element_value_le->clear();
ui->m_folio_value_le->clear();
ui->m_conductor_next_le->clear();
ui->m_element_next_le->clear();
ui->m_folio_next_le->clear();
}
/**
@brief AutoNumberingDockWidget::rowFor
@return the combo/value/increase/next widgets that make up category's row.
*/
AutoNumberingDockWidget::Row AutoNumberingDockWidget::rowFor(AutoNumCategory category) const
{
switch (category) {
case AutoNumCategory::Conductor:
return {ui->m_conductor_cb, ui->m_conductor_value_le,
ui->m_conductor_increase_sb, ui->m_conductor_next_le};
case AutoNumCategory::Element:
return {ui->m_element_cb, ui->m_element_value_le,
ui->m_element_increase_sb, ui->m_element_next_le};
case AutoNumCategory::Folio:
return {ui->m_folio_cb, ui->m_folio_value_le,
ui->m_folio_increase_sb, ui->m_folio_next_le};
}
return {nullptr, nullptr, nullptr, nullptr};
}
void AutoNumberingDockWidget::projectClosed()
@@ -80,35 +113,23 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
//Disconnect previous project
if (m_project && m_project_view)
{
//Conductor Signals
disconnect(m_project, SIGNAL(conductorAutoNumChanged()),
this,SLOT(conductorAutoNumChanged()));
disconnect (m_project,SIGNAL(conductorAutoNumRemoved()),
this,SLOT(conductorAutoNumChanged()));
disconnect (m_project,SIGNAL(conductorAutoNumAdded()),
this,SLOT(conductorAutoNumChanged()));
disconnect(m_project_view,SIGNAL(diagramActivated(DiagramView*)),
this,SLOT(setConductorActive(DiagramView*)));
//Element Signals
disconnect (m_project,SIGNAL(elementAutoNumRemoved(QString)),
this,SLOT(elementAutoNumChanged()));
disconnect (m_project,SIGNAL(elementAutoNumAdded(QString)),
this,SLOT(elementAutoNumChanged()));
disconnect(m_project, &QETProject::conductorAutoNumChanged, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project, &QETProject::conductorAutoNumRemoved, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project, &QETProject::conductorAutoNumAdded, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::setConductorActive);
//Element Signals
disconnect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
disconnect(m_project, &QETProject::elementAutoNumAdded, this, &AutoNumberingDockWidget::elementAutoNumChanged);
//Folio Signals
disconnect (m_project,SIGNAL(folioAutoNumRemoved()),
this,SLOT(folioAutoNumChanged()));
disconnect (m_project,SIGNAL(folioAutoNumAdded()),
this,SLOT(folioAutoNumChanged()));
disconnect (this,
SIGNAL(folioAutoNumChanged(QString)),
&m_project_view->currentDiagram()->diagram()->border_and_titleblock,
SLOT (slot_setAutoPageNum(QString)));
disconnect(m_project, SIGNAL(defaultTitleBlockPropertiesChanged()),
this,SLOT(setActive()));
disconnect(m_project, &QETProject::folioAutoNumRemoved, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
disconnect(m_project, &QETProject::folioAutoNumAdded, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
disconnect(m_project, &QETProject::defaultTitleBlockPropertiesChanged, this, &AutoNumberingDockWidget::setActive);
//Conductor, Element and Folio Signals
disconnect(m_project, &QETProject::autoNumContextUpdated,
this, &AutoNumberingDockWidget::refreshValueFields);
disconnect(m_project, &QETProject::destroyed,
this, &AutoNumberingDockWidget::projectClosed);
}
@@ -117,35 +138,23 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
m_project_view = projectview;
this->setEnabled(true);
//Conductor Signals
connect(m_project, SIGNAL(conductorAutoNumChanged()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project,SIGNAL(conductorAutoNumRemoved()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project,SIGNAL(conductorAutoNumAdded()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project_view,SIGNAL(diagramActivated(DiagramView*)),
this,SLOT(setConductorActive(DiagramView*)));
connect(m_project, &QETProject::conductorAutoNumChanged, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project, &QETProject::conductorAutoNumRemoved, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project, &QETProject::conductorAutoNumAdded, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::setConductorActive);
//Element Signals
connect (m_project,SIGNAL(elementAutoNumRemoved(QString)),
this,SLOT(elementAutoNumChanged()));
connect (m_project,SIGNAL(elementAutoNumAdded(QString)),
this,SLOT(elementAutoNumChanged()));
//Element Signals
connect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
connect(m_project, &QETProject::elementAutoNumAdded, this, &AutoNumberingDockWidget::elementAutoNumChanged);
//Folio Signals
connect (m_project,SIGNAL(folioAutoNumRemoved()),
this,SLOT(folioAutoNumChanged()));
connect (m_project,SIGNAL(folioAutoNumAdded()),
this,SLOT(folioAutoNumChanged()));
connect (this,
SIGNAL(folioAutoNumChanged(QString)),
&m_project_view->currentDiagram()->diagram()->border_and_titleblock,
SLOT (slot_setAutoPageNum(QString)));
connect(m_project, SIGNAL(defaultTitleBlockPropertiesChanged()),
this,SLOT(setActive()));
connect(m_project, &QETProject::folioAutoNumRemoved, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
connect(m_project, &QETProject::folioAutoNumAdded, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
connect(m_project, &QETProject::defaultTitleBlockPropertiesChanged, this, &AutoNumberingDockWidget::setActive);
//Conductor, Element and Folio Signals
connect(m_project, &QETProject::autoNumContextUpdated,
this, &AutoNumberingDockWidget::refreshValueFields);
connect(m_project, &QETProject::destroyed,
this, &AutoNumberingDockWidget::projectClosed);
@@ -189,6 +198,12 @@ void AutoNumberingDockWidget::setContext()
{ ui->m_folio_cb -> addItem(str);}
}
//The combo boxes have just been repopulated, so the value fields next
//to them are showing whatever the previous project left there.
refreshRow(AutoNumCategory::Conductor);
refreshRow(AutoNumCategory::Element);
refreshRow(AutoNumCategory::Folio);
this->setActive();
}
@@ -263,6 +278,7 @@ void AutoNumberingDockWidget::on_m_conductor_cb_activated(int)
m_project->setCurrentConductorAutoNum(current_autonum);
m_project_view->currentDiagram()->diagram()->setConductorsAutonumName(current_autonum);
m_project_view->currentDiagram()->diagram()->loadCndFolioSeq();
refreshRow(AutoNumCategory::Conductor);
}
/**
@@ -291,6 +307,7 @@ void AutoNumberingDockWidget::on_m_element_cb_activated(int)
{
m_project->setCurrrentElementAutonum(ui->m_element_cb->currentText());
m_project_view->currentDiagram()->diagram()->loadElmtFolioSeq();
refreshRow(AutoNumCategory::Element);
}
/**
@@ -327,7 +344,17 @@ void AutoNumberingDockWidget::on_m_folio_cb_activated(int) {
ip.folio = "%id/%total";
m_project->setDefaultTitleBlockProperties(ip);
}
emit(folioAutoNumChanged(current_autonum));
if (m_project_view && m_project_view->currentDiagram()) {
Diagram *diagram = m_project_view->currentDiagram()->diagram();
TitleBlockProperties old_properties = diagram->border_and_titleblock.exportTitleBlock();
TitleBlockProperties new_properties = old_properties;
new_properties.auto_page_num = ip.auto_page_num;
new_properties.folio = ip.folio;
if (new_properties != old_properties)
diagram->undoStack().push(new ChangeTitleBlockCommand(diagram, old_properties, new_properties));
}
refreshRow(AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_configure_pb_clicked()
@@ -339,3 +366,324 @@ void AutoNumberingDockWidget::on_m_configure_pb_clicked()
ppd.exec();
}
}
void AutoNumberingDockWidget::on_m_conductor_reset_start_pb_clicked()
{
resetAutoNum(ui->m_conductor_cb, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_reset_start_pb_clicked()
{
resetAutoNum(ui->m_element_cb, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_reset_start_pb_clicked()
{
resetAutoNum(ui->m_folio_cb, AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_conductor_value_le_editingFinished()
{
applyValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_value_le_editingFinished()
{
applyValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_value_le_editingFinished()
{
applyValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_conductor_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_conductor_cb, ui->m_conductor_increase_sb, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_element_cb, ui->m_element_increase_sb, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_folio_cb, ui->m_folio_increase_sb, AutoNumCategory::Folio);
}
/**
@brief AutoNumberingDockWidget::contextFor
@return the numerotation context named by combo_box, for category
*/
NumerotationContext AutoNumberingDockWidget::contextFor(QComboBox *combo_box, AutoNumCategory category) const
{
const QString key = combo_box->currentText();
switch (category) {
case AutoNumCategory::Conductor: return m_project->conductorAutoNum(key);
case AutoNumCategory::Element: return m_project->elementAutoNum(key);
case AutoNumCategory::Folio: return m_project->folioAutoNum(key);
}
return NumerotationContext();
}
/**
@brief AutoNumberingDockWidget::storeContext
Write context back under the name selected in combo_box and flag the
project as modified -- without that last step the change is not saved
and the user is never asked to save it on close.
*/
void AutoNumberingDockWidget::storeContext(QComboBox *combo_box, AutoNumCategory category, const NumerotationContext &context)
{
const QString key = combo_box->currentText();
switch (category) {
case AutoNumCategory::Conductor: m_project->addConductorAutoNum(key, context); break;
case AutoNumCategory::Element: m_project->addElementAutoNum(key, context); break;
case AutoNumCategory::Folio: m_project->addFolioAutoNum(key, context); break;
}
m_project->setModified(true);
}
/**
@brief AutoNumberingDockWidget::counterIndex
@return the index of the part the value field shows: the last one that
actually progresses, i.e. the least significant digit of the number.
-1 when the context has no progressing part at all.
*/
int AutoNumberingDockWidget::counterIndex(const NumerotationContext &context)
{
for (int i = context.size() - 1 ; i >= 0 ; --i)
{
const QString type = context.itemAt(i).at(0);
if (type == QLatin1String("unit")
|| type == QLatin1String("ten")
|| type == QLatin1String("hundred")
|| type == QLatin1String("unitfolio")
|| type == QLatin1String("tenfolio")
|| type == QLatin1String("hundredfolio")
|| type == QLatin1String("wrap")
|| type == QLatin1String("alpha"))
return i;
}
return -1;
}
/**
@brief AutoNumberingDockWidget::refreshValueFields
Re-read all three value fields from the project. Called whenever a
numerotation context's values change, which includes every element or
conductor that consumes the next number -- without this the field only
caught up when the user re-picked a rule from the combo box, because
the combo's activated() signal fires on user interaction alone.
*/
void AutoNumberingDockWidget::refreshValueFields()
{
//Leave alone a row the user is typing in: numbering an element
//refreshes all three rows, and overwriting a half-typed value or
//increment under the cursor is worse than showing it a moment out
//of date. Only this automatic path skips; an explicit refresh after
//a reset or an edit still writes, so the row always ends up
//canonical. The next-value preview has no such guard: it is
//read-only, so there is nothing a refresh could clobber.
for (AutoNumCategory category : {AutoNumCategory::Conductor,
AutoNumCategory::Element,
AutoNumCategory::Folio})
{
const Row row = rowFor(category);
if (!row.value->hasFocus() && !row.increase->hasFocus())
refreshRow(category);
}
}
/**
@brief AutoNumberingDockWidget::refreshValueField
Show the current value of the selected context's counter, so the field
always reflects where the numbering has actually got to.
*/
void AutoNumberingDockWidget::refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
{
line_edit->clear();
line_edit->setEnabled(false);
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
line_edit->setEnabled(index >= 0);
line_edit->setText(index >= 0 ? context.itemAt(index).at(1) : QString());
}
/**
@brief AutoNumberingDockWidget::applyValueField
Write the value typed in line_edit to the counter it displays. An empty
field is treated as "no change" rather than as an empty value, so
clearing the box by accident cannot wipe the counter.
*/
void AutoNumberingDockWidget::applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
return;
const QString typed = line_edit->text();
if (typed.isEmpty() || typed == context.itemAt(index).at(1))
{
refreshRow(category);
return;
}
context.replaceValue(index, typed);
storeContext(combo_box, category, context);
refreshRow(category);
}
/**
@brief AutoNumberingDockWidget::refreshIncreaseField
Show the counter's current step size (bug #331: previously only
reachable from the full configuration dialog, via "Configurer").
*/
void AutoNumberingDockWidget::refreshIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category)
{
//QSpinBox::setValue() emits valueChanged() even when called
//programmatically. Without blocking it, this refresh would
//immediately re-trigger on_..._increase_sb_valueChanged() ->
//applyIncreaseField() -> storeContext() -> the project's
//autoNumContextUpdated signal -> refreshValueFields() -> back here.
const QSignalBlocker blocker(increase_sb);
if (!m_project || combo_box->currentText().isEmpty())
{
increase_sb->setEnabled(false);
increase_sb->setValue(increase_sb->minimum());
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
increase_sb->setEnabled(index >= 0);
increase_sb->setValue(index >= 0 ? context.itemAt(index).at(2).toInt()
: increase_sb->minimum());
}
/**
@brief AutoNumberingDockWidget::applyIncreaseField
Write the spin box's step size to the counter it displays (bug #331).
*/
void AutoNumberingDockWidget::applyIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
return;
if (increase_sb->value() == context.itemAt(index).at(2).toInt())
return;
context.replaceIncrease(index, increase_sb->value());
storeContext(combo_box, category, context);
refreshRow(category);
}
/**
@brief AutoNumberingDockWidget::refreshNextField
Show what this counter will read after one more step (bug #331: "visualiser
la prochaine numérotation qui sera appliquée"). Advances a copy of the
whole context through NumerotationContextCommands -- the same engine the
Suivant button in the full configuration dialog uses to step a context --
so wrap-and-carry into this part from a following part, or out of it into
a preceding one, comes out identical to what will actually happen when the
number is next consumed.
*/
void AutoNumberingDockWidget::refreshNextField(QComboBox *combo_box, QLineEdit *next_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
{
next_edit->clear();
next_edit->setEnabled(false);
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
{
next_edit->clear();
next_edit->setEnabled(false);
return;
}
Diagram *diagram = (m_project_view && m_project_view->currentDiagram())
? m_project_view->currentDiagram()->diagram()
: nullptr;
NumerotationContextCommands ncc(context, diagram);
const NumerotationContext next_context = ncc.next();
next_edit->setEnabled(true);
next_edit->setText(NumerotationContext::formatValue(next_context.itemAt(index)));
}
/**
@brief AutoNumberingDockWidget::refreshRow
Refresh a category's value, increment and next-value preview together --
every call site that used to refresh just the value field needs the
other two kept in step with it as well.
*/
void AutoNumberingDockWidget::refreshRow(AutoNumCategory category)
{
const Row row = rowFor(category);
refreshValueField(row.combo, row.value, category);
refreshIncreaseField(row.combo, row.increase, category);
refreshNextField(row.combo, row.next, category);
}
/**
@brief AutoNumberingDockWidget::resetAutoNum
Reset the numerotation context currently selected in combo_box back to
a per-type starting value, then write it back. Does nothing if no
context is selected.
Only parts that actually progress are touched: folio-anchored numeric
types go back to their own stored initialvalue, plain numeric types go
back to "1", a wrap part goes back to "0" because a modulo counter
cycles over [0, modulus) -- a PLC card addressed %IX0.0..%IX0.31 starts
at 0, not 1 -- and alpha goes back to "a". Non-incrementing types
(string, plant, locmach, idfolio, folio, elementline, elementcolumn,
elementprefix) are left alone: there is no "start" for them distinct
from the fixed or contextual value the user configured.
*/
void AutoNumberingDockWidget::resetAutoNum(QComboBox *combo_box, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
for (int i = 0 ; i < context.size() ; ++i)
{
const QStringList item = context.itemAt(i);
const QString &type = item.at(0);
if (type == QLatin1String("unitfolio")
|| type == QLatin1String("tenfolio")
|| type == QLatin1String("hundredfolio"))
context.replaceValue(i, item.size() > 3 ? item.at(3) : QStringLiteral("1"));
else if (type == QLatin1String("unit")
|| type == QLatin1String("ten")
|| type == QLatin1String("hundred"))
context.replaceValue(i, QStringLiteral("1"));
else if (type == QLatin1String("wrap"))
context.replaceValue(i, QStringLiteral("0"));
else if (type == QLatin1String("alpha"))
context.replaceValue(i, QStringLiteral("a"));
}
storeContext(combo_box, category, context);
refreshRow(category);
}
+70 -2
View File
@@ -23,6 +23,10 @@
#include <QDockWidget>
class QComboBox;
class QLineEdit;
class QSpinBox;
namespace Ui {
class AutoNumberingDockWidget;
}
@@ -51,13 +55,77 @@ class AutoNumberingDockWidget : public QDockWidget
void folioAutoNumChanged();
void clear();
void projectClosed();
void refreshValueFields();
void on_m_configure_pb_clicked();
void on_m_conductor_reset_start_pb_clicked();
void on_m_element_reset_start_pb_clicked();
void on_m_folio_reset_start_pb_clicked();
void on_m_conductor_value_le_editingFinished();
void on_m_element_value_le_editingFinished();
void on_m_folio_value_le_editingFinished();
void on_m_conductor_increase_sb_valueChanged(int);
void on_m_element_increase_sb_valueChanged(int);
void on_m_folio_increase_sb_valueChanged(int);
signals:
void folioAutoNumChanged(QString);
private:
enum class AutoNumCategory { Conductor, Element, Folio };
/// The four widgets that make up one category's row, bundled so
/// refreshRow() can be called with just a category instead of
/// four pointers that must always be passed in matching sets.
struct Row
{
QComboBox *combo;
QLineEdit *value;
QSpinBox *increase;
QLineEdit *next;
};
Row rowFor(AutoNumCategory category) const;
/**
@brief resetAutoNum
Reset the numerotation context currently selected in combo_box
(for the given category) to a per-type starting value. Does
nothing if no context is selected.
*/
void resetAutoNum(QComboBox *combo_box, AutoNumCategory category);
/// Read/write the numerotation context named in combo_box.
NumerotationContext contextFor(QComboBox *combo_box, AutoNumCategory category) const;
void storeContext(QComboBox *combo_box, AutoNumCategory category, const NumerotationContext &context);
/// Index of the counter the value field shows and edits: the last
/// part that actually progresses. Returns -1 when the context has
/// no such part (e.g. it is only fixed text).
static int counterIndex(const NumerotationContext &context);
/// Refresh a value field from its context, and apply a typed value.
void refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
void applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
/// Refresh/apply the increment spin box the same way.
void refreshIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category);
void applyIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category);
/// Show what the counter will read after one more step, using
/// the same NumerotationContextCommands engine the Suivant
/// button in the full configuration dialog already advances
/// the whole context with -- so the preview can never disagree
/// with what actually happens when the number is next consumed.
void refreshNextField(QComboBox *combo_box, QLineEdit *next_edit, AutoNumCategory category);
/// Refresh a whole row -- value, increment and next-value
/// preview -- in one call. Every refresh call site needs the
/// increment and preview kept in step with the value now, so
/// this replaces refreshValueField() at each of them.
void refreshRow(AutoNumCategory category);
Ui::AutoNumberingDockWidget *ui;
QETProject* m_project = nullptr;
ProjectView* m_project_view = nullptr;
+253 -1
View File
@@ -15,6 +15,36 @@
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="3">
<widget class="QLabel" name="value_header_label">
<property name="text">
<string>Valeur</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QLabel" name="increase_header_label">
<property name="text">
<string>Incrément</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QLabel" name="next_header_label">
<property name="text">
<string>Suivant</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="m_element_cb"/>
</item>
@@ -28,6 +58,80 @@
<item row="2" column="1">
<widget class="QComboBox" name="m_conductor_cb"/>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="m_conductor_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLineEdit" name="m_conductor_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="2" column="4">
<widget class="QSpinBox" name="m_conductor_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="2" column="5">
<widget class="QLineEdit" name="m_conductor_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label">
<property name="text">
@@ -42,9 +146,157 @@
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="m_element_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QLineEdit" name="m_element_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QSpinBox" name="m_element_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="3" column="5">
<widget class="QLineEdit" name="m_element_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="m_folio_cb"/>
</item>
<item row="4" column="2">
<widget class="QPushButton" name="m_folio_reset_start_pb">
<property name="maximumSize">
<size>
<width>24</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Réinitialiser à la valeur de départ</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QLineEdit" name="m_folio_value_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Valeur actuelle du compteur. Saisir une nouvelle valeur et valider pour la modifier.</string>
</property>
</widget>
</item>
<item row="4" column="4">
<widget class="QSpinBox" name="m_folio_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="4" column="5">
<widget class="QLineEdit" name="m_folio_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
@@ -67,7 +319,7 @@
<string>Configurer</string>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="configure" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/configure.png</normaloff>:/ico/16x16/configure.png</iconset>
</property>
</widget>
@@ -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>
@@ -54,6 +54,10 @@ FolioAutonumberingW::~FolioAutonumberingW()
*/
void FolioAutonumberingW::setContext(QList <QString> autonums)
{
// Replace the list rather than append to it: this is called again
// whenever the project's numberings change (import, for instance),
// and appending gave a second copy of every name.
ui->m_autonums_cb->clear();
foreach (QString str, autonums) { ui->m_autonums_cb->addItem(str);}
}
+41 -5
View File
@@ -18,6 +18,8 @@
#include "numparteditorw.h"
#include "ui_numparteditorw.h"
#include "../numerotationcontext.h"
#include <QRegularExpressionValidator>
/**
@@ -35,6 +37,9 @@ NumPartEditorW::NumPartEditorW(int type, QWidget *parent) :
{
ui -> setupUi(this);
setVisibleItems();
//The mask is a run of zeros and nothing else, so it cannot be typed
//into a state the renderer would have to reject.
ui -> format_le -> setValidator(new QRegularExpressionValidator(QRegularExpression("0*"), this));
setType(NumPartEditorW::unit, true);
}
@@ -99,6 +104,7 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
ui -> increase_spinBox -> setValue(strl.at(2).toInt());
if (strl.at(0)=="wrap" && strl.size() > 4)
ui -> modulus_spinBox -> setValue(strl.at(4).toInt());
ui -> format_le -> setText(NumerotationContext::formatOf(strl));
}
}
@@ -219,23 +225,30 @@ NumerotationContext NumPartEditorW::toNumContext()
type_str = "alpha";
break;
}
const QString number_format = ui -> format_le -> text();
if (type_str == "unitfolio"
|| type_str == "tenfolio"
|| type_str == "hundredfolio")
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
ui->value_field->displayText().toInt());
ui->value_field->displayText().toInt(),
0,
number_format);
else if (type_str == "wrap")
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
0,
ui -> modulus_spinBox -> value());
ui -> modulus_spinBox -> value(),
number_format);
else
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value());
ui -> increase_spinBox -> value(),
0,
0,
number_format);
return nc;
}
@@ -317,6 +330,14 @@ void NumPartEditorW::on_increase_spinBox_valueChanged(int) {
@brief NumPartEditorW::on_modulus_spinBox_valueChanged
emit changed when modulus_spinBox value changed
*/
/**
@brief NumPartEditorW::on_format_le_textEdited
emit changed when the display format is edited
*/
void NumPartEditorW::on_format_le_textEdited(const QString &) {
emit changed();
}
void NumPartEditorW::on_modulus_spinBox_valueChanged(int) {
if (!ui -> value_field -> text().isEmpty()) emit changed();
}
@@ -359,8 +380,6 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> value_field -> setValidator(intValidator);
ui -> increase_spinBox -> setEnabled(true);
ui -> increase_spinBox -> setValue(1);
if (t == wrap)
ui -> modulus_spinBox -> setValue(8);
}
//@t isn't a numeric type
else if (t == string
@@ -414,7 +433,24 @@ void NumPartEditorW::setType(NumPartEditorW::type t, bool fnum) {
ui -> increase_spinBox -> setDisabled(true);
}
}
//A modulus of 0 means "no cycle", which makes a Cyclique part behave
//exactly like a plain digit. Defaulting it used to live in the numeric
//behavior block above, which is skipped when the previous type was
//itself numeric -- so the ordinary path of turning the default
//"Chiffre 1" into a "Cyclique (modulo)" left the modulus at 0 and
//produced a wrap part that never wrapped. Kept out of that block so it
//applies whatever the part was before, and only when the current value
//is unusable, so a modulus the user chose on purpose is not clobbered.
if (t == wrap && ui -> modulus_spinBox -> value() <= 0)
ui -> modulus_spinBox -> setValue(8);
ui -> modulus_spinBox -> setEnabled(t == wrap);
//A padding mask only means anything for a part rendered as a number.
const bool numeric = (t == unit || t == unitfolio || t == ten
|| t == tenfolio || t == hundred || t == hundredfolio
|| t == wrap);
ui -> format_le -> setEnabled(numeric);
if (!numeric)
ui -> format_le -> clear();
type_= t;
}
+1
View File
@@ -66,6 +66,7 @@ class NumPartEditorW : public QWidget
void on_value_field_textEdited();
void on_increase_spinBox_valueChanged(int);
void on_modulus_spinBox_valueChanged(int);
void on_format_le_textEdited(const QString &);
void setType (NumPartEditorW::type t, bool=false);
signals:
+22
View File
@@ -126,6 +126,28 @@
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="format_le">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Format d'affichage : une suite de zéros donne le nombre minimum de chiffres (00 = 07, 000 = 007). Vide = largeur naturelle du type.</string>
</property>
<property name="placeholderText">
<string>0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
+3 -3
View File
@@ -112,7 +112,7 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
else {
for (int i=0; i<m_context.size(); ++i) { //build with the content of @context
NumPartEditorW *part= new NumPartEditorW(m_context, i, m_edited_type, this);
connect (part, SIGNAL(changed()), this, SLOT(applyEnable()));
connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
num_part_list_ << part;
ui -> editor_layout -> addWidget(part);
}
@@ -145,7 +145,7 @@ void SelectAutonumW::on_add_button_clicked()
{
applyEnable(false);
NumPartEditorW *part = new NumPartEditorW(m_edited_type, this);
connect (part, SIGNAL(changed()), this, SLOT(applyEnable()));
connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
num_part_list_ << part;
ui -> editor_layout -> addWidget(part);
ui -> remove_button -> setEnabled(true);
@@ -160,7 +160,7 @@ void SelectAutonumW::on_remove_button_clicked()
//remove if @num_part_list contains more than one item
if (num_part_list_.size() > 1) {
NumPartEditorW *part = num_part_list_.takeLast();
disconnect(part, SIGNAL(changed()), this, SLOT(applyEnable()));
// deliberately not disconnecting as not possible to resolve with lambda and will happen automatically when the object "part" is destroyed.
delete part;
if (num_part_list_.size() == 1) {
ui -> remove_button -> setDisabled(true);
+5 -5
View File
@@ -63,7 +63,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="edit-delete" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-delete.png</normaloff>:/ico/16x16/edit-delete.png</iconset>
</property>
</widget>
@@ -136,7 +136,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/list-remove.png</normaloff>:/ico/22x22/list-remove.png</iconset>
</property>
<property name="flat">
@@ -153,7 +153,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/list-add.png</normaloff>:/ico/22x22/list-add.png</iconset>
</property>
<property name="flat">
@@ -170,7 +170,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="arrow-left" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/arrow-left.png</normaloff>:/ico/16x16/arrow-left.png</iconset>
</property>
</widget>
@@ -184,7 +184,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="arrow-right" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/arrow-right.png</normaloff>:/ico/16x16/arrow-right.png</iconset>
</property>
</widget>
+237
View File
@@ -0,0 +1,237 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "autobreakconductor.h"
#include "diagram.h"
#include "qetproject.h"
#include "conductorautonumerotation.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/terminal.h"
#include "qetgraphicsitem/conductor.h"
#include "undocommand/addgraphicsobjectcommand.h"
#include "undocommand/deleteqgraphicsitemcommand.h"
#include <QPainterPath>
#include <QMap>
#include <limits>
namespace {
/**
@brief distanceToSegment
@param point : point to measure from
@param segment : the finite segment (not the infinite line through it)
@return the distance from @a point to the closest point actually on
@a segment. Unlike QET::orthogonalProjection(), which reports a hit
for any point on the segment's infinite extension, this clamps the
projection to the segment itself: a point collinear with a wire but
past its actual drawn end is correctly reported as far away, not "on"
the wire.
*/
qreal distanceToSegment(const QPointF &point, const QLineF &segment)
{
const QPointF a = segment.p1();
const QPointF b = segment.p2();
const QPointF ab = b - a;
const qreal len2 = QPointF::dotProduct(ab, ab);
if (len2 <= 0.0)
return QLineF(point, a).length();
qreal t = QPointF::dotProduct(point - a, ab) / len2;
t = qBound(0.0, t, 1.0);
return QLineF(point, a + t * ab).length();
}
} // anonymous namespace
/**
@brief autoBreakConductors
For each terminal of @a element, check if its dock point lies on an
existing conductor. If so, break the conductor and reconnect through
the element's terminal. Broken conductors from the same circuit
(sharing the same far-end endpoint) are broken together; independent
crossing conductors are left untouched.
*/
QSet<Terminal *> autoBreakConductors(
Diagram *diagram,
Element *element,
QUndoCommand *parent,
QList<Conductor *> &conductors_handled,
QSet<Terminal *> &used_terminals)
{
QSet<Terminal *> broken_endpoints;
if (!diagram->project()->autoBreakConductor())
return broken_endpoints;
foreach (Terminal *t, element->terminals())
{
if (used_terminals.contains(t))
continue;
QPointF t_dock = t->dockConductor();
struct ConductorMatch {
Conductor *conductor;
Terminal *connect_to;
Terminal *other;
};
QList<ConductorMatch> all_matches;
foreach (Conductor *c, diagram->conductors())
{
if (conductors_handled.contains(c) ||
c->terminal1->parentElement() == element ||
c->terminal2->parentElement() == element)
continue;
QPointF local_dock = c->mapFromScene(t_dock);
bool point_on_conductor = false;
QPainterPath path = c->path();
for (int i = 0; i < path.elementCount() - 1; ++i)
{
const QPainterPath::Element &e1 = path.elementAt(i);
const QPainterPath::Element &e2 = path.elementAt(i + 1);
QLineF segment(QPointF(e1.x, e1.y), QPointF(e2.x, e2.y));
if (distanceToSegment(local_dock, segment) < 5.0)
{
point_on_conductor = true;
break;
}
}
if (!point_on_conductor)
continue;
Terminal *c1 = c->terminal1;
Terminal *c2 = c->terminal2;
QPointF c1_dock = c1->dockConductor();
QPointF c2_dock = c2->dockConductor();
Terminal *connect_to = nullptr;
Terminal *other = nullptr;
switch (t->orientation()) {
case Qet::North:
if (c1_dock.y() < t_dock.y()) { connect_to = c1; other = c2; }
else if (c2_dock.y() < t_dock.y()) { connect_to = c2; other = c1; }
break;
case Qet::South:
if (c1_dock.y() > t_dock.y()) { connect_to = c1; other = c2; }
else if (c2_dock.y() > t_dock.y()) { connect_to = c2; other = c1; }
break;
case Qet::East:
if (c1_dock.x() > t_dock.x()) { connect_to = c1; other = c2; }
else if (c2_dock.x() > t_dock.x()) { connect_to = c2; other = c1; }
break;
case Qet::West:
if (c1_dock.x() < t_dock.x()) { connect_to = c1; other = c2; }
else if (c2_dock.x() < t_dock.x()) { connect_to = c2; other = c1; }
break;
}
if (!connect_to) {
qreal d1 = QLineF(t_dock, c1_dock).length();
qreal d2 = QLineF(t_dock, c2_dock).length();
if (d1 <= d2) { connect_to = c1; other = c2; }
else { connect_to = c2; other = c1; }
}
all_matches.append({c, connect_to, other});
}
if (all_matches.isEmpty())
continue;
QMap<Terminal *, QList<ConductorMatch>> groups;
for (const auto &m : all_matches) {
groups[m.other].append(m);
}
Terminal *best_other = nullptr;
int best_count = 0;
for (auto it = groups.constBegin(); it != groups.constEnd(); ++it) {
if (it.value().size() > best_count) {
best_count = it.value().size();
best_other = it.key();
}
}
const QList<ConductorMatch> &matches = groups[best_other];
used_terminals.insert(t);
DiagramContent content;
for (const auto &m : matches) {
content.m_other_conductors.append(m.conductor);
conductors_handled.append(m.conductor);
}
new DeleteQGraphicsItemCommand(diagram, content, parent);
for (const auto &m : matches) {
Conductor *new_c = new Conductor(m.connect_to, t);
new AddGraphicsObjectCommand(new_c, diagram, QPointF(), parent);
ConductorAutoNumerotation can(new_c, diagram, parent);
can.numerate();
if (diagram->freezeNewConductors() || diagram->project()->isFreezeNewConductors())
new_c->setFreezeLabel(true);
broken_endpoints.insert(m.connect_to);
}
QPointF other_dock = best_other->dockConductor();
Terminal *other_terminal = nullptr;
qreal best_dist = std::numeric_limits<qreal>::max();
foreach (Terminal *ot, element->terminals())
{
if (used_terminals.contains(ot))
continue;
QPointF ot_dock = ot->dockConductor();
bool orientation_ok = false;
switch (ot->orientation()) {
case Qet::North: orientation_ok = other_dock.y() < ot_dock.y(); break;
case Qet::South: orientation_ok = other_dock.y() > ot_dock.y(); break;
case Qet::East: orientation_ok = other_dock.x() > ot_dock.x(); break;
case Qet::West: orientation_ok = other_dock.x() < ot_dock.x(); break;
}
if (!orientation_ok)
continue;
qreal dist = QLineF(ot_dock, other_dock).length();
if (dist < best_dist) {
best_dist = dist;
other_terminal = ot;
}
}
if (other_terminal) {
Conductor *new_c2 = new Conductor(best_other, other_terminal);
new AddGraphicsObjectCommand(new_c2, diagram, QPointF(), parent);
ConductorAutoNumerotation can2(new_c2, diagram, parent);
can2.numerate();
if (diagram->freezeNewConductors() || diagram->project()->isFreezeNewConductors())
new_c2->setFreezeLabel(true);
broken_endpoints.insert(best_other);
used_terminals.insert(other_terminal);
}
}
return broken_endpoints;
}
+57
View File
@@ -0,0 +1,57 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef AUTOBREAKCONDUCTOR_H
#define AUTOBREAKCONDUCTOR_H
#include <QSet>
#include <QList>
class Diagram;
class Element;
class Terminal;
class Conductor;
class QUndoCommand;
/**
@brief autoBreakConductors
For each terminal of @a element, check if its dock point lies on an
existing conductor. If so, break the conductor and reconnect through the
element's terminal. Broken conductors from the same circuit (sharing the
same far-end endpoint) are broken together; independent crossing
conductors are left untouched.
@param diagram the diagram containing the conductors
@param element the element whose terminals trigger breaks
@param parent undo command under which break/reconnect
sub-commands are created
@param conductors_handled shared list of conductors already claimed by
a previous call in the same batch (prevents
double-processing when multiple elements are
broken in one pass)
@param used_terminals shared set of terminals already used as
connect_to or other_terminal in the same batch
@return set of terminals that were connected to a broken conductor's
far-end (useful for preventing duplicate auto-connect)
*/
QSet<Terminal *> autoBreakConductors(
Diagram *diagram,
Element *element,
QUndoCommand *parent,
QList<Conductor *> &conductors_handled,
QSet<Terminal *> &used_terminals);
#endif // AUTOBREAKCONDUCTOR_H
+133
View File
@@ -0,0 +1,133 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "bomexport.h"
#include <QSaveFile>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QVariant>
namespace {
QString csvField(QString value)
{
value.replace(QLatin1Char('"'), QStringLiteral("\"\""));
return QLatin1Char('"') + value + QLatin1Char('"');
}
QByteArray csvRecord(const QStringList &values)
{
QStringList escaped;
for (const auto &value : values) {
escaped.append(csvField(value));
}
return (escaped.join(QLatin1Char(';')) + QLatin1Char('\n')).toUtf8();
}
}
QStringList BomExport::defaultColumns()
{
return {
QStringLiteral("label"),
QStringLiteral("designation"),
QStringLiteral("manufacturer"),
QStringLiteral("manufacturer_reference"),
QStringLiteral("model"),
QStringLiteral("description"),
QStringLiteral("category"),
QStringLiteral("quantity"),
QStringLiteral("voltage_rating"),
QStringLiteral("current_rating"),
QStringLiteral("folio"),
QStringLiteral("notes")
};
}
QString BomExport::defaultQuery()
{
//Slaves and terminals are included because both are routinely
//separately orderable hardware. A circuit breaker can carry ten or
//twenty auxiliary blocks, each with its own order code, and a
//terminal block is a purchased part in its own right. Neither shares
//a line with its master: the query is ungrouped, one row per element,
//so each appears as the distinct item it is.
//
//Anything that should not be ordered is kept out by setting
//exclude_from_bom on the element, which the view already honours --
//a relay's own auxiliary contact, say.
//
//Thumbnails are deliberately left out for now even though ten of
//them in the shipped examples carry manufacturer and reference data
//(the assembly-plan mounting-plate symbols), because that has not
//been asked for and is a separate question. The folio report arrows
//and the conductor definition stay out because they are not hardware.
//
//See discussion #847.
return QStringLiteral("SELECT %1 FROM element_nomenclature_view "
"WHERE element_type IN "
"('simple', 'master', 'slave', 'terminal') "
"ORDER BY diagram_position, position, label")
.arg(defaultColumns().join(QStringLiteral(", ")));
}
QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers, int *row_count)
{
QByteArray csv("\xEF\xBB\xBF");
if (include_headers) {
csv += csvRecord(headers);
}
int rows = 0;
while (query.next())
{
QStringList values;
for (int i = 0; i < query.record().count(); ++i) {
values.append(query.value(i).toString());
}
csv += csvRecord(values);
++rows;
}
if (row_count) {
*row_count = rows;
}
return csv;
}
bool BomExport::writeCsv(const QString &file_path, const QByteArray &csv,
QString *error)
{
if (error) {
error->clear();
}
QSaveFile file(file_path);
if (!file.open(QIODevice::WriteOnly) || file.write(csv) != csv.size())
{
if (error) {
*error = file.errorString();
}
file.cancelWriting();
return false;
}
if (!file.commit())
{
if (error) {
*error = file.errorString();
}
return false;
}
return true;
}
+37
View File
@@ -0,0 +1,37 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BOMEXPORT_H
#define BOMEXPORT_H
#include <QByteArray>
#include <QString>
#include <QStringList>
class QSqlQuery;
namespace BomExport
{
QStringList defaultColumns();
QString defaultQuery();
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers = true, int *row_count = nullptr);
bool writeCsv(const QString &file_path, const QByteArray &csv,
QString *error = nullptr);
}
#endif // BOMEXPORT_H
+4 -9
View File
@@ -55,12 +55,6 @@ BorderTitleBlock::BorderTitleBlock(QObject *parent) :
m_titleblock_template_renderer = new TitleBlockTemplateRenderer(this);
m_titleblock_template_renderer -> setTitleBlockTemplate(QETApp::defaultTitleBlockTemplate());
// disable the QPicture-based cache from Qt 4.8 to avoid rendering errors and crashes
#if QT_VERSION < QT_VERSION_CHECK(4, 8, 0) // ### Qt 6: remove
#else
m_titleblock_template_renderer -> setUseCache(false);
#endif
// dimensions par defaut du schema
importBorder(BorderProperties());
@@ -410,12 +404,10 @@ QString BorderTitleBlock::titleBlockTemplateName() const
@brief BorderTitleBlock::titleBlockTemplateChanged
This slot may be used to inform this class that the given title block
template has changed.
The title block-dedicated rendering cache will thus be flushed.
@param template_name : Name of the title block template that has changed
*/
void BorderTitleBlock::titleBlockTemplateChanged(const QString &template_name) {
if (titleBlockTemplateName() != template_name) return;
m_titleblock_template_renderer -> invalidateRenderedTemplate();
}
/**
@@ -513,7 +505,10 @@ void BorderTitleBlock::draw(QPainter *painter)
{
//Set the QPainter
painter -> save();
QPen pen(Qt::black);
//Use a pen color that contrasts with the background
QColor border_color = Diagram::background_color.lightness() < 128
? QColor(Qt::white) : QColor(Qt::black);
QPen pen(border_color);
painter -> setPen(pen);
painter -> setBrush(Qt::NoBrush);
+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.
+26 -2
View File
@@ -24,6 +24,7 @@
#include "qet.h"
#include "qetdiagrameditor.h"
#include "ui/potentialselectordialog.h"
#include "undocommand/setautonumcontextcommand.h"
/**
@brief ConductorAutoNumerotation::ConductorAutoNumerotation
@@ -156,7 +157,16 @@ void ConductorAutoNumerotation::newProperties(
autonum::setSequential(formula, seq, context, diagram, autoNum_name);
NumerotationContextCommands ncc (context, diagram);
diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = diagram->project();
auto *undo = new SetAutoNumContextCommand(
[project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);},
autoNum_name,
context,
new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
diagram->undoStack().push(undo);
}
/**
@@ -245,7 +255,21 @@ void ConductorAutoNumerotation::numerateNewConductor()
autoNum_name);
NumerotationContextCommands ncc (context, m_diagram);
m_diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = m_diagram->project();
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);};
if (m_parent_undo)
{
new SetAutoNumContextCommand(setter, autoNum_name, context, new_context, m_parent_undo);
}
else
{
auto *undo = new SetAutoNumContextCommand(setter, autoNum_name, context, new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
m_diagram->undoStack().push(undo);
}
}
applyText(autonum::AssignVariables::formulaToLabel(
+10 -16
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;
@@ -130,15 +124,15 @@ void ConductorNumExport::fillHash()
continue;
}
//We must define if the connected terminal is a folio report, if it is the case
//we don't add the num to the hash because the terminal doesn't represent a real terminal.
if(!(c->terminal1->parentElement()->linkType() & Element::AllReport))
{
int value = m_hash.value(num, 0);
++value;
m_hash.insert(num, value);
}
if(!(c->terminal2->parentElement()->linkType() & Element::AllReport))
//We must define if the connected terminals are folio reports, if
//both are the case we don't add the num to the hash because
//neither terminal represents a real terminal. Otherwise the
//conductor counts once, regardless of how many of its two ends
//are real -- counting per terminal instead of per conductor
//doubled every ordinary two-real-terminal conductor's tally.
bool terminal1_is_report = c->terminal1->parentElement()->linkType() & Element::AllReport;
bool terminal2_is_report = c->terminal2->parentElement()->linkType() & Element::AllReport;
if (!(terminal1_is_report && terminal2_is_report))
{
int value = m_hash.value(num, 0);
++value;
+13 -7
View File
@@ -499,6 +499,7 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
horiz_rotate_text = cp.horiz_rotate_text;
m_vertical_alignment = cp.m_vertical_alignment;
m_horizontal_alignment = cp.m_horizontal_alignment;
style = cp.style;
return;
}
@@ -555,6 +556,18 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
m_dash_size = i_value;
equal = true;
//style
Qt::PenStyle pen_style;
pen_style = clist.first().style;
for(ConductorProperties cp : clist)
{
if (cp.style != pen_style)
equal = false;
}
if (equal)
style = pen_style;
equal = true;
//text
s_value = clist.first().text;
for(ConductorProperties cp : clist)
@@ -811,14 +824,7 @@ void ConductorProperties::readStyle(const QString &style_string) {
if (style_string.isEmpty()) return;
// recupere la liste des couples style / valeur
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QStringList styles = style_string.split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code QString::SkipEmptyParts for QT 5.14 or later")
#endif
QStringList styles = style_string.split(";", Qt::SkipEmptyParts);
#endif
QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$");
if (!Rx.isValid())
+3 -4
View File
@@ -74,10 +74,9 @@ ConfigDialog::ConfigDialog(QWidget *parent) : QDialog(parent) {
setLayout(dialog_layout);
// connexion signaux / slots
connect(buttons, SIGNAL(accepted()), this, SLOT(applyConf()));
connect(buttons, SIGNAL(rejected()), this, SLOT(reject()));
connect(pages_list, SIGNAL(currentRowChanged(int)),
pages_widget, SLOT(setCurrentIndex(int)));
connect(buttons, &QDialogButtonBox::accepted, this, &ConfigDialog::applyConf);
connect(buttons, &QDialogButtonBox::rejected, this, &ConfigDialog::reject);
connect(pages_list, &QListWidget::currentRowChanged, pages_widget, &QStackedWidget::setCurrentIndex);
// set maximum a bit smaller than available size = (screen-size - Task-Bar):
setMaximumSize((int)(0.94 * MachineInfo::instance()->i_max_available_width()),
+91
View File
@@ -0,0 +1,91 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTUSAGE_H
#define CONTACTUSAGE_H
#include <algorithm>
/**
@brief The ContactUsage struct
How many slave contacts a master element currently uses, broken down
by contact type.
Header-only and free of any graphics dependency so that the counting
rules can be unit tested on their own. MasterElement::contactUsage()
is the thin wrapper that feeds it the linked elements.
This counts contacts, which is what tells you how many contacts an
auxiliary block must provide. It is deliberately not the count that
MasterElement::isFull() uses: a master's max_slaves is a number of
slots, and a slave fills exactly one slot however many contacts it
carries.
Two rules are easy to get wrong, and both live here so that every
caller gets them right:
- a slave stands for as many contacts as its "number" kind
information says, so a 4 pole contact counts as 4, not as 1
- a changeover contact is counted once, as sw. CrossRefItem's
NOElements() and NCElements() both return it, so adding those two
lists together would count it twice.
*/
struct ContactUsage
{
/**
Contact types a slave can declare. Mirrors
ElementData::SlaveState, which is not used directly so that this
header stays free of the element data dependencies and can be
unit tested on its own. MasterElement::contactUsage() maps
between the two.
*/
enum Type
{
NO, ///< Normally open
NC, ///< Normally closed
SW, ///< Changeover
Other ///< Neither of the above
};
int no = 0; ///< Normally open
int nc = 0; ///< Normally closed
int sw = 0; ///< Changeover
int other = 0; ///< Neither of the above
int total() const { return no + nc + sw + other; }
/**
Add one slave element to the tally.
@param type the contact type the slave declares
@param contacts how many contacts it stands for. Values below 1
are treated as 1: an element which declares no
contact count is still one contact.
*/
void addSlave(Type type, int contacts)
{
const int n = std::max(1, contacts);
switch (type)
{
case NO: no += n; break;
case NC: nc += n; break;
case SW: sw += n; break;
case Other: other += n; break;
}
}
};
#endif // CONTACTUSAGE_H
+637 -40
View File
@@ -21,11 +21,14 @@
#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"
#include <QLocale>
#include <QRegularExpression>
#include <QSqlError>
#include <QSqlDriver>
@@ -51,6 +54,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 +87,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
@@ -100,43 +133,150 @@ QETProject *projectDataBase::project() const
}
/**
@brief projectDataBase::newQuery
@return a QSqlquery with query as query
and the internal database of this class as database to use.
@brief projectDataBase::isReadOnlySelect
Every query that reaches newQuery() goes through this check first --
including one loaded from a saved nomenclature/summary table's <query>
element (ProjectDBModel::fromXml()), which makes this a defense against
a crafted project file, not just a careless custom-SQL edit
(qelectrotech-source-mirror#886 asked for read-only enforcement on the
custom SQL reports feature; this covers every path into newQuery(), not
just that one dialog).
Deliberately simple rather than a real SQL parser: reject more than one
statement (blocks stacking a write after a leading SELECT with `;`), and
require the query to start with SELECT or WITH. A determined attacker
with arbitrary SQL access to a local SQLite connection can still find
tricks a simple prefix check won't catch; this is meant to stop the
ordinary mistake and the obvious payload, not to be a security boundary
against a hostile file assumed to already run in some other trust
context.
@param query the raw SQL text to check
@param error set to a human-readable reason when this returns false
@return true if @p query looks like a single read-only SELECT/WITH
*/
QSqlQuery projectDataBase::newQuery(const QString &query) {
bool projectDataBase::isReadOnlySelect(const QString &query, QString *error)
{
if (error) {
error->clear();
}
QString trimmed = query.trimmed();
if (trimmed.endsWith(QLatin1Char(';'))) {
trimmed.chop(1);
trimmed = trimmed.trimmed();
}
if (trimmed.isEmpty()) {
if (error) {
*error = projectDataBase::tr("La requête est vide.");
}
return false;
}
if (trimmed.contains(QLatin1Char(';'))) {
if (error) {
*error = projectDataBase::tr("Une seule requête SELECT est autorisée"
" (le caractère ';' ne peut apparaître"
" qu'à la toute fin).");
}
return false;
}
const int first_space = trimmed.indexOf(QRegularExpression(QStringLiteral("\\s")));
const QString first_word = (first_space == -1 ? trimmed : trimmed.left(first_space)).toUpper();
if (first_word != QLatin1String("SELECT") && first_word != QLatin1String("WITH")) {
if (error) {
*error = projectDataBase::tr("Seules les requêtes en lecture seule"
" (SELECT ou WITH ... SELECT) sont"
" autorisées.");
}
return false;
}
return true;
}
/**
@brief projectDataBase::newQuery
@param query the SQL text to run -- must be a single read-only
SELECT/WITH statement, see isReadOnlySelect()
@param error set to a human-readable reason when the query was rejected
before ever reaching the database
@return a QSqlQuery with query as query and the internal database of
this class as database to use, or an unexecuted, harmless QSqlQuery if
the query was rejected
*/
QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
QString reason;
if (!isReadOnlySelect(query, &reason)) {
qWarning().noquote() << "projectDataBase::newQuery: rejected query:" << reason << "--" << query;
if (error) {
*error = reason;
}
return QSqlQuery(m_data_base);
}
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 +290,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 +305,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 +320,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 +337,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 +366,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 +434,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 +645,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 +719,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 +782,21 @@ 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.
//Slave is here because an auxiliary contact block is
//separately orderable hardware with its own part
//number, even though it shares its master's BMK.
//Anything that should not be ordered -- a relay's
//own auxiliary contact, say -- is kept out by
//exclude_from_bom above, not by its base type.
//See discussion #847.
" AND e.type IN ('simple', 'terminal', 'master', 'slave', 'thumbnail')");
QSqlQuery query(m_data_base);
if (!query.exec(create_view)) {
@@ -442,6 +835,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 +908,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 +944,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 +968,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 +996,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 +1140,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 +1180,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());
+49 -2
View File
@@ -27,6 +27,8 @@
class Element;
class QETProject;
class Diagram;
class Conductor;
class Terminal;
class sqlite3;
/**
@@ -45,8 +47,22 @@ class projectDataBase : public QObject
virtual ~projectDataBase() override;
void updateDB();
/**
Suppress the full rebuild performed by updateDB().
While blocked, updateDB() returns immediately instead of
repopulating every table. Meant for bulk operations -- notably
loading a project, where each table model re-queries the
database as it is built and would otherwise trigger one
complete rebuild of it. The caller unblocks and calls
updateDB() once when done; @see QETProject::readProjectXml().
*/
void setUpdateBlocked(bool blocked);
QETProject *project() const;
QSqlQuery newQuery(const QString &query = QString());
QSqlQuery newQuery(const QString &query = QString(), QString *error = nullptr);
static bool isReadOnlySelect(const QString &query, QString *error = nullptr);
QSqlDatabase database() const {return m_data_base;}
int excludedConductorCount() const;
void addElement (Element *element);
void removeElement (Element *element);
@@ -58,6 +74,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 +91,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 +125,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:
+183 -21
View File
@@ -20,8 +20,15 @@
#include "../../properties/elementdata.h"
#include "../../qetapp.h"
#include "../../qetinformation.h"
#include "../projectdatabase.h"
#include "ui_elementquerywidget.h"
#include <QFile>
#include <QFileDialog>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QRegularExpression>
/**
@@ -48,14 +55,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)
@@ -100,6 +101,9 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
setUpItems();
fillSavedQuery();
connect(ui->m_sql_query, &QLineEdit::textChanged, this, &ElementQueryWidget::checkQueryValidity);
checkQueryValidity();
}
/**
@@ -166,6 +170,8 @@ void ElementQueryWidget::setQuery(const QString &query)
{
auto str_type = rxm.captured(1);
where.remove(str_type);
const bool all_master_types = str_type.contains(
ElementData::typeToString(ElementData::Master));
int c=0;
ui->m_simple_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Simple)) ? true : false);
@@ -176,23 +182,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 +364,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 +400,10 @@ QString ElementQueryWidget::queryStr() const
where.clear();
}
QString exclude_condition = "(exclude_from_bom IS NULL OR exclude_from_bom != '1')";
filter_ += " AND " + exclude_condition;
// -------------------------------------------------------------
// exclude_from_bom is already filtered by element_nomenclature_view
// (see createElementNomenclatureView() in projectdatabase.cpp); this
// widget's query reads FROM that view, so a flagged element never
// reaches this point in the first place.
if (where.isEmpty() && !filter_.isEmpty()) {
filter_.remove(0, 4); //Remove the first " AND" of filter.
@@ -593,6 +609,8 @@ void ElementQueryWidget::on_m_edit_sql_query_cb_clicked()
m_custom_query = ui->m_sql_query->text();
updateQueryLine();
}
checkQueryValidity();
}
/**
@@ -666,6 +684,10 @@ void ElementQueryWidget::on_m_load_pb_clicked()
*/
void ElementQueryWidget::on_m_save_current_conf_pb_clicked()
{
if (!projectDataBase::isReadOnlySelect(queryStr())) {
return;
}
QFile file_(QETApp::configDir() % "/nomenclature.json");
if (file_.open(QFile::ReadWrite))
@@ -692,7 +714,147 @@ void ElementQueryWidget::on_m_save_current_conf_pb_clicked()
}
void ElementQueryWidget::on_m_save_name_le_textChanged(const QString &arg1) {
ui->m_save_current_conf_pb->setDisabled(arg1.isEmpty());
Q_UNUSED(arg1)
checkQueryValidity();
}
/**
@brief ElementQueryWidget::checkQueryValidity
Live feedback for the custom-SQL box: only the free-text path can carry
anything other than a SELECT (the column/filter builder always produces
one), so this only actually restricts something once "Requête SQL
personnalisée" is checked. Same rule projectDataBase::isReadOnlySelect()
enforces at execution time -- this just tells the user *before* they hit
Preview/Export/OK instead of after (qelectrotech-source-mirror#886).
*/
void ElementQueryWidget::checkQueryValidity()
{
if (!ui->m_edit_sql_query_cb->isChecked()) {
ui->m_query_warning_label->clear();
ui->m_save_current_conf_pb->setEnabled(!ui->m_save_name_le->text().isEmpty());
return;
}
QString reason;
const bool valid = projectDataBase::isReadOnlySelect(ui->m_sql_query->text(), &reason);
ui->m_query_warning_label->setText(valid ? QString() : reason);
ui->m_save_current_conf_pb->setEnabled(valid && !ui->m_save_name_le->text().isEmpty());
}
/**
@brief ElementQueryWidget::on_m_export_reports_pb_clicked
Write every saved report in nomenclature.json to a file the user picks,
so it can be handed to a colleague or another install
(qelectrotech-source-mirror#886's "import and export report definitions
for sharing between users or company installations").
*/
void ElementQueryWidget::on_m_export_reports_pb_clicked()
{
QFile source(QETApp::configDir() % "/nomenclature.json");
if (!source.open(QFile::ReadOnly)) {
QMessageBox::information(this, tr("Exporter"), tr("Aucun rapport enregistré à exporter."));
return;
}
const auto content = source.readAll();
source.close();
if (QJsonDocument::fromJson(content).object().isEmpty()) {
QMessageBox::information(this, tr("Exporter"), tr("Aucun rapport enregistré à exporter."));
return;
}
const QString file_path = QFileDialog::getSaveFileName(
this, tr("Exporter les rapports"), QStringLiteral("rapports_qet.json"),
tr("Fichiers JSON (*.json)"));
if (file_path.isEmpty()) {
return;
}
QFile dest(file_path);
if (!dest.open(QFile::WriteOnly) || dest.write(content) == -1) {
QMessageBox::critical(this, tr("Erreur"), tr("Impossible d'écrire dans %1.").arg(file_path));
}
}
/**
@brief ElementQueryWidget::on_m_import_reports_pb_clicked
Merge a previously-exported reports file into this install's
nomenclature.json. A name already used locally is not overwritten
silently -- the user is asked, per report, whether to replace it.
*/
void ElementQueryWidget::on_m_import_reports_pb_clicked()
{
const QString file_path = QFileDialog::getOpenFileName(
this, tr("Importer des rapports"), QString(), tr("Fichiers JSON (*.json)"));
if (file_path.isEmpty()) {
return;
}
QFile source(file_path);
if (!source.open(QFile::ReadOnly)) {
QMessageBox::critical(this, tr("Erreur"), tr("Impossible de lire %1.").arg(file_path));
return;
}
QJsonParseError parse_error;
const auto incoming_doc = QJsonDocument::fromJson(source.readAll(), &parse_error);
source.close();
if (parse_error.error != QJsonParseError::NoError || !incoming_doc.isObject()) {
QMessageBox::critical(
this, tr("Erreur"),
tr("%1 ne contient pas des rapports QElectroTech valides.").arg(file_path));
return;
}
const auto incoming = incoming_doc.object();
if (incoming.isEmpty()) {
QMessageBox::information(this, tr("Importer"), tr("Ce fichier ne contient aucun rapport."));
return;
}
QFile dest_file(QETApp::configDir() % "/nomenclature.json");
QJsonObject existing;
if (dest_file.open(QFile::ReadOnly)) {
existing = QJsonDocument::fromJson(dest_file.readAll()).object();
dest_file.close();
}
int imported = 0, skipped = 0;
for (auto it = incoming.begin(); it != incoming.end(); ++it)
{
if (existing.contains(it.key()))
{
const auto answer = QMessageBox::question(
this, tr("Rapport déjà existant"),
tr("Un rapport nommé « %1 » existe déjà. Le remplacer ?").arg(it.key()),
QMessageBox::Yes | QMessageBox::No);
if (answer != QMessageBox::Yes) {
++skipped;
continue;
}
}
existing[it.key()] = it.value();
++imported;
}
if (dest_file.open(QFile::WriteOnly | QFile::Truncate))
{
dest_file.write(QJsonDocument(existing).toJson());
dest_file.close();
}
else
{
QMessageBox::critical(this, tr("Erreur"), tr("Impossible d'écrire la configuration locale."));
return;
}
ui->m_conf_cb->clear();
fillSavedQuery();
QMessageBox::information(
this, tr("Importer"),
tr("%1 rapport(s) importé(s), %2 ignoré(s).").arg(imported).arg(skipped));
}
void ElementQueryWidget::on_m_choosen_list_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous)
+3
View File
@@ -59,6 +59,9 @@ class ElementQueryWidget : public QWidget
void on_m_load_pb_clicked();
void on_m_save_current_conf_pb_clicked();
void on_m_save_name_le_textChanged(const QString &arg1);
void on_m_import_reports_pb_clicked();
void on_m_export_reports_pb_clicked();
void checkQueryValidity();
void on_m_choosen_list_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
void on_m_var_list_itemDoubleClicked(QListWidgetItem *item);
void on_m_choosen_list_itemDoubleClicked(QListWidgetItem *item);
+39 -6
View File
@@ -88,7 +88,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-up" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-up.png</normaloff>:/ico/16x16/go-up.png</iconset>
</property>
<property name="checkable">
@@ -105,7 +105,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
</property>
</widget>
@@ -119,7 +119,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
</property>
</widget>
@@ -133,7 +133,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-down" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-down.png</normaloff>:/ico/16x16/go-down.png</iconset>
</property>
</widget>
@@ -353,7 +353,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="folder-open" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/folder-open.png</normaloff>:/ico/16x16/folder-open.png</iconset>
</property>
</widget>
@@ -380,7 +380,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="document-save" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/document-save.png</normaloff>:/ico/16x16/document-save.png</iconset>
</property>
<property name="flat">
@@ -388,6 +388,26 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="m_import_reports_pb">
<property name="toolTip">
<string>Importer des rapports depuis un fichier</string>
</property>
<property name="text">
<string>Importer...</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="m_export_reports_pb">
<property name="toolTip">
<string>Exporter tous les rapports enregistrés vers un fichier</string>
</property>
<property name="text">
<string>Exporter...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -429,6 +449,19 @@
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="m_query_warning_label">
<property name="styleSheet">
<string notr="true">color: red;</string>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
+29 -3
View File
@@ -63,7 +63,6 @@ QString SummaryQueryWidget::queryStr() const
QStringList keys = selectedKeys();
QString select ="SELECT ";
QString order_by = " ORDER BY ";
QString column;
bool first = true;
@@ -72,14 +71,29 @@ QString SummaryQueryWidget::queryStr() const
first = false;
} else {
column += ", ";
order_by +=", ";
}
column += key;
order_by += key;
}
QString from = " FROM project_summary_view";
//Always ordered by the folio's own position, never by the columns
//the user happened to choose to display. This is a table of
//contents: it lists the folios of the project and its order is the
//project's order. Ordering by the displayed columns instead meant
//that choosing, say, Title as the first column silently sorted the
//summary alphabetically (bugtracker #238).
//
//project_summary_view exposes the position as "pos", from
//diagram.pos, which is an INTEGER -- so this sorts numerically and
//folio 10 does not land between folio 1 and folio 2. One row per
//folio means pos fully determines the order and no secondary key is
//needed.
//
//A user who wants a different order can still write the query by
//hand; that path returns above, untouched.
QString order_by = " ORDER BY pos";
QString q(select + column + from + order_by);
return q;
}
@@ -95,6 +109,7 @@ void SummaryQueryWidget::setQuery(const QString &query)
if (query.startsWith("SELECT"))
{
reset();
ui->m_edit_sql_query_cb->setChecked(false);
ui->m_user_query_le->setText(query);
QString select = query;
@@ -115,6 +130,17 @@ void SummaryQueryWidget::setQuery(const QString &query)
}
}
}
//If the query this widget would build from the columns just
//parsed above does not match the query as loaded byte-for-byte,
//the user wrote it by hand (a join, a subquery, a different
//view) and accepting the dialog unmodified must not silently
//replace it with a generated one (bugtracker #885).
const bool custom_query = query != queryStr();
m_custom_query = custom_query ? query : QString();
ui->m_edit_sql_query_cb->setChecked(custom_query);
ui->m_user_query_le->setEnabled(custom_query);
ui->m_info_widget->setDisabled(custom_query);
}
}
+4 -4
View File
@@ -76,7 +76,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-up" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-up.png</normaloff>:/ico/16x16/go-up.png</iconset>
</property>
</widget>
@@ -87,7 +87,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
</property>
</widget>
@@ -98,7 +98,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
</property>
</widget>
@@ -109,7 +109,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<iconset theme="go-down" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-down.png</normaloff>:/ico/16x16/go-down.png</iconset>
</property>
</widget>
+307 -17
View File
@@ -26,6 +26,7 @@
#include "diagramposition.h"
#include "factory/elementfactory.h"
#include "qetapp.h"
#include "qetpalette.h"
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/conductortextitem.h"
@@ -40,9 +41,66 @@
#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)
{
//Position alone is not a total order: two elements can sit at the
//same x/y (lmdg.qet has a pair of text elements both at 780,350).
//With equal keys std::stable_sort falls back to the order the
//scene handed us, which varies per run, so those two swapped
//places on every save. The uuid breaks the tie.
//
//For an element with a persisted uuid attribute this is fully
//deterministic. For a legacy element without one, fromXml()
//invents a fresh uuid per load, so a collision between two such
//elements is no better ordered than before -- but no worse
//either, and the tiebreaker is only consulted when the positions
//are equal.
return positionKey(elmt->pos())
+ QLatin1Char(':') + elmt->uuid().toString();
}
/// Sort key for a terminal: its parent element's position, then the
/// terminal's own position local to that element (from the .elmt
/// definition, fixed regardless of where the element is placed).
QString terminalSortKey(Terminal *terminal)
{
if (!terminal)
return QString();
Element *parent = terminal->parentElement();
return (parent ? positionKey(parent->pos()) : QStringLiteral("?"))
+ QLatin1Char(':') + positionKey(terminal->pos());
}
/// Sort key for Diagram::toXml()'s <conductors> block. Built from both
/// endpoints' terminalSortKey(), not Conductor::uuid(): in every example
/// project checked, conductors have no persisted uuid attribute at all,
/// so uuid() is a freshly-minted random value on every load -- exactly
/// as unusable for cross-run determinism as the element case above, just
/// with no persisted fallback to reach for instead. Canonicalised
/// (smaller key first) since a conductor's two ends are unordered for
/// this purpose.
QString conductorSortKey(Conductor *cond)
{
QString a = terminalSortKey(cond->terminal1);
QString b = terminalSortKey(cond->terminal2);
return (a <= b) ? (a + QLatin1Char('>') + b) : (b + QLatin1Char('>') + a);
}
}
int Diagram::xGrid = 10;
int Diagram::yGrid = 10;
int Diagram::xKeyGrid = 10;
@@ -67,6 +125,7 @@ Diagram::Diagram(QETProject *project) :
m_project (project),
use_border_ (true),
draw_terminals_ (true),
draw_terminal_names_ (true),
draw_colored_conductors_ (true),
m_event_interface (nullptr),
m_freeze_new_elements (false),
@@ -225,10 +284,11 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
* if background color is black,
* then grid spots shall be white,
* else they shall be black in color.
* A view that shows the sheet with its lightness inverted
* gets softer dots, see QET::Palette::gridDotColor.
*/
QPen pen;
Diagram::background_color == Qt::black? pen.setColor(Qt::white)
: pen.setColor(Qt::black);
pen.setColor(QET::Palette::gridDotColor(Diagram::background_color, m_inverted_lightness));
pen.setCosmetic(true);
p->setPen(pen);
@@ -392,6 +452,34 @@ void Diagram::wheelEvent(QGraphicsSceneWheelEvent *event)
QGraphicsScene::wheelEvent(event);
}
/**
@brief Diagram::event
QGraphicsScene has its own Tab/Shift+Tab item-focus-chain traversal
(mirroring QWidget's), checked before keyPressEvent() is ever reached:
by default it would silently consume Tab/Backtab to move focus among
the scene's own focusable items. Intercepting the key press here,
ahead of that, is the only way to reliably override it: unlike
QWidget::focusNextPrevChild(), QGraphicsScene::focusNextPrevChild() is
only virtual starting in Qt 6 (guarded by the QT6_VIRTUAL macro), so a
Diagram:: override of it would silently do nothing on a Qt 5 build.
@param event
*/
bool Diagram::event(QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
auto *key_event = static_cast<QKeyEvent *>(event);
if ((key_event->key() == Qt::Key_Tab || key_event->key() == Qt::Key_Backtab)
&& !isReadOnly() && !focusItem()) {
bool forward = key_event->key() == Qt::Key_Tab
&& !(key_event->modifiers() & Qt::ShiftModifier);
selectNextItem(forward);
event->accept();
return true;
}
}
return QGraphicsScene::event(event);
}
/**
@brief Diagram::keyPressEvent
This event is managed by diagram event interface if any.
@@ -605,6 +693,79 @@ QUuid Diagram::uuid()
return m_uuid;
}
/**
@brief Diagram::uuidUsedByOtherDiagram
A hand-edited or merged project file can contain two folios with the same
uuid. The uuid is used as a key (e.g. in the project database), so the
second one must get another one.
@param uuid
@return true if another diagram of the parent project already uses @p uuid
*/
bool Diagram::uuidUsedByOtherDiagram(const QUuid &uuid) const
{
if (!m_project) {
return false;
}
const auto diagrams = m_project->diagrams();
for (const Diagram *diagram : diagrams) {
if (diagram != this && diagram->m_uuid == uuid) {
return true;
}
}
return false;
}
/**
@brief Diagram::derivedUuid
Name-based (version 5) uuid for a folio that has no usable uuid in the
file it is loaded from : either the file predates the uuid attribute, or
the uuid it carries is already taken by another folio of the project.
A random uuid would do as an identity, but it would make saving an
unmodified legacy project non-reproducible : every load would invent a
different one and write it out (see #754). The name is therefore built
only from data read from the file itself -- the project title, the
position of the folio in the file and its title -- so the same input file
always yields the same uuid. It is computed once, when the folio is
loaded, and saved from then on : renaming or moving the folio later does
not change it.
@param root : the <diagram> element being loaded
@param reason : distinguishes the two cases above, so that they can not
produce the same name
@return a uuid not used by any other diagram of the project
*/
QUuid Diagram::derivedUuid(const QDomElement &root, const QString &reason) const
{
//Fixed namespace for QElectroTech folio uuids, never change it :
//doing so would change the uuid given to every legacy folio.
static const QUuid folio_namespace(
QStringLiteral("{d5951240-154d-44d6-8277-0092a31d1920}"));
const int index = m_project
? m_project->diagrams().indexOf(const_cast<Diagram *>(this))
: -1;
const QString project_title = root.ownerDocument()
.documentElement()
.attribute(QStringLiteral("title"));
const QString base = QStringLiteral("qet-folio\n%1\n%2\n%3\n%4")
.arg(reason,
project_title,
QString::number(index),
root.attribute(QStringLiteral("title")));
//A clash is only possible with a hand-crafted file, but the uuid is a
//key : salt the name until it is free. Still deterministic.
QUuid uuid = QUuid::createUuidV5(folio_namespace, base);
for (int salt = 1 ; uuidUsedByOtherDiagram(uuid) ; ++salt) {
uuid = QUuid::createUuidV5(folio_namespace,
base + QStringLiteral("\n")
+ QString::number(salt));
}
return uuid;
}
/**
@brief Diagram::setEventInterface
Set event_interface has current interface.
@@ -642,6 +803,11 @@ void Diagram::clearEventInterface()
}
}
bool Diagram::eventInterfaceIsRunning() const
{
return m_event_interface && m_event_interface->isRunning();
}
/**
@brief Diagram::conductorsAutonumName
@return the name of autonum to use.
@@ -820,6 +986,11 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
// schema properties
// proprietes du schema
if (whole_content) {
//Persist the folio identity, so that a folio keeps the same uuid
//across save/load. Without it every load invents a new one, and
//nothing outside the running instance (version control, a lock,
//a diff tool...) can tell which folio is which.
dom_root.setAttribute(QStringLiteral("uuid"), m_uuid.toString());
border_and_titleblock.titleBlockToXml(dom_root);
border_and_titleblock.borderToXml(dom_root);
@@ -1013,6 +1184,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;
@@ -1306,6 +1491,21 @@ bool Diagram::fromXml(QDomElement &document,
// Read attributes of this diagram
if (consider_informations)
{
// Restore the persisted folio uuid. Done first, before any item is
// loaded, so that everything created below sees the final uuid.
// A folio without a usable one (file written before the uuid was
// persisted, or uuid already taken by another folio) gets a
// deterministic one instead, see derivedUuid().
const QUuid persisted_uuid(root.attribute(QStringLiteral("uuid")));
if (persisted_uuid.isNull()) {
m_uuid = derivedUuid(root, QStringLiteral("legacy"));
} else if (uuidUsedByOtherDiagram(persisted_uuid)) {
m_uuid = derivedUuid(root, QStringLiteral("duplicate ")
+ persisted_uuid.toString());
} else {
m_uuid = persisted_uuid;
}
// Load border and titleblock
border_and_titleblock.titleBlockFromXml(root);
border_and_titleblock.borderFromXml(root);
@@ -1509,10 +1709,7 @@ bool Diagram::fromXml(QDomElement &document,
//Get the top left corner of the rectangle that contain all added items
QRectF items_rect;
for (auto item : added_items) {
items_rect = items_rect.united(
item->mapToScene(
item->boundingRect()
).boundingRect());
items_rect = items_rect.united(item->mapToScene(item->boundingRect()).boundingRect());
}
QPointF point_ = items_rect.topLeft();
@@ -1520,8 +1717,12 @@ bool Diagram::fromXml(QDomElement &document,
position.y() - point_.y()));
//Translate all added items
for (auto qgi : added_items)
for (auto qgi : added_items) {
qgi->setPos(qgi->pos() += pos_);
}
}
else
{
}
// Load conductor
@@ -1555,14 +1756,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());
@@ -1573,7 +1766,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);
}
@@ -1674,6 +1866,7 @@ void Diagram::addItem(QGraphicsItem *item)
conductor->terminal1->addConductor(conductor);
conductor->terminal2->addConductor(conductor);
conductor->calculateTextItemPosition();
m_project->dataBase()->addConductor(conductor);
break;
}
default: {break;}
@@ -1704,6 +1897,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;}
@@ -1837,6 +2031,85 @@ void Diagram::invertSelection()
emit selectionChanged();
}
/**
@brief Diagram::selectAllConductors
Select every conductor on this diagram, deselecting anything else.
*/
void Diagram::selectAllConductors()
{
if (items().isEmpty()) return;
blockSignals(true);
for (auto item : items()) {
item -> setSelected(dynamic_cast<Conductor *>(item) != nullptr);
}
blockSignals(false);
emit selectionChanged();
}
/**
@brief Diagram::selectAllTextFields
Select every text field on this diagram (independent/static text,
conductor labels, and dynamic element texts), deselecting anything else.
*/
void Diagram::selectAllTextFields()
{
if (items().isEmpty()) return;
blockSignals(true);
for (auto item : items()) {
item -> setSelected(dynamic_cast<DiagramTextItem *>(item) != nullptr);
}
blockSignals(false);
emit selectionChanged();
}
/**
@brief Diagram::selectNextItem
Select the next (or, if @a forward is false, the previous) selectable
item on this diagram, cycling through items() (z-order) and wrapping
around at either end. If nothing is currently selected, selects the
first (or last) item. Uses the same "what counts as a real selectable
diagram item" filter as invertSelection(), so the candidate list and
its order always match what the user could reach by clicking.
@param forward true to select the next item, false for the previous one
*/
void Diagram::selectNextItem(bool forward)
{
QList<QGraphicsItem *> candidates;
for (auto item : items()) {
if (dynamic_cast<QetGraphicsItem *>(item) ||
dynamic_cast<DiagramTextItem *>(item) ||
dynamic_cast<Conductor *>(item)) {
candidates << item;
}
}
if (candidates.isEmpty())
return;
int current_index = -1;
for (int i = 0; i < candidates.size(); ++i) {
if (candidates.at(i) -> isSelected()) {
current_index = i;
break;
}
}
int next_index;
if (current_index == -1) {
next_index = forward ? 0 : candidates.size() - 1;
} else {
next_index = forward
? (current_index + 1) % candidates.size()
: (current_index - 1 + candidates.size()) % candidates.size();
}
clearSelection();
QGraphicsItem *next_item = candidates.at(next_index);
next_item -> setSelected(true);
next_item -> ensureVisible();
}
/**
@brief Diagram::insertFolioSeqHash
This class inserts a stringlist containing all
@@ -2319,6 +2592,7 @@ ExportProperties Diagram::applyProperties(
old_properties.draw_border = border_and_titleblock.borderIsDisplayed();
old_properties.draw_titleblock = border_and_titleblock.titleBlockIsDisplayed();
old_properties.draw_terminals = drawTerminals();
old_properties.draw_terminal_names = drawTerminalNames();
old_properties.draw_colored_conductors = drawColoredConductors();
old_properties.exported_area = useBorder() ? QET::BorderArea
: QET::ElementsArea;
@@ -2327,6 +2601,7 @@ ExportProperties Diagram::applyProperties(
// applique les nouvelles options de rendu
setUseBorder (new_properties.exported_area == QET::BorderArea);
setDrawTerminals (new_properties.draw_terminals);
setDrawTerminalNames (new_properties.draw_terminal_names);
setDrawColoredConductors (new_properties.draw_colored_conductors);
setDisplayGrid (new_properties.draw_grid);
setDisplayGuides (new_properties.draw_guides);
@@ -2397,9 +2672,24 @@ QPointF Diagram::snapToGrid(const QPointF &p)
\~French true pour afficher les bornes, false sinon
*/
void Diagram::setDrawTerminals(bool dt) {
draw_terminals_ = dt;
foreach(QGraphicsItem *qgi, items()) {
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
t -> setVisible(dt);
t -> update();
}
}
}
/**
@brief Diagram::setDrawTerminalNames
Defines whether or not to display the terminal names/labels
@param dt : true to display the terminal names, false otherwise
*/
void Diagram::setDrawTerminalNames(bool dt) {
draw_terminal_names_ = dt;
foreach(QGraphicsItem *qgi, items()) {
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
t -> update();
}
}
}
+36 -1
View File
@@ -123,10 +123,12 @@ class Diagram : public QGraphicsScene
qreal diagram_qet_version_;
bool draw_grid_;
bool m_inverted_lightness = false;
bool use_border_;
bool draw_guides_;
QList<Diagram::Guide> m_guides_list;
bool draw_terminals_;
bool draw_terminal_names_;
bool draw_colored_conductors_;
QString m_conductors_autonum_name;
@@ -135,6 +137,9 @@ class Diagram : public QGraphicsScene
bool m_freeze_new_elements;
bool m_freeze_new_conductors_;
QUuid m_uuid = QUuid::createUuid();
bool uuidUsedByOtherDiagram(const QUuid &uuid) const;
QUuid derivedUuid(const QDomElement &root, const QString &reason) const;
// METHODS
protected:
@@ -149,8 +154,11 @@ class Diagram : public QGraphicsScene
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void keyReleaseEvent (QKeyEvent *) override;
bool event(QEvent *event) override;
private:
void selectNextItem(bool forward);
public:
void correctTextPos(Element* elmt);
void restoreText(Element* elmt);
@@ -209,11 +217,13 @@ class Diagram : public QGraphicsScene
// methods related to graphics items addition/removal on the diagram
virtual void addItem (QGraphicsItem *item);
virtual void removeItem (QGraphicsItem *item);
bool eventInterfaceIsRunning() const;
// methods related to graphics options
ExportProperties applyProperties(const ExportProperties &);
void setDisplayGrid(bool);
bool displayGrid();
void setInvertedLightness(bool);
void setDisplayGuides(bool);
bool displayGuides();
void updateProjectGuides(const QList<GuideProperties> &guides);
@@ -226,6 +236,8 @@ class Diagram : public QGraphicsScene
bool drawTerminals() const;
void setDrawTerminals(bool);
bool drawTerminalNames() const;
void setDrawTerminalNames(bool);
bool drawColoredConductors() const;
void setDrawColoredConductors(bool);
@@ -284,6 +296,8 @@ class Diagram : public QGraphicsScene
void selectAll();
void deselectAll();
void invertSelection();
void selectAllConductors();
void selectAllTextFields();
signals:
void showDiagram (Diagram *);
@@ -343,6 +357,18 @@ inline void Diagram::setDisplayGrid(bool dg) {
draw_grid_ = dg;
}
/**
@brief Diagram::setInvertedLightness
Tell the diagram whether the view painting it will show the result
with its lightness inverted (PaletteGraphicsView on a dark palette).
drawBackground draws a softer grid in that case. Printing
and export never set this.
@param inverted
*/
inline void Diagram::setInvertedLightness(bool inverted) {
m_inverted_lightness = inverted;
}
/**
@brief Diagram::displayGrid
@return draw_grid_ true if the grid is drawn, false otherwise.
@@ -426,6 +452,15 @@ inline bool Diagram::drawTerminals() const
return(draw_terminals_);
}
/**
@brief Diagram::drawTerminalNames
@return true if terminal names are rendered, false otherwise
*/
inline bool Diagram::drawTerminalNames() const
{
return(draw_terminal_names_);
}
/**
@brief Diagram::drawColoredConductors
@return true if conductors colors are rendered, false otherwise.
+88 -2
View File
@@ -19,8 +19,10 @@
#include "diagram.h"
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/elementtextitemgroup.h"
#include "qetinformation.h"
#include "qgimanager.h"
/**
@@ -75,6 +77,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)
@@ -82,6 +91,62 @@ void PasteDiagramCommand::redo()
//make new uuid, because old uuid are the uuid of the copied element
e -> newUuid();
// PLC slaves carry master-specific data (type, address,
// function, cross-ref, etc.) in their elementInformations.
// Always clear those on paste so the duplicate starts clean,
// regardless of the user's erase-label-on-copy preference.
const bool is_slave = (e->linkType() == Element::Slave);
if (is_slave) {
DiagramContext dc = e->elementInformations();
dc.remove(QETInformation::ELMT_PLC_TYPE);
dc.remove(QETInformation::ELMT_PLC_ADDRESS);
dc.remove(QETInformation::ELMT_PLC_FUNCTION);
dc.remove(QETInformation::ELMT_PLC_COMMENT);
dc.remove(QETInformation::ELMT_PLC_CROSSREF);
dc.remove(QETInformation::ELMT_LABEL);
dc.remove(QETInformation::ELMT_PLC_TC);
dc.remove(QETInformation::ELMT_PLC_T1);
dc.remove(QETInformation::ELMT_PLC_T2);
dc.remove(QETInformation::ELMT_PLC_T3);
dc.remove(QETInformation::ELMT_PLC_T4);
dc.remove(QStringLiteral("xref"));
// Block alignment before setElementInformations so
// that elementInfoChanged() resolves texts without
// finishAlignment() shifting right/center-aligned items.
for (DynamicElementTextItem *deti : e->dynamicTextItems())
deti->m_block_alignment = true;
for (auto *group : e->textGroups())
group->blockAlignmentUpdate(true);
e->setElementInformations(dc);
for (DynamicElementTextItem *deti : e->dynamicTextItems())
deti->m_block_alignment = false;
for (auto *group : e->textGroups())
group->blockAlignmentUpdate(false);
// After setElementInformations, elementInfoChanged()
// resolves composite text with cleaned dc. Clear
// all non-UserText items directly as a safety net.
for (DynamicElementTextItem *deti : e->dynamicTextItems()) {
if (deti->textFrom() != DynamicElementTextItem::UserText) {
deti->m_block_alignment = true;
deti->setPlainText(QString());
deti->m_block_alignment = false;
}
}
for (auto *group : e->textGroups()) {
for (DynamicElementTextItem *deti : group->texts()) {
if (deti->textFrom() != DynamicElementTextItem::UserText) {
deti->m_block_alignment = true;
deti->setPlainText(QString());
deti->m_block_alignment = false;
}
}
}
}
if (settings.value("diagramcommands/erase-label-on-copy", true).toBool())
{
//Reset the information about the label, the comment and location
@@ -90,14 +155,35 @@ void PasteDiagramCommand::redo()
dc.addValue("label", "");
dc.addValue("comment", "");
dc.addValue("location", "");
// Block alignment during setElementInformations
// for non-slaves, same as Element::fromXml() (line 890-896).
if (!is_slave) {
for (DynamicElementTextItem *deti : e->dynamicTextItems())
deti->m_block_alignment = true;
for (auto *group : e->textGroups())
group->blockAlignmentUpdate(true);
}
e->setElementInformations(dc);
for (DynamicElementTextItem *deti : e->dynamicTextItems())
deti->m_block_alignment = false;
for (auto *group : e->textGroups())
group->blockAlignmentUpdate(false);
//Reset the text of conductors
//Reset the text of conductors, the same way the label/comment/
//location above are reset to "" rather than to some other
//value - "erase on copy" means erase, not "replace with the
//project's default new-conductor text" (which happens to
//default to a literal "_" character, unrelated to whether the
//user wanted this copy's old label kept or cleared; see
//issue #413).
const QList <Conductor *> conductors_list = content.m_conductors_to_move;
for (Conductor *c : conductors_list)
{
ConductorProperties cp = c -> properties();
cp.text = c->diagram() ? c -> diagram() -> defaultConductorProperties.text : "_";
cp.text = "";
c -> setProperties(cp);
}
}
+1 -1
View File
@@ -17,7 +17,7 @@
*/
#ifndef DIAGRAM_CONTEXT_H
#define DIAGRAM_CONTEXT_H
#include "pugixml/src/pugixml.hpp"
#include "pugixml.hpp"
#include <QDomElement>
#include <QHash>
@@ -20,11 +20,17 @@
#include "../conductorautonumerotation.h"
#include "../diagram.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../undocommand/deleteqgraphicsitemcommand.h"
#include "../factory/elementfactory.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/terminal.h"
#include "../qet.h"
#include "../autobreakconductor.h"
#include <QPainterPath>
#include <limits>
/**
@brief DiagramEventAddElement::DiagramEventAddElement
@@ -248,16 +254,32 @@ void DiagramEventAddElement::addElement()
QUndoCommand *undo_object = new QUndoCommand(tr("Ajouter %1").arg(element->name()));
new AddGraphicsObjectCommand(element, m_diagram, m_element -> pos(), undo_object);
//When we search for free aligned terminal we temporally remove m_element to
//avoid any interaction with the function Element::AlignedFreeTerminals
//This is useful when an element has two (or more) terminals on opposite sides,
//because m_element is exactly at the same pos of the new element
//added to the scene so new conductor are created between terminal of the new element
//and the opposite terminal of m_element.
//When we search for free aligned terminal we temporally remove m_element to
//avoid any interaction with the function Element::AlignedFreeTerminals
//This is useful when an element has two (or more) terminals on opposite sides,
//because m_element is exactly at the same pos of the new element
//added to the scene so new conductor are created between terminal of the new element
//and the opposite terminal of m_element.
m_diagram->removeItem(m_element);
while (!element -> AlignedFreeTerminals().isEmpty() && m_diagram -> project() -> autoConductor())
//Auto break conductor: if a terminal of the new element lies on an existing
//conductor, break the conductor and reconnect through the new element's terminal.
//Track the endpoints of broken conductors so auto-connect doesn't create duplicates.
QList<Conductor *> conductors_handled;
QSet<Terminal *> used_terminals;
QSet<Terminal *> broken_endpoints = autoBreakConductors(m_diagram, element, undo_object,
conductors_handled, used_terminals);
//Auto-connect: collect all aligned pairs first, then filter and process.
QList<QPair<Terminal *, Terminal *>> aligned_pairs;
if (m_diagram->project()->autoConductor())
aligned_pairs = element->AlignedFreeTerminals();
for (const QPair<Terminal *, Terminal *> &pair : aligned_pairs)
{
QPair <Terminal *, Terminal *> pair = element -> AlignedFreeTerminals().takeFirst();
//Skip if the other terminal was an endpoint of a broken conductor
if (broken_endpoints.contains(pair.second))
continue;
Conductor *conductor = new Conductor(pair.first, pair.second);
new AddGraphicsObjectCommand(conductor, m_diagram, QPointF(), undo_object);
@@ -271,7 +293,12 @@ void DiagramEventAddElement::addElement()
}
m_diagram->addItem(m_element);
//Autonum the new element before pushing undo_object, so the counter
//change it triggers is part of the same undo macro as the element's
//own placement (one Ctrl+Z reverts both, instead of silently
//leaving the counter advanced).
element->setUpFormula(true, undo_object);
m_diagram -> undoStack().push(undo_object);
element->setUpFormula();
element->freezeNewAddedElement();
}
+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
+28 -17
View File
@@ -17,6 +17,7 @@
#include <QGraphicsSceneMouseEvent>
#include <QStatusBar>
#include <QPainter>
#include <QSettings>
DiagramEventAddMacro::DiagramEventAddMacro(const ElementsLocation &location, Diagram *diagram, QPointF pos) :
DiagramEventInterface(diagram),
@@ -43,11 +44,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;
@@ -67,9 +64,22 @@ m_preview_item(nullptr)
QDomElement diagram_node = root.firstChildElement("diagram_content").firstChildElement("diagram");
if (!diagram_node.isNull()) {
dummy_diagram->setDisplayGrid(false);
dummy_diagram->fromXml(diagram_node, QPointF(0, 0), false, nullptr);
// Compute bounding rect of TOP-LEVEL items only (matching fromXml's added_items logic)
// Child items (DynamicElementTextItem, Terminal) are NOT included - they move with parents
QRectF top_level_rect;
for (auto *item : dummy_diagram->items()) {
if (!item->parentItem()) {
top_level_rect = top_level_rect.united(
item->mapToScene(item->boundingRect()).boundingRect());
}
}
m_items_top_left = top_level_rect.topLeft();
QRectF scene_rect = dummy_diagram->itemsBoundingRect();
if (!scene_rect.isEmpty()) {
QPixmap pixmap(scene_rect.toAlignedRect().size());
pixmap.fill(Qt::transparent);
@@ -82,10 +92,11 @@ m_preview_item(nullptr)
}
}
if (m_preview_item) {
m_preview_item->setPos(Diagram::snapToGrid(pos));
m_preview_item->setOpacity(0.6);
m_diagram->addItem(m_preview_item);
if (m_preview_item) {
QPointF snapped = Diagram::snapToGrid(pos);
m_preview_item->setPos(snapped);
m_preview_item->setOpacity(0.6);
m_diagram->addItem(m_preview_item);
m_running = true;
}
@@ -119,6 +130,7 @@ void DiagramEventAddMacro::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (m_preview_item) {
const auto pos_{Diagram::snapToGrid(event->scenePos())};
m_preview_item->setPos(pos_);
if (m_status_bar) {
@@ -143,7 +155,8 @@ void DiagramEventAddMacro::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
emit finish();
}
else if (event->button() == Qt::LeftButton) {
addMacro(Diagram::snapToGrid(event->scenePos()));
QPointF snapped = Diagram::snapToGrid(event->scenePos());
addMacro(snapped);
}
}
event->setAccepted(true);
@@ -202,11 +215,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;
@@ -244,14 +253,16 @@ void DiagramEventAddMacro::addMacro(QPointF final_pos)
if (!diagram_node.isNull()) {
QDomElement cloned_node = diagram_node.cloneNode(true).toElement();
QPointF target_pos = final_pos;
DiagramContent pasted_content;
m_diagram->fromXml(cloned_node, target_pos, false, &pasted_content);
m_diagram->fromXml(cloned_node, final_pos + m_items_top_left, false, &pasted_content);
m_diagram->refreshContents();
// Prevent PasteDiagramCommand from erasing labels (BMK)
QSettings settings;
bool saved_erase = settings.value("diagramcommands/erase-label-on-copy", true).toBool();
settings.setValue("diagramcommands/erase-label-on-copy", false);
m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, pasted_content));
settings.setValue("diagramcommands/erase-label-on-copy", saved_erase);
}
}
@@ -40,6 +40,7 @@ private:
QDomDocument m_macro_doc;
QGraphicsPixmapItem *m_preview_item;
QPointer<QStatusBar> m_status_bar;
QPointF m_items_top_left; // top-left of bounding rect of top-level items in the macro (for correct placement offset)
};
#endif // DIAGRAMEVENTADDMACRO_H
@@ -0,0 +1,344 @@
/*
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 "diagrameventaddpaste.h"
#include "../diagram.h"
#include "../diagramcommands.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../qetgraphicsitem/conductor.h"
#include <QSettings>
#include <QApplication>
#include <QClipboard>
#include <QGraphicsSceneMouseEvent>
#include <QKeyEvent>
#include <QStatusBar>
/**
@brief DiagramEventAddPaste::DiagramEventAddPaste
@param diagram : diagram to paste into
@param start_pos : where the pasted items first appear, in scene
coordinates -- normally the cursor
*/
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) :
DiagramEventInterface(diagram)
{
//DiagramEventInterface::init() is called by Diagram::setEventInterface
//only when it is replacing an earlier interface, so call it here as
//DiagramEventAddMacro does.
init();
const QString clipboard_text = QApplication::clipboard()->text();
if (clipboard_text.isEmpty()) return;
QDomDocument document_xml;
if (!document_xml.setContent(clipboard_text)) return;
//Load items at their original XML coordinates.
m_diagram->fromXml(document_xml, QPointF(), false, &m_content);
if (!m_content.count()) return;
const QList<QGraphicsItem *> movable = m_content.items(MovableItems);
if (movable.isEmpty()) return;
//Compute the top-left of all items' positions (not bounding
//rects) and snap to grid: this is the point that gets placed
//under the cursor, and the baseline moveTo() measures from.
QPointF top_left;
bool first = true;
for (auto *item : movable) {
const QPointF p = item->pos();
if (first) {
top_left = p;
first = false;
} else {
if (p.x() < top_left.x()) top_left.setX(p.x());
if (p.y() < top_left.y()) top_left.setY(p.y());
}
}
QSettings settings;
const int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
const int yGrid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
const auto snapGrid = [xGrid, yGrid](const QPointF &p) -> QPointF {
return QPointF(
qRound(p.x() / xGrid) * xGrid,
qRound(p.y() / yGrid) * yGrid);
};
const QPointF grid_origin = snapGrid(top_left);
//Move the group to the cursor, rather than the cursor to the
//group. Both put the copy under the pointer, but warping the
//pointer also drags it back to the original's position, so the
//copy appears exactly on top of what was copied until the mouse
//is moved -- which is the thing pasting under the cursor was
//meant to avoid (issue #913). Taking the pointer away from
//where the user put it is also its own surprise.
m_group_origin = snapGrid(start_pos);
const QPointF offset = m_group_origin - grid_origin;
//Store each item's position after the move. moveTo() applies a
//grid-snapped delta from the baseline to these, so items
//preserve their layout and move in whole grid steps.
for (auto *item : movable) {
item->setPos(item->pos() + offset);
m_relative_pos.insert(item, item->pos());
}
//The conductors were laid out against the old terminal
//positions, so re-route them before anything is drawn.
const QList<Conductor *> conductors = m_content.conductors(DiagramContent::AnyConductor);
for (auto *conductor : conductors) {
conductor->updatePath();
}
//The baseline is known now, so moveTo() does not have to
//capture one from the first mouse movement.
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
m_diagram->clearSelection();
for (auto *item : movable) {
item->setSelected(true);
}
if (!m_diagram->views().isEmpty()) {
if (auto *view = m_diagram->views().at(0)) {
if (const auto qde = QETApp::diagramEditorAncestorOf(view)) {
m_status_bar = qde->statusBar();
}
}
}
showHint();
m_running = true;
}
/**
@brief DiagramEventAddPaste::~DiagramEventAddPaste
If the placement never finished -- the editor closed, or another tool took
over -- the items are still on the folio with nothing on the undo stack to
account for them, so take them away.
*/
DiagramEventAddPaste::~DiagramEventAddPaste()
{
if (!m_finished && m_diagram) {
removeItems();
m_finished = true;
m_running = false;
}
if (m_status_bar) {
m_status_bar->clearMessage();
}
//Give the context menu back. init() turned it off so a right
//click would cancel the placement instead of opening a menu over
//it, and nothing turned it on again: one Ctrl+V left the folio's
//right-click menu dead for the rest of the session, taking
//"Coller ici", "Collage multiple" and the folio properties with
//it. Every other DiagramEvent* class restores it here; this one
//did not.
if (m_diagram) {
const auto views = m_diagram->views();
for (auto *view : views) {
view->setContextMenuPolicy(Qt::DefaultContextMenu);
}
}
}
/**
@brief DiagramEventAddPaste::clipboardHasDiagram
@return true if the clipboard holds a diagram fragment
*/
bool DiagramEventAddPaste::clipboardHasDiagram()
{
return Diagram::clipboardMayContainDiagram();
}
/**
@brief DiagramEventAddPaste::init
Suppress the context menu while placing, so a right click can cancel
instead of opening a menu over the items being positioned.
*/
void DiagramEventAddPaste::init()
{
if (!m_diagram) return;
const auto views = m_diagram->views();
for (auto *view : views) {
view->setContextMenuPolicy(Qt::NoContextMenu);
}
}
void DiagramEventAddPaste::showHint()
{
if (m_status_bar) {
m_status_bar->showMessage(
tr("Cliquez pour poser le collage, Échap ou clic droit pour annuler",
"status bar tip while positioning a paste"));
}
}
/**
@brief DiagramEventAddPaste::moveTo
Compute a grid-snapped delta from the initial cursor position and
apply it to every item's stored position. Working from a delta
against a fixed baseline, rather than from the previous position,
keeps all items exactly on grid points regardless of modifier keys
and stops rounding accumulating over a long drag.
*/
void DiagramEventAddPaste::moveTo(const QPointF &scene_pos)
{
QSettings settings;
const int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
const int yGrid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
const auto snapGrid = [xGrid, yGrid](const QPointF &p) -> QPointF {
return QPointF(
qRound(p.x() / xGrid) * xGrid,
qRound(p.y() / yGrid) * yGrid);
};
//The constructor normally sets the baseline, having just put the
//group there. This covers the case where it could not -- no view
//to map through -- by taking the first cursor position instead.
//Tested with m_baseline_captured rather than
//m_initial_cursor.isNull(), which silently re-baselines when the
//baseline is legitimately scene (0,0).
if (!m_baseline_captured) {
m_initial_cursor = snapGrid(scene_pos);
m_baseline_captured = true;
return;
}
const QPointF delta = snapGrid(scene_pos) - m_initial_cursor;
for (auto it = m_relative_pos.constBegin() ; it != m_relative_pos.constEnd() ; ++it) {
if (it.key()) {
it.key()->setPos(it.value() + delta);
}
}
//Update conductor paths so they follow the moved terminals.
const QList<Conductor *> conductors = m_content.conductors(DiagramContent::AnyConductor);
for (auto *conductor : conductors) {
conductor->updatePath();
}
}
void DiagramEventAddPaste::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_running) return;
moveTo(event->scenePos());
event->setAccepted(true);
}
void DiagramEventAddPaste::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_running) return;
//Swallowed so the press cannot start a rubber band or drag an item
//out of the group; the release is what decides.
event->setAccepted(true);
}
void DiagramEventAddPaste::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_running) return;
event->setAccepted(true);
if (event->button() == Qt::LeftButton) {
commit();
} else if (event->button() == Qt::RightButton) {
cancel();
}
}
void DiagramEventAddPaste::keyPressEvent(QKeyEvent *event)
{
if (!m_running) return;
switch (event->key()) {
case Qt::Key_Escape:
event->setAccepted(true);
cancel();
break;
//Return and Enter drop the paste where it stands, so the whole
//operation can be completed without a mouse.
case Qt::Key_Return:
case Qt::Key_Enter:
event->setAccepted(true);
commit();
break;
default:
break;
}
}
/**
@brief DiagramEventAddPaste::commit
Hand the items to the undo stack where they stand.
PasteDiagramCommand's first redo() does not add the items to the scene --
it assumes they are already there, which is what Diagram::fromXml did when
this started. So pushing it here adopts them rather than duplicating them.
*/
void DiagramEventAddPaste::commit()
{
if (m_finished || !m_diagram) return;
m_finished = true;
m_running = false;
m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, m_content));
emit finish();
}
/**
@brief DiagramEventAddPaste::cancel
Take the items back off the folio. Nothing was pushed to the undo stack,
so there is nothing to undo afterwards.
*/
void DiagramEventAddPaste::cancel()
{
if (m_finished || !m_diagram) return;
removeItems();
m_finished = true;
m_running = false;
emit finish(); // only the user-driven path signals
}
void DiagramEventAddPaste::removeItems()
{
const QList<Conductor *> conductors = m_content.conductors(DiagramContent::AnyConductor);
for (auto *conductor : conductors) {
m_diagram->removeItem(conductor);
delete conductor;
}
const QList<QGraphicsItem *> rest = m_content.items(MovableItems);
for (auto *item : rest) {
m_diagram->removeItem(item);
delete item;
}
m_content.clear();
m_relative_pos.clear();
}
@@ -0,0 +1,99 @@
/*
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 DIAGRAMEVENTADDPASTE_H
#define DIAGRAMEVENTADDPASTE_H
#include "diagrameventinterface.h"
#include "../diagramcontent.h"
#include <QHash>
#include <QPointer>
class QStatusBar;
/**
@brief The DiagramEventAddPaste class
Paste the clipboard as a placement you can still move.
The pasted items are added straight away and follow the cursor until a
left click drops them; Escape or a right click takes them away again.
This is the same interaction as placing a new element, so a paste behaves
like every other "put something on the folio" action.
The items are the real ones from the start, not a preview: Diagram::fromXml
creates them, this class moves them, and PasteDiagramCommand is pushed only
once they are dropped. That keeps one copy of the paste logic rather than
two, and means a cancelled paste leaves nothing on the undo stack.
*/
class DiagramEventAddPaste : public DiagramEventInterface
{
Q_OBJECT
public:
///Items with a position of their own. Conductors are left out
///deliberately: they are drawn from their terminals, so they
///follow when the elements they attach to move.
static const int MovableItems =
DiagramContent::Elements
| DiagramContent::TextFields
| DiagramContent::Images
| DiagramContent::Shapes
| DiagramContent::Tables
| DiagramContent::TerminalStrip;
DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos);
~DiagramEventAddPaste() override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void init() override;
///@return true if the clipboard holds something this can paste.
static bool clipboardHasDiagram();
private:
void moveTo(const QPointF &scene_pos);
void commit();
void cancel();
void showHint();
void removeItems();
DiagramContent m_content;
///Each movable item's position relative to the group's top left,
///taken once so repeated moves cannot accumulate rounding drift.
QHash<QGraphicsItem *, QPointF> m_relative_pos;
///Where the group's grid-snapped top left was put when the paste
///started, in scene coordinates -- the cursor, so the copy
///appears under the pointer rather than on top of what was
///copied.
QPointF m_group_origin;
///Cursor position (scene coords) the delta-based movement in
///moveTo() measures from. Equal to m_group_origin, since the
///group is placed at the cursor.
QPointF m_initial_cursor;
///Whether m_initial_cursor holds a usable baseline. A flag rather
///than testing m_initial_cursor.isNull(), which cannot tell "not
///set yet" from a baseline that is legitimately scene (0,0).
bool m_baseline_captured{false};
QPointer<QStatusBar> m_status_bar;
bool m_finished{false};
};
#endif // DIAGRAMEVENTADDPASTE_H
@@ -0,0 +1,427 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagrameventaddpath.h"
#include "../diagram.h"
#include "../lastusedstyle.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include <QGraphicsLineItem>
#include <QGraphicsSceneMouseEvent>
#include <QKeyEvent>
#include <QLineF>
#include <QStatusBar>
#include <QTimer>
/**
@brief DiagramEventAddPath::DiagramEventAddPath
@param diagram : the diagram where this event must operate
*/
DiagramEventAddPath::DiagramEventAddPath(Diagram *diagram) :
DiagramEventInterface(diagram),
m_shape_item (nullptr),
m_help_horiz (nullptr),
m_help_verti (nullptr)
{
m_running = true;
init();
// Deferred for the same reason as DiagramEventAddShape's own
// constructor-time hint: Diagram::setEventInterface() destroys
// whatever tool was previously active *after* this constructor
// returns, and that tool's own destructor clears the status bar --
// an immediate show here would just get wiped out moments later.
QTimer::singleShot(0, this, [this]() { showHint(); });
}
DiagramEventAddPath::~DiagramEventAddPath()
{
if ((m_running || m_abort) && m_shape_item)
{
m_diagram->removeItem(m_shape_item);
delete m_shape_item;
}
delete m_help_horiz;
delete m_help_verti;
if (m_diagram && !m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
foreach (QGraphicsView *v, m_diagram->views())
v->setContextMenuPolicy(Qt::DefaultContextMenu);
}
/**
@brief DiagramEventAddPath::showHint
Re-asserted on every move within the canvas (see mouseMoveEvent), not
just once at activation: Qt's own built-in "show an action's
statusTip on hover" has its own internal "restore whatever was there
before" logic for when the hover ends. Since this message is first
shown *during* that same hover session (the user is still over the
toolbar icon when the deferred constructor-time call above fires),
Qt's hover-tracking has no idea this code changed the status bar in
the meantime -- the moment the mouse leaves the icon for the canvas,
it silently restores whatever it remembers being there before its
own tip started, overwriting this one. Re-showing it on every move
within the canvas simply outlasts that one-time restore.
*/
void DiagramEventAddPath::showHint() const
{
if (!m_diagram || m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(tr("Clic: point anguleux. Cliquer-glisser: point courbe. "
"Clic sur le premier point: fermer. Échap/Entrée: terminer. "
"Clic droit: annuler le dernier point."));
}
void DiagramEventAddPath::init()
{
foreach (QGraphicsView *v, m_diagram->views())
v->setContextMenuPolicy(Qt::NoContextMenu);
}
QPointF DiagramEventAddPath::snapped(const QPointF &scenePos, Qt::KeyboardModifiers mods) const
{
return mods == Qt::ControlModifier ? scenePos : Diagram::snapToGrid(scenePos);
}
int DiagramEventAddPath::confirmedNodeCount() const
{
// The trailing element is always the live preview while m_shape_item
// exists; with no shape yet there are no nodes of any kind.
return m_shape_item ? qMax(0, m_nodes.size() - 1) : 0;
}
/**
@brief DiagramEventAddPath::mousePressEvent
Left click: on the very first click, creates the shape with a real
node *and* an immediate preview node at the same spot, so a segment
exists (even if zero-length) from the start rather than requiring a
second click before anything is visible. On later clicks: either
confirms the live preview into a real point and appends a fresh one
for the next segment, or -- if close enough to the first node --
closes the path.
*/
void DiagramEventAddPath::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (Q_UNLIKELY(m_diagram->isReadOnly()))
return;
if (event->button() != Qt::LeftButton)
{
// Accept every button while this tool is running, not just the
// one it actually acts on -- Diagram::mousePressEvent falls
// through to Qt's own default scene handling for anything left
// unaccepted, which is exactly the kind of competing control
// this tool can't afford while it's supposed to have exclusive
// ownership of input.
event->setAccepted(true);
return;
}
const QPointF pos = snapped(event->scenePos(), event->modifiers());
if (!m_shape_item)
{
m_shape_item = new QetShapeItem(pos, pos, QetShapeItem::Path);
if (LastUsedStyle::hasShapePen())
m_shape_item->setPen(LastUsedStyle::shapePen());
if (LastUsedStyle::hasShapeBrush())
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
m_diagram->addItem(m_shape_item);
// Handles only ever get built for a selected item.
m_shape_item->setSelected(true);
QetShapeItem::PathNode node;
node.anchor = pos;
m_nodes << node;
m_nodes << node; // live preview, tracks the mouse from here on
m_shape_item->setPathNodes(m_nodes);
m_shape_item->enableNodeEditMode();
m_dragging_node = 0;
event->setAccepted(true);
return;
}
if (confirmedNodeCount() >= 2 && nearFirstNode(pos))
{
finishPath(true);
event->setAccepted(true);
return;
}
// Confirm the preview node as a real point, then append a fresh
// preview (a plain Corner, not a copy of the just-confirmed node's
// kind/handles) for the segment after it.
m_dragging_node = m_nodes.size() - 1;
m_nodes[m_dragging_node].anchor = pos;
QetShapeItem::PathNode preview;
preview.anchor = pos;
m_nodes << preview;
m_shape_item->setPathNodes(m_nodes);
m_shape_item->enableNodeEditMode();
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseMoveEvent
Two mutually exclusive behaviours, matching whether a button is held:
with the left button down on a just-placed node, dragging shapes that
node's handles (same convention as editing an existing node -- see
QetShapeItem::dragPathControlHandle()). With no button held, the
trailing preview node instead tracks the mouse, giving the live
rubber-band segment.
*/
void DiagramEventAddPath::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
updateHelpCross(event->scenePos());
showHint();
if (m_shape_item)
{
const QPointF pos = snapped(event->scenePos(), event->modifiers());
if (m_dragging_node >= 0 && (event->buttons() & Qt::LeftButton))
{
QetShapeItem::PathNode &node = m_nodes[m_dragging_node];
const QPointF delta = pos - node.anchor;
// A small threshold so an accidental few-pixel wobble on
// what was meant to be a plain click doesn't silently add
// curve handles the user never intended.
if (QLineF(QPointF(), delta).length() > 3.0)
{
node.kind = QetShapeItem::NodeKind::Smooth;
node.outHandle = delta;
node.inHandle = -delta;
}
else
{
node.kind = QetShapeItem::NodeKind::Corner;
node.outHandle.reset();
node.inHandle.reset();
}
m_shape_item->setPathNodes(m_nodes);
}
else if (!(event->buttons() & Qt::LeftButton) && !m_nodes.isEmpty())
{
m_nodes.last().anchor = pos;
m_shape_item->setPathNodes(m_nodes);
}
}
// Ours unconditionally while running: a stray, unaccepted move event
// falling through to Qt's default handling risks it dragging our
// selected, movable in-progress shape out from under the tool.
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseReleaseEvent
Left release just ends the current node's drag phase (the trailing
preview resumes tracking the mouse on the next move). Right release
steps back one *confirmed* point (the preview is left alone), or
cancels outright once only one remains, or exits the tool entirely if
nothing is in progress at all.
*/
void DiagramEventAddPath::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
m_dragging_node = -1;
}
else if (event->button() == Qt::RightButton)
{
if (m_shape_item)
{
if (confirmedNodeCount() > 1)
{
m_nodes.remove(m_nodes.size() - 2); // the last *confirmed* node; keep the trailing preview
m_shape_item->setPathNodes(m_nodes);
}
else
{
cancelPath();
}
}
else
{
m_running = false;
emit finish();
}
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseDoubleClickEvent
A double-click is a press, release, press, release, doubleclick
sequence -- the second press already confirmed the preview into a
duplicate point (mousePressEvent can't distinguish a double-click
from two single clicks in the same place) and appended a fresh
preview after it. Dropping the last node here removes that fresh
preview; finishPath()'s own trailing-preview removal then removes the
duplicate underneath it, leaving only the genuinely-placed points.
*/
void DiagramEventAddPath::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
if (m_shape_item && event->button() == Qt::LeftButton && !m_nodes.isEmpty())
{
m_nodes.removeLast();
finishPath(false);
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::keyPressEvent
Escape or Enter finish the path open once at least two real points
exist; Escape with fewer (or none placed at all) cancels/exits
instead, since there's nothing meaningful to keep.
*/
void DiagramEventAddPath::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Escape)
{
if (m_shape_item && confirmedNodeCount() >= 2)
finishPath(false);
else if (m_shape_item)
cancelPath();
else
{
m_running = false;
emit finish();
}
event->accept();
}
else if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
&& m_shape_item && confirmedNodeCount() >= 2)
{
finishPath(false);
event->accept();
}
}
/**
@brief DiagramEventAddPath::finishPath
Strips the trailing live-preview node, commits the in-progress path
onto the undo stack, and resets so the tool is ready to draw another
one immediately -- matching every other shape tool's own behaviour
after finishing a shape.
*/
void DiagramEventAddPath::finishPath(bool closed)
{
if (!m_shape_item)
return;
if (!m_nodes.isEmpty())
m_nodes.removeLast();
if (m_nodes.size() < 2)
{
cancelPath();
return;
}
if (closed)
m_shape_item->setClosed(true);
m_shape_item->setPathNodes(m_nodes);
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_shape_item, m_diagram));
m_shape_item = nullptr;
m_nodes.clear();
m_dragging_node = -1;
}
/**
@brief DiagramEventAddPath::cancelPath
Discards the in-progress path entirely -- nothing worth keeping (an
empty or single-point path isn't a usable shape).
*/
void DiagramEventAddPath::cancelPath()
{
if (m_shape_item)
{
m_diagram->removeItem(m_shape_item);
delete m_shape_item;
m_shape_item = nullptr;
}
m_nodes.clear();
m_dragging_node = -1;
}
/**
@brief DiagramEventAddPath::nearFirstNode
m_shape_item's pos()/transform() stay at their identity defaults for
its entire construction here -- nothing during drawing ever touches
them -- so the first node's anchor, stored in local coordinates, is
directly comparable to a scene position without any mapping.
*/
bool DiagramEventAddPath::nearFirstNode(const QPointF &scenePos) const
{
if (m_nodes.isEmpty())
return false;
return QLineF(m_nodes.first().anchor, scenePos).length() <= CLOSE_THRESHOLD;
}
/**
@brief DiagramEventAddPath::updateHelpCross
Same crosshair guide as every other shape tool (see
DiagramEventAddShape::updateHelpCross) -- duplicated rather than
shared, since the two classes don't otherwise share a common base
beyond DiagramEventInterface.
*/
void DiagramEventAddPath::updateHelpCross(const QPointF &p)
{
if (!m_help_horiz || !m_help_verti)
{
QPen pen;
pen.setWidthF(0.4);
pen.setCosmetic(true);
pen.setColor(Diagram::background_color == Qt::darkGray ? Qt::lightGray : Qt::darkGray);
QRectF rect = m_diagram->border_and_titleblock.insideBorderRect();
if (!m_help_horiz)
{
m_help_horiz = new QGraphicsLineItem(rect.topLeft().x(), 0, rect.topRight().x(), 0);
m_help_horiz->setPen(pen);
m_diagram->addItem(m_help_horiz);
}
if (!m_help_verti)
{
m_help_verti = new QGraphicsLineItem(0, rect.topLeft().y(), 0, rect.bottomLeft().y());
m_help_verti->setPen(pen);
m_diagram->addItem(m_help_verti);
}
}
QPointF point = Diagram::snapToGrid(p);
m_help_horiz->setY(point.y());
m_help_verti->setX(point.x());
}
@@ -0,0 +1,81 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DIAGRAMEVENTADDPATH_H
#define DIAGRAMEVENTADDPATH_H
#include "../qetgraphicsitem/qetshapeitem.h"
#include "diagrameventinterface.h"
class QGraphicsLineItem;
/**
@brief The DiagramEventAddPath class
Pen tool: interactively draw a new Path (Bezier) shape, following the
same vocabulary every vector editor's pen tool uses:
- click places a Corner node;
- press-drag-release places a Smooth node, with the drag defining a
pair of mirrored handles (same convention as
QetShapeItem::dragPathControlHandle's "Smooth" mirroring);
- clicking back on the first node closes the path;
- double-click, Enter, or Escape (once 2+ nodes exist) finishes it
open;
- right-click steps back one node;
- right-click or Escape with nothing placed yet cancels the tool.
While running, m_nodes always carries one extra "preview" node at the
end, tracking the mouse so a live rubber-band segment is always
visible -- confirmedNodeCount() excludes it; every public gesture
handler is responsible for stripping it before treating the list as
"the path so far" (see finishPath(), which does this once for every
finishing gesture).
*/
class DiagramEventAddPath : public DiagramEventInterface
{
Q_OBJECT
public:
DiagramEventAddPath(Diagram *diagram);
~DiagramEventAddPath() override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void init() override;
private:
void updateHelpCross (const QPointF &p);
void showHint () const;
void finishPath (bool closed);
void cancelPath ();
bool nearFirstNode (const QPointF &scenePos) const;
int confirmedNodeCount () const; // m_nodes always carries one trailing "preview" node while running; this excludes it
QPointF snapped (const QPointF &scenePos, Qt::KeyboardModifiers mods) const;
QetShapeItem *m_shape_item;
QVector<QetShapeItem::PathNode> m_nodes;
int m_dragging_node = -1;
QGraphicsLineItem *m_help_horiz, *m_help_verti;
// Scene units within which a click on an existing path is
// treated as "on the first node" and closes the shape, rather
// than adding yet another node right next to it.
static constexpr qreal CLOSE_THRESHOLD = 12.0;
};
#endif // DIAGRAMEVENTADDPATH_H
+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
+286 -5
View File
@@ -18,8 +18,17 @@
#include "diagrameventaddshape.h"
#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
@@ -35,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(); });
}
/**
@@ -49,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
@@ -66,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);
}
@@ -77,7 +217,25 @@ 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()) {
m_shape_item->setPen(LastUsedStyle::shapePen());
}
if (LastUsedStyle::hasShapeBrush()) {
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
}
m_diagram->addItem (m_shape_item);
updateCreationHint();
event->setAccepted(true);
return;
}
@@ -85,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
@@ -116,18 +276,82 @@ 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.
Any other key falls back to standard handling, e.g. pressing ESC aborts the operation.
*/
void DiagramEventAddShape::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Shift || event->key() == Qt::Key_Control) {
reapplyLastPosition(event);
event->setAccepted(true);
} else {
DiagramEventInterface::keyPressEvent(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
@@ -146,7 +370,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
@@ -158,6 +382,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;
}
@@ -194,6 +420,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);
}
}
@@ -204,6 +432,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
+10 -1
View File
@@ -29,7 +29,14 @@
*/
DiagramEventAddText::DiagramEventAddText(Diagram *diagram) :
DiagramEventInterface(diagram)
{}
{
//The tool is armed from the moment it is attached: the next left
//click places a text. DiagramView::keyPressEvent() asks
//Diagram::eventInterfaceIsRunning() -- which is isRunning(), i.e.
//m_running -- before letting Escape through to the tool, so without
//this the view swallows Escape and the tool cannot be cancelled.
m_running = true;
}
/**
@brief DiagramEventAddText::~DiagramEventAddText
@@ -52,6 +59,8 @@ void DiagramEventAddText::mousePressEvent(QGraphicsSceneMouseEvent *event)
event->scenePos()));
text->setTextInteractionFlags(Qt::TextEditorInteraction);
text->setFocus(Qt::MouseFocusReason);
//Placed: the tool is done before it announces it.
m_running = false;
emit finish();
event->setAccepted(true);
}
+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
+132 -71
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagramview.h"
#include "lastusedstyle.h"
#include "qetproject.h"
#include "QPropertyUndoCommand/qpropertyundocommand.h"
#include "diagramcommands.h"
@@ -38,7 +39,10 @@
#include "ElementsCollection/xmlelementcollection.h"
#include "NameList/nameslist.h"
#include "elementdialog.h"
#include <QApplication>
#include <QDropEvent>
#include <QPainter>
#include <QPointer>
/**
Constructeur
@@ -46,7 +50,7 @@
@param parent Le QWidget parent de cette vue de schema
*/
DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
QGraphicsView (parent),
PaletteGraphicsView (parent),
m_diagram (diagram)
{
grabGesture(Qt::PinchGesture);
@@ -79,7 +83,7 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
m_diagram->loadCndFolioSeq();
m_paste_here = new QAction(QET::Icons::EditPaste, tr("Coller ici", "context menu action"), this);
connect(m_paste_here, SIGNAL(triggered()), this, SLOT(pasteHere()));
connect(m_paste_here, &QAction::triggered, this, &DiagramView::pasteHere);
m_multi_paste = new QAction(QET::Icons::EditPaste, tr("Collage multiple"), this);
connect(m_multi_paste, &QAction::triggered, [this]() {
@@ -89,7 +93,7 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
// Setup the action to create a template
m_create_template = new QAction(tr("Créer un template", "context menu action"), this);
connect(m_create_template, SIGNAL(triggered()), this, SLOT(createTemplateFromSelection()));
connect(m_create_template, &QAction::triggered, this, &DiagramView::createTemplateFromSelection);
//setup three separators, to be use in context menu
for(int i=0 ; i<3 ; ++i)
@@ -98,10 +102,10 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
m_separators.last()->setSeparator(true);
}
connect(m_diagram, SIGNAL(showDiagram(Diagram*)), this, SIGNAL(showDiagram(Diagram*)));
connect(m_diagram, SIGNAL(sceneRectChanged(QRectF)), this, SLOT(adjustSceneRect()));
connect(&(m_diagram -> border_and_titleblock), SIGNAL(diagramTitleChanged(const QString &)), this, SLOT(updateWindowTitle()));
connect(diagram, SIGNAL(findElementRequired(ElementsLocation)), this, SIGNAL(findElementRequired(ElementsLocation)));
connect(m_diagram, &Diagram::showDiagram, this, &DiagramView::showDiagram);
connect(m_diagram, &QGraphicsScene::sceneRectChanged, this, &DiagramView::adjustSceneRect);
connect(&(m_diagram -> border_and_titleblock), &BorderTitleBlock::informationChanged, this, &DiagramView::updateWindowTitle);
connect(diagram, &Diagram::findElementRequired, this, &DiagramView::findElementRequired);
QShortcut *edit_conductor_color_shortcut = new QShortcut(QKeySequence(Qt::Key_F2), this);
connect(edit_conductor_color_shortcut, &QShortcut::activated, [this]()
@@ -121,7 +125,7 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
ConductorProperties initial_properties = edited_conductor->properties();
// prepare a color dialog showing the initial conductor color
QColorDialog *color_dialog = new QColorDialog(this);
QPointer<QColorDialog> color_dialog = new QColorDialog(this);
color_dialog->setWindowTitle(tr("Choisir la nouvelle couleur de ce conducteur"));
#ifdef Q_OS_MACOS
color_dialog -> setWindowFlags(Qt::Sheet);
@@ -143,8 +147,14 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
QPropertyUndoCommand *undo = new QPropertyUndoCommand(edited_conductor, "properties", old_value, new_value);
undo->setText(tr("Modifier les propriétés d'un conducteur", "undo caption"));
m_diagram->undoStack().push(undo);
// remember it for the next conductor drawn this session,
// the way LastUsedStyle already does for shapes (#879)
LastUsedStyle::setConductorColor(new_color);
}
}
if (color_dialog)
delete color_dialog;
});
}
@@ -211,11 +221,7 @@ void DiagramView::handleElementDrop(QDropEvent *event)
}
QPointF drop_pos;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
drop_pos = mapToScene(event->pos());
#else
drop_pos = event->position();
#endif
drop_pos = mapToScene(event->position().toPoint());
if (location.path().endsWith(".qetmak")) {
diagram()->setEventInterface(new DiagramEventAddMacro(location, diagram(), drop_pos));
@@ -290,17 +296,8 @@ void DiagramView::handleTextDrop(QDropEvent *e) {
iti -> setHtml (e -> mimeData() -> text());
}
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
m_diagram->undoStack().push(new AddGraphicsObjectCommand(
iti, m_diagram, mapToScene(e->pos())));
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
m_diagram->undoStack().push(new AddGraphicsObjectCommand(
iti, m_diagram, e->position()));
#endif
iti, m_diagram, mapToScene(e->position().toPoint())));
}
/**
@@ -334,6 +331,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 +462,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 +512,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 +573,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);
}
@@ -624,14 +607,7 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
QMenu *menu = new QMenu(this);
menu->addAction(act);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
menu->popup(e->globalPos());
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
menu->popup(e->pos());
#endif
menu->popup(e->globalPosition().toPoint());
}
m_free_rubberbanding = false;
@@ -720,6 +696,27 @@ void DiagramView::focusInEvent(QFocusEvent *e) {
}
}
/**
@brief DiagramView::focusNextPrevChild
By default, QWidget intercepts Tab/Shift+Tab to move keyboard focus to
the next/previous widget before a key press event is ever generated,
which would silently swallow the diagram's Tab-based item-selection
cycling (see Diagram::event()). Returning false here disables that
automatic focus-chain traversal for this view, so Tab/Shift+Tab reach
keyPressEvent() (and from there, the scene) as ordinary key presses
instead.
@return always false
*/
bool DiagramView::focusNextPrevChild(bool next)
{
//Escape asked for focus to leave; allow exactly this one traversal.
if (m_releasing_focus) {
m_releasing_focus = false;
return QGraphicsView::focusNextPrevChild(next);
}
return false;
}
/**
@brief DiagramView::keyPressEvent
Handles "key press" events. Reimplemented here to switch to visualisation
@@ -735,6 +732,23 @@ void DiagramView::keyPressEvent(QKeyEvent *e)
DiagramContent dc(m_diagram);
switch(e -> key())
{
case Qt::Key_Escape:
//Tab cycles the folio's items rather than moving focus (see
//focusNextPrevChild above), so without this there would be no
//way off the canvas for someone working without a mouse.
//Escape steps back out: first it drops the selection, then it
//hands focus to the next widget.
if (m_diagram && m_diagram->eventInterfaceIsRunning()) {
QGraphicsView::keyPressEvent(e); // let the active tool see it
return;
}
if (m_diagram && !m_diagram->selectedItems().isEmpty()) {
m_diagram->clearSelection();
} else {
m_releasing_focus = true;
focusNextChild();
}
return;
case Qt::Key_PageUp:
current_project->changeTabUp();
return;
@@ -1069,6 +1083,16 @@ bool DiagramView::event(QEvent *e) {
return(QGraphicsView::event(e));
}
/**
@brief DiagramView::paintingInverted
Reimplemented from PaletteGraphicsView: tell the diagram it is being
drawn for an inverted display, so it softens its grid.
*/
void DiagramView::paintingInverted(bool inverted)
{
m_diagram->setInvertedLightness(inverted);
}
/**
@brief DiagramView::paintEvent
Reimplemented from QGraphicsView
@@ -1076,7 +1100,7 @@ bool DiagramView::event(QEvent *e) {
*/
void DiagramView::paintEvent(QPaintEvent *event)
{
QGraphicsView::paintEvent(event);
PaletteGraphicsView::paintEvent(event);
if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
{
@@ -1236,30 +1260,70 @@ QList<QAction *> DiagramView::contextMenuActions() const
*/
void DiagramView::contextMenuEvent(QContextMenuEvent *e)
{
QGraphicsView::contextMenuEvent(e);
if(e->isAccepted())
return;
QPoint menu_pos = e->pos();
QPoint menu_global_pos = e->globalPos();
//A context menu raised from the keyboard (the Menu key, or
//Shift+F10) carries no useful position: Qt does not aim it at the
//selection. Two things then went wrong. QGraphicsView handed the
//event to whichever item held focus, which answered with its own
//generic Undo/Cut/Copy menu and accepted it, so the folio's real
//menu was never built; and had it got past that, itemAt() below
//would have looked up an unrelated point.
//
//So a keyboard-raised menu is built here directly rather than being
//offered to the items first, and aimed at the selection when there
//is one. The keyboard then gets the folio's menu, which is what a
//right-click gets.
const bool from_keyboard = e->reason() == QContextMenuEvent::Keyboard;
if (auto qgi = m_diagram->itemAt(mapToScene(e->pos()), transform()))
if (from_keyboard)
{
if (!qgi->isSelected()) {
m_diagram->clearSelection();
//Aim at the selection when there is one, so the menu appears
//beside what it acts on. With nothing selected there is nothing
//to aim at, so use the middle of the view -- the folio's own
//menu is still the right menu to show.
const auto selection = m_diagram->selectedItems();
if (!selection.isEmpty())
{
QRectF selection_rect;
for (auto *item : selection) {
selection_rect |= item->sceneBoundingRect();
}
menu_pos = mapFromScene(selection_rect.center());
}
else
{
menu_pos = viewport()->rect().center();
}
menu_global_pos = viewport()->mapToGlobal(menu_pos);
}
else
{
QGraphicsView::contextMenuEvent(e);
if(e->isAccepted())
return;
// At this step qgi can be deleted for example if qgi is a QetGraphicsHandlerItem.
// When we call clearSelection the parent item of the handler
// is deselected and so delete all handlers, in this case,
// qgi become a dangling pointer.
// we need to call again itemAt.
if (auto item_ = m_diagram->itemAt(mapToScene(e->pos()), transform())) {
item_->setSelected(true);
if (auto qgi = m_diagram->itemAt(mapToScene(menu_pos), transform()))
{
if (!qgi->isSelected()) {
m_diagram->clearSelection();
}
// At this step qgi can be deleted for example if qgi is a QetGraphicsHandlerItem.
// When we call clearSelection the parent item of the handler
// is deselected and so delete all handlers, in this case,
// qgi become a dangling pointer.
// we need to call again itemAt.
if (auto item_ = m_diagram->itemAt(mapToScene(menu_pos), transform())) {
item_->setSelected(true);
}
}
}
if (m_diagram->selectedItems().isEmpty())
{
m_paste_here_pos = e->pos();
m_paste_here_pos = menu_pos;
m_paste_here->setEnabled(Diagram::clipboardMayContainDiagram());
}
@@ -1268,7 +1332,7 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e)
{
QMenu *context_menu = new QMenu(this);
context_menu->addActions(list);
context_menu->popup(e->globalPos());
context_menu->popup(menu_global_pos);
e->accept();
}
}
@@ -1355,9 +1419,6 @@ void DiagramView::createTemplateFromSelection()
QFile file(full_path);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
out.setCodec("UTF-8"); // Qt6 QTextStream defaults to UTF-8
#endif
out << macro_doc.toString(4);
file.close();
qDebug() << "Template successfully saved to:" << full_path;
+12 -2
View File
@@ -22,7 +22,7 @@
#include "titleblock/templatelocation.h"
#include <QClipboard>
#include <QGraphicsView>
#include "palettegraphicsview.h"
class Conductor;
class Diagram;
@@ -35,7 +35,7 @@ class QGestureEvent;
This class provides a widget to render an electric diagram in an editable,
interactive way.
*/
class DiagramView : public QGraphicsView
class DiagramView : public PaletteGraphicsView
{
Q_OBJECT
@@ -80,7 +80,11 @@ class DiagramView : public QGraphicsView
void keyPressEvent(QKeyEvent *) override;
void keyReleaseEvent(QKeyEvent *) override;
bool event(QEvent *) override;
bool focusNextPrevChild(bool next) override;
///Set for one call only, by the Escape handler, to let focus leave the view.
bool m_releasing_focus = false;
void paintEvent(QPaintEvent *event) override;
void paintingInverted(bool inverted) override;
void mousePressEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
void mouseReleaseEvent(QMouseEvent *) override;
@@ -103,6 +107,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
+16 -12
View File
@@ -69,10 +69,8 @@ ElementScene::ElementScene(QETElementEditor *editor, QObject *parent) :
initPasteArea();
m_undo_stack.setClean();
m_decorator_lock = new QMutex();
connect(&m_undo_stack, SIGNAL(indexChanged(int)),
this, SLOT(managePrimitivesGroups()));
connect(this, SIGNAL(selectionChanged()),
this, SLOT(managePrimitivesGroups()));
connect(&m_undo_stack, &QUndoStack::indexChanged, this, &ElementScene::managePrimitivesGroups);
connect(this, &ElementScene::selectionChanged, this, &ElementScene::managePrimitivesGroups);
}
/**
@@ -102,8 +100,7 @@ void ElementScene::setElementData(ElementData data)
ElementScene::~ElementScene()
{
//Disconnect to avoid crash, see bug report N° 122.
disconnect(&m_undo_stack, SIGNAL(indexChanged(int)),
this, SLOT(managePrimitivesGroups()));
disconnect(&m_undo_stack, &QUndoStack::indexChanged, this, &ElementScene::managePrimitivesGroups);
delete m_decorator_lock;
if (m_event_interface)
@@ -124,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;
@@ -317,6 +314,10 @@ void ElementScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
*/
void ElementScene::drawForeground(QPainter *p, const QRectF &)
{
if (!m_hotspot_visible) {
return;
}
p -> save();
// desactive tout antialiasing, sauf pour le texte
@@ -478,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)
{
@@ -903,8 +909,8 @@ void ElementScene::slot_editAuthorInformations()
QDialogButtonBox::Ok
| QDialogButtonBox::Cancel);
dialog_layout -> addWidget(dialog_buttons);
connect(dialog_buttons, SIGNAL(accepted()),&dialog_author, SLOT(accept()));
connect(dialog_buttons, SIGNAL(rejected()),&dialog_author, SLOT(reject()));
connect(dialog_buttons, &QDialogButtonBox::accepted, &dialog_author, &QDialog::accept);
connect(dialog_buttons, &QDialogButtonBox::rejected, &dialog_author, &QDialog::reject);
// start the dialogue
// lance le dialogue
@@ -1395,9 +1401,7 @@ void ElementScene::managePrimitivesGroups()
if (!m_decorator)
{
m_decorator = new ElementPrimitiveDecorator();
connect(m_decorator,
SIGNAL(actionFinished(ElementEditionCommand*)),
this, SLOT(stackAction(ElementEditionCommand *)));
connect(m_decorator, &ElementPrimitiveDecorator::actionFinished, this, &ElementScene::stackAction);
addItem(m_decorator);
m_decorator -> hide();
}
+10
View File
@@ -90,6 +90,8 @@ class ElementScene : public QGraphicsScene
m_y_grid;
QPointer<CustomElementGraphicPart> m_single_selected_item;
bool m_hotspot_visible = true;
// methods
public:
@@ -132,6 +134,14 @@ class ElementScene : public QGraphicsScene
QETElementEditor* editor() const;
void addItems(QVector<QGraphicsItem *> items);
void removeItems(QVector<QGraphicsItem *> items);
/// Whether drawForeground() draws the red hotspot cross. On by
/// default (the normal editing view); turned off around a
/// render() call meant to capture the element's actual content
/// only, e.g. exporting to SVG -- the cross is an editing aid,
/// not part of the drawn symbol.
void setHotspotVisible(bool visible) {m_hotspot_visible = visible;}
bool hotspotVisible() const {return m_hotspot_visible;}
protected:
void mouseMoveEvent (QGraphicsSceneMouseEvent *) override;
+23 -27
View File
@@ -37,8 +37,8 @@ ElementView::ElementView(ElementScene *scene, QWidget *parent) :
setResizeAnchor(QGraphicsView::AnchorUnderMouse);
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
zoomReset();
connect(m_scene, SIGNAL(pasteAreaDefined(const QRectF &)), this, SLOT(pasteAreaDefined(const QRectF &)));
connect(m_scene, SIGNAL(needZoomFit()), this, SLOT(zoomFit()));
connect(m_scene, &ElementScene::pasteAreaDefined, this, &ElementView::pasteAreaDefined);
connect(m_scene, &ElementScene::needZoomFit, this, &ElementView::zoomFit);
}
/// Destructeur
@@ -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);
}
@@ -148,6 +148,7 @@ const QDomElement PartDynamicTextField::toXml(QDomDocument &dom_doc) const
root_element.setAttribute("frame", m_frame? "true" : "false");
root_element.setAttribute("text_width", QString::number(m_text_width));
root_element.setAttribute("keep_visual_rotation", m_keep_visual_rotation ? "true" : "false");
root_element.setAttribute("rotation_point_center", m_rotation_point_center ? "true" : "false");
QMetaEnum me = DynamicElementTextItem::textFromMetaEnum();
root_element.setAttribute("text_from", me.valueToKey(m_text_from));
@@ -212,6 +213,7 @@ void PartDynamicTextField::fromXml(const QDomElement &dom_elmt) {
setZValue(dom_elmt.attribute("z", QString::number(zValue())).toDouble());
QGraphicsObject::setRotation(QET::correctAngle(dom_elmt.attribute("rotation", QString::number(0)).toDouble()));
setKeepVisualRotation(dom_elmt.attribute("keep_visual_rotation", "true") == "true"? true : false);
setRotationPointCenter(dom_elmt.attribute("rotation_point_center", "false") == "true"? true : false);
if (dom_elmt.hasAttribute("font")) {
QFont font_;
@@ -492,6 +494,20 @@ bool PartDynamicTextField::keepVisualRotation() const {
return m_keep_visual_rotation;
}
void PartDynamicTextField::setRotationPointCenter(const bool &center)
{
if (center == this->m_rotation_point_center) {
return;
}
m_rotation_point_center = center;
emit rotationPointCenterChanged(center);
}
bool PartDynamicTextField::rotationPointCenter() const {
return m_rotation_point_center;
}
/**
@brief PartDynamicTextField::mouseMoveEvent
@param event
@@ -44,6 +44,7 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged)
Q_PROPERTY(bool keepVisualRotation READ keepVisualRotation WRITE setKeepVisualRotation NOTIFY keepVisualRotationChanged)
Q_PROPERTY(bool rotationPointCenter READ rotationPointCenter WRITE setRotationPointCenter NOTIFY rotationPointCenterChanged)
public:
///PROPERTY
@@ -62,6 +63,7 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
void alignmentChanged(Qt::Alignment alignment);
void fontChanged(QFont font);
void keepVisualRotationChanged(bool keep);
void rotationPointCenterChanged(bool center);
public:
PartDynamicTextField(QETElementEditor *editor, QGraphicsItem *parent = nullptr);
@@ -100,10 +102,12 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
void setFont(const QFont &font);
void setKeepVisualRotation(const bool &keep);
bool keepVisualRotation() const;
void setRotationPointCenter(const bool &center);
bool rotationPointCenter() const;
void setRotation(qreal angle);
void mirror();
void flip();
void mirror(qreal axis_x = 0);
void flip(qreal axis_y = 0);
protected:
@@ -129,7 +133,8 @@ class PartDynamicTextField : public QGraphicsTextItem, public CustomElementPart
bool m_frame = false,
m_first_add = true,
m_block_alignment = false,
m_keep_visual_rotation = false;
m_keep_visual_rotation = false,
m_rotation_point_center = false;
qreal m_text_width = -1;
Qt::Alignment m_alignment = Qt::AlignTop|Qt::AlignLeft;
QRectF m_alignment_rect;
+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();

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