Merge remote-tracking branch 'upstream/master' into wiring-manager

Conflicts resolved in 24 files. Notable decisions:

- ico/icon-themes.qrc: upstream's icon folder restructure kept; the 36
  cable icon entries re-added under the new qet/qet-dark path scheme
  (XML valid, every referenced file exists).
- Toolbar system: upstream's DiagramToolbarSettings now owns the
  toolbar filling. The cable tool is registered as
  diagrameditor.add_cable (generic group loop) and listed on the
  "diagram" toolbar right after the auto-break conductor action,
  separated by a divider, exactly like before the merge.
- Paste: the cable lines of a copy are handed to
  PasteNumberingImport::push() as extra commands, so paste + cable
  lines + the numbering question stay ONE undo step on both paste
  paths (DiagramView::paste and DiagramEventAddPaste::commit).
- PDF links: upstream's deterministic link sorting kept together with
  the cable cross-reference injector (injectLinkTo -> links list).
- Element renumbering: upstream's ElementAutoNumSchemeCommand::renumber
  replaces the old formula-based body of
  QETProject::renumberElementsBySchemeTitle; the cable numbering rule
  API (cableAutoNum, axis priority, rule name) is kept in full.
- Auto-numbering pages: upstream's scheme management kept, the Câbles
  tab added as index 4 (changeToCableTab), both entry points
  changeToCable() and changeToElement() kept side by side.
- SelectAutonumW: dirty tracking and the contextEdited signal moved
  into upstream's insertPartRow() row API, so single-rule mode, the
  rule-removable button and "confirm without change leaves the project
  alone" keep working.
- xrefpropertieswidget: upstream's offset clamping kept; the cable
  branch (Texte field, Police button, hidden display options) kept;
  the stack-overlapping checkbox is hidden for cables.

Verified after the merge: build green (515/515, no errors), ctest
85/86 (only the pre-existing headless tst_menubarkeyboard failure),
offscreen smoke exit 124.
This commit is contained in:
Kellermorph
2026-10-09 14:09:08 +02:00
1767 changed files with 194544 additions and 72846 deletions
@@ -709,12 +709,20 @@ pugi::xml_document ElementsLocation::pugiXml() const
#endif
if (!m_project)
{
//Read through QFile, not pugi's load_file(): on Windows load_file()
//fails once the full path reaches MAX_PATH (260 characters), while
//QFile handles long paths.
QFile file(m_file_system_path);
if (!file.open(QIODevice::ReadOnly)) {
return docu;
}
const QByteArray data = file.readAll();
#ifndef Q_OS_LINUX
if (docu.load_file(m_file_system_path.toStdWString().c_str())) {
if (docu.load_buffer(data.constData(), data.size())) {
docu.save(m_string_stream);
}
#else
docu.load_file(m_file_system_path.toStdWString().c_str());
docu.load_buffer(data.constData(), data.size());
#endif
}
else
@@ -24,6 +24,7 @@
#include <QApplication>
#include <QDir>
#include <QFile>
#include <QPainter>
#include <QPixmap>
#include <QStyle>
@@ -178,7 +179,12 @@ QString FileElementCollectionItem::localName()
bool readable = false;
QString str(fileSystemPath() % "/qet_directory");
pugi::xml_document docu;
if (docu.load_file(str.toStdWString().c_str()))
// QFile rather than pugi's load_file(), which fails on
// Windows once the full path reaches 260 characters.
QFile file(str);
const QByteArray data = file.open(QIODevice::ReadOnly)
? file.readAll() : QByteArray();
if (!data.isEmpty() && docu.load_buffer(data.constData(), data.size()))
{
if (QString(docu.document_element().name())
== "qet-directory")
@@ -157,6 +157,16 @@ QDomElement XmlElementCollection::root() const
*/
QDomElement XmlElementCollection::importCategory() const
{
//Found by name: a project may hold other top-level categories,
//and before the "import" one (lmdg.qet has "k_elem" first). Every
//lookup of an imported element is under "import/", so filing it
//anywhere else made the next placement of the same symbol fail.
for (QDomElement category = root().firstChildElement("category") ;
!category.isNull() ;
category = category.nextSiblingElement("category")) {
if (category.attribute("name") == QLatin1String("import"))
return category;
}
return root().firstChildElement("category");
}
+2 -4
View File
@@ -36,8 +36,7 @@
<string>Ajouter une ligne</string>
</property>
<property name="icon">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
<iconset theme="list-add"/>
</property>
</widget>
</item>
@@ -48,8 +47,7 @@
<string>Copier dans le presse papier</string>
</property>
<property name="icon">
<iconset theme="edit-paste" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-paste.png</normaloff>:/ico/16x16/edit-paste.png</iconset>
<iconset theme="edit-paste"/>
</property>
</item>
</widget>
@@ -1,11 +0,0 @@
FORMS += \
$$PWD/propertieseditordockwidget.ui
HEADERS += \
$$PWD/propertieseditordockwidget.h \
$$PWD/propertieseditorwidget.h \
$$PWD/propertieseditordialog.h
SOURCES += \
$$PWD/propertieseditordockwidget.cpp \
$$PWD/propertieseditorwidget.cpp
@@ -1,5 +0,0 @@
HEADERS += \
$$PWD/qpropertyundocommand.h
SOURCES += \
$$PWD/qpropertyundocommand.cpp
@@ -1,5 +0,0 @@
HEADERS += \
$$PWD/qwidgetanimation.h
SOURCES += \
$$PWD/qwidgetanimation.cpp
@@ -1,7 +0,0 @@
HEADERS += \
$$PWD/qetgraphicshandlerutility.h \
$$PWD/qetgraphicshandleritem.h
SOURCES += \
$$PWD/qetgraphicshandlerutility.cpp \
$$PWD/qetgraphicshandleritem.cpp
@@ -0,0 +1,252 @@
/*
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 "textresizehandles.h"
#include "qetgraphicshandleritem.h"
#include <QAbstractTextDocumentLayout>
#include <QCursor>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsTextItem>
#include <QTextDocument>
#include <cmath>
#include <utility>
/**
@brief TextResizeHandles::TextResizeHandles
@param text : the text to resize, becomes the parent of the handles.
The text may be in a scene or not yet.
@param handle_size : see QETUtils::graphicsHandlerSize()
*/
TextResizeHandles::TextResizeHandles(QGraphicsTextItem *text, qreal handle_size) :
m_text(text)
{
setFlag(QGraphicsItem::ItemHasNoContents);
setParentItem(text);
for (int i = TextResize::TopLeft ; i <= TextResize::BottomLeft ; ++i)
{
auto *handle = new QetGraphicsHandlerItem(handle_size);
handle->setParentItem(this);
handle->setColor(Qt::darkGreen);
handle->setToolTip(tr("Glisser pour changer la largeur du texte, "
"double-cliquer pour une largeur automatique"));
handle->setCursor(i == TextResize::TopLeft || i == TextResize::BottomRight
? Qt::SizeFDiagCursor : Qt::SizeBDiagCursor);
m_handles << handle;
}
installHandleFilters();
m_size_connection = connect(text->document()->documentLayout(),
&QAbstractTextDocumentLayout::documentSizeChanged,
this, &TextResizeHandles::updateHandlesPos);
updateHandlesPos();
}
TextResizeHandles::~TextResizeHandles()
{
disconnect(m_size_connection);
}
QRectF TextResizeHandles::boundingRect() const {
return QRectF();
}
/**
@return the handle at corner
*/
QetGraphicsHandlerItem *TextResizeHandles::handle(TextResize::Corner corner) const {
return m_handles.at(corner);
}
/**
@brief TextResizeHandles::updateHandlesPos
Put the handles at the corners of the box of the text. Called when the
size of the text changes; the owner calls it when the box changes in an
other way.
The box is boundingRect(), the box a user sees selected and resizes,
not the tighter frame of a framed text (qelectrotech#591).
*/
void TextResizeHandles::updateHandlesPos()
{
const QRectF rect = m_text->boundingRect();
for (int i = TextResize::TopLeft ; i <= TextResize::BottomLeft ; ++i)
m_handles.at(i)->setPos(TextResize::cornerOf(rect, TextResize::Corner(i)));
}
/**
@brief TextResizeHandles::itemChange
A scene drops the event filters of an item that leaves it, so the
handles stop reacting to the mouse when the text is removed from its
scene and added back (undoing a delete does that), or when they were
created before the text was in a scene. Install them again each time
these handles enter a scene.
*/
QVariant TextResizeHandles::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemSceneHasChanged)
installHandleFilters();
return QGraphicsObject::itemChange(change, value);
}
/**
@brief TextResizeHandles::installHandleFilters
Filter the mouse events of the handles, when they are in a scene.
*/
void TextResizeHandles::installHandleFilters()
{
if (!scene())
return;
for (QetGraphicsHandlerItem *handle : std::as_const(m_handles))
if (handle->scene() == scene())
handle->installSceneEventFilter(this);
}
bool TextResizeHandles::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
{
const int index = m_handles.indexOf(static_cast<QetGraphicsHandlerItem *>(watched));
if (index < 0)
return false;
switch (event->type())
{
case QEvent::GraphicsSceneMousePress:
{
auto *mouse_event = static_cast<QGraphicsSceneMouseEvent *>(event);
if (mouse_event->button() != Qt::LeftButton)
return false;
pressed(TextResize::Corner(index), mouse_event->scenePos());
return true;
}
case QEvent::GraphicsSceneMouseMove:
if (m_dragging)
moved(static_cast<QGraphicsSceneMouseEvent *>(event)->scenePos());
return true;
case QEvent::GraphicsSceneMouseRelease:
released();
return true;
case QEvent::GraphicsSceneMouseDoubleClick:
resetToAutomaticWidth();
return true;
default:
return false;
}
}
void TextResizeHandles::pressed(TextResize::Corner corner, const QPointF &scene_pos)
{
m_dragging = true;
m_press_scene = scene_pos;
m_dragged = corner;
m_old_width = currentWidth();
m_old_pos = m_text->pos();
m_press_inverse = m_text->sceneTransform().inverted();
m_fixed_local = TextResize::cornerOf(m_text->boundingRect(), TextResize::opposite(corner));
m_fixed_scene = m_text->mapToScene(m_fixed_local);
m_min_width = TextResize::minimumWidth(m_text->document());
}
void TextResizeHandles::moved(const QPointF &scene_pos)
{
//A click without a move keeps an automatic width automatic
if (scene_pos == m_press_scene)
return;
//Whole pixels, as in the width spin boxes
const qreal width = qMax(qreal(qRound(TextResize::widthForDrag(m_press_inverse, m_fixed_local,
scene_pos, m_dragged, 0))),
std::ceil(m_min_width));
m_text->setProperty("textWidth", width);
TextResize::pinCorner(m_text, TextResize::opposite(m_dragged), m_fixed_scene);
updateHandlesPos();
}
void TextResizeHandles::released()
{
if (!m_dragging)
return;
m_dragging = false;
const qreal new_width = currentWidth();
if (!qFuzzyCompare(m_old_width, new_width) || m_old_pos != m_text->pos())
emit resizeFinished(m_old_width, new_width, m_old_pos, m_text->pos());
}
/**
@brief TextResizeHandles::resetToAutomaticWidth
Back to the automatic width (-1): the text keeps the point chosen by its
alignment in place, as it does for any other change of its width.
*/
void TextResizeHandles::resetToAutomaticWidth()
{
m_dragging = false;
const qreal old_width = currentWidth();
if (old_width < 0)
return;
const QPointF old_pos = m_text->pos();
m_text->setProperty("textWidth", qreal(-1));
updateHandlesPos();
emit resizeFinished(old_width, currentWidth(), old_pos, m_text->pos());
}
qreal TextResizeHandles::currentWidth() const {
return m_text->property("textWidth").toReal();
}
/**
@brief TextResizeCommand::TextResizeCommand
@param text : a text with a "textWidth" property
@param old_width
@param new_width
@param old_pos
@param new_pos
@param parent
*/
TextResizeCommand::TextResizeCommand(QGraphicsObject *text,
qreal old_width, qreal new_width,
const QPointF &old_pos, const QPointF &new_pos,
QUndoCommand *parent) :
QUndoCommand(parent),
m_text(text),
m_old_width(old_width),
m_new_width(new_width),
m_old_pos(old_pos),
m_new_pos(new_pos)
{
setText(tr("Redimensionner un texte"));
}
void TextResizeCommand::undo() {
apply(m_old_width, m_old_pos);
}
void TextResizeCommand::redo() {
apply(m_new_width, m_new_pos);
}
void TextResizeCommand::apply(qreal width, const QPointF &pos)
{
if (!m_text)
return;
m_text->setProperty("textWidth", width);
m_text->setPos(pos);
}
@@ -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 TEXTRESIZEHANDLES_H
#define TEXTRESIZEHANDLES_H
#include "../textresize.h"
#include <QCoreApplication>
#include <QGraphicsObject>
#include <QPointer>
#include <QTransform>
#include <QUndoCommand>
#include <QVector>
class QetGraphicsHandlerItem;
class QGraphicsTextItem;
/**
@brief The TextResizeHandles class
Four corner handles to change the width of a text box with the mouse.
The opposite corner of the dragged one stays in place, the text wraps
to the new width and the height of the box follows the text.
Double-clicking a handle goes back to the automatic width.
The text must have a qreal "textWidth" property (-1 = automatic width).
The handles are a child of the text, so they follow it when it, or its
symbol, is moved, rotated or zoomed (qelectrotech#1002).
The width is changed live while dragging; the owner of the text makes
the change undoable when resizeFinished() is emitted, by pushing a
TextResizeCommand on its own undo stack.
*/
class TextResizeHandles : public QGraphicsObject
{
Q_OBJECT
public:
TextResizeHandles(QGraphicsTextItem *text, qreal handle_size = 10);
~TextResizeHandles() override;
QRectF boundingRect() const override;
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override {}
QetGraphicsHandlerItem *handle(TextResize::Corner corner) const;
void updateHandlesPos();
signals:
void resizeFinished(qreal old_width, qreal new_width,
QPointF old_pos, QPointF new_pos);
protected:
bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
void installHandleFilters();
void pressed(TextResize::Corner corner, const QPointF &scene_pos);
void moved(const QPointF &scene_pos);
void released();
void resetToAutomaticWidth();
qreal currentWidth() const;
QGraphicsTextItem *m_text;
QVector<QetGraphicsHandlerItem *> m_handles;
QMetaObject::Connection m_size_connection;
//The drag in progress
bool m_dragging = false;
TextResize::Corner m_dragged = TextResize::BottomRight;
QTransform m_press_inverse;
QPointF m_press_scene,
m_fixed_local,
m_fixed_scene,
m_old_pos;
qreal m_old_width = -1,
m_min_width = 0;
};
/**
@brief The TextResizeCommand class
Undo a width change made with TextResizeHandles: the width, then the
position, which keeps the opposite corner of the dragged one in place.
Both are set in the same order on undo and redo, because a text may
move itself when its width changes (to keep its alignment point).
*/
class TextResizeCommand : public QUndoCommand
{
Q_DECLARE_TR_FUNCTIONS(TextResizeCommand)
public:
TextResizeCommand(QGraphicsObject *text,
qreal old_width, qreal new_width,
const QPointF &old_pos, const QPointF &new_pos,
QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
void apply(qreal width, const QPointF &pos);
QPointer<QGraphicsObject> m_text;
qreal m_old_width,
m_new_width;
QPointF m_old_pos,
m_new_pos;
};
#endif // TEXTRESIZEHANDLES_H
@@ -329,7 +329,8 @@ void ReplaceConductorDialog::on_m_update_preview_pb_clicked()
void ReplaceConductorDialog::on_m_color_pb_clicked()
{
QColor color = QColorDialog::getColor(m_properties.color, this);
QColor color = QColorDialog::getColor(m_properties.color, this, QString(),
QColorDialog::DontUseNativeDialog);
if (color.isValid()) {
setColorButton(color);
}
@@ -337,7 +338,8 @@ void ReplaceConductorDialog::on_m_color_pb_clicked()
void ReplaceConductorDialog::on_m_color_2_pb_clicked()
{
QColor color = QColorDialog::getColor(m_properties.m_color_2, this);
QColor color = QColorDialog::getColor(m_properties.m_color_2, this, QString(),
QColorDialog::DontUseNativeDialog);
if (color.isValid()) {
setColorButton2(color);
}
@@ -180,8 +180,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="start" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/start.png</normaloff>:/ico/22x22/start.png</iconset>
<iconset theme="start"/>
</property>
</widget>
</item>
@@ -106,8 +106,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="go-bottom" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-bottom.png</normaloff>:/ico/16x16/go-bottom.png</iconset>
<iconset theme="go-bottom"/>
</property>
<property name="flat">
<bool>true</bool>
@@ -123,8 +122,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="go-top" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-top.png</normaloff>:/ico/16x16/go-top.png</iconset>
<iconset theme="go-top"/>
</property>
<property name="flat">
<bool>true</bool>
@@ -140,8 +138,7 @@
<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>
<iconset theme="view-refresh"/>
</property>
<property name="flat">
<bool>true</bool>
@@ -341,8 +338,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/window-close.svg</normaloff>:/ico/16x16/window-close.svg</iconset>
<iconset theme="window-close"/>
</property>
<property name="flat">
<bool>true</bool>
@@ -34,7 +34,7 @@ GroupTerminalsCommand::GroupTerminalsCommand(TerminalStrip *strip,
m_receiver(receiver_),
m_to_group(to_group)
{
setText("Grouper un ensemble de bornes");
setText(QObject::tr("Grouper un ensemble de bornes"));
}
void GroupTerminalsCommand::undo() {
@@ -56,7 +56,7 @@ UnGroupTerminalsCommand::UnGroupTerminalsCommand(TerminalStrip *strip,
m_terminal_strip(strip)
{
setUp(to_ungroup);
setText("Dégrouper un ensemble de bornes");
setText(QObject::tr("Dégrouper un ensemble de bornes"));
}
void UnGroupTerminalsCommand::undo()
@@ -84,8 +84,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="dialog-ok" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/dialog-ok.png</normaloff>:/ico/22x22/dialog-ok.png</iconset>
<iconset theme="dialog-ok"/>
</property>
</widget>
</item>
@@ -63,8 +63,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="dialog-ok" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/dialog-ok.png</normaloff>:/ico/16x16/dialog-ok.png</iconset>
<iconset theme="dialog-ok"/>
</property>
</widget>
</item>
@@ -54,8 +54,7 @@
</widget>
<action name="m_add_terminal_strip">
<property name="icon">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
<iconset theme="list-add"/>
</property>
<property name="text">
<string>Ajouter un bornier</string>
@@ -66,8 +65,7 @@
</action>
<action name="m_remove_terminal">
<property name="icon">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
<iconset theme="list-remove"/>
</property>
<property name="text">
<string>Supprimer le bornier</string>
@@ -78,8 +76,7 @@
</action>
<action name="m_reload">
<property name="icon">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/view-refresh.png</normaloff>:/ico/16x16/view-refresh.png</iconset>
<iconset theme="view-refresh"/>
</property>
<property name="text">
<string>Recharger</string>
@@ -205,6 +205,7 @@ void TerminalStripTreeDockWidget::on_m_tree_view_currentItemChanged(QTreeWidgetI
Q_UNUSED(previous)
if (!current) {
m_current_is_free_terminal = false;
setCurrentStrip(nullptr);
return;
}
@@ -225,11 +226,12 @@ void TerminalStripTreeDockWidget::on_m_tree_view_currentItemChanged(QTreeWidgetI
}
}
if (strip_ != m_current_strip) {
setCurrentStrip(strip_);
} else if (current_is_free != m_current_is_free_terminal) {
//The flag must follow every selection change, or a reload of the tree
//(e.g. after moving a free terminal) leaves it stale and the next
//click on a free terminal shows nothing (#1306)
if (strip_ != m_current_strip || current_is_free != m_current_is_free_terminal) {
m_current_is_free_terminal = current_is_free;
emit currentStripChanged(nullptr);
setCurrentStrip(strip_);
}
}
+8 -1
View File
@@ -304,6 +304,7 @@ namespace autonum
str.replace("%{designation_auxiliary1}", dc.value("designation_auxiliary1").toString());
str.replace("%{manufacturer_auxiliary1}", dc.value("manufacturer_auxiliary1").toString());
str.replace("%{manufacturer_reference_auxiliary1}", dc.value("manufacturer_reference_auxiliary1").toString());
str.replace("%{machine_manufacturer_reference_auxiliary1}", dc.value("machine_manufacturer_reference_auxiliary1").toString());
str.replace("%{supplier_auxiliary1}", dc.value("supplier_auxiliary1").toString());
str.replace("%{quantity_auxiliary1}", dc.value("quantity_auxiliary1").toString());
str.replace("%{unity_auxiliary1}", dc.value("unity_auxiliary1").toString());
@@ -313,6 +314,7 @@ namespace autonum
str.replace("%{designation_auxiliary2}", dc.value("designation_auxiliary2").toString());
str.replace("%{manufacturer_auxiliary2}", dc.value("manufacturer_auxiliary2").toString());
str.replace("%{manufacturer_reference_auxiliary2}", dc.value("manufacturer_reference_auxiliary2").toString());
str.replace("%{machine_manufacturer_reference_auxiliary2}", dc.value("machine_manufacturer_reference_auxiliary2").toString());
str.replace("%{supplier_auxiliary2}", dc.value("supplier_auxiliary2").toString());
str.replace("%{quantity_auxiliary2}", dc.value("quantity_auxiliary2").toString());
str.replace("%{unity_auxiliary2}", dc.value("unity_auxiliary2").toString());
@@ -323,6 +325,7 @@ namespace autonum
str.replace("%{designation_auxiliary3}", dc.value("designation_auxiliary3").toString());
str.replace("%{manufacturer_auxiliary3}", dc.value("manufacturer_auxiliary3").toString());
str.replace("%{manufacturer_reference_auxiliary3}", dc.value("manufacturer_reference_auxiliary3").toString());
str.replace("%{machine_manufacturer_reference_auxiliary3}", dc.value("machine_manufacturer_reference_auxiliary3").toString());
str.replace("%{supplier_auxiliary3}", dc.value("supplier_auxiliary3").toString());
str.replace("%{quantity_auxiliary3}", dc.value("quantity_auxiliary3").toString());
str.replace("%{unity_auxiliary3}", dc.value("unity_auxiliary3").toString());
@@ -333,6 +336,7 @@ namespace autonum
str.replace("%{designation_auxiliary4}", dc.value("designation_auxiliary4").toString());
str.replace("%{manufacturer_auxiliary4}", dc.value("manufacturer_auxiliary4").toString());
str.replace("%{manufacturer_reference_auxiliary4}", dc.value("manufacturer_reference_auxiliary4").toString());
str.replace("%{machine_manufacturer_reference_auxiliary4}", dc.value("machine_manufacturer_reference_auxiliary4").toString());
str.replace("%{supplier_auxiliary4}", dc.value("supplier_auxiliary4").toString());
str.replace("%{quantity_auxiliary4}", dc.value("quantity_auxiliary4").toString());
str.replace("%{unity_auxiliary4}", dc.value("unity_auxiliary4").toString());
@@ -478,7 +482,10 @@ namespace autonum
m_seq_struct.wrap.size()))
);
for (int i=1; i<=max ; i++)
// Highest number first: "%sequ_1" is also the start of
// "%sequ_10", so replacing 1 before 10 would turn %sequ_10
// into the first value followed by a "0".
for (int i=max; i>=1 ; i--)
{
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));
+297
View File
@@ -0,0 +1,297 @@
/*
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 "autonumschemecommand.h"
#include "../diagram.h"
#include "../qetproject.h"
#include <algorithm>
AutoNumSchemeCommand::AutoNumSchemeCommand(QETProject *project, Kind kind) :
m_project(project),
m_kind(kind)
{}
/// @return the titles of the schemes of @p kind, sorted
QStringList AutoNumSchemeCommand::titles(const QETProject *project, Kind kind)
{
if (!project) {
return {};
}
QStringList list = kind == Kind::Conductor ? project->conductorAutoNum().keys()
: project->folioAutoNum().keys();
list.sort(Qt::CaseInsensitive);
return list;
}
bool AutoNumSchemeCommand::contains(const QETProject *project, Kind kind, const QString &title)
{
if (!project) {
return false;
}
return kind == Kind::Conductor ? project->conductorAutoNum().contains(title)
: project->folioAutoNum().contains(title);
}
NumerotationContext AutoNumSchemeCommand::contextOf(const QETProject *project, Kind kind, const QString &title)
{
if (!project) {
return NumerotationContext();
}
return kind == Kind::Conductor ? project->conductorAutoNum(title)
: project->folioAutoNum(title);
}
/**
@return the title of the scheme @p name clashes with, empty if none:
the same name, whatever the case or the white space
@param ignored_title : the scheme being renamed
*/
QString AutoNumSchemeCommand::nameClash(const QETProject *project, Kind kind,
const QString &name, const QString &ignored_title)
{
const QString wanted = QETProject::normalizedAutoNumName(name);
for (const QString &title : titles(project, kind)) {
if (title == ignored_title) {
continue;
}
if (QString::compare(QETProject::normalizedAutoNumName(title), wanted,
Qt::CaseInsensitive) == 0) {
return title;
}
}
return QString();
}
/// @return why @p name cannot be the name of a scheme, translated, empty if it can
QString AutoNumSchemeCommand::nameProblem(const QETProject *project, Kind kind,
const QString &name, const QString &ignored_title)
{
if (QETProject::normalizedAutoNumName(name).isEmpty()) {
return tr("Le nom de la numérotation ne peut pas être vide.");
}
const QString clash = nameClash(project, kind, name, ignored_title);
if (!clash.isEmpty()) {
return tr("Une numérotation nommée « %1 » existe déjà.").arg(clash);
}
return QString();
}
/**
@return the folios which follow the scheme @p title
@param references : every folio which names it, for a folio numbering,
even if its number has been written into the folio field already (see
the class description)
*/
QList<Diagram *> AutoNumSchemeCommand::usersOf(const QETProject *project, Kind kind,
const QString &title, bool references)
{
QList<Diagram *> users;
if (!project || title.isEmpty()) {
return users;
}
for (Diagram *diagram : project->diagrams()) {
if (kind == Kind::Conductor) {
if (diagram->conductorsAutonumName() == title) {
users << diagram;
}
} else if (diagram->border_and_titleblock.autoPageNum() == title
&& (references || diagram->border_and_titleblock.folio().contains(QLatin1String("%autonum")))) {
users << diagram;
}
}
return users;
}
AutoNumSchemeCommand *AutoNumSchemeCommand::create(QETProject *project, Kind kind,
const QString &title,
const NumerotationContext &context,
bool make_current)
{
if (!project || !nameProblem(project, kind, title).isEmpty()) {
return nullptr;
}
auto *cmd = new AutoNumSchemeCommand(project, kind);
cmd->m_after = Scheme{QETProject::normalizedAutoNumName(title), context};
cmd->m_current_before = project->conductorCurrentAutoNum();
cmd->m_current_after = (kind == Kind::Conductor && make_current)
? cmd->m_after->title
: cmd->m_current_before;
cmd->setText(tr("Créer la numérotation %1").arg(cmd->m_after->title));
return cmd;
}
/**
Rename the scheme @p old_title to @p new_title and give it @p context.
@param make_current : for conductors, make it the scheme new conductors
take; otherwise it stays so if it was
@return nullptr if there is no such scheme, the new name is not
acceptable, or nothing changes
*/
AutoNumSchemeCommand *AutoNumSchemeCommand::edit(QETProject *project, Kind kind,
const QString &old_title,
const QString &new_title,
const NumerotationContext &context,
bool make_current)
{
if (!project || !contains(project, kind, old_title)) {
return nullptr;
}
if (!nameProblem(project, kind, new_title, old_title).isEmpty()) {
return nullptr;
}
auto *cmd = new AutoNumSchemeCommand(project, kind);
cmd->m_before = Scheme{old_title, contextOf(project, kind, old_title)};
cmd->m_after = Scheme{QETProject::normalizedAutoNumName(new_title), context};
cmd->m_current_before = project->conductorCurrentAutoNum();
cmd->m_current_after = kind == Kind::Conductor
? ((make_current || cmd->m_current_before == old_title)
? cmd->m_after->title : cmd->m_current_before)
: cmd->m_current_before;
if (cmd->m_before->title != cmd->m_after->title) {
cmd->m_users = [&] {
QList<QPointer<Diagram>> list;
for (Diagram *d : usersOf(project, kind, old_title, true)) list << d;
return list;
}();
}
bool same_definition = cmd->m_before->context.size() == cmd->m_after->context.size();
for (int i = 0 ; same_definition && i < cmd->m_before->context.size() ; ++i) {
same_definition = cmd->m_before->context[i] == cmd->m_after->context[i];
}
if (cmd->m_before->title == cmd->m_after->title && same_definition
&& cmd->m_current_before == cmd->m_current_after) {
delete cmd;
return nullptr;
}
if (cmd->m_before->title != cmd->m_after->title && same_definition) {
cmd->setText(tr("Renommer la numérotation %1 en %2")
.arg(cmd->m_before->title, cmd->m_after->title));
} else {
cmd->setText(tr("Modifier la numérotation %1").arg(cmd->m_after->title));
}
return cmd;
}
/**
Remove the scheme @p title.
@return nullptr if there is no such scheme or a folio still follows it
*/
AutoNumSchemeCommand *AutoNumSchemeCommand::remove(QETProject *project, Kind kind,
const QString &title)
{
if (!project || !contains(project, kind, title)) {
return nullptr;
}
if (!usersOf(project, kind, title).isEmpty()) {
return nullptr;
}
auto *cmd = new AutoNumSchemeCommand(project, kind);
cmd->m_before = Scheme{title, contextOf(project, kind, title)};
cmd->m_current_before = project->conductorCurrentAutoNum();
cmd->m_current_after = (kind == Kind::Conductor && cmd->m_current_before == title)
? QString() : cmd->m_current_before;
cmd->setText(tr("Supprimer la numérotation %1").arg(title));
return cmd;
}
/// Let the windows which list the schemes read them again
void AutoNumSchemeCommand::announce(bool added, bool removed)
{
if (m_kind == Kind::Conductor) {
if (removed) m_project->conductorAutoNumRemoved();
if (added) m_project->conductorAutoNumAdded();
} else {
if (removed) m_project->folioAutoNumRemoved();
if (added) m_project->folioAutoNumAdded();
}
}
/// Give the scheme @p from the title @p to, and the folios which follow it
void AutoNumSchemeCommand::rename(const QString &from, const QString &to)
{
const NumerotationContext context = contextOf(m_project, m_kind, from);
if (m_kind == Kind::Conductor) {
m_project->removeConductorAutoNum(from);
m_project->addConductorAutoNum(to, context);
if (m_project->conductorCurrentAutoNum() == from) {
m_project->setCurrentConductorAutoNum(to);
}
for (Diagram *d : m_project->diagrams()) {
//The highest folio number given, kept by scheme title
for (auto *hash : {&d->m_cnd_unitfolio_max, &d->m_cnd_tenfolio_max, &d->m_cnd_hundredfolio_max}) {
if (hash->contains(from)) {
hash->insert(to, hash->take(from));
}
}
}
} else {
m_project->removeFolioAutoNum(from);
m_project->addFolioAutoNum(to, context);
}
for (const QPointer<Diagram> &d : std::as_const(m_users)) {
if (!d) continue;
if (m_kind == Kind::Conductor) {
d->setConductorsAutonumName(to);
} else {
d->border_and_titleblock.setAutoPageNum(to);
}
}
}
void AutoNumSchemeCommand::redo()
{
if (!m_project) return;
if (m_before && m_after && m_before->title != m_after->title) {
rename(m_before->title, m_after->title);
} else if (m_before && !m_after) {
if (m_kind == Kind::Conductor) m_project->removeConductorAutoNum(m_before->title);
else m_project->removeFolioAutoNum(m_before->title);
}
if (m_after) {
if (m_kind == Kind::Conductor) m_project->addConductorAutoNum(m_after->title, m_after->context);
else m_project->addFolioAutoNum(m_after->title, m_after->context);
}
if (m_kind == Kind::Conductor) {
m_project->setCurrentConductorAutoNum(m_current_after);
}
announce(m_after.has_value(), m_before.has_value() && (!m_after || m_after->title != m_before->title));
}
void AutoNumSchemeCommand::undo()
{
if (!m_project) return;
if (m_before && m_after && m_before->title != m_after->title) {
rename(m_after->title, m_before->title);
} else if (!m_before && m_after) {
if (m_kind == Kind::Conductor) m_project->removeConductorAutoNum(m_after->title);
else m_project->removeFolioAutoNum(m_after->title);
}
if (m_before) {
if (m_kind == Kind::Conductor) m_project->addConductorAutoNum(m_before->title, m_before->context);
else m_project->addFolioAutoNum(m_before->title, m_before->context);
}
if (m_kind == Kind::Conductor) {
m_project->setCurrentConductorAutoNum(m_current_before);
}
announce(m_before.has_value(), m_after.has_value() && (!m_before || m_after->title != m_before->title));
}
+111
View File
@@ -0,0 +1,111 @@
/*
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 AUTONUMSCHEMECOMMAND_H
#define AUTONUMSCHEMECOMMAND_H
#include "numerotationcontext.h"
#include <QCoreApplication>
#include <QList>
#include <QPointer>
#include <QUndoCommand>
#include <optional>
class Diagram;
class QETProject;
/**
@brief The AutoNumSchemeCommand class
Undoable creation, edition (rename and/or new definition) and removal of
one conductor or folio numbering scheme of a project.
A folio refers to the scheme it follows by its title, as the project file
has always had it (the title of the conductor numbering a folio reads,
the title of the folio numbering its title block follows). So a rename
moves those references with it, in the same undo step, and a scheme which
a folio still follows cannot be removed: the folio would be left reading
a numbering which does not exist.
A folio numbering is applied once: the number is written into the folio
field of the title block, in place of %autonum, and the title block keeps
the name of the numbering it came from. So a folio really depends on a
folio numbering only while its folio field still holds %autonum, and
only then can the numbering not be removed; every title block which
names it has its name moved by a rename.
Conductors keep the formula they were numbered with, and the numbers
folios show are worked out from the scheme when they are drawn: a new
definition changes what is numbered from then on, nothing is renumbered.
Counter changes made by numbering something are not edits of the scheme
and do not come here.
*/
class AutoNumSchemeCommand : public QUndoCommand
{
Q_DECLARE_TR_FUNCTIONS(AutoNumSchemeCommand)
public:
enum class Kind { Conductor, Folio };
struct Scheme
{
QString title;
NumerotationContext context;
};
static QStringList titles(const QETProject *project, Kind kind);
static bool contains(const QETProject *project, Kind kind, const QString &title);
static NumerotationContext contextOf(const QETProject *project, Kind kind, const QString &title);
static QString nameClash(const QETProject *project, Kind kind,
const QString &name, const QString &ignored_title = QString());
static QString nameProblem(const QETProject *project, Kind kind,
const QString &name, const QString &ignored_title = QString());
static QList<Diagram *> usersOf(const QETProject *project, Kind kind,
const QString &title, bool references = false);
static AutoNumSchemeCommand *create(QETProject *project, Kind kind,
const QString &title,
const NumerotationContext &context,
bool make_current = false);
static AutoNumSchemeCommand *edit(QETProject *project, Kind kind,
const QString &old_title,
const QString &new_title,
const NumerotationContext &context,
bool make_current = false);
static AutoNumSchemeCommand *remove(QETProject *project, Kind kind,
const QString &title);
void undo() override;
void redo() override;
private:
AutoNumSchemeCommand(QETProject *project, Kind kind);
void rename(const QString &from, const QString &to);
void announce(bool added, bool removed);
QETProject *m_project = nullptr;
Kind m_kind;
std::optional<Scheme> m_before;
std::optional<Scheme> m_after;
QString m_current_before;
QString m_current_after;
QList<QPointer<Diagram>> m_users; ///< the folios which follow the scheme being renamed
};
#endif // AUTONUMSCHEMECOMMAND_H
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,244 @@
/*
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 ELEMENTAUTONUMSCHEMECOMMAND_H
#define ELEMENTAUTONUMSCHEMECOMMAND_H
#include "numerotationcontext.h"
#include "renumberelementscommand.h"
#include <QCoreApplication>
#include <QDomDocument>
#include <QDomElement>
#include <QList>
#include <QMap>
#include <QSet>
#include <QUndoCommand>
#include <QUuid>
#include <QVector>
#include <optional>
class Element;
class QETProject;
/**
@brief The ElementAutoNumSchemeCommand class
Undoable creation, edition (rename and/or new definition) and removal
of one element numbering scheme of a project, together with what it
does to the elements following the scheme.
Elements follow a scheme by its uuid (QETInformation::ELMT_FORMULA_ID),
so a rename touches no element. A new definition with a different
formula rewrites the formula of every element following the scheme:
when its sequential parts are the same (only the fixed text or the
folio parts changed), each element keeps its numbers; otherwise the
elements are numbered again, in folio and position order, as
QETProject::renumberElementsBySchemeTitle() does.
A scheme whose formula would change cannot be edited while an element
with a frozen label follows it: that element would keep a label and a
formula the scheme no longer defines (see editBlockedBy(); edit()
refuses). A rename, or a change of the counter, touches no formula and
is allowed. renumber() leaves a frozen element exactly as it is,
label, formula and numbers, and does not give its label to another
element: the numbering goes past it (see reservedLabels()).
Counter changes made by placing elements are not edits of the scheme:
they stay with SetAutoNumContextCommand.
Build one with create(), edit() or remove(); each returns nullptr when
the change is refused or changes nothing. Show changedElementCount()
to the user before pushing it when it is not zero.
*/
class ElementAutoNumSchemeCommand : public QUndoCommand
{
Q_DECLARE_TR_FUNCTIONS(ElementAutoNumSchemeCommand)
public:
struct Scheme
{
QString title;
QUuid id;
NumerotationContext context;
};
static ElementAutoNumSchemeCommand *create(
QETProject *project,
const QString &title,
const NumerotationContext &context,
const QUuid &id = QUuid(),
bool make_current = true);
static ElementAutoNumSchemeCommand *edit(
QETProject *project,
const QString &old_title,
const QString &new_title,
const NumerotationContext &context,
bool make_current = true);
static ElementAutoNumSchemeCommand *remove(
QETProject *project,
const QString &title);
static QString nameProblem(const QETProject *project,
const QString &name,
const QString &ignored_title = QString());
static bool sameSequentialParts(const QString &formula_a,
const QString &formula_b);
static NumerotationContext resetForRenumber(const NumerotationContext &context);
static QVector<RenumberElementsCommand::ElementChange> renumberChanges(
const QString &hash_key,
const NumerotationContext &context,
QVector<Element *> elements,
const QString &formula,
NumerotationContext *final_context,
bool from_start = true,
const QUuid &scheme_id = QUuid(),
const QSet<QString> &reserved = QSet<QString>());
static QSet<QString> reservedLabels(const QETProject *project,
const QString &title,
const QVector<Element *> &changing);
static RenumberElementsCommand *renumber(QETProject *project,
const QString &title,
QVector<Element *> *frozen,
const QString &text);
static QVector<Element *> frozenFollowers(const QETProject *project,
const QString &title);
static QVector<Element *> editBlockedBy(const QETProject *project,
const QString &title,
const NumerotationContext &context);
/// Why assign() leaves an element as it is
enum class Skipped {
None,
FollowsAlready, ///< already follows this scheme
Frozen, ///< its label is frozen
OtherFormula, ///< follows another scheme or has a formula of its own
Linked ///< slave or report: takes its label from its master
};
static Skipped skipReason(const QETProject *project,
const QString &title,
const Element *element);
/// What assign() would do with each of the given elements
struct AssignPlan
{
QVector<Element *> todo; ///< given the scheme
QVector<Element *> frozen; ///< a frozen label: left alone unless overwrite
QVector<Element *> otherFormula; ///< follow or hold another formula: left alone unless overwrite
int followsAlready = 0; ///< follow this scheme already
int linked = 0; ///< slave or report: take their label from their master
};
static AssignPlan assignPlan(const QETProject *project,
const QString &title,
const QVector<Element *> &elements);
static RenumberElementsCommand *assign(
QETProject *project,
const QString &title,
const QVector<Element *> &elements,
bool overwrite,
int *skipped = nullptr,
QUndoCommand *parent = nullptr);
/// What a numbered element's frozen state becomes
enum class FreezeRule {
Unfrozen, ///< free: the user asked for this numbering
NewElementPolicy ///< as an element just placed: frozen if the folio or project freezes new ones
};
static RenumberElementsCommand *numberElements(
QETProject *project,
const QString &title,
const QVector<Element *> &elements,
const QString &text,
FreezeRule freeze = FreezeRule::Unfrozen,
QUndoCommand *parent = nullptr);
static QString schemeForFormula(const QETProject *project, const QString &formula);
static QString followedScheme(const QETProject *project, const DiagramContext &info);
static void writeCopiedSchemes(QDomDocument &document,
QDomElement &root,
const QETProject *project,
const QVector<Element *> &copied);
static QList<Scheme> copiedSchemes(const QDomElement &root);
static QList<Scheme> missingForPaste(const QETProject *project,
const QList<Scheme> &copied,
const QList<Element *> &pasted);
/// How the elements of a scheme are numbered by hand (see numberSupport())
struct NumberSupport
{
bool supported = false;
int partIndex = -1; ///< the one number part of the definition
QString type; ///< "unit", "ten" or "hundred"
int increase = 1;
};
struct GapRange
{
int from = 0;
int to = 0;
};
/// A counter which would meet numbers already in use
struct CounterConflict
{
int counter = 0; ///< the next number it would hand out
int highest = 0; ///< the highest number in use
int count = 0; ///< how many elements have a number of at least counter
};
static std::optional<CounterConflict> counterConflict(const QETProject *project,
const QString &title,
const NumerotationContext &proposed);
static QSet<QString> labelsHeldBesides(const QETProject *project, const Element *element);
static NumberSupport numberSupport(const NumerotationContext &context);
static std::optional<int> numberOf(const NumberSupport &support, const Element *element);
static QList<GapRange> gapRanges(const QETProject *project, const QString &title);
static QList<int> freeNumbers(const QETProject *project, const QString &title,
const Element *element, int limit = 500);
static QString labelForNumber(const QETProject *project, const QString &title,
Element *element, int number);
static RenumberElementsCommand *assignNumber(QETProject *project,
Element *element,
int number,
QString *problem = nullptr,
QUndoCommand *parent = nullptr);
static QMap<QString, QVector<Element *>> pastedSchemes(
const QETProject *project,
const QList<Element *> &elements);
static void linkPasted(const QETProject *project,
const QList<Element *> &elements);
static int numberPasted(QETProject *project,
const QMap<QString, QVector<Element *>> &schemes,
QUndoCommand *parent);
/// Elements whose label and formula change
int changedElementCount() const {return m_changes.size();}
const QVector<RenumberElementsCommand::ElementChange> &changes() const {return m_changes;}
std::optional<Scheme> before() const {return m_before;}
std::optional<Scheme> after() const {return m_after;}
void undo() override;
void redo() override;
private:
explicit ElementAutoNumSchemeCommand(QETProject *project);
QETProject *m_project = nullptr;
std::optional<Scheme> m_before;
std::optional<Scheme> m_after;
QString m_current_before;
QString m_current_after;
QVector<RenumberElementsCommand::ElementChange> m_changes;
};
#endif // ELEMENTAUTONUMSCHEMECOMMAND_H
+21 -15
View File
@@ -26,8 +26,9 @@ RenumberElementsCommand::RenumberElementsCommand(
QVector<ElementChange> changes,
QHash<QString, NumerotationContext> old_ctx,
QHash<QString, NumerotationContext> new_ctx,
const QString &text)
: QUndoCommand(text)
const QString &text,
QUndoCommand *parent)
: QUndoCommand(text, parent)
, project_(project)
, changes_(std::move(changes))
, old_ctx_(std::move(old_ctx))
@@ -61,18 +62,23 @@ void RenumberElementsCommand::apply(bool use_new)
// Apply per-element changes
for (const ElementChange &c : changes_) {
if (!c.element) continue;
const bool frozen = use_new ? c.new_frozen : c.old_frozen;
const auto &infos = use_new ? c.new_infos : c.old_infos;
const auto &seq = use_new ? c.new_seq : c.old_seq;
// Temporarily unfreeze so that label/infos update correctly.
const bool was_frozen = c.element->isFreezeLabel();
if (was_frozen) c.element->freezeLabel(false);
c.element->rSequenceStruct() = seq;
c.element->setElementInformations(infos);
c.element->freezeLabel(frozen);
applyChange(c, use_new);
}
}
void RenumberElementsCommand::applyChange(const ElementChange &c, bool use_new)
{
if (!c.element) return;
const bool frozen = use_new ? c.new_frozen : c.old_frozen;
const auto &infos = use_new ? c.new_infos : c.old_infos;
const auto &seq = use_new ? c.new_seq : c.old_seq;
// Temporarily unfreeze so that label/infos update correctly.
const bool was_frozen = c.element->isFreezeLabel();
if (was_frozen) c.element->freezeLabel(false);
c.element->rSequenceStruct() = seq;
c.element->setElementInformations(infos);
c.element->freezeLabel(frozen);
}
+7 -1
View File
@@ -51,11 +51,17 @@ public:
QVector<ElementChange> changes,
QHash<QString, NumerotationContext> old_ctx,
QHash<QString, NumerotationContext> new_ctx,
const QString &text);
const QString &text,
QUndoCommand *parent = nullptr);
const QVector<ElementChange> &changes() const { return changes_; }
void undo() override;
void redo() override;
/// Apply one change's old (@p use_new false) or new state.
static void applyChange(const ElementChange &change, bool use_new);
private:
void apply(bool use_new);
@@ -19,6 +19,11 @@
#include "../../diagram.h"
#include "../../diagramview.h"
#include "../../qetgraphicsitem/element.h"
#include "../../qeticons.h"
#include "../elementautonumschemecommand.h"
#include "counterwarning.h"
#include "renumberpreviewdialog.h"
#include "../../qetapp.h"
#include "../../shortcutmanager.h"
#include "../../titleblockproperties.h"
@@ -29,7 +34,10 @@
#include "../../undocommand/changetitleblockcommand.h"
#include <QComboBox>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSpinBox>
@@ -43,9 +51,127 @@ AutoNumberingDockWidget::AutoNumberingDockWidget(QWidget *parent) :
ui(new Ui::AutoNumberingDockWidget)
{
ui->setupUi(this);
//The element row gets a button to the left of its reset button
//which gives the numbering shown to the selected elements
QPushButton *reset = ui->m_element_reset_start_pb;
ui->gridLayout->removeWidget(reset);
auto *actions = new QWidget(this);
auto *actions_layout = new QHBoxLayout(actions);
actions_layout->setContentsMargins(0, 0, 0, 0);
actions_layout->setSpacing(2);
m_element_apply_pb = new QPushButton(QET::Icons::DialogOk, QString(), actions);
m_element_apply_pb->setObjectName(QStringLiteral("m_element_apply_pb"));
m_element_apply_pb->setMaximumWidth(24);
m_element_apply_pb->setToolTip(tr("Appliquer cette numérotation aux éléments sélectionnés"));
reset->setParent(actions);
actions_layout->addWidget(m_element_apply_pb);
actions_layout->addWidget(reset);
ui->gridLayout->addWidget(actions, 3, 2);
connect(m_element_apply_pb, &QPushButton::clicked,
this, &AutoNumberingDockWidget::applyElementSchemeToSelection);
connect(ui->m_element_cb, qOverload<int>(&QComboBox::currentIndexChanged),
this, &AutoNumberingDockWidget::updateApplyEnabled);
m_element_apply_pb->setEnabled(false);
this->setDisabled(true);
}
/**
@brief AutoNumberingDockWidget::applyElementSchemeToSelection
Give the element numbering shown in the list to the elements selected
in the current folio: each takes its formula and the next number, in
folio and position order, and the counter moves on.
What would change is shown first. An element which holds another
numbering, or whose label is frozen, is left as it is, unless the user
asks to replace them too (a frozen one is then given back). One undo
step.
*/
void AutoNumberingDockWidget::applyElementSchemeToSelection()
{
if (!m_project || m_project->isReadOnly()) {
return;
}
const QString caption = tr("Appliquer la numérotation");
const QString title = ui->m_element_cb->currentText();
DiagramView *view = m_project_view ? m_project_view->currentDiagram() : nullptr;
Diagram *diagram = view ? view->diagram() : nullptr;
if (!diagram || !m_project->elementAutoNum().contains(title)) {
QMessageBox::information(this, caption,
tr("Choisissez une numérotation d'éléments du projet."));
return;
}
QVector<Element *> selected;
const auto items = diagram->selectedItems();
for (QGraphicsItem *item : items) {
if (auto *el = qgraphicsitem_cast<Element *>(item)) {
selected << el;
}
}
if (selected.isEmpty()) {
QMessageBox::information(this, caption,
tr("Sélectionnez d'abord des éléments dans le folio."));
return;
}
const auto plan = ElementAutoNumSchemeCommand::assignPlan(m_project, title, selected);
QVector<RenumberPreviewDialog::LeftAlone> left;
for (Element *el : plan.frozen) {
left.append({el, tr("(figé)")});
}
for (Element *el : plan.otherFormula) {
left.append({el, tr("(autre numérotation)")});
}
const int replaceable = static_cast<int>(left.size());
auto *cmd = ElementAutoNumSchemeCommand::assign(m_project, title, selected, false);
if (!cmd && !replaceable) {
QMessageBox::information(
this, caption,
tr("Rien à appliquer : les éléments sélectionnés suivent déjà "
"la numérotation « %1 », ou prennent leur nom d'un autre élément.").arg(title));
return;
}
const QString intro = cmd
? tr("%n élément(s) vont recevoir la numérotation « %1 », dans l'ordre des folios "
"et des positions.", "", static_cast<int>(plan.todo.size())).arg(title)
: tr("Aucun élément ne peut recevoir la numérotation « %1 » sans remplacer "
"ce qu'il a.").arg(title);
const auto answer = RenumberPreviewDialog::ask(
this, caption, intro,
cmd ? cmd->changes() : QVector<RenumberElementsCommand::ElementChange>(),
left,
replaceable
? tr("Remplacer aussi les %n élément(s) laissés comme ils sont", "", replaceable)
: QString());
if (answer == RenumberPreviewDialog::Answer::Cancel) {
delete cmd;
return;
}
int applied = cmd ? static_cast<int>(cmd->changes().size()) : 0;
if (answer == RenumberPreviewDialog::Answer::GoAndReplace) {
delete cmd;
cmd = ElementAutoNumSchemeCommand::assign(m_project, title, selected, true);
applied = cmd ? static_cast<int>(cmd->changes().size()) : 0;
}
if (cmd) {
m_project->undoStack()->push(cmd);
}
const int not_given = static_cast<int>(selected.size()) - applied;
if (not_given > 0) {
QMessageBox::information(
this, caption,
tr("%n élément(s) numérotés.", "", applied)
+ QLatin1Char('\n')
+ tr("%n élément(s) sélectionnés sont restés comme ils sont.", "", not_given));
}
}
/**
@brief AutoNumberingDockWidget::~AutoNumberingDockWidget
Destructor
@@ -117,6 +243,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
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);
disconnect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::followSelectionOf);
followSelectionOf(nullptr);
//Element Signals
disconnect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
@@ -142,6 +270,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
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);
connect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::followSelectionOf);
followSelectionOf(m_project_view->currentDiagram());
//Element Signals
connect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
@@ -207,6 +337,56 @@ void AutoNumberingDockWidget::setContext()
this->setActive();
}
/**
@brief AutoNumberingDockWidget::followSelectionOf
Enable the apply button only while elements are selected in the folio
shown: it gives a numbering to the selected elements, and with none
selected there is nothing for it to do.
@param view : the folio now shown, nullptr for none
*/
void AutoNumberingDockWidget::followSelectionOf(DiagramView *view)
{
Diagram *diagram = view ? view->diagram() : nullptr;
if (m_selection_diagram) {
disconnect(m_selection_diagram, &QGraphicsScene::selectionChanged,
this, &AutoNumberingDockWidget::updateApplyEnabled);
}
m_selection_diagram = diagram;
if (m_selection_diagram) {
connect(m_selection_diagram, &QGraphicsScene::selectionChanged,
this, &AutoNumberingDockWidget::updateApplyEnabled);
}
updateApplyEnabled();
}
/**
@brief AutoNumberingDockWidget::updateApplyEnabled
The apply button needs a numbering to give and elements selected.
*/
void AutoNumberingDockWidget::updateApplyEnabled()
{
if (!m_element_apply_pb) {
return;
}
bool elements_selected = false;
if (m_selection_diagram) {
const auto items = m_selection_diagram->selectedItems();
for (QGraphicsItem *item : items) {
if (qgraphicsitem_cast<Element *>(item)) {
elements_selected = true;
break;
}
}
}
m_element_apply_pb->setEnabled(
elements_selected && m_project && !m_project->isReadOnly()
&& ui->m_element_cb->count() > 0);
m_element_apply_pb->setToolTip(
elements_selected
? tr("Appliquer cette numérotation aux éléments sélectionnés")
: tr("Sélectionnez d'abord des éléments dans le folio"));
}
/**
@brief AutoNumberingDockWidget::setConductorActive
@param dv: activated diagramview
@@ -539,6 +719,12 @@ void AutoNumberingDockWidget::applyValueField(QComboBox *combo_box, QLineEdit *l
}
context.replaceValue(index, typed);
//An element counter put back on numbers in use: ask first
if (category == AutoNumCategory::Element
&& !CounterWarning::confirm(this, m_project, combo_box->currentText(), context)) {
refreshRow(category);
return;
}
storeContext(combo_box, category, context);
refreshRow(category);
}
@@ -684,6 +870,12 @@ void AutoNumberingDockWidget::resetAutoNum(QComboBox *combo_box, AutoNumCategory
context.replaceValue(i, QStringLiteral("a"));
}
//Back to the first number, over numbers in use: ask first
if (category == AutoNumCategory::Element
&& !CounterWarning::confirm(this, m_project, combo_box->currentText(), context)) {
return;
}
storeContext(combo_box, category, context);
refreshRow(category);
}
@@ -22,9 +22,11 @@
#include "../../qetproject.h"
#include <QDockWidget>
#include <QPointer>
class QComboBox;
class QLineEdit;
class QPushButton;
class QSpinBox;
namespace Ui {
@@ -58,6 +60,9 @@ class AutoNumberingDockWidget : public QDockWidget
void refreshValueFields();
void on_m_configure_pb_clicked();
void applyElementSchemeToSelection();
void updateApplyEnabled();
void followSelectionOf(DiagramView *view);
void on_m_conductor_reset_start_pb_clicked();
void on_m_element_reset_start_pb_clicked();
@@ -126,6 +131,8 @@ class AutoNumberingDockWidget : public QDockWidget
/// this replaces refreshValueField() at each of them.
void refreshRow(AutoNumCategory category);
QPushButton *m_element_apply_pb = nullptr;
QPointer<Diagram> m_selection_diagram; ///< the folio whose selection enables the apply button
Ui::AutoNumberingDockWidget *ui;
QETProject* m_project = nullptr;
ProjectView* m_project_view = nullptr;
@@ -73,8 +73,7 @@
<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>
<iconset theme="view-refresh"/>
</property>
</widget>
</item>
@@ -161,8 +160,7 @@
<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>
<iconset theme="view-refresh"/>
</property>
</widget>
</item>
@@ -238,8 +236,7 @@
<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>
<iconset theme="view-refresh"/>
</property>
</widget>
</item>
@@ -319,8 +316,7 @@
<string>Configurer</string>
</property>
<property name="icon">
<iconset theme="configure" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/configure.png</normaloff>:/ico/16x16/configure.png</iconset>
<iconset theme="configure"/>
</property>
</widget>
</item>
@@ -16,8 +16,11 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "autonumberingmanagementw.h"
#include "../elementautonumschemecommand.h"
#include "renumberpreviewdialog.h"
#include "../../diagram.h"
#include "../../qetgraphicsitem/element.h"
#include "../../qetproject.h"
#include "../numerotationcontextcommands.h"
#include "formulaautonumberingw.h"
@@ -122,7 +125,42 @@ void AutoNumberingManagementW::on_m_renumber_elements_pb_clicked()
RenumberElementsDialog dlg(titles, this);
if (dlg.exec() != QDialog::Accepted) return;
project_->renumberElementsBySchemeTitle(dlg.selectedSchemeTitle());
const QString title = dlg.selectedSchemeTitle();
QVector<Element *> frozen;
auto *cmd = ElementAutoNumSchemeCommand::renumber(
project_, title, &frozen,
title.isEmpty() ? tr("Renuméroter les éléments")
: tr("Renuméroter les éléments (%1)").arg(title));
if (!cmd)
{
QMessageBox::information(
this, tr("Renuméroter les éléments"),
frozen.isEmpty()
? tr("Aucun élément ne suit cette numérotation.")
: tr("Les %n élément(s) qui suivent cette numérotation ont un nom "
"figé : rien n'est renuméroté.", "", frozen.size()));
return;
}
const int renumbered = static_cast<int>(cmd->changes().size());
const int changed = RenumberPreviewDialog::changedLabelCount(cmd->changes());
if (!RenumberPreviewDialog::confirm(
this, tr("Renuméroter les éléments"),
tr("%n élément(s) vont être renumérotés, à partir du premier numéro, "
"dans l'ordre des folios et des positions.", "", renumbered),
cmd->changes(), frozen)) {
delete cmd;
return;
}
project_->undoStack()->push(cmd);
QString summary = tr("%n élément(s) renumérotés, dont %1 avec un nouveau nom.", "", renumbered)
.arg(changed);
if (!frozen.isEmpty()) {
summary += QLatin1Char('\n') + tr("%n élément(s) au nom figé n'ont pas été touchés, "
"et leur numéro n'a pas été redonné.", "", frozen.size());
}
QMessageBox::information(this, tr("Renuméroter les éléments"), summary);
}
/**
+50
View File
@@ -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/>.
*/
#include "counterwarning.h"
#include "../elementautonumschemecommand.h"
#include "../../qetmessagebox.h"
/**
@brief CounterWarning::confirm
@param title : the element numbering
@param proposed : its definition, with the counter to give it
@return true if the counter meets no number in use, or the user goes
on; false if the user gives up (and, with nobody to ask, always)
*/
bool CounterWarning::confirm(QWidget *parent,
const QETProject *project,
const QString &title,
const NumerotationContext &proposed)
{
const auto conflict = ElementAutoNumSchemeCommand::counterConflict(project, title, proposed);
if (!conflict) {
return true;
}
const auto answer = QET::QetMessageBox::question(
parent,
tr("Compteur de la numérotation"),
tr("Le prochain numéro serait %1, mais %n élément(s) de la numérotation « %2 » "
"ont déjà un numéro égal ou supérieur (jusqu'à %3).", "", conflict->count)
.arg(conflict->counter).arg(title).arg(conflict->highest)
+ tr("\nLes nouveaux éléments sauteront les numéros déjà pris : ils ne recevront "
"pas forcément les numéros à partir de %1.\n\nContinuer ?").arg(conflict->counter),
QMessageBox::Yes | QMessageBox::Cancel,
QMessageBox::Cancel);
return answer == QMessageBox::Yes;
}
+46
View File
@@ -0,0 +1,46 @@
/*
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 COUNTERWARNING_H
#define COUNTERWARNING_H
#include "../numerotationcontext.h"
#include <QCoreApplication>
class QETProject;
class QWidget;
/**
@brief The CounterWarning class
Asks before the counter of an element numbering is set to a number which
elements of it have already: the next elements skip the numbers in use,
so the next number is not the one asked for. The user may go on or give
up.
*/
class CounterWarning
{
Q_DECLARE_TR_FUNCTIONS(CounterWarning)
public:
static bool confirm(QWidget *parent,
const QETProject *project,
const QString &title,
const NumerotationContext &proposed);
};
#endif // COUNTERWARNING_H
+116
View File
@@ -0,0 +1,116 @@
/*
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 "pastenumberingimport.h"
#include "../../diagram.h"
#include "../../diagramcommands.h"
#include "../../qetmessagebox.h"
#include "../../qetproject.h"
#include <QSettings>
/// @return the numberings a copy carries, which pasted elements follow
QList<ElementAutoNumSchemeCommand::Scheme> PasteNumberingImport::copiedBy(const QDomDocument &clipboard)
{
return ElementAutoNumSchemeCommand::copiedSchemes(clipboard.documentElement());
}
/**
@brief PasteNumberingImport::push
Push the paste of @p content on the undo stack of @p diagram, after
asking whether to import the numberings of @p copied which the pasted
elements follow and the project does not have.
@param parent : parent of the question
@param copied : the numberings the copy carries
@param extra : commands the copy brings beside the paste itself
(the cable lines of a fragment, for example); they are pushed inside
the same macro, so one Ctrl+Z undoes all of it at once.
*/
void PasteNumberingImport::push(QWidget *parent,
Diagram *diagram,
const DiagramContent &content,
const QList<ElementAutoNumSchemeCommand::Scheme> &copied,
const QList<QUndoCommand *> &extra)
{
QETProject *project = diagram->project();
auto *paste = new PasteDiagramCommand(diagram, content);
//The paste and the commands beside it are one step: only the
//macros really need more than the single push.
const auto push_all = [&]() {
if (extra.isEmpty()) {
diagram->undoStack().push(paste);
return;
}
diagram->undoStack().beginMacro(paste->text());
diagram->undoStack().push(paste);
for (QUndoCommand *cmd : extra) {
diagram->undoStack().push(cmd);
}
diagram->undoStack().endMacro();
};
QList<ElementAutoNumSchemeCommand::Scheme> missing;
if (project && !copied.isEmpty()
&& QSettings().value("diagramcommands/autonumber-pasted-elements", true).toBool()) {
missing = ElementAutoNumSchemeCommand::missingForPaste(project, copied, content.m_elements);
}
if (missing.isEmpty()) {
push_all();
return;
}
QStringList names;
for (const auto &scheme : std::as_const(missing)) {
names << QStringLiteral("« %1 »").arg(scheme.title);
}
const auto answer = QET::QetMessageBox::question(
parent,
tr("Numérotation absente de ce projet"),
tr("Les éléments collés suivent %n numérotation(s) qui n'existe(nt) pas dans ce projet : %1.\n\n"
"Les importer ? Les éléments recevront alors les prochains numéros, à partir de 1. "
"Sinon ils seront collés sans numérotation.", "", static_cast<int>(missing.size()))
.arg(names.join(QStringLiteral(", "))),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
push_all();
return;
}
diagram->undoStack().beginMacro(paste->text());
for (const auto &scheme : std::as_const(missing))
{
//Under its own name, or one which is free here
QString title = scheme.title;
for (int n = 2 ; !project->elementAutoNumNameClash(title).isEmpty() ; ++n) {
title = QStringLiteral("%1 (%2)").arg(scheme.title).arg(n);
}
if (auto *cmd = ElementAutoNumSchemeCommand::create(
project, title,
ElementAutoNumSchemeCommand::resetForRenumber(scheme.context),
scheme.id, false)) {
diagram->undoStack().push(cmd);
}
}
diagram->undoStack().push(paste);
for (QUndoCommand *cmd : extra) {
diagram->undoStack().push(cmd);
}
diagram->undoStack().endMacro();
}
+64
View File
@@ -0,0 +1,64 @@
/*
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 PASTENUMBERINGIMPORT_H
#define PASTENUMBERINGIMPORT_H
#include "../elementautonumschemecommand.h"
#include "../../diagramcontent.h"
#include <QCoreApplication>
#include <QList>
#include <QUndoCommand>
class Diagram;
class QWidget;
/**
@brief The PasteNumberingImport class
Pastes elements copied from another project, which may follow element
numberings this project does not have.
Such an element would be left with a formula which names no numbering
here, and with a label nothing explains. So the user is asked, once
for all the numberings missing, whether to import them. Imported, they
number the pasted elements from their first number (a number of the other
project means nothing here); not imported, the elements are pasted
without a numbering.
The import and the paste are one undo step.
A paste which brings more than itself (the cable lines of the copy,
for instance, which are no part of a DiagramContent) hands those
commands over through @p extra: they are pushed inside the very
same macro, so one Ctrl+Z still takes the whole of it back.
*/
class PasteNumberingImport
{
Q_DECLARE_TR_FUNCTIONS(PasteNumberingImport)
public:
static void push(QWidget *parent,
Diagram *diagram,
const DiagramContent &content,
const QList<ElementAutoNumSchemeCommand::Scheme> &copied,
const QList<QUndoCommand *> &extra = QList<QUndoCommand *>());
static QList<ElementAutoNumSchemeCommand::Scheme> copiedBy(const QDomDocument &clipboard);
};
#endif // PASTENUMBERINGIMPORT_H
@@ -0,0 +1,187 @@
/*
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 "renumberpreviewdialog.h"
#include "../../diagram.h"
#include "../../qetgraphicsitem/element.h"
#include "../../qetinformation.h"
#include <QBrush>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFont>
#include <QHeaderView>
#include <QLabel>
#include <QPushButton>
#include <QTableWidget>
#include <QVBoxLayout>
namespace {
QString labelOf(const DiagramContext &info)
{
return info.value(QETInformation::ELMT_LABEL).toString();
}
QString folioOf(const Element *element)
{
return element && element->diagram()
? QString::number(element->diagram()->folioIndex() + 1)
: QString();
}
} // namespace
/**
@brief RenumberPreviewDialog::changedLabelCount
@return how many of @p changes give their element another label
*/
int RenumberPreviewDialog::changedLabelCount(
const QVector<RenumberElementsCommand::ElementChange> &changes)
{
int count = 0;
for (const auto &change : changes) {
if (labelOf(change.old_infos) != labelOf(change.new_infos)) {
++count;
}
}
return count;
}
/**
@brief RenumberPreviewDialog::ask
@param intro : text above the list
@param changes : what the operation would do to elements
@param left_alone : the elements it leaves as they are, each with the
reason shown in place of its new name
@param replace_text : when not empty, a third button of that text
which goes on and also replaces the elements left alone
@return what the user chose
*/
RenumberPreviewDialog::Answer RenumberPreviewDialog::ask(
QWidget *parent,
const QString &title,
const QString &intro,
const QVector<RenumberElementsCommand::ElementChange> &changes,
const QVector<LeftAlone> &left_alone,
const QString &replace_text)
{
QDialog dialog(parent);
dialog.setWindowTitle(title);
dialog.resize(640, 460);
auto *layout = new QVBoxLayout(&dialog);
const int changed = changedLabelCount(changes);
QString text = intro;
if (changes.size() > changed) {
text += QLatin1Char('\n') + tr("%n élément(s) gardent le nom qu'ils ont.", "", changes.size() - changed);
}
if (!left_alone.isEmpty()) {
text += QLatin1Char('\n') + tr("%n élément(s) restent comme ils sont.", "", left_alone.size());
}
auto *intro_label = new QLabel(text, &dialog);
intro_label->setWordWrap(true);
layout->addWidget(intro_label);
auto *table = new QTableWidget(&dialog);
table->setColumnCount(4);
table->setHorizontalHeaderLabels({tr("Folio"), tr("Élément"), tr("Nom actuel"), tr("Nouveau nom")});
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->verticalHeader()->setVisible(false);
table->horizontalHeader()->setStretchLastSection(true);
table->setAlternatingRowColors(true);
const auto add_row = [table](const QStringList &cells, bool grey) {
const int row = table->rowCount();
table->insertRow(row);
for (int column = 0 ; column < cells.size() ; ++column) {
auto *item = new QTableWidgetItem(cells.at(column));
if (grey) {
QFont font = item->font();
font.setItalic(true);
item->setFont(font);
item->setForeground(QBrush(Qt::gray));
}
table->setItem(row, column, item);
}
};
for (const auto &change : changes) {
const QString before = labelOf(change.old_infos);
const QString after = labelOf(change.new_infos);
if (before != after) {
add_row({folioOf(change.element),
change.element ? change.element->name() : QString(),
before, after}, false);
}
}
for (const LeftAlone &left : left_alone) {
if (left.element) {
add_row({folioOf(left.element), left.element->name(),
labelOf(left.element->elementInformations()), left.note}, true);
}
}
table->resizeColumnsToContents();
layout->addWidget(table, 1);
//With no label to change, OK would do nothing: only Cancel (and, where
//there is one, the button which replaces what is left alone) is offered
const bool something_to_do = !changes.isEmpty();
auto *buttons = new QDialogButtonBox(
something_to_do ? (QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
: QDialogButtonBox::Cancel, &dialog);
Answer answer = Answer::Cancel;
QPushButton *replace = nullptr;
if (!replace_text.isEmpty()) {
replace = buttons->addButton(replace_text, QDialogButtonBox::ActionRole);
}
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (replace) {
QObject::connect(replace, &QPushButton::clicked, &dialog, [&]() {
answer = Answer::GoAndReplace;
dialog.accept();
});
}
layout->addWidget(buttons);
if (dialog.exec() == QDialog::Accepted && answer != Answer::GoAndReplace) {
answer = Answer::Go;
}
return answer;
}
/**
@brief RenumberPreviewDialog::confirm
@param intro : text above the list
@param changes : what the operation would do to elements
@param frozen : the elements it leaves as they are, a frozen label
@return true if the user agrees to go on
*/
bool RenumberPreviewDialog::confirm(
QWidget *parent,
const QString &title,
const QString &intro,
const QVector<RenumberElementsCommand::ElementChange> &changes,
const QVector<Element *> &frozen)
{
QVector<LeftAlone> left;
for (Element *el : frozen) {
left.append({el, tr("(figé)")});
}
return ask(parent, title, intro, changes, left) == Answer::Go;
}
@@ -0,0 +1,68 @@
/*
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 RENUMBERPREVIEWDIALOG_H
#define RENUMBERPREVIEWDIALOG_H
#include "../renumberelementscommand.h"
#include <QCoreApplication>
#include <QVector>
class Element;
class QWidget;
/**
@brief The RenumberPreviewDialog class
Shows what a numbering operation is about to do to the labels of
elements -- each changed element with its label now and the one it
would get -- and the elements with a frozen label it leaves as they are,
and asks to go on or not. Nothing is changed by the dialog itself.
*/
class RenumberPreviewDialog
{
Q_DECLARE_TR_FUNCTIONS(RenumberPreviewDialog)
public:
/// An element an operation leaves as it is, and why
struct LeftAlone
{
Element *element = nullptr;
QString note;
};
enum class Answer { Cancel, Go, GoAndReplace };
static Answer ask(
QWidget *parent,
const QString &title,
const QString &intro,
const QVector<RenumberElementsCommand::ElementChange> &changes,
const QVector<LeftAlone> &left_alone,
const QString &replace_text = QString());
static bool confirm(
QWidget *parent,
const QString &title,
const QString &intro,
const QVector<RenumberElementsCommand::ElementChange> &changes,
const QVector<Element *> &frozen);
static int changedLabelCount(
const QVector<RenumberElementsCommand::ElementChange> &changes);
};
#endif // RENUMBERPREVIEWDIALOG_H
+165 -50
View File
@@ -18,14 +18,17 @@
#include "selectautonumw.h"
#include "../assignvariables.h"
#include "../numerotationcontextcommands.h"
#include "formulaautonumberingw.h"
#include "numparteditorw.h"
#include "ui_formulaautonumberingw.h"
#include "ui_selectautonumw.h"
#include "../../qeticons.h"
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPushButton>
#include <QToolButton>
/**
@brief SelectAutonumW::SelectAutonumW
@@ -40,6 +43,24 @@ SelectAutonumW::SelectAutonumW(int type, QWidget *parent) :
{
ui->setupUi(this);
ui->m_comboBox->lineEdit()->setClearButtonEnabled(true);
//The add button goes under the last part, at the left; each part
//has its own remove and move buttons at its right. The one remove
//button above the parts, which could only remove the last one, is
//not needed any more.
ui->remove_button->hide();
ui->horizontalLayout->removeWidget(ui->add_button);
m_add_row = new QWidget(this);
auto *add_layout = new QHBoxLayout(m_add_row);
add_layout->setContentsMargins(0, 0, 0, 0);
ui->add_button->setParent(m_add_row);
add_layout->addWidget(ui->add_button);
add_layout->addStretch();
ui->editor_layout->addWidget(m_add_row);
ui->buttonBox->button(QDialogButtonBox::Reset)->setText(tr("Annuler"));
ui->buttonBox->button(QDialogButtonBox::Reset)->setToolTip(
tr("Revenir à la définition enregistrée"));
if (m_edited_type == 0)
{
m_feaw = new FormulaAutonumberingW();
@@ -104,35 +125,132 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
{
m_context = context;
qDeleteAll(num_part_list_);
num_part_list_.clear();
clearPartRows();
if (m_context.size() == 0) { //@context contain nothing, build a default numPartEditor
on_add_button_clicked();
insertPartRow(new NumPartEditorW(m_edited_type, this));
}
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, &NumPartEditorW::changed, this, [this]() {
m_dirty = true;
applyEnable();
emit contextEdited();
});
num_part_list_ << part;
ui -> editor_layout -> addWidget(part);
insertPartRow(new NumPartEditorW(m_context, i, m_edited_type, this));
}
}
num_part_list_.size() == 1 ?
ui -> remove_button -> setDisabled(true):
ui -> remove_button -> setEnabled (true);
updatePartButtons();
applyEnable(false);
//Everything the rule held before is read now, so nothing of it
//is left to write: what is on show is what there is.
m_dirty = false;
}
/**
@brief SelectAutonumW::insertPartRow
Show @p part as the last part of the definition, with the buttons which
remove it and move it up or down at its right.
*/
void SelectAutonumW::insertPartRow(NumPartEditorW *part)
{
connect(part, &NumPartEditorW::changed, this, [this]() {
m_dirty = true;
applyEnable();
emit contextEdited();
});
PartRow r;
r.part = part;
r.row = new QWidget(this);
auto *layout = new QHBoxLayout(r.row);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(2);
part->setParent(r.row);
layout->addWidget(part, 1);
const auto make_button = [&](const QIcon &icon, const QString &tip) {
auto *button = new QToolButton(r.row);
button->setIcon(icon);
button->setToolTip(tip);
button->setAutoRaise(true);
layout->addWidget(button);
return button;
};
r.up = make_button(QET::Icons::GoUp, tr("Monter cette variable"));
r.down = make_button(QET::Icons::GoDown, tr("Descendre cette variable"));
r.remove = make_button(QET::Icons::EditDelete, tr("Supprimer cette variable"));
QWidget *row = r.row;
connect(r.up, &QToolButton::clicked, this, [this, row]() { movePartRow(row, -1); });
connect(r.down, &QToolButton::clicked, this, [this, row]() { movePartRow(row, +1); });
connect(r.remove, &QToolButton::clicked, this, [this, row]() { removePartRow(row); });
m_rows << r;
num_part_list_ << part;
//Before the add button, which stays under the last part
ui->editor_layout->insertWidget(ui->editor_layout->count() - 1, r.row);
updatePartButtons();
}
/// Take away every part of the definition shown
void SelectAutonumW::clearPartRows()
{
for (const PartRow &r : std::as_const(m_rows)) {
delete r.row; //with the part, which it holds
}
m_rows.clear();
num_part_list_.clear();
}
/**
@brief SelectAutonumW::updatePartButtons
The first part cannot move up, the last cannot move down, and the only
one cannot be removed: those buttons are greyed out.
*/
void SelectAutonumW::updatePartButtons()
{
for (int i = 0 ; i < m_rows.size() ; ++i) {
m_rows.at(i).up->setEnabled(i > 0);
m_rows.at(i).down->setEnabled(i < m_rows.size() - 1);
m_rows.at(i).remove->setEnabled(m_rows.size() > 1);
}
}
/// Remove the part shown in @p row; the definition keeps at least one
void SelectAutonumW::removePartRow(QWidget *row)
{
if (m_rows.size() <= 1) {
return;
}
for (int i = 0 ; i < m_rows.size() ; ++i) {
if (m_rows.at(i).row == row) {
const PartRow r = m_rows.takeAt(i);
num_part_list_.removeAll(r.part);
delete r.row; //with the part, which it holds
break;
}
}
updatePartButtons();
applyEnable();
}
/// Move the part shown in @p row up (-1) or down (+1)
void SelectAutonumW::movePartRow(QWidget *row, int step)
{
for (int i = 0 ; i < m_rows.size() ; ++i) {
if (m_rows.at(i).row != row) {
continue;
}
const int to = i + step;
if (to < 0 || to >= m_rows.size()) {
return;
}
m_rows.swapItemsAt(i, to);
num_part_list_.swapItemsAt(i, to);
ui->editor_layout->removeWidget(row);
ui->editor_layout->insertWidget(1 + to, row); //after the header of the columns
break;
}
updatePartButtons();
applyEnable();
}
/**
@brief SelectAutonumW::toNumContext
@return the content to num_part_list to NumerotationContext
@@ -151,17 +269,10 @@ NumerotationContext SelectAutonumW::toNumContext() const
*/
void SelectAutonumW::on_add_button_clicked()
{
applyEnable(false);
insertPartRow(new NumPartEditorW(m_edited_type, this));
m_dirty = true;
NumPartEditorW *part = new NumPartEditorW(m_edited_type, this);
connect(part, &NumPartEditorW::changed, this, [this]() {
m_dirty = true;
applyEnable();
emit contextEdited();
});
num_part_list_ << part;
ui -> editor_layout -> addWidget(part);
ui -> remove_button -> setEnabled(true);
applyEnable();
emit contextEdited();
}
@@ -174,9 +285,7 @@ void SelectAutonumW::on_remove_button_clicked()
//remove if @num_part_list contains more than one item
if (num_part_list_.size() > 1) {
m_dirty = true;
NumPartEditorW *part = num_part_list_.takeLast();
// deliberately not disconnecting as not possible to resolve with lambda and will happen automatically when the object "part" is destroyed.
delete part;
removePartRow(m_rows.last().row);
if (num_part_list_.size() == 1) {
ui -> remove_button -> setDisabled(true);
}
@@ -409,6 +518,8 @@ void SelectAutonumW::on_buttonBox_clicked(QAbstractButton *button)
*/
void SelectAutonumW::applyEnable(bool b)
{
//Apply and Cancel both stand for a change: grey until there is one
ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(b);
if (b){
bool valid= true;
foreach (NumPartEditorW *npe, num_part_list_)
@@ -448,28 +559,6 @@ void SelectAutonumW::contextToFormula()
}
}
/**
@brief SelectAutonumW::on_m_next_pb_clicked
Increase NumerotationContext
*/
void SelectAutonumW::on_m_next_pb_clicked()
{
NumerotationContextCommands ncc (toNumContext());
setContext(ncc.next());
applyEnable(true);
}
/**
@brief SelectAutonumW::on_m_previous_pb_clicked
Decrease NumerotationContext
*/
void SelectAutonumW::on_m_previous_pb_clicked()
{
NumerotationContextCommands ncc (toNumContext());
setContext(ncc.previous());
applyEnable(true);
}
void SelectAutonumW::on_m_comboBox_currentTextChanged(const QString &arg1)
{
Q_UNUSED(arg1);
@@ -480,3 +569,29 @@ void SelectAutonumW::on_m_remove_pb_clicked()
{
emit removeClicked();
}
/**
@brief SelectAutonumW::setExplicitNaming
Make the list of numberings a plain choice: no typing a name into it
(an easy way to create a numbering by accident, or one with an empty
name), but a button to create a numbering and one to rename the shown
one, which emit newClicked() and renameClicked() for the owner to ask
for the name.
*/
void SelectAutonumW::setExplicitNaming()
{
ui->m_comboBox->setEditable(false);
auto *row = ui->horizontalLayout_2;
const int index = row->indexOf(ui->m_remove_pb);
auto *new_pb = new QPushButton(QET::Icons::Add, QString(), this);
new_pb->setToolTip(tr("Nouvelle numérotation…"));
connect(new_pb, &QPushButton::clicked, this, &SelectAutonumW::newClicked);
auto *rename_pb = new QPushButton(QET::Icons::EditRename, QString(), this);
rename_pb->setToolTip(tr("Renommer la numérotation…"));
connect(rename_pb, &QPushButton::clicked, this, &SelectAutonumW::renameClicked);
row->insertWidget(index, rename_pb);
row->insertWidget(index, new_pb);
}
+23 -3
View File
@@ -28,6 +28,8 @@ class QAbstractButton;
class FormulaAutonumberingW;
class QComboBox;
class QPushButton;
class QToolButton;
class QHBoxLayout;
namespace Ui {
class SelectAutonumW;
@@ -89,6 +91,7 @@ class SelectAutonumW : public QWidget
@param can_remove true when a rule is really there
*/
void setRuleRemovable(bool can_remove);
void setExplicitNaming();
signals:
void applyPressed();
@@ -101,6 +104,9 @@ class SelectAutonumW : public QWidget
write now.
*/
void contextEdited();
/// Only with setExplicitNaming()
void newClicked();
void renameClicked();
//SLOT
private slots:
@@ -108,16 +114,30 @@ class SelectAutonumW : public QWidget
void on_remove_button_clicked();
void on_buttonBox_clicked(QAbstractButton *);
void applyEnable (bool = true);
void on_m_next_pb_clicked();
void on_m_previous_pb_clicked();
void on_m_comboBox_currentTextChanged(const QString &arg1);
//ATTRIBUTES
void on_m_remove_pb_clicked();
private:
/// A part of the definition, with the buttons at its right
struct PartRow
{
QWidget *row = nullptr;
NumPartEditorW *part = nullptr;
QToolButton *up = nullptr;
QToolButton *down = nullptr;
QToolButton *remove = nullptr;
};
void insertPartRow(NumPartEditorW *part);
void removePartRow(QWidget *row);
void movePartRow(QWidget *row, int step);
void updatePartButtons();
void clearPartRows();
Ui::SelectAutonumW *ui;
QList <NumPartEditorW *> num_part_list_;
QList <PartRow> m_rows;
QWidget *m_add_row = nullptr; ///< holds the add button, under the last part
NumerotationContext m_context;
FormulaAutonumberingW *m_feaw;
FormulaAutonumberingW *m_fcaw;
+6 -34
View File
@@ -63,8 +63,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="edit-delete" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-delete.png</normaloff>:/ico/16x16/edit-delete.png</iconset>
<iconset theme="edit-delete"/>
</property>
</widget>
</item>
@@ -136,8 +135,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/list-remove.png</normaloff>:/ico/22x22/list-remove.png</iconset>
<iconset theme="list-remove"/>
</property>
<property name="flat">
<bool>false</bool>
@@ -153,42 +151,13 @@
<string/>
</property>
<property name="icon">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/list-add.png</normaloff>:/ico/22x22/list-add.png</iconset>
<iconset theme="list-add"/>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_previous_pb">
<property name="toolTip">
<string>Précédent</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="arrow-left" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/arrow-left.png</normaloff>:/ico/16x16/arrow-left.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="m_next_pb">
<property name="toolTip">
<string>Suivant</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="arrow-right" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/arrow-right.png</normaloff>:/ico/16x16/arrow-right.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
@@ -232,6 +201,9 @@
</item>
<item>
<widget class="QLabel" name="value_label">
<property name="toolTip">
<string>Pour un numéro : le prochain numéro attribué. Pour un texte : le texte fixe.</string>
</property>
<property name="text">
<string>Valeur</string>
</property>
+34 -4
View File
@@ -58,7 +58,32 @@ QStringList BomExport::defaultColumns()
};
}
QString BomExport::defaultQuery()
/**
@brief BomExport::junctionFilter
@return a condition true for every row that is not a junction: a
terminal-type element with no label and no part data. Some users draw
the dots and bends where wires branch as their own terminal-type
symbols, and each one became an empty row of the parts list (#1178).
A terminal block with a label or a part number is kept.
*/
QString BomExport::junctionFilter()
{
return QStringLiteral("NOT (element_type = 'terminal' "
"AND IFNULL(label, '') = '' "
"AND IFNULL(designation, '') = '' "
"AND IFNULL(manufacturer, '') = '' "
"AND IFNULL(manufacturer_reference, '') = '')");
}
/**
@brief BomExport::defaultQuery
@param include_slaves : false leaves out the contact blocks (slave
elements), which otherwise appear as rows of their own
@param include_junctions : false leaves out the junctions, see
junctionFilter()
@return the query of the parts list
*/
QString BomExport::defaultQuery(bool include_slaves, bool include_junctions)
{
//Slaves and terminals are included because both are routinely
//separately orderable hardware. A circuit breaker can carry ten or
@@ -78,11 +103,16 @@ QString BomExport::defaultQuery()
//and the conductor definition stay out because they are not hardware.
//
//See discussion #847.
const QString types = include_slaves
? QStringLiteral("'simple', 'master', 'slave', 'terminal'")
: QStringLiteral("'simple', 'master', 'terminal'");
const QString junctions = include_junctions
? QString()
: QStringLiteral(" AND ") + junctionFilter();
return QStringLiteral("SELECT %1 FROM element_nomenclature_view "
"WHERE element_type IN "
"('simple', 'master', 'slave', 'terminal') "
"WHERE element_type IN (%2)%3 "
"ORDER BY diagram_position, position, label")
.arg(defaultColumns().join(QStringLiteral(", ")));
.arg(defaultColumns().join(QStringLiteral(", ")), types, junctions);
}
QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers,
+3 -1
View File
@@ -27,7 +27,9 @@ class QSqlQuery;
namespace BomExport
{
QStringList defaultColumns();
QString defaultQuery();
QString defaultQuery(bool include_slaves = true,
bool include_junctions = true);
QString junctionFilter();
/// One CSV record: every field quoted, ';' between them, '\n'
/// after -- the format every CSV export of this program writes.
/// Public so a list which is not read from the project database
+8 -2
View File
@@ -117,9 +117,15 @@ void BorderProperties::toXml(QDomElement &e) const
*/
void BorderProperties::fromXml(QDomElement &e) {
if (e.hasAttribute("cols")) columns_count = e.attribute("cols").toInt();
if (e.hasAttribute("colsize")) columns_width = e.attribute("colsize").toInt();
if (e.hasAttribute("rows")) rows_count = e.attribute("rows").toInt();
if (e.hasAttribute("rowsize")) rows_height = e.attribute("rowsize").toInt();
// The sizes are decimals (toXml() writes them with %1), read as
// BorderTitleBlock::borderFromXml() does; an unreadable value is
// left as it was.
bool ok;
qreal size = e.attribute("colsize").toDouble(&ok);
if (ok && qIsFinite(size)) columns_width = size;
size = e.attribute("rowsize").toDouble(&ok);
if (ok && qIsFinite(size)) rows_height = size;
if (e.hasAttribute("displaycols")) display_columns = e.attribute("displaycols") == "true";
if (e.hasAttribute("displayrows")) display_rows = e.attribute("displayrows") == "true";
}
+17 -4
View File
@@ -708,13 +708,26 @@ void BorderTitleBlock::drawDxf(
// render the titleblock, using the TitleBlockTemplate object
if (display_titleblock_) {
Createdxf::layer = DxfExport::Layer::TitleBlock;
//qp -> translate(titleblock_rect_.topLeft());
QRectF rect = titleBlockRect();
//Laid out horizontally, then turned like the QPainter in
//draw(): a quarter turn counter-clockwise around its top
//left, which for a title block at the right is the bottom
//right of the diagram (issue #1339).
QRectF rect = titleBlockRectForQPainter();
QTransform dxf_transform;
if (m_edge != Qt::BottomEdge)
{
const QPointF pivot(rect.left() * Createdxf::xScale,
Createdxf::sheetHeight
- rect.top() * Createdxf::yScale);
dxf_transform.translate(pivot.x(), pivot.y());
dxf_transform.rotate(90);
dxf_transform.translate(-pivot.x(), -pivot.y());
}
m_titleblock_template_renderer -> renderDxf(rect,
rect.width(),
file_path,
color);
//qp -> translate(-titleblock_rect_.topLeft());
color,
dxf_transform);
}
// Transform back to QET scale
+19 -15
View File
@@ -133,6 +133,8 @@ class BorderTitleBlock : public QObject
return (additional_fields_); }
/// @return the value of the title block
QString autoPageNum() const { return(btb_auto_page_num_); }
/// The title of the folio numbering this folio follows, nothing else changes
void setAutoPageNum(const QString &title) { btb_auto_page_num_ = title; }
/// @return the value of the total number of folios
int folioTotal() const { return(folio_total_);}
@@ -252,38 +254,40 @@ class BorderTitleBlock : public QObject
QString btb_indexrev_; ///< titleblock index rev
QString btb_final_folio_; ///< titleblock final folio
QString btb_auto_page_num_; ///< titleblock auto page num
int folio_index_; ///< titleblock index
int folio_total_; ///< titleblock total
int folio_index_ = 0; ///< titleblock index
int folio_total_ = 0; ///< titleblock total
QString btb_filename_; ///< titleblock filename
QString btb_version_; ///< titleblock version
/// titleblock additional fields
DiagramContext additional_fields_;
Qt::Edge m_edge; ///< titleblock edge
Qt::Edge m_edge = Qt::BottomEdge; ///< titleblock edge
QString m_next_folio_num; ///< titleblock next folio num
QString m_previous_folio_num; ///< titleblock previous folio num
// border dimensions (rows and columns)
// columns: number and dimensions
int columns_count_; ///< columns count
qreal columns_width_; ///< columns width
qreal columns_header_height_; ///< columns header height
int columns_count_ = 0; ///< columns count
qreal columns_width_ = 0; ///< columns width
qreal columns_header_height_ = 0; ///< columns header height
// rows: number and dimensions
int rows_count_; ///< rows count
qreal rows_height_; ///< rows height
qreal rows_header_width_; ///< rows header width
int rows_count_ = 0; ///< rows count
qreal rows_height_ = 0; ///< rows height
qreal rows_header_width_ = 0; ///< rows header width
// title block dimensions
qreal titleblock_height_;
qreal titleblock_height_ = 0;
// rectangles used for drawing operations
QRectF diagram_rect_;
// display options
bool display_titleblock_;
bool display_columns_;
bool display_rows_;
bool display_border_;
// display options. Initialised here because the constructor sets
// them through importBorder() and importTitleBlock(), whose setters
// compare the new value with the old one before storing it.
bool display_titleblock_ = false;
bool display_columns_ = false;
bool display_rows_ = false;
bool display_border_ = false;
TitleBlockTemplateRenderer *m_titleblock_template_renderer;
};
#endif
+74 -11
View File
@@ -43,6 +43,7 @@
#include <QDirIterator>
#include <QDomDocument>
#include <QDate>
#include <QDateTime>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
@@ -59,6 +60,7 @@
#include <QSqlQuery>
#include <QSvgGenerator>
#include <QTextStream>
#include <QTimeZone>
#include <QTransform>
namespace {
@@ -147,6 +149,23 @@ void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target,
diagram->setDrawTerminalNames(was_drawing_terminal_names);
}
/// The time SOURCE_DATE_EPOCH names, in seconds since 1970 UTC, or an
/// invalid QDateTime when it is unset. A value that is not a whole number of
/// seconds is reported and ignored.
QDateTime sourceDateEpoch()
{
const QByteArray value = qgetenv("SOURCE_DATE_EPOCH");
if (value.isEmpty())
return {};
bool ok = false;
const qlonglong seconds = value.toLongLong(&ok);
if (!ok || seconds < 0) {
err << "SOURCE_DATE_EPOCH '" << value << "' is not a number of seconds; ignored.\n";
return {};
}
return QDateTime::fromSecsSinceEpoch(seconds, QTimeZone::utc());
}
int exportPdf(QETProject &project, const QString &output,
bool showTerminals = false)
{
@@ -166,16 +185,37 @@ int exportPdf(QETProject &project, const QString &output,
writer.setCreator("QElectroTech");
writer.setResolution(96);
// SOURCE_DATE_EPOCH (reproducible-builds.org) asks for the same file
// from the same input: the time it names instead of now, and a document
// id from what the PDF shows instead of a random one. Qt has no setter
// for the dates, and the content is not known yet, so both are
// rewritten once the file is written; Qt writes a fixed id until then.
// The id does not come from the project file, whose uuids are new each
// time a project is generated again from the same data. Before Qt 6.8
// there is no document id to set: it is only written for PDF/A.
const QDateTime sourceDate = sourceDateEpoch();
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
if (sourceDate.isValid())
writer.setDocumentId(PdfLinks::placeholderDocumentId());
#endif
QPainter painter;
bool first = true;
for (Diagram *diagram : diagrams) {
const QRect r = diagramRect(diagram);
// Match the page to the diagram (in points: 1px @ 96dpi = 0.75pt).
const QPageSize page(QSizeF(r.width() * 72.0 / 96.0,
r.height() * 72.0 / 96.0),
QPageSize::Point);
writer.setPageSize(page);
writer.setPageMargins(QMarginsF(0, 0, 0, 0));
// QPageSize rounds a size within 3 pt of a standard sheet to the
// sheet, but knows the sheets upright only (bar Ledger), so a wide
// folio is matched upright and turned: otherwise an A3 landscape
// folio became a 1190 x 841 pt page while an A3 portrait one was
// 842 x 1191, the sheet.
QSizeF points(r.width() * 72.0 / 96.0, r.height() * 72.0 / 96.0);
const bool wide = points.width() > points.height();
if (wide) points.transpose();
writer.setPageLayout(QPageLayout(QPageSize(points, QPageSize::Point),
wide ? QPageLayout::Landscape
: QPageLayout::Portrait,
QMarginsF(0, 0, 0, 0)));
if (first) {
if (!painter.begin(&writer)) {
@@ -233,6 +273,10 @@ int exportPdf(QETProject &project, const QString &output,
// the cross-references jump inside the document in any PDF viewer.
PdfLinks::convertUriToGoTo(output);
PdfLinks::removeUnusedPdfxNamespace(output);
if (sourceDate.isValid()) {
PdfLinks::setDocumentDate(output, sourceDate);
PdfLinks::setDocumentIdFromContent(output);
}
out << "Exported " << diagrams.size() << " page(s) -> " << output << "\n";
return 0;
@@ -287,7 +331,8 @@ int exportImages(QETProject &project, const QString &format,
/// One DXF file per diagram, written by the same code as the export dialog
/// (DxfExport) with the dialog's default options -- the export settings of
/// the preferences -- so both give the same file.
int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals)
int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals,
bool dxfBlocks, bool dxfAttributes)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
@@ -300,6 +345,10 @@ int exportDxf(QETProject &project, const QString &out_dir, bool showTerminals)
properties.format = QStringLiteral("DXF");
if (showTerminals)
properties.draw_terminals = true;
if (dxfBlocks || dxfAttributes)
properties.dxf_blocks = true;
if (dxfAttributes)
properties.dxf_attributes = true;
int index = 0;
bool has_images = false;
@@ -364,10 +413,12 @@ int exportCsv(QETProject &project, const QString &format, const QString &output)
/// Bill of materials from the same project database and default query as the
/// GUI nomenclature export.
int exportBom(QETProject &project, const QString &output)
int exportBom(QETProject &project, const QString &output,
bool includeSlaves, bool includeJunctions)
{
project.dataBase()->updateDB();
QSqlQuery query = project.dataBase()->newQuery(BomExport::defaultQuery());
QSqlQuery query = project.dataBase()->newQuery(
BomExport::defaultQuery(includeSlaves, includeJunctions));
if (!query.exec()) {
err << "BOM query failed: " << query.lastError().text() << "\n";
return 1;
@@ -620,6 +671,12 @@ QHash<Element *, int> folioIndex(QETProject &project)
/// From-to wiring list: one row per conductor, each endpoint resolved to its
/// element label and terminal name.
///
/// Most symbols leave their terminals unnamed, so each end also carries the
/// terminal's index -- the one the scripting API's addConductor() takes,
/// empty for two terminals at one point -- and its uuid, the one the project
/// file names the terminal by. These come last, after the columns the list
/// has always had, so a reader of those is not disturbed.
///
/// 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
@@ -632,7 +689,8 @@ int exportWiring(QETProject &project, const QString &output)
static const QStringList columns {
"wire_number", "from_element_label", "from_terminal",
"to_element_label", "to_terminal", "diagram_position", "conductor_uuid"
"to_element_label", "to_terminal", "diagram_position", "conductor_uuid",
"from_terminal_index", "from_terminal_uuid", "to_terminal_index", "to_terminal_uuid"
};
QSqlQuery query = project.dataBase()->newQuery(
@@ -929,6 +987,11 @@ int run(const QStringList &args)
// collected below.
QStringList filtered = args;
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
const bool dxfBlocks = filtered.removeAll("--dxf-blocks") > 0;
const bool dxfAttributes = filtered.removeAll("--dxf-attributes") > 0;
// --no-slaves and --no-junctions leave rows out of --export-bom.
const bool includeSlaves = filtered.removeAll("--no-slaves") == 0;
const bool includeJunctions = filtered.removeAll("--no-junctions") == 0;
QString flag;
QStringList rest;
@@ -982,11 +1045,11 @@ int run(const QStringList &args)
if (format == "pdf")
return exportPdf(project, output, showTerminals);
if (format == "dxf")
return exportDxf(project, output, showTerminals);
return exportDxf(project, output, showTerminals, dxfBlocks, dxfAttributes);
if (format == "cables" || format == "wires")
return exportCsv(project, format, output);
if (format == "bom")
return exportBom(project, output);
return exportBom(project, output, includeSlaves, includeJunctions);
if (format == "wiring")
return exportWiring(project, output);
if (format == "nets")
+12 -2
View File
@@ -45,10 +45,10 @@ namespace CLIExport {
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-dxf <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals] [--dxf-blocks] [--dxf-attributes]
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv> [--no-slaves] [--no-junctions]
qelectrotech --export-wiring <project.qet> <output.csv>
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
@@ -66,9 +66,19 @@ namespace CLIExport {
does; off by default, matching the GUI export dialog's
default. For DXF, draws the terminal markers. Has no effect on
the list export modes.
--dxf-blocks: write each symbol as a DXF block, placed with one
INSERT per use, so a CAD program selects it as one object.
--dxf-attributes: as --dxf-blocks, and a symbol's texts (its label,
function...) are attributes of its INSERT. LibreCAD does not
show attributes.
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.
--no-slaves: leave the contact blocks (slave elements) out of the
bill of materials.
--no-junctions: leave the junctions out of the bill of materials:
terminal-type elements with no label, designation,
manufacturer or manufacturer reference.
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,
+213
View File
@@ -0,0 +1,213 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
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 CONDUCTORMULTIEDIT_H
#define CONDUCTORMULTIEDIT_H
#include "QPropertyUndoCommand/qpropertyundocommand.h"
#include "conductorproperties.h"
#include <QCoreApplication>
#include <QList>
#include <QPointF>
#include <QSet>
#include <algorithm>
/**
@brief ConductorMultiEdit
The rules for editing several conductors at once in the Selection
properties panel (#500). They are templates over the conductor type so
they can be tested without a diagram: T needs properties(), a
"properties" Q_PROPERTY, and relatedPotentialConductors() returning
QSet<T *>.
*/
namespace ConductorMultiEdit
{
/**
@brief sortByPosition
Sort top to bottom, then left to right, so the panel shows the same
"first" conductor whatever order the scene lists the selection in.
@param list : the conductors
@param pos_of : callable giving a conductor's position
*/
template <typename T, typename PosOf>
void sortByPosition(QList<T *> &list, PosOf pos_of)
{
std::stable_sort(list.begin(), list.end(), [&pos_of](T *a, T *b) {
const QPointF pa = pos_of(a);
const QPointF pb = pos_of(b);
if (pa.y() != pb.y())
return pa.y() < pb.y();
return pa.x() < pb.x();
});
}
/**
@brief targets
@param selected : the edited conductors
@param apply_all : true to add every conductor of their potentials
@return each conductor an edit reaches, once
*/
template <typename T>
QList<T *> targets(const QList<T *> &selected, bool apply_all)
{
QList<T *> list;
QSet<T *> seen;
for (T *conductor : selected)
{
//Already reached through an earlier conductor's potential:
//its own potential is the same one.
if (!conductor || seen.contains(conductor))
continue;
seen.insert(conductor);
list << conductor;
if (!apply_all)
continue;
const QSet<T *> potential = conductor->relatedPotentialConductors();
for (T *other : potential)
{
if (!seen.contains(other))
{
seen.insert(other);
list << other;
}
}
}
return list;
}
/**
@brief undo
@param targets : the conductors to change, see targets()
@param shown : the properties as the panel showed them
@param edited : the properties after the user's edit
@return one undo step giving each target the fields that differ
between shown and edited, or nullptr if no conductor changes
*/
template <typename T>
QUndoCommand *undo(const QList<T *> &targets,
const ConductorProperties &shown,
const ConductorProperties &edited)
{
if (edited == shown)
return nullptr;
auto *undo = new QUndoCommand();
int changed = 0;
for (T *conductor : targets)
{
const ConductorProperties old_properties = conductor->properties();
ConductorProperties properties = old_properties;
properties.applyChanges(shown, edited);
if (properties == old_properties)
continue;
QVariant old_value, new_value;
old_value.setValue(old_properties);
new_value.setValue(properties);
new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo);
++changed;
}
if (!changed)
{
delete undo;
return nullptr;
}
undo->setText(changed == 1
? QCoreApplication::translate("ConductorPropertiesEditorWidget",
"Modifier les propriétés d'un conducteur", "undo caption")
: QCoreApplication::translate("ConductorPropertiesEditorWidget",
"Modifier les propriétés de plusieurs conducteurs", "undo caption"));
return undo;
}
/// The fields a user types in. When the selected conductors do not
/// agree on one, the panel shows it blank.
enum TextField {
Text, Formula, Function, TensionProtocol,
WireColor, WireSection, Cable, Bus
};
inline QList<TextField> textFields()
{
return {Text, Formula, Function, TensionProtocol,
WireColor, WireSection, Cable, Bus};
}
inline QString &textField(ConductorProperties &p, TextField field)
{
switch (field)
{
case Text: return p.text;
case Formula: return p.m_formula;
case Function: return p.m_function;
case TensionProtocol: return p.m_tension_protocol;
case WireColor: return p.m_wire_color;
case WireSection: return p.m_wire_section;
case Cable: return p.m_cable;
case Bus: break;
}
return p.m_bus;
}
/**
@brief mixedTextFields
@return the text fields whose value is not the same in all of list
*/
inline QList<TextField> mixedTextFields(QList<ConductorProperties> list)
{
QList<TextField> mixed;
if (list.size() < 2)
return mixed;
for (TextField field : textFields())
{
const QString first = textField(list.first(), field);
for (ConductorProperties &p : list)
{
if (textField(p, field) != first)
{
mixed << field;
break;
}
}
}
return mixed;
}
/**
@brief shown
@return what the panel shows for list: the first conductor's
properties, with the mixed text fields blank. Typing any value
in a blank field, even the first conductor's, then differs from
what was shown, so it is applied to every conductor.
*/
inline ConductorProperties shown(const QList<ConductorProperties> &list,
const QList<TextField> &mixed)
{
if (list.isEmpty())
return ConductorProperties();
ConductorProperties p = list.first();
for (TextField field : mixed)
textField(p, field).clear();
return p;
}
}
#endif // CONDUCTORMULTIEDIT_H
+5 -3
View File
@@ -38,10 +38,12 @@ class ConductorProfile {
public:
/// Segments composing the conductor
QList<ConductorSegmentProfile *> segments;
/// Orientation of the start terminal
Qet::Orientation beginOrientation;
/// Orientation of the start terminal. Initialised because the default
/// constructor leaves it as is, and the copy constructor and operator=
/// then read it from such a profile.
Qet::Orientation beginOrientation = Qet::North;
/// Orientation of the end terminal.
Qet::Orientation endOrientation;
Qet::Orientation endOrientation = Qet::North;
// methods
public:
+4 -1
View File
@@ -447,7 +447,10 @@ void ConductorProperties::fromSettings(QSettings &settings, const QString &prefi
m_wire_color = settings.value(prefix + "conductor_color", "").toString();
m_wire_section = settings.value(prefix + "conductor_section", "").toString();
text_size = settings.value(prefix + "textsize", "7").toInt();
cond_size = settings.value(prefix + "size", "1").toInt();
// Saved with QString::number(cond_size): a decimal such as 1.4,
// which toInt() would read as 0.
cond_size = settings.value(prefix + "size", "1").toDouble();
if (!qIsFinite(cond_size) || cond_size <= 0) cond_size = 1;
m_show_text = settings.value(prefix + "displaytext", true).toBool();
m_one_text_per_folio = settings.value(prefix + "onetextperfolio", false).toBool();
verti_rotate_text = settings.value((prefix + "vertirotatetext"), "270").toDouble();
+433
View File
@@ -0,0 +1,433 @@
/*
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/>.
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#include "conductorrouter.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <queue>
#include <vector>
namespace {
using ConductorRouter::Direction;
constexpr qreal eps = 1e-6;
///Larger than any folio needs: a 2000 x 2000 grid
constexpr int max_nodes = 4000000;
///What each thing costs, in scene units of length. A bend is worth
///three grid steps of extra wire, so the search prefers a slightly
///longer route to a more crooked one, as a person drawing would.
constexpr qreal bend_steps = 3.0;
///Running along another wire costs this much more per unit of length
constexpr qreal along_factor = 3.0;
///Each crossing of another wire costs two grid steps
constexpr qreal cross_steps = 2.0;
QPointF step(Direction d)
{
switch (d) {
case Direction::North: return {0, -1};
case Direction::East: return {1, 0};
case Direction::South: return {0, 1};
case Direction::West: return {-1, 0};
}
return {0, 0};
}
Direction opposite(Direction d)
{
return static_cast<Direction>((static_cast<int>(d) + 2) % 4);
}
bool strictlyInside(const QPointF &p, const QRectF &r)
{
return p.x() > r.left() + eps && p.x() < r.right() - eps
&& p.y() > r.top() + eps && p.y() < r.bottom() - eps;
}
bool insideAny(const QPointF &p, const QList<QRectF> &rects)
{
for (const QRectF &r : rects)
if (strictlyInside(p, r)) return true;
return false;
}
///A wire segment, kept as the fixed coordinate and the range along the
///other axis
struct Span { qreal at, from, to; };
///One grid step out of a terminal in its direction, snapped to the
///grid the way Conductor::extendTerminal() snaps it
QPointF firstStep(const QPointF &dock, Direction d, qreal grid)
{
const QPointF s = step(d);
QPointF p = dock;
if (s.x() != 0)
p.setX(std::round((dock.x() + s.x() * grid) / grid) * grid);
else
p.setY(std::round((dock.y() + s.y() * grid) / grid) * grid);
return p;
}
///Whether the horizontal or vertical segment from @p a to @p b runs
///through the inside of @p rect; along its edge does not count.
bool crossesInside(const QPointF &a, const QPointF &b, const QRectF &rect)
{
const QRectF seg = QRectF(a, b).normalized();
if (seg.width() < eps)
return seg.left() > rect.left() + eps && seg.left() < rect.right() - eps
&& std::min(seg.bottom(), rect.bottom()) - std::max(seg.top(), rect.top()) > eps;
return seg.top() > rect.top() + eps && seg.top() < rect.bottom() - eps
&& std::min(seg.right(), rect.right()) - std::max(seg.left(), rect.left()) > eps;
}
///The first point of a route after a terminal: firstStep(), then on
///until it is clear of every obstacle -- of the terminal's own symbol
///above all.
///When the terminal's own symbol is known, never through another
///symbol, only through the margin around it: a terminal pointing
///straight into one has no exit, where walking on through it would
///give a route that crosses that symbol and loops back. @p others are
///the other symbols, without their margin.
bool exitPoint(const QPointF &dock, Direction d, const ConductorRouter::Request &r,
const QList<QRectF> &obstacles, const QList<QRectF> &others,
bool own_known, QPointF &out)
{
const QPointF s = step(d);
QPointF from = dock, p = firstStep(dock, d, r.grid);
for (int i = 0; i < 200; ++i) {
if (own_known)
for (const QRectF &o : others)
if (crossesInside(from, p, o)) return false;
if (!insideAny(p, obstacles)) {
out = p;
return true;
}
from = p;
p += s * r.grid;
}
return false;
}
///Sorted, without duplicates: the grid lines inside [low, high], plus
///the extra coordinates (the two exit points, which need not be on the
///grid when a terminal is not)
QVector<qreal> axis(qreal low, qreal high, qreal grid, std::initializer_list<qreal> extra)
{
QVector<qreal> v;
for (qreal c = std::ceil(low / grid) * grid; c <= high + eps; c += grid)
v << c;
for (qreal c : extra) v << c;
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end(),
[](qreal a, qreal b) { return std::abs(a - b) < eps; }),
v.end());
return v;
}
int indexOf(const QVector<qreal> &v, qreal c)
{
auto it = std::lower_bound(v.begin(), v.end(), c - eps);
return (it != v.end() && std::abs(*it - c) < eps) ? int(it - v.begin()) : -1;
}
///[first, last) of the sorted v: the values strictly between low and high
std::pair<int, int> openRange(const QVector<qreal> &v, qreal low, qreal high)
{
const int first = int(std::upper_bound(v.begin(), v.end(), low) - v.begin());
const int last = int(std::lower_bound(v.begin(), v.end(), high) - v.begin());
return {first, std::max(first, last)};
}
///The cost of every edge along one axis, or infinity where an obstacle
///blocks it. Edge (k, i) runs from pos[i] to pos[i + 1] at the fixed
///coordinate lines[k], and is stored at [k * pos.size() + i];
///horizontal says which axis pos is. Each obstacle and wire visits only
///the edges it can touch, so the cost grows with the folio, not with
///the folio times everything drawn on it.
std::vector<qreal> edgeCosts(bool horizontal,
const QVector<qreal> &pos, const QVector<qreal> &lines,
const QList<QRectF> &obstacles,
const QVector<Span> &along, const QVector<Span> &across,
qreal grid)
{
const int np = pos.size(), nl = lines.size();
std::vector<qreal> cost(size_t(np) * nl, std::numeric_limits<qreal>::infinity());
for (int k = 0; k < nl; ++k)
for (int i = 0; i + 1 < np; ++i)
cost[size_t(k) * np + i] = pos[i + 1] - pos[i];
// The edges i that can overlap (low, high): pos[i + 1] > low and
// pos[i] < high. The exact test is applied to each one found.
const auto edges = [&](qreal low, qreal high) {
const int first = int(std::upper_bound(pos.begin(), pos.end(), low) - pos.begin()) - 1;
const int last = int(std::lower_bound(pos.begin(), pos.end(), high) - pos.begin());
return std::pair<int, int>{std::max(0, first), std::min(np - 1, last)};
};
for (const QRectF &r : obstacles) {
const qreal lo = horizontal ? r.left() : r.top();
const qreal hi = horizontal ? r.right() : r.bottom();
const qreal flo = horizontal ? r.top() : r.left();
const qreal fhi = horizontal ? r.bottom(): r.right();
const auto [k0, k1] = openRange(lines, flo, fhi);
const auto [i0, i1] = edges(lo, hi);
for (int k = k0; k < k1; ++k) {
if (!(lines[k] > flo + eps && lines[k] < fhi - eps)) continue;
for (int i = i0; i < i1; ++i)
if (std::min(pos[i + 1], hi) - std::max(pos[i], lo) > eps)
cost[size_t(k) * np + i] = std::numeric_limits<qreal>::infinity();
}
}
for (const Span &s : along) {
const int k0 = int(std::lower_bound(lines.begin(), lines.end(), s.at - 0.5) - lines.begin());
const auto [i0, i1] = edges(s.from, s.to);
for (int k = k0; k < nl && lines[k] <= s.at + 0.5; ++k) {
if (std::abs(s.at - lines[k]) > 0.5) continue;
for (int i = i0; i < i1; ++i) {
const qreal overlap = std::min(pos[i + 1], s.to) - std::max(pos[i], s.from);
if (overlap > eps) cost[size_t(k) * np + i] += overlap * along_factor;
}
}
}
for (const Span &s : across) {
// Half-open (a, b], so a crossing on a grid node is counted
// once, by the edge that ends on it; and only through the
// other wire's interior, not at its end, which is a junction.
const auto [k0, k1] = openRange(lines, s.from, s.to);
const auto [i0, i1] = edges(s.at - 1.0, s.at + 1.0);
for (int k = k0; k < k1; ++k) {
if (!(lines[k] > s.from + 0.5 && lines[k] < s.to - 0.5)) continue;
for (int i = i0; i < i1; ++i)
if (s.at > pos[i] + eps && s.at <= pos[i + 1] + eps)
cost[size_t(k) * np + i] += cross_steps * grid;
}
}
return cost;
}
} // namespace
ConductorRouter::Result ConductorRouter::route(const Request &r)
{
Result result;
if (r.grid <= 0) {
result.error = QStringLiteral("the grid step must be positive");
return result;
}
const QRectF own1 = r.start_symbol.normalized(), own2 = r.end_symbol.normalized();
QList<QRectF> obstacles, others, other_symbols;
for (const QRectF &o : r.obstacles) {
const QRectF n = o.normalized();
const bool own = (own1.isValid() && n == own1) || (own2.isValid() && n == own2);
// A symbol drawn around a terminal's own one is a frame the
// wire starts or ends inside: crossing its edge is the way in
// or out, and its inside is the place to route.
if (!own && ((own1.isValid() && n.contains(own1))
|| (own2.isValid() && n.contains(own2))))
continue;
const QRectF padded = n.adjusted(-r.margin, -r.margin, r.margin, r.margin);
obstacles << padded;
if (!own) {
others << padded;
other_symbols << n;
}
}
// Two terminals facing each other on one line, with nothing
// between them: the straight line, even when they are too close
// for each to step out a grid square first. With room, it keeps
// the step out of each terminal, as the search does (see the
// corners below); without, the point between them gives the path
// the three points a conductor's path needs.
const QPointF s = step(r.start_direction);
const QPointF ahead = r.end - r.start;
if (r.end_direction == opposite(r.start_direction)
&& std::abs(s.x() != 0 ? ahead.y() : ahead.x()) < eps
&& ahead.x() * s.x() + ahead.y() * s.y() > eps) {
bool clear = true;
for (const QRectF &o : others)
if (crossesInside(r.start, r.end, o)) { clear = false; break; }
// nor along another wire, which the search would avoid
const bool vertical = s.x() == 0;
const qreal line = vertical ? r.start.x() : r.start.y();
const qreal lo = vertical ? std::min(r.start.y(), r.end.y()) : std::min(r.start.x(), r.end.x());
const qreal hi = vertical ? std::max(r.start.y(), r.end.y()) : std::max(r.start.x(), r.end.x());
for (const QVector<QPointF> &w : r.wires) {
for (int i = 0; clear && i + 1 < w.size(); ++i) {
const QPointF a = w.at(i), b = w.at(i + 1);
const qreal a_at = vertical ? a.x() : a.y(), b_at = vertical ? b.x() : b.y();
if (std::abs(a_at - line) > 0.5 || std::abs(b_at - line) > 0.5) continue;
const qreal a_on = vertical ? a.y() : a.x(), b_on = vertical ? b.y() : b.x();
if (std::min(hi, std::max(a_on, b_on)) - std::max(lo, std::min(a_on, b_on)) > eps)
clear = false;
}
}
if (clear) {
const QPointF e1 = firstStep(r.start, r.start_direction, r.grid);
const QPointF e2 = firstStep(r.end, r.end_direction, r.grid);
const QPointF gap = e2 - e1;
result.points << r.start;
if (gap.x() * s.x() + gap.y() * s.y() > eps)
result.points << e1 << e2;
else if (std::abs(gap.x()) < eps && std::abs(gap.y()) < eps)
result.points << e1;
else
result.points << (r.start + r.end) / 2;
result.points << r.end;
return result;
}
}
QPointF s1, s2;
if (!exitPoint(r.start, r.start_direction, r, obstacles, other_symbols, own1.isValid(), s1)
|| !exitPoint(r.end, r.end_direction, r, obstacles, other_symbols, own2.isValid(), s2)) {
result.error = QStringLiteral("a terminal points straight into another symbol, "
"or has no way out of the symbols around it");
return result;
}
// The area searched: everything involved, with room to go round
// it, kept on the folio when there is one.
QRectF box = QRectF(s1, s2).normalized();
for (const QRectF &o : obstacles) box = box.united(o);
box.adjust(-3 * r.grid, -3 * r.grid, 3 * r.grid, 3 * r.grid);
if (r.bounds.isValid()) box = box.intersected(r.bounds);
box = box.united(QRectF(s1, s2).normalized());
const QVector<qreal> xs = axis(box.left(), box.right(), r.grid, {s1.x(), s2.x()});
const QVector<qreal> ys = axis(box.top(), box.bottom(), r.grid, {s1.y(), s2.y()});
const int nx = xs.size(), ny = ys.size();
if (qint64(nx) * ny > max_nodes) {
result.error = QStringLiteral("the area to search is too large");
return result;
}
QVector<Span> horizontal_wires, vertical_wires;
for (const QVector<QPointF> &w : r.wires) {
for (int i = 0; i + 1 < w.size(); ++i) {
const QPointF a = w.at(i), b = w.at(i + 1);
if (std::abs(a.y() - b.y()) < 0.5 && std::abs(a.x() - b.x()) > eps)
horizontal_wires << Span{a.y(), std::min(a.x(), b.x()), std::max(a.x(), b.x())};
else if (std::abs(a.x() - b.x()) < 0.5 && std::abs(a.y() - b.y()) > eps)
vertical_wires << Span{a.x(), std::min(a.y(), b.y()), std::max(a.y(), b.y())};
}
}
// The cost of the edge from each node to the next one east,
// h_cost[j * nx + i], and south, v_cost[i * ny + j]; infinity
// where blocked or none.
const qreal inf = std::numeric_limits<qreal>::infinity();
const std::vector<qreal> h_cost = edgeCosts(true, xs, ys, obstacles,
horizontal_wires, vertical_wires, r.grid);
const std::vector<qreal> v_cost = edgeCosts(false, ys, xs, obstacles,
vertical_wires, horizontal_wires, r.grid);
const int start_node = indexOf(ys, s1.y()) * nx + indexOf(xs, s1.x());
const int goal_node = indexOf(ys, s2.y()) * nx + indexOf(xs, s2.x());
const qreal bend = bend_steps * r.grid;
// The wire arrives at the second terminal moving opposite to the
// way it points.
const Direction arrival = opposite(r.end_direction);
// Dijkstra over (node, direction of travel): the direction is what
// lets a bend be charged for.
const size_t states = size_t(nx) * ny * 4;
std::vector<qreal> dist(states, inf);
std::vector<int> previous(states, -1);
using Entry = std::pair<qreal, int>;
std::priority_queue<Entry, std::vector<Entry>, std::greater<Entry>> queue;
const int first = start_node * 4 + int(r.start_direction);
dist[size_t(first)] = 0;
queue.push({0, first});
qreal best = inf;
int best_state = -1;
while (!queue.empty()) {
const auto [cost, state] = queue.top();
queue.pop();
if (cost > dist[size_t(state)] || cost >= best) {
if (cost >= best) break;
continue;
}
const int node = state / 4;
const Direction d = static_cast<Direction>(state % 4);
if (node == goal_node && d != r.end_direction) {
const qreal total = cost + (d == arrival ? 0 : bend);
if (total < best) { best = total; best_state = state; }
}
const int i = node % nx, j = node / nx;
for (int k = 0; k < 4; ++k) {
const Direction nd = static_cast<Direction>(k);
if (nd == opposite(d)) continue;
int ni = i, nj = j;
qreal edge = inf;
switch (nd) {
case Direction::East: if (i + 1 < nx) { ni = i + 1; edge = h_cost[size_t(j) * nx + i]; } break;
case Direction::West: if (i > 0) { ni = i - 1; edge = h_cost[size_t(j) * nx + ni]; } break;
case Direction::South: if (j + 1 < ny) { nj = j + 1; edge = v_cost[size_t(i) * ny + j]; } break;
case Direction::North: if (j > 0) { nj = j - 1; edge = v_cost[size_t(i) * ny + nj]; } break;
}
if (edge == inf) continue;
const qreal next_cost = cost + edge + (nd == d ? 0 : bend);
const int next = (nj * nx + ni) * 4 + k;
if (next_cost < dist[size_t(next)]) {
dist[size_t(next)] = next_cost;
previous[size_t(next)] = state;
queue.push({next_cost, next});
}
}
}
if (best_state < 0) {
result.error = QStringLiteral("no path around the symbols was found");
return result;
}
QList<QPointF> chain;
for (int s = best_state; s >= 0; s = previous[size_t(s)]) {
const int node = s / 4;
chain.prepend(QPointF(xs[node % nx], ys[node / nx]));
}
// Keep only the corners. The two exit points stay even when
// straight on, as Conductor::generateConductorPath() keeps them:
// the segment from a terminal to its exit point is the one the
// application holds fixed when a wire is edited by hand.
QList<QPointF> corners;
for (int k = 0; k < chain.size(); ++k) {
const QPointF p = chain.at(k);
if (k > 0 && k + 1 < chain.size()) {
const QPointF a = chain.at(k - 1), b = chain.at(k + 1);
const bool straight = (std::abs(a.x() - p.x()) < eps && std::abs(b.x() - p.x()) < eps)
|| (std::abs(a.y() - p.y()) < eps && std::abs(b.y() - p.y()) < eps);
if (straight) continue;
}
corners << p;
}
result.points << r.start;
for (const QPointF &p : corners)
if (result.points.last() != p) result.points << p;
if (result.points.last() != r.end) result.points << r.end;
return result;
}
+83
View File
@@ -0,0 +1,83 @@
/*
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/>.
*/
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef CONDUCTORROUTER_H
#define CONDUCTORROUTER_H
#include <QList>
#include <QPointF>
#include <QRectF>
#include <QString>
#include <QVector>
/**
@brief The ConductorRouter namespace
Finds a path for a conductor that goes around obstacles -- the bounding
rectangles of the symbols on the folio -- instead of the default two or
three straight segments, which run through whatever lies between the two
terminals. Kept free of any QGraphicsItem so the geometry can be tested
on its own.
The path is made of horizontal and vertical segments only, like every
conductor. It leaves each terminal in the terminal's own direction,
runs on the folio grid, and is the cheapest one found by a search that
charges for length, for every bend, and for running along or crossing
the wires already drawn.
*/
namespace ConductorRouter
{
///The direction a terminal points, outward from its symbol.
///Same order as Qet::Orientation.
enum class Direction { North, East, South, West };
struct Request
{
QPointF start; ///< the first terminal's docking point
Direction start_direction = Direction::North;
QPointF end; ///< the second terminal's docking point
Direction end_direction = Direction::North;
///Areas no segment may cross. A margin is added to each.
QList<QRectF> obstacles;
///Each terminal's own symbol, when known, as it appears in
///obstacles. A route steps out of it first and never walks
///through another symbol to get out; an obstacle drawn around
///it (a cabinet made as one element) is left out.
QRectF start_symbol;
QRectF end_symbol;
///The other wires on the folio, each as its list of points.
///Running along one or crossing one costs extra.
QList<QVector<QPointF>> wires;
///The route stays inside this, when it is valid (the folio).
QRectF bounds;
qreal grid = 10.0;
///Added around each obstacle, so a route keeps clear of it
qreal margin = 5.0;
};
struct Result
{
///From start to end, both included; empty if none was found
QList<QPointF> points;
///Why there is no route, when points is empty
QString error;
};
Result route(const Request &request);
}
#endif // CONDUCTORROUTER_H
+279 -130
View File
@@ -22,6 +22,9 @@
#include <QString>
#include "dxfexport.h"
#include <QtMath>
#include <cmath>
const double Createdxf::sheetWidth = 4000;
const double Createdxf::sheetHeight = 2700;
@@ -65,7 +68,16 @@ Createdxf::~Createdxf()
/* Header section of every DXF file.
@param layers : the layers the entities will be written on, declared in
the LAYER table beside layer "0", which every DXF has. */
void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers)
/**
@brief Createdxf::dxfBegin
Write the header, the tables with @a layers, and the BLOCKS section,
then open the ENTITIES section. @a writeBlocks, if given, is called
inside the BLOCKS section to write the block definitions
(dxfBlockBegin(), the entities, dxfBlockEnd()): a block has to be
defined before any entity, so before anything is drawn.
*/
void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers,
const std::function<void()> &writeBlocks)
{
layer = QStringLiteral("0");
@@ -273,17 +285,203 @@ void Createdxf::dxfBegin (const QString& fileName, const QStringList &layers)
To_Dxf << "SECTION" << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << "BLOCKS" << "\r\n";
file.close();
}
if (writeBlocks)
writeBlocks();
QFile entities(fileName);
if (entities.open(QFile::Append)) {
QTextStream To_Dxf(&entities);
To_Dxf << 0 << "\r\n";
To_Dxf << "ENDSEC" << "\r\n";
To_Dxf << 0 << "\r\n";
To_Dxf << "SECTION" << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << "ENTITIES" << "\r\n";
file.close();
entities.close();
}
}
}
/**
@brief Createdxf::dxfBlockBegin
Start the definition of block @a name, with its base point at @a x,
@a y. Only inside dxfBegin()'s writeBlocks; every entity written until
dxfBlockEnd() belongs to the block.
@param hasAttributes : the block has attribute definitions
*/
void Createdxf::dxfBlockBegin(const QString &fileName,
const QString &name,
double x,
double y,
bool hasAttributes)
{
QFile file(fileName);
if (!file.open(QFile::Append))
return;
QTextStream To_Dxf(&file);
To_Dxf << 0 << "\r\n";
To_Dxf << "BLOCK" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << "0" << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << name << "\r\n";
To_Dxf << 70 << "\r\n";
To_Dxf << (hasAttributes ? 2 : 0) << "\r\n";
To_Dxf << 10 << "\r\n";
To_Dxf << x << "\r\n";
To_Dxf << 20 << "\r\n";
To_Dxf << y << "\r\n";
To_Dxf << 30 << "\r\n";
To_Dxf << 0.0 << "\r\n";
file.close();
}
/**
@brief Createdxf::dxfBlockEnd
End the block started by dxfBlockBegin().
*/
void Createdxf::dxfBlockEnd(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::Append))
return;
QTextStream To_Dxf(&file);
To_Dxf << 0 << "\r\n";
To_Dxf << "ENDBLK" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << "0" << "\r\n";
file.close();
}
/**
@brief Createdxf::drawInsert
Place block @a name at @a x, @a y, turned counter-clockwise by
@a rotation degrees, on the current layer, followed by its
@a attributes.
*/
void Createdxf::drawInsert(const QString &fileName,
const QString &name,
double x,
double y,
double rotation,
const QList<Attribute> &attributes)
{
QFile file(fileName);
if (!file.open(QFile::Append))
return;
QTextStream To_Dxf(&file);
To_Dxf << 0 << "\r\n";
To_Dxf << "INSERT" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << layer << "\r\n";
if (!attributes.isEmpty()) {
To_Dxf << 66 << "\r\n";
To_Dxf << 1 << "\r\n";
}
To_Dxf << 2 << "\r\n";
To_Dxf << name << "\r\n";
To_Dxf << 10 << "\r\n";
To_Dxf << x << "\r\n";
To_Dxf << 20 << "\r\n";
To_Dxf << y << "\r\n";
To_Dxf << 30 << "\r\n";
To_Dxf << 0.0 << "\r\n";
To_Dxf << 50 << "\r\n";
To_Dxf << rotation << "\r\n";
for (const Attribute &attribute : attributes) {
To_Dxf << 0 << "\r\n";
To_Dxf << "ATTRIB" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << (attribute.layer.isEmpty() ? layer : attribute.layer) << "\r\n";
To_Dxf << 62 << "\r\n";
To_Dxf << entityColour(attribute.colour) << "\r\n";
To_Dxf << 10 << "\r\n";
To_Dxf << attribute.x << "\r\n";
To_Dxf << 20 << "\r\n";
To_Dxf << attribute.y << "\r\n";
To_Dxf << 30 << "\r\n";
To_Dxf << 0.0 << "\r\n";
To_Dxf << 40 << "\r\n";
To_Dxf << attribute.height << "\r\n";
To_Dxf << 1 << "\r\n";
To_Dxf << singleLine(attribute.text) << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << attribute.tag << "\r\n";
To_Dxf << 70 << "\r\n";
To_Dxf << (attribute.invisible ? 1 : 0) << "\r\n";
To_Dxf << 50 << "\r\n";
To_Dxf << attribute.rotation << "\r\n";
To_Dxf << 41 << "\r\n";
To_Dxf << attribute.xScaleW << "\r\n";
}
if (!attributes.isEmpty()) {
To_Dxf << 0 << "\r\n";
To_Dxf << "SEQEND" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << layer << "\r\n";
}
file.close();
}
/**
@brief Createdxf::drawAttdef
Define an attribute of the block being written (between
dxfBlockBegin() and dxfBlockEnd()): its tag, where it sits and how it
looks; @a attribute's text is the default value.
*/
void Createdxf::drawAttdef(const QString &fileName, const Attribute &attribute)
{
QFile file(fileName);
if (!file.open(QFile::Append))
return;
QTextStream To_Dxf(&file);
To_Dxf << 0 << "\r\n";
To_Dxf << "ATTDEF" << "\r\n";
To_Dxf << 8 << "\r\n";
To_Dxf << (attribute.layer.isEmpty() ? layer : attribute.layer) << "\r\n";
To_Dxf << 62 << "\r\n";
To_Dxf << entityColour(attribute.colour) << "\r\n";
To_Dxf << 10 << "\r\n";
To_Dxf << attribute.x << "\r\n";
To_Dxf << 20 << "\r\n";
To_Dxf << attribute.y << "\r\n";
To_Dxf << 30 << "\r\n";
To_Dxf << 0.0 << "\r\n";
To_Dxf << 40 << "\r\n";
To_Dxf << attribute.height << "\r\n";
To_Dxf << 1 << "\r\n";
To_Dxf << singleLine(attribute.text) << "\r\n";
To_Dxf << 3 << "\r\n";
To_Dxf << attribute.tag << "\r\n";
To_Dxf << 2 << "\r\n";
To_Dxf << attribute.tag << "\r\n";
To_Dxf << 70 << "\r\n";
To_Dxf << (attribute.invisible ? 1 : 0) << "\r\n";
To_Dxf << 50 << "\r\n";
To_Dxf << attribute.rotation << "\r\n";
To_Dxf << 41 << "\r\n";
To_Dxf << attribute.xScaleW << "\r\n";
file.close();
}
/**
@brief Createdxf::blockName
@return @a name as a block or attribute name an R10 reader accepts:
upper case, only A-Z, 0-9, _, - and $, at most 31 characters
*/
QString Createdxf::blockName(const QString &name)
{
QString result;
for (const QChar c : name.toUpper()) {
const ushort u = c.unicode();
const bool ok = (u >= 'A' && u <= 'Z') || (u >= '0' && u <= '9')
|| u == '_' || u == '-' || u == '$';
result += ok ? c : QChar('_');
}
return result.left(31);
}
/**
@brief Createdxf::dxfEnd
End Section of every DXF File
@@ -504,6 +702,24 @@ int Createdxf::dxfColor(QPen pen) {
return Createdxf::dxfColor(pen.color());
}
/**
@brief Createdxf::drawArcEllipse
Draw an arc of an ellipse, or a whole ellipse, in DXF units.
@param x, y : top left of the ellipse's bounding rect (y is the top, the
DXF Y axis going up)
@param w, h : width and height of that rect
@param startAngle, spanAngle : in degrees, as QPainter::drawArc() takes
them: counter-clockwise from 3 o'clock
@param hotspot_x, hotspot_y, rotation_angle : the whole shape is turned
clockwise by rotation_angle degrees around the hotspot, as a rotated
symbol is
A circle, or an arc of one, is written as one exact CIRCLE or ARC. R10,
the version this file declares, has no ELLIPSE entity, so a true
ellipse is written as a polyline through points of the curve, one every
5 degrees. It used to be four ARCs fitted through a point between the
curve and its chord, which drew every circle as a flattened diamond
(issue #1339).
*/
void Createdxf::drawArcEllipse(
const QString &file_path,
qreal x,
@@ -517,138 +733,46 @@ void Createdxf::drawArcEllipse(
qreal rotation_angle,
const int &colorcode)
{
// vector of parts of arc (stored as a pair of startAngle and spanAngle) for each quadrant.
QVector< QPair<qreal,qreal> > arc_parts_vector;
if (qFuzzyIsNull(spanAngle) || w <= 0 || h <= 0)
return;
if (spanAngle > 0) {
qreal start = startAngle;
qreal span;
int i;
for ( i = startAngle; i < startAngle+spanAngle; i++ ) {
int absolute_theta = (i > 0) ? i : -i;
if (absolute_theta == 0 || absolute_theta == 90 ||
absolute_theta == 180 || absolute_theta == 270 ||
absolute_theta == 360) {
span = i - start;
QPair<qreal, qreal> newPart(start,span);
arc_parts_vector.push_back(newPart);
start = i;
}
}
if (start != i) {
span = i - start;
QPair<qreal, qreal> newPart(start,span);
arc_parts_vector.push_back(newPart);
}
} else {
qreal start = startAngle;
qreal span;
int i;
for ( i = startAngle; i > startAngle+spanAngle; i-- ) {
int absolute_theta = (i > 0) ? i : -i;
if (absolute_theta == 0 || absolute_theta == 90 ||
absolute_theta == 180 || absolute_theta == 270 ||
absolute_theta == 360) {
span = i - start;
QPair<qreal, qreal> newPart(start,span);
arc_parts_vector.push_back(newPart);
start = i;
}
}
if (start != i) {
span = i - start;
QPair<qreal, qreal> newPart(start,span);
arc_parts_vector.push_back(newPart);
const qreal a = w/2;
const qreal b = h/2;
const QPointF center = DxfExport::rotation_transformed(
x + a, y - b, hotspot_x, hotspot_y, rotation_angle);
const bool full_turn = qAbs(spanAngle) >= 360;
if (qAbs(a - b) <= 1e-6 * qMax(a, b))
{
if (full_turn) {
drawCircle(file_path, a, center.x(), center.y(), colorcode);
return;
}
//A DXF ARC always runs counter-clockwise from 50 to 51.
qreal start = startAngle - rotation_angle;
if (spanAngle < 0)
start += spanAngle;
start = std::fmod(start, 360.0);
if (start < 0)
start += 360;
drawArc(file_path, center.x(), center.y(), a,
start, start + qAbs(spanAngle), colorcode);
return;
}
for (int i = 0; i < arc_parts_vector.size(); i++) {
QPair<qreal,qreal> arc = arc_parts_vector[i];
if (arc.second == 0)
continue;
qreal arc_startAngle = arc.first * 3.142/180;
qreal arc_spanAngle = arc.second * 3.142/180;
qreal a = w/2;
qreal b = h/2;
qreal x1 = x + w/2 + a*cos(arc_startAngle);
qreal y1 = y - h/2 + b*sin(arc_startAngle);
qreal x2 = x + w/2 + a*cos(arc_startAngle + arc_spanAngle);
qreal y2 = y - h/2 + b*sin(arc_startAngle + arc_spanAngle);
qreal mid_ellipse_x = x + w/2 + a*cos(arc_startAngle + arc_spanAngle/2);
qreal mid_ellipse_y = y - h/2 + b*sin(arc_startAngle + arc_spanAngle/2);
qreal mid_line_x = (x1+x2)/2;
qreal mid_line_y = (y1+y2)/2;
qreal x3 = (mid_ellipse_x + mid_line_x)/2;
qreal y3 = (mid_ellipse_y + mid_line_y)/2;
// find circumcenter of points (x1,y1), (x3,y3) and (x2,y2)
qreal a1 = 2*x2 - 2*x1;
qreal b1 = 2*y2 - 2*y1;
qreal c1 = x1*x1 + y1*y1 - x2*x2 - y2*y2;
qreal a2 = 2*x3 - 2*x1;
qreal b2 = 2*y3 - 2*y1;
qreal c2 = x1*x1 + y1*y1 - x3*x3 - y3*y3;
qreal center_x = (b1*c2 - b2*c1) / (a1*b2 - a2*b1);
qreal center_y = (a1*c2 - a2*c1) / (b1*a2 - b2*a1);
qreal radius = sqrt( (x1-center_x)*(x1-center_x) + (y1-center_y)*(y1-center_y) );
if ( x1 > center_x && y1 > center_y )
arc_startAngle = asin( (y1 - center_y) / radius );
else if ( x1 > center_x && y1 < center_y )
arc_startAngle = 3.142*2 - asin( (center_y - y1) / radius );
else if ( x1 < center_x && y1 < center_y )
arc_startAngle = 3.142 + asin( (center_y - y1) / radius );
else
arc_startAngle = 3.142 - asin( (y1 - center_y) / radius );
qreal arc_endAngle;
if ( x2 > center_x && y2 > center_y )
arc_endAngle = asin( (y2 - center_y) / radius );
else if ( x2 > center_x && y2 < center_y )
arc_endAngle = 3.142*2 - asin( (center_y - y2) / radius );
else if ( x2 < center_x && y2 < center_y )
arc_endAngle = 3.142 + asin( (center_y - y2) / radius );
else
arc_endAngle = 3.142 - asin( (y2 - center_y) / radius );
if (arc_endAngle < arc_startAngle) {
qreal temp = arc_startAngle;
arc_startAngle = arc_endAngle;
arc_endAngle = temp;
}
QPointF transformed_point = DxfExport::rotation_transformed(
center_x,
center_y,
hotspot_x,
hotspot_y,
rotation_angle);
center_x = transformed_point.x();
center_y = transformed_point.y();
arc_endAngle *= 180/3.142;
arc_startAngle *= 180/3.142;
arc_endAngle -= rotation_angle;
arc_startAngle -= rotation_angle;
drawArc(
file_path,
center_x,
center_y,
radius,
arc_startAngle,
arc_endAngle,
colorcode);
const qreal span = full_turn ? 360 : spanAngle;
const int steps = qMax(2, qCeil(qAbs(span) / 5));
QPolygonF poly;
poly.reserve(steps + 1);
for (int i = 0 ; i <= steps ; ++i)
{
const qreal theta = qDegreesToRadians(startAngle + span * i / steps);
poly << DxfExport::rotation_transformed(
x + a + a*std::cos(theta),
y - b + b*std::sin(theta),
hotspot_x, hotspot_y, rotation_angle);
}
drawPolyline(file_path, poly, colorcode, true);
}
@@ -809,6 +933,31 @@ void Createdxf::drawTextAligned(
double xAlign,
double xScaleW,
int colour)
{
drawTextAligned(fileName, text, x, y, height, rotation, oblique,
hAlign, vAlign, xAlign, y, xScaleW, colour);
}
/**
@brief Createdxf::drawTextAligned
As above, with the second alignment point given in full (@a xAlign,
@a yAlign) instead of on the same horizontal line as the insertion
point, which is what a rotated aligned text needs.
*/
void Createdxf::drawTextAligned(
const QString& fileName,
const QString& text,
double x,
double y,
double height,
double rotation,
double oblique,
int hAlign,
int vAlign,
double xAlign,
double yAlign,
double xScaleW,
int colour)
{
if (!fileName.isEmpty()) {
QFile file(fileName);
@@ -869,7 +1018,7 @@ void Createdxf::drawTextAligned(
To_Dxf << 11 << "\r\n"; // XYZ
To_Dxf << xAlign << "\r\n"; // X in UCS (User Coordinate System)coordinates
To_Dxf << 21 << "\r\n";
To_Dxf << y << "\r\n"; // Y in UCS (User Coordinate System)coordinates
To_Dxf << yAlign << "\r\n"; // Y in UCS (User Coordinate System)coordinates
To_Dxf << 31 << "\r\n";
To_Dxf << 0.0 << "\r\n"; // Z in UCS (User Coordinate System)coordinates
}
+47 -1
View File
@@ -22,6 +22,8 @@
#include <QtCore>
#include <QtWidgets>
#include <functional>
/**
@brief The Createdxf class
This class exports the project to DXF Format
@@ -31,8 +33,38 @@ class Createdxf
public:
Createdxf();
~Createdxf();
static void dxfBegin (const QString&, const QStringList &layers = QStringList());
static void dxfBegin (const QString&, const QStringList &layers = QStringList(),
const std::function<void()> &writeBlocks = {});
static void dxfEnd(const QString&);
static void dxfBlockBegin(const QString &fileName,
const QString &name,
double x,
double y,
bool hasAttributes = false);
static void dxfBlockEnd(const QString &fileName);
/// One attribute of a placed block, in DXF units
struct Attribute
{
QString tag;
QString text;
double x = 0;
double y = 0;
double height = 0;
double rotation = 0;
double xScaleW = 1;
int colour = 0;
bool invisible = false;
QString layer; ///< empty: the INSERT's layer
};
static void drawInsert(const QString &fileName,
const QString &name,
double x,
double y,
double rotation,
const QList<Attribute> &attributes = {});
static void drawAttdef(const QString &fileName,
const Attribute &attribute);
static QString blockName(const QString &name);
// you can add more functions to create more drawings.
static void drawCircle(
const QString&,
@@ -141,6 +173,20 @@ class Createdxf
double xAlign,
double xScale,
int colour);
static void drawTextAligned(
const QString& fileName,
const QString& text,
double x,
double y,
double height,
double rotation,
double oblique,
int hAlign,
int vAlign,
double xAlign,
double yAlign,
double xScale,
int colour);
static void drawPolyline(
const QString &filepath,
+42
View File
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef DARK_IMAGE_RENDERING_H
#define DARK_IMAGE_RENDERING_H
#include <QPainter>
#include <QPixmap>
#include <functional>
namespace DarkImageRendering {
// View-local raster composition boundary. Qt still paints the scene in
// its native order, with its own transforms, clipping and opacity.
// A different painter (printing, export, another view or an item cache)
// cannot consume this display-only context.
struct Context {
QPainter *painter;
bool invertScene;
std::function<void(bool, const QRect &)> flush;
};
inline thread_local Context *current = nullptr;
class Scope {
Context *previous;
public:
explicit Scope(Context *context) : previous(current) { current = context; }
~Scope() { current = previous; }
Scope(const Scope &) = delete;
Scope &operator=(const Scope &) = delete;
};
inline void paintPixmap(QPainter *painter, const QPixmap &pixmap, bool adapt)
{
Context *context = current;
const bool boundary = context && context->painter == painter && adapt != context->invertScene;
if (boundary) context->flush(context->invertScene, QRect());
painter->drawPixmap(pixmap.rect(), pixmap);
if (boundary) {
// Only the raster was drawn since the preceding flush. Avoid a
// whole-viewport copy for a small logo; include filtering/AA edges.
const QRect area = painter->worldTransform().mapRect(QRectF(pixmap.rect())).toAlignedRect().adjusted(-2,-2,2,2);
context->flush(adapt, area);
}
}
}
#endif
+79 -10
View File
@@ -180,8 +180,37 @@ struct DocumentTerminal
QString uuid;
QString name;
bool master_label = false;
QVariant index;
};
//The index each of @p points has in Element::terminals() -- the index
//the scripting API's addConductor() and conductor calls take -- or a
//null QVariant for a point another terminal shares. Element::
//parseTerminal() sorts the list top to bottom, then left to right, on
//each terminal's position in its definition, and the sort is not
//stable, so which of two terminals at the same point comes first is
//not defined: no index is given rather than one that can change.
QList<QVariant> terminalIndexes(const QList<QPointF> &points)
{
QList<int> order;
for (int i = 0 ; i < points.size() ; ++i) {
order << i;
}
std::stable_sort(order.begin(), order.end(), [&points](int a, int b) {
if (points.at(a).y() == points.at(b).y()) {
return points.at(a).x() < points.at(b).x();
}
return points.at(a).y() < points.at(b).y();
});
QList<QVariant> indexes(points.size());
for (int i = 0 ; i < order.size() ; ++i) {
if (points.count(points.at(order.at(i))) == 1) {
indexes[order.at(i)] = i;
}
}
return indexes;
}
struct DocumentDefinition
{
QString type;
@@ -207,7 +236,7 @@ struct DocumentElement
//the attributes files written before <sequentialNumbers> carry.
bool readSequence(const QDomElement &item, autonum::sequentialNumbers *sequence)
{
for (const char *name : {"sequ_1", "sequf_1", "seqt_1", "seqtf_1", "seqh_1"}) {
for (const char *name : {"sequ_1", "sequf_1", "seqt_1", "seqtf_1", "seqh_1", "seqhf_1"}) {
if (item.hasAttribute(QLatin1String(name))) {
return false;
}
@@ -391,6 +420,25 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
data.fromXml(definition);
read.type = data.typeToString();
read.sub_type = data.masterTypeToString();
//Every terminal the element is built with counts towards
//the indexes, as Element::parseTerminal() keeps every one
//whose position reads, in every <description>.
QList<QDomElement> parsed;
QList<QPointF> points;
for (QDomElement d = definition.firstChildElement(QStringLiteral("description")) ;
!d.isNull() ; d = d.nextSiblingElement(QStringLiteral("description"))) {
for (QDomElement t = d.firstChildElement(QStringLiteral("terminal")) ;
!t.isNull() ; t = t.nextSiblingElement(QStringLiteral("terminal")))
{
qreal x, y;
if (QET::attributeIsAReal(t, QStringLiteral("x"), &x)
&& QET::attributeIsAReal(t, QStringLiteral("y"), &y)) {
parsed << t;
points << QPointF(x, y);
}
}
}
const QList<QVariant> indexes = terminalIndexes(points);
const QDomElement description = definition.firstChildElement(QStringLiteral("description"));
for (QDomElement t = description.firstChildElement(QStringLiteral("terminal")) ;
!t.isNull() ; t = t.nextSiblingElement(QStringLiteral("terminal")))
@@ -403,6 +451,10 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
terminal.uuid = terminal_uuid.toString();
terminal.name = t.attribute(QStringLiteral("name"));
terminal.master_label = t.attribute(QStringLiteral("use_master_label")) == QLatin1String("true");
const int place = parsed.indexOf(t);
if (place >= 0) {
terminal.index = indexes.at(place);
}
read.terminals.insert(terminal_uuid, terminal);
}
known = definitions.insert(type, read);
@@ -582,8 +634,8 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
for (const auto &end : {std::make_pair(conductor.element1, conductor.terminal1),
std::make_pair(conductor.element2, conductor.terminal2)}) {
const DocumentElement &owner = elements.at(element_index.value(end.first));
insertTerminal(end.second, end.first,
owner.terminals.value(QUuid(end.second)).name);
const DocumentTerminal &terminal = owner.terminals.value(QUuid(end.second));
insertTerminal(end.second, end.first, terminal.name, terminal.index);
}
m_insert_conductor_query.bindValue(QStringLiteral(":uuid"), conductor.uuid);
m_insert_conductor_query.bindValue(QStringLiteral(":diagram_uuid"), conductor.diagram_uuid);
@@ -1196,6 +1248,8 @@ void projectDataBase::addDrawingItem(QGraphicsItem *item)
}
else if (auto text = qobject_cast<IndependentTextItem *>(object)) {
connect(text, &IndependentTextItem::uuidChanged, this, &projectDataBase::drawingItemChanged, unique);
//Also changes the height of the text, kept in the row
connect(text, &IndependentTextItem::textWidthChanged, this, &projectDataBase::drawingItemChanged, unique);
//Sent by the document, not the item: drawingItemChanged() walks
//back up to the item. It is the one signal that catches every way
//the text changes -- typing, undo, a script's setTextContent().
@@ -1372,6 +1426,9 @@ bool projectDataBase::writeDrawingItem(QObject *object)
query = &m_insert_independent_text_query;
query->bindValue(QStringLiteral(":text"), text->toPlainText());
query->bindValue(QStringLiteral(":rotation"), text->rotation());
//NULL for the automatic width
query->bindValue(QStringLiteral(":text_width"), text->textWidth() > 0
? QVariant(text->textWidth()) : QVariant());
}
else if (auto image = qobject_cast<DiagramImageItem *>(object))
{
@@ -1560,6 +1617,7 @@ bool projectDataBase::createDataBase()
"uuid VARCHAR(50) NOT NULL, "
"element_uuid VARCHAR(50) NOT NULL,"
"name VARCHAR(50),"
"terminal_index INTEGER,"
"PRIMARY KEY (uuid, element_uuid),"
"FOREIGN KEY (element_uuid) REFERENCES element (uuid)"
")");
@@ -1614,7 +1672,7 @@ bool projectDataBase::createDataBase()
"type VARCHAR(20), color VARCHAR(20), fill VARCHAR(20), "
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid))",
QStringLiteral("CREATE TABLE independent_text (") + drawing_columns +
"text TEXT, rotation REAL, "
"text TEXT, rotation REAL, text_width REAL, "
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid))",
QStringLiteral("CREATE TABLE image (") + drawing_columns +
"pixel_width INTEGER, pixel_height INTEGER, "
@@ -1812,7 +1870,11 @@ void projectDataBase::createWiringListView()
"t2.element_uuid AS to_element_uuid,"
"ei2.label AS to_element_label,"
"t2.name AS to_terminal,"
"d.pos AS diagram_position"
"d.pos AS diagram_position,"
"t1.uuid AS from_terminal_uuid,"
"t1.terminal_index AS from_terminal_index,"
"t2.uuid AS to_terminal_uuid,"
"t2.terminal_index AS to_terminal_index"
" 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"
@@ -1999,9 +2061,15 @@ void projectDataBase::populateConductorTable()
*/
void projectDataBase::insertTerminal(Terminal *terminal)
{
const QList<Terminal *> terminals = terminal->parentElement()->terminals();
QList<QPointF> points;
for (const Terminal *t : terminals) {
points << terminal->parentElement()->mapFromScene(t->dockConductor());
}
insertTerminal(terminal->stableUuid().toString(),
terminal->parentElement()->uuid().toString(),
terminal->name());
terminal->name(),
terminalIndexes(points).value(terminals.indexOf(terminal)));
}
/**
@@ -2009,11 +2077,12 @@ void projectDataBase::insertTerminal(Terminal *terminal)
insertTerminal(Terminal *) from values rather than a live terminal.
*/
void projectDataBase::insertTerminal(const QString &uuid, const QString &element_uuid,
const QString &name)
const QString &name, const QVariant &index)
{
m_insert_terminal_query.bindValue(":uuid", uuid);
m_insert_terminal_query.bindValue(":element_uuid", element_uuid);
m_insert_terminal_query.bindValue(":name", name);
m_insert_terminal_query.bindValue(":terminal_index", index);
if (!m_insert_terminal_query.exec()) {
qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError();
}
@@ -2059,8 +2128,8 @@ void projectDataBase::prepareQuery()
", :type, :color, :fill)");
m_insert_independent_text_query = QSqlQuery(m_data_base);
m_insert_independent_text_query.prepare("INSERT OR REPLACE INTO independent_text (" + drawing_columns +
", text, rotation) VALUES (" + drawing_values +
", :text, :rotation)");
", text, rotation, text_width) VALUES (" + drawing_values +
", :text, :rotation, :text_width)");
m_insert_image_query = QSqlQuery(m_data_base);
m_insert_image_query.prepare("INSERT OR REPLACE INTO image (" + drawing_columns +
", pixel_width, pixel_height) VALUES (" + drawing_values +
@@ -2146,7 +2215,7 @@ void projectDataBase::prepareQuery()
//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)");
m_insert_terminal_query.prepare("INSERT OR IGNORE INTO terminal (uuid, element_uuid, name, terminal_index) VALUES (:uuid, :element_uuid, :name, :terminal_index)");
//INSERT CONDUCTOR
m_insert_conductor_query = QSqlQuery(m_data_base);
+1 -1
View File
@@ -150,7 +150,7 @@ class projectDataBase : public QObject
void watchConductor(Conductor *conductor);
void insertTerminal(Terminal *terminal);
void insertTerminal(const QString &uuid, const QString &element_uuid,
const QString &name);
const QString &name, const QVariant &index);
void prepareQuery();
static QHash<QString, QString> elementInfoToString(
Element *elmt);
+75 -18
View File
@@ -56,6 +56,7 @@ 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);
m_button_group.addButton(ui->m_slave_cb, 8);
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
{
@@ -78,27 +79,12 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
}
else
{
int checked = 0;
for (int i=1 ; i<8 ; ++i) {
if (m_button_group.button(i)->isChecked()) {++checked;}
}
switch (checked)
{
case 0 :
check_box->setCheckState(Qt::Unchecked);
break;
case 7:
check_box->setCheckState(Qt::Checked);
break;
default:
check_box->setCheckState(Qt::PartiallyChecked);
break;
}
updateAllCheckState();
}
updateQueryLine();
});
updateAllCheckState();
setUpItems();
fillSavedQuery();
@@ -203,8 +189,12 @@ void ElementQueryWidget::setQuery(const QString &query)
if (ui->m_plc_cb->isChecked()) {
++c;
}
ui->m_slave_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Slave)));
if (ui->m_slave_cb->isChecked()) {
++c;
}
if (c == 7) {
if (c == 8) {
ui->m_all_cb->setCheckState(Qt::Checked);
} else if (c > 0) {
ui->m_all_cb->setCheckState(Qt::PartiallyChecked);
@@ -395,6 +385,11 @@ QString ElementQueryWidget::queryStr() const
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
b = true;
}
if (ui->m_slave_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_type = '") += ElementData::typeToString(ElementData::Slave) += "'";
b = true;
}
where.append(")");
if (where == " WHERE ()") {
@@ -406,6 +401,10 @@ QString ElementQueryWidget::queryStr() const
// widget's query reads FROM that view, so a flagged element never
// reaches this point in the first place.
if (!m_extra_filter.isEmpty()) {
filter_ += QStringLiteral(" AND ") + m_extra_filter;
}
if (where.isEmpty() && !filter_.isEmpty()) {
filter_.remove(0, 4); //Remove the first " AND" of filter.
filter_.prepend( " WHERE");
@@ -456,6 +455,64 @@ void ElementQueryWidget::setCount(QString text, bool set)
updateQueryLine();
}
/**
@brief ElementQueryWidget::setSlavesIncluded
Check or uncheck the contact blocks (slave elements) in the element
type filter. The box is unchecked by default, which keeps the tables
made before it existed as they were.
@param included
*/
void ElementQueryWidget::setSlavesIncluded(bool included)
{
ui->m_slave_cb->setChecked(included);
updateAllCheckState();
updateQueryLine();
}
/**
@brief ElementQueryWidget::updateAllCheckState
Set the "all" check box from the element type check boxes.
*/
void ElementQueryWidget::updateAllCheckState()
{
int checked = 0;
for (int i=1 ; i<9 ; ++i) {
if (m_button_group.button(i)->isChecked()) {++checked;}
}
switch (checked)
{
case 0 :
ui->m_all_cb->setCheckState(Qt::Unchecked);
break;
case 8:
ui->m_all_cb->setCheckState(Qt::Checked);
break;
default:
ui->m_all_cb->setCheckState(Qt::PartiallyChecked);
break;
}
}
/**
@brief ElementQueryWidget::setExtraFilter
Add a condition every row of the query must meet, on top of the filters
chosen in this widget.
@param text : the condition, ex : "label != ''"
@param set :
true by default -> the condition is used.
false -> the condition is removed.
*/
void ElementQueryWidget::setExtraFilter(const QString &text, bool set)
{
if (set) {
m_extra_filter = text;
} else {
m_extra_filter.clear();
}
updateQueryLine();
}
/**
@brief ElementQueryWidget::updateQueryLine
*/
+5 -1
View File
@@ -45,6 +45,8 @@ class ElementQueryWidget : public QWidget
QString queryStr() const;
void setGroupBy(QString text, bool set = true);
void setCount(QString text, bool set = true);
void setSlavesIncluded(bool included);
void setExtraFilter(const QString &text, bool set = true);
static QString modelIdentifier() {return "nomenclature";}
@@ -67,6 +69,7 @@ class ElementQueryWidget : public QWidget
void on_m_choosen_list_itemDoubleClicked(QListWidgetItem *item);
void updateQueryLine();
void updateAllCheckState();
QStringList selectedKeys() const;
void setUpItems();
QPair<int, QString> FilterFor(const QString &key) const;
@@ -82,7 +85,8 @@ class ElementQueryWidget : public QWidget
QList <QListWidgetItem *> m_items_list;
QString m_custom_query,
m_group_by,
m_count;
m_count,
m_extra_filter;
QHash <QString, QPair<int, QString>> m_filter;
};
+16 -12
View File
@@ -88,8 +88,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="go-up" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-up.png</normaloff>:/ico/16x16/go-up.png</iconset>
<iconset theme="go-up"/>
</property>
<property name="checkable">
<bool>false</bool>
@@ -105,8 +104,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
<iconset theme="list-add"/>
</property>
</widget>
</item>
@@ -119,8 +117,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
<iconset theme="list-remove"/>
</property>
</widget>
</item>
@@ -133,8 +130,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="go-down" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-down.png</normaloff>:/ico/16x16/go-down.png</iconset>
<iconset theme="go-down"/>
</property>
</widget>
</item>
@@ -322,6 +318,16 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="m_slave_cb">
<property name="text">
<string>Contacts esclaves</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -353,8 +359,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="folder-open" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/folder-open.png</normaloff>:/ico/16x16/folder-open.png</iconset>
<iconset theme="folder-open"/>
</property>
</widget>
</item>
@@ -380,8 +385,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="document-save" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/document-save.png</normaloff>:/ico/16x16/document-save.png</iconset>
<iconset theme="document-save"/>
</property>
<property name="flat">
<bool>false</bool>
+4 -8
View File
@@ -76,8 +76,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="go-up" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-up.png</normaloff>:/ico/16x16/go-up.png</iconset>
<iconset theme="go-up"/>
</property>
</widget>
</item>
@@ -87,8 +86,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
<iconset theme="list-add"/>
</property>
</widget>
</item>
@@ -98,8 +96,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
<iconset theme="list-remove"/>
</property>
</widget>
</item>
@@ -109,8 +106,7 @@
<string/>
</property>
<property name="icon">
<iconset theme="go-down" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-down.png</normaloff>:/ico/16x16/go-down.png</iconset>
<iconset theme="go-down"/>
</property>
</widget>
</item>
+43 -17
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagram.h"
#include "autoNum/elementautonumschemecommand.h"
#include "ElementsCollection/elementcollectionhandler.h"
#include "TerminalStrip/GraphicsItem/terminalstripitem.h"
@@ -46,6 +47,7 @@
#include "diagramsortkeys.h"
#include "itemgroups.h"
#include "textgrid.h"
#include "foliogrid.h"
#include <QGraphicsView>
#include <QTextStream>
#include <algorithm>
@@ -340,10 +342,8 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
p -> setBrush(Qt::NoBrush);
int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
int yGrid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
const int xGrid = FolioGrid::step(settings.value(FolioGrid::x_key), Diagram::xGrid);
const int yGrid = FolioGrid::step(settings.value(FolioGrid::y_key), Diagram::yGrid);
qreal limit_x = rect.x() + rect.width();
qreal limit_y = rect.y() + rect.height();
@@ -931,7 +931,7 @@ bool Diagram::toPaintDevice(QPaintDevice &pix,
// determine la zone source = contenu du schema + marges
QRectF source_area;
if (!use_border_) {
source_area = itemsBoundingRect();
source_area = visibleItemsBoundingRect();
source_area.translate(-margin, -margin);
source_area.setWidth (source_area.width () + 2.0 * margin);
source_area.setHeight(source_area.height() + 2.0 * margin);
@@ -999,7 +999,7 @@ QSize Diagram::imageSize() const
// determine la zone source = contenu du schema + marges
qreal image_width, image_height;
if (!use_border_) {
QRectF items_rect = itemsBoundingRect();
QRectF items_rect = visibleItemsBoundingRect();
image_width = items_rect.width();
image_height = items_rect.height();
} else {
@@ -1292,6 +1292,13 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
std::stable_sort(list_conductors.begin(), list_conductors.end(),
[](Conductor *a, Conductor *b) { return conductorSortKey(a) < conductorSortKey(b); });
// A copy carries the numberings its elements follow: pasted into
// another project, which does not know them, it can offer to import
// them (see PasteNumberingImport)
if (is_copy_command) {
ElementAutoNumSchemeCommand::writeCopiedSchemes(document, dom_root, m_project, list_elements);
}
// 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;
@@ -1610,10 +1617,14 @@ bool Diagram::fromXml(QDomElement &document,
m_conductors_autonum_name = root.attribute(QStringLiteral("conductorAutonum"));
// Load Freeze New Element
m_freeze_new_elements = root.attribute(QStringLiteral("freezeNewElement")).toInt();
// Written as "true"/"false" by toXml(), so compare the text:
// toInt() of either word is 0.
m_freeze_new_elements = root.attribute(QStringLiteral("freezeNewElement"))
== QLatin1String("true");
// Load Freeze New Conductor
m_freeze_new_conductors_ = root.attribute(QStringLiteral("freezeNewConductor")).toInt();
m_freeze_new_conductors_ = root.attribute(QStringLiteral("freezeNewConductor"))
== QLatin1String("true");
//Load Element Folio Sequential
folioSequentialsFromXml(root,
@@ -2854,10 +2865,29 @@ void Diagram::adjustSceneRect()
{
QRectF old_rect = sceneRect();
setSceneRect(border_and_titleblock.borderAndTitleBlockRect().united(
itemsBoundingRect()));
visibleItemsBoundingRect()));
update(old_rect.united(sceneRect()));
}
/**
@brief Diagram::visibleItemsBoundingRect
Same as QGraphicsScene::itemsBoundingRect(), but only counts items that
are shown. A hidden item keeps whatever position it last had: the text of
a single-line wire, and the wire texts hidden by "one text per potential",
are never positioned again and can sit far outside the drawing (#1281).
@return the bounding rect of the visible items, in scene coordinates
*/
QRectF Diagram::visibleItemsBoundingRect() const
{
QRectF rect;
const auto scene_items = items();
for (QGraphicsItem *item : scene_items) {
if (item->isVisible())
rect |= item->sceneBoundingRect();
}
return rect;
}
/**
@brief Diagram::applyProperties
This method allows you to apply new rendering options while
@@ -2931,10 +2961,8 @@ DiagramPosition Diagram::convertPosition(const QPointF &pos) {
QPointF Diagram::snapToGrid(const QPointF &p)
{
QSettings settings;
int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
int yGrid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
const int xGrid = FolioGrid::step(settings.value(FolioGrid::x_key), Diagram::xGrid);
const int yGrid = FolioGrid::step(settings.value(FolioGrid::y_key), Diagram::yGrid);
//Return a point rounded to the nearest pixel
if (QApplication::keyboardModifiers().testFlag(Qt::ControlModifier))
@@ -2966,10 +2994,8 @@ QPointF Diagram::snapToTextGrid(const QPointF &p)
: settings.value(TextGrid::settings_key, 1).toReal();
return TextGrid::snap(p,
settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt(),
settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt(),
FolioGrid::step(settings.value(FolioGrid::x_key), Diagram::xGrid),
FolioGrid::step(settings.value(FolioGrid::y_key), Diagram::yGrid),
divisor);
}
+1
View File
@@ -266,6 +266,7 @@ class Diagram : public QGraphicsScene
bool toPaintDevice(QPaintDevice &, int = -1, int = -1,
Qt::AspectRatioMode = Qt::KeepAspectRatio);
QSize imageSize() const;
QRectF visibleItemsBoundingRect() const;
bool isEmpty() const;
+73
View File
@@ -17,8 +17,10 @@
*/
#include "diagramcommands.h"
#include "autoNum/elementautonumschemecommand.h"
#include "diagram.h"
#include "itemgroups.h"
#include "qetproject.h"
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/diagramimageitem.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
@@ -29,6 +31,19 @@
#include "qetinformation.h"
#include "qgimanager.h"
namespace {
/// Is @p element among those the numberings in @p schemes are given to?
bool pasted_schemes_has(const QMap<QString, QVector<Element *>> &schemes, const Element *element)
{
for (auto it = schemes.constBegin() ; it != schemes.constEnd() ; ++it) {
if (it.value().contains(const_cast<Element *>(element))) {
return true;
}
}
return false;
}
} // namespace
/**
@brief PasteDiagramCommand::PasteDiagramCommand
Constructor
@@ -64,6 +79,10 @@ void PasteDiagramCommand::undo()
{
diagram -> showMe();
//The numbering of the pasted elements, if any: gives the numbers
//back to the numberings they came from
QUndoCommand::undo();
foreach(QGraphicsItem *item, content.items(filter))
diagram->removeItem(item);
}
@@ -98,6 +117,18 @@ void PasteDiagramCommand::redo()
e->initLink(elmts_list);
}
//The numberings the pasted elements follow, known now: the label
//erasing below empties their formula. Pasted elements get the next
//number of their numbering, as placed ones do, instead of the
//label of the element they were copied from.
const bool autonumber = m_autonumber && settings.value(
"diagramcommands/autonumber-pasted-elements", true).toBool();
QETProject *project = diagram->project();
QMap<QString, QVector<Element *>> pasted_schemes;
if (project && autonumber) {
pasted_schemes = ElementAutoNumSchemeCommand::pastedSchemes(project, elmts_list);
}
//make new uuid for every pasted conductor, because old uuid are
//the uuid of the copied conductor
const QList <Conductor *> all_pasted_conductors = content.conductors();
@@ -221,6 +252,44 @@ void PasteDiagramCommand::redo()
}
}
//What the pasted elements keep of their numbering names a
//numbering of this project, or none (a paste from another
//project, or from a file written before the ids)
if (project) {
ElementAutoNumSchemeCommand::linkPasted(project, elmts_list);
if (autonumber)
{
//A formula which names no numbering of this project is
//not kept: it would stand for a numbering which does
//not exist here, with a label which is not its result.
//The label is what the erase preference made of it.
for (Element *e : elmts_list)
{
const DiagramContext &info = e->elementInformations();
if (e->linkType() == Element::Slave || (e->linkType() & Element::AllReport)
|| info.value(QETInformation::ELMT_FORMULA).toString().isEmpty()
|| !ElementAutoNumSchemeCommand::followedScheme(project, info).isEmpty()
|| pasted_schemes_has(pasted_schemes, e)) {
continue;
}
DiagramContext dc = info;
dc.addValue(QETInformation::ELMT_FORMULA, QString());
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);
}
}
if (!pasted_schemes.isEmpty()) {
ElementAutoNumSchemeCommand::numberPasted(project, pasted_schemes, this);
}
}
//Pasted groups become new groups: the members of one source
//group all get the same new uuid, never the source's, or the
//copy would join the original's group. After the elements got
@@ -250,6 +319,10 @@ void PasteDiagramCommand::redo()
}
}
//The numbering of the pasted elements, if any, on the first redo
//as well as on the next ones
QUndoCommand::redo();
const QList<QGraphicsItem *> qgis_list = content.items();
for (QGraphicsItem *qgi : qgis_list)
qgi -> setSelected(true);
+8
View File
@@ -42,6 +42,12 @@ class PasteDiagramCommand : public QUndoCommand {
public:
void undo() override;
void redo() override;
/**
Whether pasted elements which follow an element numbering are
numbered by the paste (default true, if the preference allows it).
Off for a caller which numbers them itself.
*/
void setAutoNumbering(bool enabled) {m_autonumber = enabled;}
// attributes
private:
@@ -53,6 +59,8 @@ class PasteDiagramCommand : public QUndoCommand {
int filter;
/// prevent the first call to redo()
bool first_redo;
/// number the pasted elements which follow an element numbering
bool m_autonumber = true;
};
/**
+1
View File
@@ -42,6 +42,7 @@ void DiagramContext::add(DiagramContext other)
*/
void DiagramContext::remove(const QString &key) {
m_content.remove(key);
m_content_show.remove(key);
}
/**
@@ -274,7 +274,10 @@ void DiagramEventAddMacro::addMacro(QPointF final_pos)
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));
//and does not number them either: the labels are the ones saved
auto *paste = new PasteDiagramCommand(m_diagram, pasted_content);
paste->setAutoNumbering(false);
m_diagram->undoStack().push(paste);
settings.setValue("diagramcommands/erase-label-on-copy", saved_erase);
}
}
+88 -57
View File
@@ -16,12 +16,14 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagrameventaddpaste.h"
#include "../autoNum/ui/pastenumberingimport.h"
#include "../cable/addcablecommand.h"
#include "../cable/cablecopy.h"
#include "../cable/cablepart.h"
#include "../cable/editcablecommand.h"
#include "../diagram.h"
#include "../foliogrid.h"
#include "../diagramcommands.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
@@ -33,42 +35,30 @@
#include <QApplication>
#include <QClipboard>
#include <QCoreApplication>
#include <QCursor>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QKeyEvent>
#include <QStatusBar>
#include <utility>
/**
@brief batchWith
Keep the cable lines of a paste together with the rest of it, so one
Ctrl+Z takes the whole thing back in one go. Mirrors the helper
DiagramView uses for its one-step paste; BatchCommand's own name
says "Supprimer", so the name of what the user actually did is put
back onto the batch.
@param first the step already assembled, may be null
@param second the step to add; ownership goes to the result
@return both steps as one command, or the only one of them
*/
static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second)
{
if (!first) return second;
if (!second) return first;
auto *batched = new BatchCommand(first, second);
if (!first->text().isEmpty()) batched->setText(first->text());
return batched;
}
/**
@brief DiagramEventAddPaste::DiagramEventAddPaste
@param diagram : diagram to paste into
@param start_pos : where the pasted items first appear, in scene
coordinates -- normally the cursor
coordinates -- normally the cursor. Only used for UnderCursor; an
AtOrigin paste stays at the coordinates it was copied from.
@param placement : UnderCursor moves the group under the cursor
(default), AtOrigin leaves it at its original XML position and warps
the OS cursor to the group's origin so baseline and screen match.
*/
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) :
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos,
PastePlacement placement) :
DiagramEventInterface(diagram)
{
m_placement = placement;
//DiagramEventInterface::init() is called by Diagram::setEventInterface
//only when it is replacing an earlier interface, so call it here as
//DiagramEventAddMacro does.
@@ -97,6 +87,7 @@ static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second)
//items -- and it travels as a whole, colour labels and all,
//whether or not the place it is going to holds a wire.
m_diagram->fromXml(document_xml, QPointF(), false, &m_content, &m_cables);
m_copied_schemes = PasteNumberingImport::copiedBy(document_xml);
if (db) {
db->blockSignals(false);
@@ -135,30 +126,38 @@ static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second)
}
const QPointF top_left = items_rect.topLeft();
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 int xGrid = FolioGrid::step(settings.value(FolioGrid::x_key), Diagram::xGrid);
const int yGrid = FolioGrid::step(settings.value(FolioGrid::y_key), Diagram::yGrid);
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(start_pos);
const QPointF grid_origin = (m_placement == AtOrigin)
? snapGrid(top_left)
: snapGrid(start_pos);
//Land the pasted content under the cursor immediately, rather than
//leaving it at the copied source's own coordinates: fromXml() above
//loads items at their original position purely because it doesn't
//know the target yet, not because that is where a paste should end
//up. The previous approach instead left items there and warped the
//OS cursor to match -- QCursor::setPos() is silently ignored by
//many window managers and compositors (Wayland in particular), so
//on any of those the warp simply never happened and the paste was
//left wherever it had originally been copied from, which could be
//anywhere on the folio -- exactly the "far from the cursor" bug.
const QPointF initial_delta = grid_origin - snapGrid(top_left);
for (auto *item : movable) {
item->setPos(item->pos() + initial_delta);
if (m_placement == UnderCursor) {
//Land the pasted content under the cursor immediately, rather than
//leaving it at the copied source's own coordinates: fromXml() above
//loads items at their original position purely because it doesn't
//know the target yet, not because that is where a paste should end
//up. The previous approach instead left items there and warped the
//OS cursor to match -- QCursor::setPos() is silently ignored by
//many window managers and compositors (Wayland in particular), so
//on any of those the warp simply never happened and the paste was
//left wherever it had originally been copied from, which could be
//anywhere on the folio -- exactly the "far from the cursor" bug.
const QPointF initial_delta = grid_origin - snapGrid(top_left);
for (auto *item : movable) {
item->setPos(item->pos() + initial_delta);
}
} else {
//AtOrigin: the items keep the coordinates they were copied from,
//so the copy stands exactly where the original stands. No delta is
//applied; what has to move is the cursor -- it is warped to the
//group's grid-snapped origin below, once m_group_origin is set,
//so the baseline and the physical cursor position agree.
}
//Store each item's now-placed position. moveTo() applies a
@@ -176,10 +175,41 @@ static QUndoCommand *batchWith(QUndoCommand *first, QUndoCommand *second)
conductor->updatePath();
}
//The baseline is the group's grid-snapped origin, so moveTo()
//does not have to capture one from the first mouse movement.
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
//The baseline is the group's grid-snapped origin. For UnderCursor
//the group was just put there, so the baseline is known outright.
//For AtOrigin the group stayed where it was copied from and the
//OS cursor is warped to match instead -- but only a warp that
//verifiably landed may be trusted as the baseline: QCursor::setPos()
//needs pointer focus inside the target window (on Wayland it goes
//through wp_pointer_warp_v1, and is a silent no-op without it), and
//a baseline that assumes a warp which did not happen would move the
//whole group by the warp-sized delta on the first mouse move. So
//when the warp cannot be confirmed, leave m_baseline_captured
//false: moveTo() re-baselines from the first real mouse position
//instead, which costs at most the first movement but never flings
//the group across the folio.
if (m_placement == AtOrigin && !m_diagram->views().isEmpty()) {
if (auto *view = m_diagram->views().at(0)) {
//An origin outside the visible area cannot be warped to
//(the compositor rejects targets outside the window) and
//would leave the paste invisible, so scroll it into view
//first.
if (!view->viewport()->rect().contains(view->mapFromScene(m_group_origin))) {
view->ensureVisible(items_rect, 50, 50);
}
const QPoint view_pos = view->mapFromScene(m_group_origin);
const QPoint global_pos = view->viewport()->mapToGlobal(view_pos);
QCursor::setPos(global_pos);
if ((QCursor::pos() - global_pos).manhattanLength() <= 4) {
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
}
}
}
if (m_placement == UnderCursor) {
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
}
m_diagram->clearSelection();
for (auto *item : movable) {
@@ -273,10 +303,8 @@ void DiagramEventAddPaste::showHint()
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 int xGrid = FolioGrid::step(settings.value(FolioGrid::x_key), Diagram::xGrid);
const int yGrid = FolioGrid::step(settings.value(FolioGrid::y_key), Diagram::yGrid);
const auto snapGrid = [xGrid, yGrid](const QPointF &p) -> QPointF {
return QPointF(
@@ -396,10 +424,10 @@ void DiagramEventAddPaste::commit()
m_content.conductors(DiagramContent::AnyConductor));
m_cables.clear();
QUndoCommand *command = nullptr;
if (m_content.count()) {
command = new PasteDiagramCommand(m_diagram, m_content);
}
//The cable lines the copy brings are not part of the paste
//itself: they are handed to the paste so that one Ctrl+Z takes
//the whole of it back in one go.
QList<QUndoCommand *> cable_commands;
for (const CableCopy::Wired &line : std::as_const(wired))
{
if (!line.part) continue;
@@ -407,12 +435,15 @@ void DiagramEventAddPaste::commit()
auto *added = new AddCableCommand(line.part->cable(), line.part.data(),
m_diagram, true, line.taken);
added->setText(QCoreApplication::translate("CableCopy", "Coller un câble"));
command = batchWith(command, added);
}
if (command) {
m_diagram->undoStack().push(command);
cable_commands << added;
}
//Asks whether to import the numberings the copy brings, if the
//project has not got them; the paste itself and the cable lines
//travel inside the same step of the undo stack.
PasteNumberingImport::push(m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
m_diagram, m_content, m_copied_schemes, cable_commands);
//The undo stack has just run its first redo, which may have
//renewed the identity of a pasted conductor: the cores are
//pointed at the wires again so their cable field survives it.
+27 -7
View File
@@ -20,6 +20,7 @@
#include "diagrameventinterface.h"
#include "../diagramcontent.h"
#include "../autoNum/elementautonumschemecommand.h"
#include <QHash>
#include <QPointer>
@@ -58,7 +59,21 @@ class DiagramEventAddPaste : public DiagramEventInterface
| DiagramContent::Tables
| DiagramContent::TerminalStrip;
DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos);
///Where the group starts, relative to what was copied.
enum PastePlacement {
///Move the group so its top left lands at \a start_pos
///(normally the cursor): Ctrl+V, the copy appears under
///the pointer and follows it.
UnderCursor,
///Leave the group at its original XML position and warp
///the OS cursor to the group's origin so the baseline
///matches what is on screen: Ctrl+Shift+V, "paste at the
///origin point".
AtOrigin
};
DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos,
PastePlacement placement = UnderCursor);
~DiagramEventAddPaste() override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
@@ -97,17 +112,22 @@ class DiagramEventAddPaste : public DiagramEventInterface
///mouse move would add it again each time and let the line run
///away from the rest of the paste.
QPointF m_cable_delta;
/// The numberings the pasted copy carries
QList<ElementAutoNumSchemeCommand::Scheme> m_copied_schemes;
///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.
///Where this paste starts: UnderCursor moves the group to
///start_pos, AtOrigin leaves it at its original XML position.
PastePlacement m_placement{UnderCursor};
///Where the group's grid-snapped top left sits when the paste
///started, in scene coordinates -- under the cursor for
///UnderCursor, the copy's own origin for AtOrigin.
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.
///moveTo() measures from. Equal to m_group_origin: either the
///group was put there (UnderCursor) or the cursor was warped
///there (AtOrigin).
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
+343
View File
@@ -0,0 +1,343 @@
/*
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 "diagrameventfillet.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../qetgraphicsitem/qetshapeitem.h"
#include "../qgimanager.h"
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QInputDialog>
#include <QSettings>
#include <QStatusBar>
#include <QTimer>
#include <QUndoCommand>
#include <QtMath>
#include <algorithm>
#include <cmath>
namespace {
const char *radiusSettingKey = "diagrameditor/fillet_radius";
/**
Adds the fillet's arc to the diagram exactly where it was computed.
AddGraphicsObjectCommand is not used: its redo() calls setPos(),
and QetShapeItem::setPos() snaps the outline's corner to the grid,
which would pull the arc off the two lines it has to touch.
*/
class AddFilletArcCommand : public QUndoCommand
{
public:
AddFilletArcCommand(QetShapeItem *arc, Diagram *diagram, QUndoCommand *parent) :
QUndoCommand(parent), m_arc(arc), m_diagram(diagram)
{ m_diagram->qgiManager().manage(m_arc); }
~AddFilletArcCommand() override
{ m_diagram->qgiManager().release(m_arc); }
void redo() override { m_diagram->addItem(m_arc); }
void undo() override { m_diagram->removeItem(m_arc); }
private:
QetShapeItem *m_arc;
Diagram *m_diagram;
};
qreal length(const QPointF &v) { return std::hypot(v.x(), v.y()); }
qreal dot(const QPointF &a, const QPointF &b) { return a.x() * b.x() + a.y() * b.y(); }
qreal distanceToSegment(const QPointF &p, const QLineF &segment)
{
const QPointF d = segment.p2() - segment.p1();
const qreal len2 = dot(d, d);
if (len2 <= 0)
return length(p - segment.p1());
const qreal t = qBound(qreal(0), dot(p - segment.p1(), d) / len2, qreal(1));
return length(p - (segment.p1() + t * d));
}
QLineF sceneLine(const QetShapeItem *line)
{
return QLineF(line->mapToScene(line->line().p1()), line->mapToScene(line->line().p2()));
}
//Angle of a point on a circle, in the convention QPainterPath::arcTo()
//and QetShapeItem use: degrees, anticlockwise from three o'clock.
qreal arcAngle(const QPointF &centre, const QPointF &p)
{
return qRadiansToDegrees(std::atan2(-(p.y() - centre.y()), p.x() - centre.x()));
}
//Same test as QetShapeItem::setStartAngle()/setEndAngle() use to snap
//an arc back to a full ellipse.
bool nearlyFullTurn(qreal span)
{
qreal wrapped = std::fmod(span, 360.0);
if (wrapped < 0) wrapped += 360.0;
return wrapped < 5.0 || wrapped > 355.0;
}
} // namespace
DiagramEventFillet::DiagramEventFillet(Diagram *diagram) :
DiagramEventInterface(diagram)
{
m_running = true;
//Deferred for the same reason as DiagramEventAddShape's hint:
//the previous tool's destructor clears the status bar after
//this constructor returns.
QTimer::singleShot(0, this, [this]() { showDefaultHint(); });
}
DiagramEventFillet::~DiagramEventFillet()
{
clearFirstPick();
if (m_diagram && !m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
}
void DiagramEventFillet::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
event->setAccepted(true);
if (Q_UNLIKELY(m_diagram->isReadOnly()))
return;
if (event->button() == Qt::RightButton)
{
if (m_first_line) {
clearFirstPick();
showDefaultHint();
} else {
m_running = false;
emit finish();
}
return;
}
if (event->button() != Qt::LeftButton)
return;
const QPointF pos = event->scenePos();
const QList<QetShapeItem *> lines = linesNear(pos);
if (lines.isEmpty()) {
showHint(tr("Aucune ligne ici : cliquez sur une ligne dessinée sur le folio"));
return;
}
if (!m_first_line)
{
//One click where two lines meet picks both of them
if (lines.size() >= 2) {
filletLines({lines.at(0), pos}, {lines.at(1), pos});
return;
}
m_first = {lines.first(), pos};
m_first_line = lines.first();
m_diagram->clearSelection();
m_first_line->setSelected(true);
showHint(tr("Cliquez sur la deuxième ligne ; clic droit : annuler"));
return;
}
const auto other = std::find_if(lines.cbegin(), lines.cend(),
[this](QetShapeItem *line) { return line != m_first_line; });
if (other == lines.cend()) {
showHint(tr("Cliquez sur une autre ligne ; clic droit : annuler"));
return;
}
const Pick first = m_first;
clearFirstPick();
filletLines(first, {*other, pos});
}
/**
@brief DiagramEventFillet::linesNear
@return the Line shapes passing within a few screen pixels of
scenePos, nearest first. Only the items under the cursor are looked
at; the diagram is not walked.
*/
QList<QetShapeItem *> DiagramEventFillet::linesNear(const QPointF &scenePos) const
{
qreal scale = 1;
if (!m_diagram->views().isEmpty())
scale = m_diagram->views().constFirst()->transform().m11();
const qreal tolerance = 6.0 / qMax(scale, qreal(0.01));
QList<QPair<qreal, QetShapeItem *>> found;
const QRectF area(scenePos - QPointF(tolerance, tolerance), QSizeF(2 * tolerance, 2 * tolerance));
for (QGraphicsItem *item : m_diagram->items(area))
{
if (item->type() != QetShapeItem::Type)
continue;
auto *shape = static_cast<QetShapeItem *>(item);
if (shape->shapeType() != QetShapeItem::Line)
continue;
const qreal distance = distanceToSegment(scenePos, sceneLine(shape));
if (distance <= tolerance)
found.append({distance, shape});
}
std::sort(found.begin(), found.end(),
[](const auto &a, const auto &b) { return a.first < b.first; });
QList<QetShapeItem *> lines;
for (const auto &f : found)
lines.append(f.second);
return lines;
}
/**
@brief DiagramEventFillet::filletLines
Rounds the corner between the two picked lines. For each line, the
part kept is the one on the side of the corner where it was clicked;
a click on the corner itself keeps the longer part. The other end of
each line moves to the point where the arc touches it -- shortening
the line, or lengthening it when the two lines did not reach each
other. One undo step undoes the whole fillet.
*/
void DiagramEventFillet::filletLines(const Pick &a, const Pick &b)
{
const QLineF s1 = sceneLine(a.line);
const QLineF s2 = sceneLine(b.line);
QPointF corner;
if (s1.intersects(s2, &corner) == QLineF::NoIntersection) {
showHint(tr("Ces deux lignes sont parallèles : pas de congé possible"));
return;
}
qreal scale = 1;
if (!m_diagram->views().isEmpty())
scale = m_diagram->views().constFirst()->transform().m11();
const qreal cornerTolerance = 6.0 / qMax(scale, qreal(0.01));
auto keptEnd = [&](const QLineF &segment, const QPointF &pick) -> int {
const QPointF ends[2] = {segment.p1(), segment.p2()};
const QPointF towardPick = pick - corner;
const bool onCorner = length(towardPick) < cornerTolerance;
int best = -1;
for (int pass = 0; pass < 2 && best < 0; ++pass) {
qreal bestLength = -1;
for (int i = 0; i < 2; ++i) {
const QPointF v = ends[i] - corner;
if (pass == 0 && !onCorner && dot(v, towardPick) <= 0)
continue;
if (length(v) > bestLength) { best = i; bestLength = length(v); }
}
}
return best;
};
const int keep1 = keptEnd(s1, a.pos);
const int keep2 = keptEnd(s2, b.pos);
const QPointF e1 = keep1 == 0 ? s1.p1() : s1.p2();
const QPointF e2 = keep2 == 0 ? s2.p1() : s2.p2();
const qreal len1 = length(e1 - corner);
const qreal len2 = length(e2 - corner);
if (len1 < 1e-6 || len2 < 1e-6) {
showHint(tr("Une des lignes s'arrête au coin : rien à arrondir de ce côté"));
return;
}
const QPointF u1 = (e1 - corner) / len1;
const QPointF u2 = (e2 - corner) / len2;
const qreal theta = std::acos(qBound(qreal(-1), dot(u1, u2), qreal(1)));
if (qRadiansToDegrees(theta) > 174.0) {
showHint(tr("Ces deux lignes sont presque alignées : pas de congé possible"));
return;
}
QSettings settings;
bool ok = false;
QWidget *parent = m_diagram->views().isEmpty() ? nullptr : m_diagram->views().constFirst();
const qreal radius = QInputDialog::getDouble(parent, tr("Congé"), tr("Rayon :"),
settings.value(radiusSettingKey, 20.0).toDouble(), 0.1, 100000.0, 1, &ok);
if (!ok || !m_diagram)
return;
settings.setValue(radiusSettingKey, radius);
const qreal tangentDistance = radius / std::tan(theta / 2);
if (tangentDistance > len1 + 1e-6 || tangentDistance > len2 + 1e-6) {
showHint(tr("Rayon trop grand pour ces lignes (%1 au plus)")
.arg(std::min(len1, len2) * std::tan(theta / 2), 0, 'f', 1));
return;
}
const QPointF t1 = corner + u1 * tangentDistance;
const QPointF t2 = corner + u2 * tangentDistance;
const QPointF bisector = (u1 + u2) / length(u1 + u2);
const QPointF centre = corner + bisector * (radius / std::sin(theta / 2));
//Each line keeps its picked end and moves its other end to the
//tangent point; p1/p2 keep their roles, mapped back into the
//line's own coordinates (it may be rotated or skewed).
auto newLine = [](QetShapeItem *line, int keep, const QPointF &kept, const QPointF &tangent) {
const QPointF p1 = keep == 0 ? kept : tangent;
const QPointF p2 = keep == 0 ? tangent : kept;
return QLineF(line->mapFromScene(p1), line->mapFromScene(p2));
};
auto *arc = new QetShapeItem(centre - QPointF(radius, radius),
centre + QPointF(radius, radius), QetShapeItem::Ellipse);
arc->setPen(a.line->pen());
qreal start = std::fmod(arcAngle(centre, t1), 360.0);
if (start < 0) start += 360.0;
const qreal span = std::fmod(arcAngle(centre, t2) - start + 540.0, 360.0) - 180.0;
//Set the two angles in an order whose intermediate state is not
//close to a full turn, which the setters would snap to an ellipse.
if (!nearlyFullTurn(360.0 - start)) {
arc->setStartAngle(start);
arc->setEndAngle(start + span);
} else {
arc->setEndAngle(start + span);
arc->setStartAngle(start);
}
auto *undo = new QUndoCommand(tr("Ajouter un congé"));
new QPropertyUndoCommand(a.line, "line", a.line->line(), newLine(a.line, keep1, e1, t1), undo);
new QPropertyUndoCommand(b.line, "line", b.line->line(), newLine(b.line, keep2, e2, t2), undo);
new AddFilletArcCommand(arc, m_diagram, undo);
m_diagram->undoStack().push(undo);
showDefaultHint();
}
void DiagramEventFillet::clearFirstPick()
{
if (m_first_line)
m_first_line->setSelected(false);
m_first_line.clear();
m_first = Pick();
}
void DiagramEventFillet::showHint(const QString &text) const
{
if (!m_diagram || m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(text);
}
void DiagramEventFillet::showDefaultHint() const
{
showHint(tr("Congé : cliquez sur deux lignes, ou une fois là où elles se rejoignent ; "
"Échap ou clic droit : terminer"));
}
+62
View File
@@ -0,0 +1,62 @@
/*
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 DIAGRAMEVENTFILLET_H
#define DIAGRAMEVENTFILLET_H
#include "diagrameventinterface.h"
#include <QPointer>
#include <QPointF>
class QetShapeItem;
/**
@brief The DiagramEventFillet class
The "add a fillet" tool: rounds the corner between two drawn lines,
the way a sketch fillet does in a CAD program. Click the two lines
one after the other, or once where they meet; a dialog asks for the
radius. The lines are trimmed (or extended) to the points where the
arc touches them, and an arc is added between them -- an ordinary
Ellipse shape with a start and end angle, so nothing new is saved.
*/
class DiagramEventFillet : public DiagramEventInterface
{
Q_OBJECT
public:
DiagramEventFillet(Diagram *diagram);
~DiagramEventFillet() override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
private:
struct Pick {
QetShapeItem *line = nullptr;
QPointF pos; // where the line was clicked, in scene coordinates
};
QList<QetShapeItem *> linesNear(const QPointF &scenePos) const;
void filletLines(const Pick &a, const Pick &b);
void clearFirstPick();
void showHint(const QString &text) const;
void showDefaultHint() const;
Pick m_first;
QPointer<QetShapeItem> m_first_line; // m_first.line, guarded: the line can be deleted between the two clicks
};
#endif // DIAGRAMEVENTFILLET_H
+14 -9
View File
@@ -58,14 +58,16 @@ bool DiagramGestureOverlay::isEnabled()
/**
@brief DiagramGestureOverlay::showAt
Show the ring centred on @a center with @a actions, at most eight,
clockwise from the top.
Show the ring centred on @a center with @a sectors directions (4 or 8)
and @a actions, one per direction clockwise from the top.
*/
void DiagramGestureOverlay::showAt(const QPoint &center,
const QList<QAction *> &actions)
const QList<QAction *> &actions,
int sectors)
{
m_center = center;
m_actions = actions.mid(0, sectors);
m_sectors = sectors == 4 ? 4 : 8;
m_actions = actions.mid(0, m_sectors);
m_active = -1;
move(center - QPoint(overlay_width / 2, outer_radius + 1));
show();
@@ -88,7 +90,7 @@ int DiagramGestureOverlay::sectorAt(const QPoint &viewport_pos) const
if (angle < 0) {
angle += 360;
}
return int(qRound(angle / (360.0 / sectors))) % sectors;
return int(qRound(angle / (360.0 / m_sectors))) % m_sectors;
}
/**
@@ -102,7 +104,7 @@ QAction *DiagramGestureOverlay::actionAt(const QPoint &viewport_pos) const
return nullptr;
}
QAction *action = m_actions.at(sector);
return action->isEnabled() ? action : nullptr;
return action && action->isEnabled() ? action : nullptr;
}
void DiagramGestureOverlay::setPointer(const QPoint &viewport_pos)
@@ -120,7 +122,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QPointF c(overlay_width / 2, outer_radius + 1);
const qreal step = 360.0 / sectors;
const qreal step = 360.0 / m_sectors;
QColor base = palette().color(QPalette::Window);
base.setAlpha(225);
@@ -130,7 +132,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
//Highlight the sector the mouse points at, if it holds a command
if (m_active >= 0 && m_active < m_actions.count()
&& m_actions.at(m_active)->isEnabled())
&& m_actions.at(m_active) && m_actions.at(m_active)->isEnabled())
{
QPainterPath wedge;
wedge.moveTo(c);
@@ -151,6 +153,9 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
for (int i = 0 ; i < m_actions.count() ; ++i)
{
QAction *action = m_actions.at(i);
if (!action) {
continue;
}
const qreal a = qDegreesToRadians(i * step);
const QPointF p(c.x() + icon_radius * qSin(a), c.y() - icon_radius * qCos(a));
const QRect r(int(p.x()) - icon_size / 2, int(p.y()) - icon_size / 2,
@@ -167,7 +172,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
}
//Name of the highlighted command, under the ring
if (m_active >= 0 && m_active < m_actions.count())
if (m_active >= 0 && m_active < m_actions.count() && m_actions.at(m_active))
{
const QString name = m_actions.at(m_active)->text().remove(QLatin1Char('&'));
const QRect label(0, 2 * outer_radius + 4, width(), 20);
+8 -5
View File
@@ -29,8 +29,9 @@ class QAction;
mouse's direction highlighted. Releasing runs it, releasing near the
centre cancels -- SolidWorks' mouse gestures.
Commands are placed clockwise from the top. The ring only draws; the
view tracks the mouse and asks sectorAt() on release.
Commands are placed clockwise from the top, one per direction (4 or 8).
A null entry is an empty direction. The ring only draws; the view
tracks the mouse and asks sectorAt() on release.
*/
class DiagramGestureOverlay : public QWidget
{
@@ -39,14 +40,15 @@ class DiagramGestureOverlay : public QWidget
public:
explicit DiagramGestureOverlay(QWidget *viewport);
void showAt(const QPoint &center, const QList<QAction *> &actions);
void showAt(const QPoint &center, const QList<QAction *> &actions,
int sectors = 8);
void setPointer(const QPoint &viewport_pos);
int sectorAt(const QPoint &viewport_pos) const;
QAction *actionAt(const QPoint &viewport_pos) const;
static bool isEnabled();
int sectors() const { return m_sectors; }
static const int sectors = 8;
static bool isEnabled();
protected:
void paintEvent(QPaintEvent *event) override;
@@ -55,6 +57,7 @@ class DiagramGestureOverlay : public QWidget
QPoint m_center;
QList<QAction *> m_actions;
int m_active = -1;
int m_sectors = 8;
};
#endif // DIAGRAMGESTUREOVERLAY_H
+357
View File
@@ -0,0 +1,357 @@
/*
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 "diagramtoolbarsettings.h"
#include "shortcutmanager.h"
#include <QApplication>
#include <QCoreApplication>
#include <QSettings>
#include <QToolBar>
namespace {
const QString CONTENTS = QStringLiteral("diagrameditor/toolbars/");
const QString CUSTOM_NAMES = QStringLiteral("diagrameditor/custom_toolbars/names");
const QString CUSTOM_TITLES = QStringLiteral("diagrameditor/custom_toolbars/titles");
//Set on a window by markWindow()
const char *const MARKED = "qetDiagramToolbars";
}
/**
@return every toolbar, built-in first, then the user's own in the order
they were added
*/
QList<DiagramToolbarSettings::Toolbar> DiagramToolbarSettings::toolbars()
{
QList<Toolbar> list;
for (const QString &name : builtInNames()) {
list << Toolbar{name, builtInTitle(name), false};
}
return list << customToolbars();
}
/**
@return the object names of the toolbars the diagram editor always has.
The scripts toolbar is not one: it is filled from the scripts folder.
*/
QStringList DiagramToolbarSettings::builtInNames()
{
return {QStringLiteral("toolbar"),
QStringLiteral("display"),
QStringLiteral("diagram"),
QStringLiteral("adding"),
QStringLiteral("diagram_depth_toolbar")};
}
QString DiagramToolbarSettings::builtInTitle(const QString &name)
{
if (name == QLatin1String("toolbar"))
return QCoreApplication::translate("QETDiagramEditor", "Outils");
if (name == QLatin1String("display"))
return QCoreApplication::translate("QETDiagramEditor", "Affichage");
if (name == QLatin1String("diagram"))
return QCoreApplication::translate("QETDiagramEditor", "Schéma");
if (name == QLatin1String("adding"))
return QCoreApplication::translate("QETDiagramEditor", "Ajouter");
if (name == QLatin1String("diagram_depth_toolbar"))
return QCoreApplication::translate("QETDiagramEditor", "Profondeur", "toolbar title");
return QString();
}
/**
@return the toolbars the user added, in the order they were added
*/
QList<DiagramToolbarSettings::Toolbar> DiagramToolbarSettings::customToolbars()
{
QSettings settings;
const QStringList names = settings.value(CUSTOM_NAMES).toStringList();
const QStringList titles = settings.value(CUSTOM_TITLES).toStringList();
QList<Toolbar> list;
for (int i = 0 ; i < names.size() ; ++i) {
list << Toolbar{names.at(i), titles.value(i, names.at(i)), true};
}
return list;
}
/**
@brief DiagramToolbarSettings::setCustomToolbars
Store the user's own toolbars. The contents of one that is no longer
in @a toolbars are removed with it.
*/
void DiagramToolbarSettings::setCustomToolbars(const QList<Toolbar> &toolbars)
{
QStringList names, titles;
for (const Toolbar &toolbar : toolbars) {
names << toolbar.name;
titles << toolbar.title;
}
for (const Toolbar &old : customToolbars()) {
if (!names.contains(old.name)) {
removeIds(old.name);
}
}
QSettings settings;
if (names.isEmpty()) {
settings.remove(CUSTOM_NAMES);
settings.remove(CUSTOM_TITLES);
} else {
settings.setValue(CUSTOM_NAMES, names);
settings.setValue(CUSTOM_TITLES, titles);
}
}
/**
@return an object name for a new toolbar that none of @a existing has
and none had before (stored positions stay with the toolbar they
belonged to)
*/
QString DiagramToolbarSettings::newCustomName(const QList<Toolbar> &existing)
{
QStringList taken;
for (const Toolbar &toolbar : existing) taken << toolbar.name;
for (const Toolbar &toolbar : customToolbars()) taken << toolbar.name;
for (int i = 1 ; ; ++i) {
const QString name = QStringLiteral("custom_%1").arg(i);
if (!taken.contains(name)) return name;
}
}
/**
@return the ids toolbar @a name holds: the user's list if they saved
one, the defaults otherwise. A saved empty list stays empty.
*/
QStringList DiagramToolbarSettings::ids(const QString &name)
{
QSettings settings;
const QString key = CONTENTS + name;
if (!settings.contains(key)) {
return defaultIds(name);
}
return settings.value(key).toStringList();
}
/**
@return what toolbar @a name holds for a new user: the toolbars as
they were built before they could be changed. A user's own toolbar
starts empty.
*/
QStringList DiagramToolbarSettings::defaultIds(const QString &name)
{
const QString sep = separatorId();
if (name == QLatin1String("toolbar")) {
return {QStringLiteral("diagrameditor.new_file"),
QStringLiteral("diagrameditor.open_file"),
QStringLiteral("diagrameditor.save_file"),
QStringLiteral("diagrameditor.save_file_as"),
QStringLiteral("diagrameditor.close_file"),
QStringLiteral("diagrameditor.print"),
QStringLiteral("diagrameditor.export_to_pdf"),
sep,
QStringLiteral("diagrameditor.undo"),
QStringLiteral("diagrameditor.redo"),
sep,
QStringLiteral("diagrameditor.cut"),
QStringLiteral("diagrameditor.copy"),
QStringLiteral("diagrameditor.paste"),
QStringLiteral("diagrameditor.duplicate"),
sep,
QStringLiteral("diagrameditor.delete_selection"),
QStringLiteral("diagrameditor.rotate_selection")};
}
if (name == QLatin1String("display")) {
return {QStringLiteral("diagrameditor.mode_selection"),
QStringLiteral("diagrameditor.mode_visualise"),
sep,
QStringLiteral("widget:handler_size"),
sep,
QStringLiteral("diagrameditor.draw_grid"),
QStringLiteral("widget:text_grid"),
QStringLiteral("diagrameditor.draw_guides"),
QStringLiteral("widget:background_color"),
sep,
QStringLiteral("diagrameditor.zoom_content"),
QStringLiteral("diagrameditor.zoom_fit"),
QStringLiteral("diagrameditor.zoom_reset")};
}
if (name == QLatin1String("diagram")) {
return {QStringLiteral("diagrameditor.edit_diagram_properties"),
QStringLiteral("diagrameditor.conductor_reset"),
QStringLiteral("diagrameditor.auto_conductor"),
QStringLiteral("diagrameditor.auto_break_conductor"),
//The cable tool is drawn across the conductors those two
//actions work on, so it sits with them rather than with
//the shapes of the "Ajouter" toolbar.
sep,
QStringLiteral("diagrameditor.add_cable"),
QStringLiteral("widget:conductor_color")};
}
if (name == QLatin1String("adding")) {
//add_pdf is skipped when built without QtPdf, as before
return {QStringLiteral("diagrameditor.add_text"),
QStringLiteral("diagrameditor.add_image"),
QStringLiteral("diagrameditor.add_pdf"),
QStringLiteral("diagrameditor.add_line"),
QStringLiteral("diagrameditor.add_rectangle"),
QStringLiteral("diagrameditor.add_ellipse"),
QStringLiteral("diagrameditor.add_arc"),
QStringLiteral("diagrameditor.add_polyline"),
QStringLiteral("diagrameditor.add_path"),
QStringLiteral("diagrameditor.add_fillet"),
QStringLiteral("diagrameditor.add_terminal_strip")};
}
if (name == QLatin1String("diagram_depth_toolbar")) {
return {QStringLiteral("depth.forward"),
QStringLiteral("depth.raise"),
QStringLiteral("depth.lower"),
QStringLiteral("depth.backward")};
}
return {};
}
/**
@brief DiagramToolbarSettings::setIds
Save @a ids for toolbar @a name. Saving the defaults removes the key, so
a later change of defaults still reaches this user.
*/
void DiagramToolbarSettings::setIds(const QString &name, const QStringList &ids)
{
if (ids == defaultIds(name)) {
removeIds(name);
} else {
QSettings().setValue(CONTENTS + name, ids);
}
}
void DiagramToolbarSettings::removeIds(const QString &name)
{
QSettings().remove(CONTENTS + name);
}
QString DiagramToolbarSettings::separatorId()
{
return QStringLiteral("separator");
}
/**
@return the toolbar buttons that are widgets, not commands. Each can be
on one toolbar at a time.
*/
QStringList DiagramToolbarSettings::widgetIds()
{
return {QStringLiteral("widget:handler_size"),
QStringLiteral("widget:text_grid"),
QStringLiteral("widget:background_color"),
QStringLiteral("widget:conductor_color")};
}
bool DiagramToolbarSettings::isWidget(const QString &id)
{
return id.startsWith(QLatin1String("widget:"));
}
QString DiagramToolbarSettings::widgetTitle(const QString &id)
{
if (id == QLatin1String("widget:handler_size"))
return QCoreApplication::translate("DiagramToolbarSettings", "Taille des poignées");
if (id == QLatin1String("widget:text_grid"))
return QCoreApplication::translate("DiagramToolbarSettings", "Grille des textes");
if (id == QLatin1String("widget:background_color"))
return QCoreApplication::translate("DiagramToolbarSettings", "Couleur de fond du folio");
if (id == QLatin1String("widget:conductor_color"))
return QCoreApplication::translate("DiagramToolbarSettings", "Couleur des conducteurs");
return id;
}
/**
@return every command that can go on a toolbar: the diagram editor's,
its stored scripts', and the depth commands it shares with the element
editor
*/
QStringList DiagramToolbarSettings::availableCommandIds()
{
QStringList ids;
for (const ShortcutManager::ShortcutInfo &info :
ShortcutManager::instance().allShortcuts())
{
if (info.id.startsWith(QLatin1String("diagrameditor."))
|| info.id.startsWith(QLatin1String("depth."))) {
ids << info.id;
}
}
return ids;
}
/**
@brief DiagramToolbarSettings::fill
Empty @a toolbar and put the commands of @a ids on it, in order.
@a resolve gives the window's action for an id, a widget id included;
an id it has no action for is skipped. A separator never starts or
ends the toolbar and never follows another: those are left out, as
Qt would otherwise draw them.
*/
void DiagramToolbarSettings::fill(QToolBar *toolbar, const QStringList &ids,
const std::function<QAction *(const QString &)> &resolve)
{
//Separators belong to the toolbar that made them; commands do not
const QList<QAction *> old_actions = toolbar->actions();
toolbar->clear();
for (QAction *action : old_actions) {
if (action->isSeparator() && action->parent() == toolbar) delete action;
}
bool pending_separator = false;
for (const QString &id : ids)
{
if (id == separatorId()) {
pending_separator = !toolbar->actions().isEmpty();
continue;
}
QAction *action = resolve(id);
if (!action) {
continue;
}
if (pending_separator) {
toolbar->addSeparator();
pending_separator = false;
}
toolbar->addAction(action);
}
}
/**
@brief DiagramToolbarSettings::markWindow
Mark @a window as one whose toolbars applyToAll() rebuilds. It must
have a rebuildToolBars() slot or invokable.
*/
void DiagramToolbarSettings::markWindow(QObject *window)
{
window->setProperty(MARKED, true);
}
/**
@brief DiagramToolbarSettings::applyToAll
Rebuild the toolbars of every open window marked by markWindow(),
after the settings change.
*/
void DiagramToolbarSettings::applyToAll()
{
for (QWidget *widget : QApplication::topLevelWidgets()) {
if (widget->property(MARKED).toBool()) {
QMetaObject::invokeMethod(widget, "rebuildToolBars");
}
}
}
+79
View File
@@ -0,0 +1,79 @@
/*
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 DIAGRAMTOOLBARSETTINGS_H
#define DIAGRAMTOOLBARSETTINGS_H
#include <QList>
#include <QString>
#include <QStringList>
#include <functional>
class QAction;
class QToolBar;
/**
@brief What each of the diagram editor's toolbars holds.
A toolbar is a list of ids, in order: ShortcutManager command ids, the
separator id, and the ids of the few toolbar buttons that are widgets
rather than commands (the colour buttons, the text grid, the handle
size). Stored in QSettings, one key per toolbar, by the toolbar's
object name; a toolbar the user never changed uses the defaults below,
which are the toolbars as they were before they could be changed.
The user can also add toolbars of their own. They start empty.
An id no command carries any more is skipped, so a list saved by
another version still opens.
*/
class DiagramToolbarSettings
{
public:
struct Toolbar {
QString name; ///< object name, used for storage and by QMainWindow::saveState()
QString title; ///< shown to the user
bool custom = false;
};
static QList<Toolbar> toolbars();
static QStringList builtInNames();
static QString builtInTitle(const QString &name);
static QList<Toolbar> customToolbars();
static void setCustomToolbars(const QList<Toolbar> &toolbars);
static QString newCustomName(const QList<Toolbar> &existing);
static QStringList ids(const QString &name);
static QStringList defaultIds(const QString &name);
static void setIds(const QString &name, const QStringList &ids);
static void removeIds(const QString &name);
static QString separatorId();
static QStringList widgetIds();
static bool isWidget(const QString &id);
static QString widgetTitle(const QString &id);
static QStringList availableCommandIds();
static void fill(QToolBar *toolbar, const QStringList &ids,
const std::function<QAction *(const QString &)> &resolve);
static void markWindow(QObject *window);
static void applyToAll();
};
#endif // DIAGRAMTOOLBARSETTINGS_H
+56 -27
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagramview.h"
#include "autoNum/ui/pastenumberingimport.h"
#include "cellruler.h"
#include "lastusedstyle.h"
#include "utils/colordialogdoubleclick.h"
@@ -43,8 +44,10 @@
#include "utils/conductorcreator.h"
#include "undocommand/addgraphicsobjectcommand.h"
#include "diagram.h"
#include "foliogrid.h"
#include "diagramcontexttoolbar.h"
#include "diagramgestureoverlay.h"
#include "gesturesettings.h"
#include "shortcutbarsettings.h"
#include "shortcutmanager.h"
#include "ElementsCollection/xmlelementcollection.h"
@@ -203,6 +206,9 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
#ifdef Q_OS_MACOS
color_dialog -> setWindowFlags(Qt::Sheet);
#endif
// Qt's own dialog on every platform: the GTK one keeps its own
// recent-colours row and ignores the custom colours QET saves
color_dialog->setOption(QColorDialog::DontUseNativeDialog);
color_dialog->setCurrentColor(initial_properties.color);
ColorDialogDoubleClick::install(color_dialog);
@@ -506,8 +512,9 @@ void DiagramView::zoomFit()
*/
void DiagramView::zoomContent()
{
fitInView(m_diagram -> itemsBoundingRect(), Qt::KeepAspectRatio);
adjustGridToZoom();
const QRectF content = m_diagram->visibleItemsBoundingRect();
if (!content.isNull())
zoomToRect(content);
}
/**
@@ -638,16 +645,13 @@ void DiagramView::paste(const QPointF &pos, QClipboard::Mode clipboard_mode) {
//One single step for the whole paste, cable lines included:
//undoing has to take back everything which came out of the
//clipboard in one go, and redoing has to put it all back.
QUndoCommand *command = nullptr;
if (content_pasted.count()) {
command = new PasteDiagramCommand(m_diagram, content_pasted);
}
QList<QUndoCommand *> cable_commands;
for (const CableCopy::Wired &line : std::as_const(wired)) {
if (!line.part) continue;
auto *added = new AddCableCommand(line.part->cable(), line.part.data(),
m_diagram, true, line.taken);
added->setText(QCoreApplication::translate("CableCopy", "Coller un câble"));
command = batchWith(command, added);
cable_commands << added;
}
//The copies end up selected, like everything else a paste
//just added, so the next Ctrl+V or Ctrl+D carries on from them
@@ -655,9 +659,12 @@ void DiagramView::paste(const QPointF &pos, QClipboard::Mode clipboard_mode) {
if (line.part) line.part->setSelected(true);
}
if (command) {
m_diagram -> undoStack().push(command);
}
//Asks whether to import the numberings the copy brings, if the
//project has not got them; the paste itself and the cable lines
//travel inside the same step of the undo stack.
PasteNumberingImport::push(this, m_diagram, content_pasted,
PasteNumberingImport::copiedBy(document_xml),
cable_commands);
//The stack has just renewed the identity of a pasted
//conductor: the cores are pointed at the wires again and
//the cable fields written out once more, so the entries
@@ -726,10 +733,8 @@ void DiagramView::duplicate(const QPoint &stepOffset)
if (selection.isEmpty()) return;
QSettings settings;
const int x_grid = settings.value(QStringLiteral("diagrameditor/Xgrid"),
Diagram::xGrid).toInt();
const int y_grid = settings.value(QStringLiteral("diagrameditor/Ygrid"),
Diagram::yGrid).toInt();
const int x_grid = FolioGrid::step(settings.value(FolioGrid::x_key), Diagram::xGrid);
const int y_grid = FolioGrid::step(settings.value(FolioGrid::y_key), Diagram::yGrid);
const QPointF offset(stepOffset.x() * x_grid, stepOffset.y() * y_grid);
// Mirrors copy(), but does not touch the system clipboard: Ctrl+D
@@ -938,7 +943,8 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
m_diagram->clearEventInterface();
m_swallow_native_menu = true;
}
m_gesture_overlay->showAt(m_gesture_origin, selectionCommands());
m_gesture_overlay->showAt(m_gesture_origin, gestureCommands(),
GestureSettings::directions());
}
if (m_gesture_overlay->isVisible()) {
m_gesture_overlay->setPointer(pos);
@@ -1125,6 +1131,28 @@ QList<QAction *> DiagramView::selectionCommands() const
return actions;
}
/**
@brief DiagramView::gestureCommands
@return the gesture ring's commands for the current selection, as this
window's actions. A list the user picked keeps its directions: a
command this window lacks leaves its direction empty (nullptr).
Otherwise the ring shows the shortcut bar's commands, in order.
*/
QList<QAction *> DiagramView::gestureCommands() const
{
QETDiagramEditor *qde = diagramEditor();
const auto context = ShortcutBarSettings::contextFor(m_diagram->selectedItems());
if (!qde || !GestureSettings::isCustom(context)) {
return selectionCommands();
}
QList<QAction *> actions;
for (const QString &id : GestureSettings::ids(context)) {
actions << (id.isEmpty() ? nullptr
: ShortcutManager::instance().action(id, qde));
}
return actions;
}
/**
@brief DiagramView::showContextToolbar
After a click that leaves something selected, show the shortcut bar's
@@ -1972,6 +2000,7 @@ QList<QAction *> DiagramView::contextMenuActions() const
list << qde->m_cut;
list << qde->m_copy;
list << m_multi_paste;
list << qde->m_paste_element_info;
list << m_separators.at(0);
list << m_create_template; // Add the create template action
//Disabled, and so left out below, unless a selected element
@@ -2173,8 +2202,8 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e)
@brief DiagramView::updateFolioReportMenu
Fill the "Renvoi de folio" submenu of the context menu with the folio
report elements this project already uses -- the ones in its embedded
collection -- or, when it has none yet, the coming and going arrows of
the common collection. An entry places its element where the context
collection -- and the coming and going arrows of the common collection,
each name listed once. An entry places its element where the context
menu was opened. Left empty, and so hidden, on a read-only diagram.
*/
void DiagramView::updateFolioReportMenu()
@@ -2188,9 +2217,9 @@ void DiagramView::updateFolioReportMenu()
QETProject *project = m_diagram->project();
XmlElementCollection *collection =
project ? project->embeddedElementCollection() : nullptr;
QSet<QString> names;
if (collection)
{
QSet<QString> names;
const QDomNodeList definitions =
collection->root().elementsByTagName(QStringLiteral("definition"));
for (int i = 0 ; i < definitions.count() ; ++i)
@@ -2213,16 +2242,16 @@ void DiagramView::updateFolioReportMenu()
}
}
if (locations.isEmpty())
//Always offer both standard arrows: placing one copies it into the
//embedded collection, and must not hide the other one.
for (const auto path : {
"common://10_electric/10_allpole/100_folio_referencing/01coming_arrow.elmt",
"common://10_electric/10_allpole/100_folio_referencing/02going_arrow.elmt"})
{
for (const auto path : {
"common://10_electric/10_allpole/100_folio_referencing/01coming_arrow.elmt",
"common://10_electric/10_allpole/100_folio_referencing/02going_arrow.elmt"})
{
const ElementsLocation location(QString::fromLatin1(path));
if (location.exist()) {
locations << location;
}
const ElementsLocation location(QString::fromLatin1(path));
if (location.exist() && !names.contains(location.name())) {
names.insert(location.name());
locations << location;
}
}
+1
View File
@@ -168,6 +168,7 @@ class DiagramView : public PaletteGraphicsView
void placeCellRulers();
void showContextToolbar(const QPoint &viewport_pos);
QList<QAction *> selectionCommands() const;
QList<QAction *> gestureCommands() const;
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents wheel-zoom from driving the transform to overflow, which
+443 -142
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "dxfexport.h"
#include "shownkinds.h"
#include "conductorsegment.h"
#include "createdxf.h"
@@ -34,12 +35,239 @@
#include "qetgraphicsitem/independenttextitem.h"
#include "qetgraphicsitem/qetshapeitem.h"
#include "qetgraphicsitem/terminal.h"
#include "qetinformation.h"
#include "textlines.h"
#include <QFontMetricsF>
#include <QGraphicsSimpleTextItem>
#include <QSet>
#include <cmath>
#include <utility>
namespace {
/**
Draw the drawing of @a elmt's definition: its static texts, lines,
rectangles, circles, polygons and arcs, and its terminals when
@a draw_terminals, as if it were placed at @a elem_pos_x,
@a elem_pos_y and turned @a rotation_angle degrees, with @a elmt's
mirrors and its texts kept readable (Element::symbolTextsTransform())
when @a as_placed. Used for every symbol drawn in full, and once per
block for a block's content.
*/
void drawSymbol(const QString &file_path, Element *elmt,
qreal elem_pos_x, qreal elem_pos_y,
double rotation_angle, bool draw_terminals,
bool as_placed = false)
{
using namespace DxfExport;
//The mirrors of an element are about its own axes, before it is
//rotated, see Element::setMirror()
const bool mirror_h = as_placed && elmt -> hasHorizontalMirror();
const bool mirror_v = as_placed && elmt -> hasVerticalMirror();
const QTransform texts_transform = as_placed
? elmt -> symbolTextsTransform()
: QTransform();
const qreal text_turn = as_placed && elmt -> hasUprightSymbolTexts()
? 0
: rotation_angle;
const qreal mirror_x = mirror_h ? -1 : 1;
const qreal mirror_y = mirror_v ? -1 : 1;
ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location());
Createdxf::layer = Layer::SymbolTexts;
for(QGraphicsSimpleTextItem *text : primitives.m_texts)
{
qreal fontSize = text->font().pointSizeF();
if (fontSize < 0)
fontSize = text->font().pixelSize();
QPointF text_pos = text->pos();
//A text the element mirrors, or turns in a project that keeps
//symbol texts horizontal, keeps reading as in the symbol:
//only its box moves, as ElementPictureFactory draws it on the
//folio. It is then drawn as in a symbol neither mirrored nor
//turned, moved to where the element puts the box's centre.
if (!texts_transform.isIdentity())
{
QTransform box_transform;
box_transform.translate(text->pos().x(), text->pos().y());
box_transform.rotate(text->rotation());
const QFontMetricsF metrics(text->font());
const QRectF box(0, -metrics.ascent(),
text->boundingRect().width(),
text->boundingRect().height());
const QPointF centre = box_transform.mapRect(box).center();
text_pos += texts_transform.map(centre) - centre;
}
qreal x = elem_pos_x + text_pos.x();
qreal y = elem_pos_y + text_pos.y();
qreal angle = text -> rotation() + text_turn;
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
QPointF transformed_point = DxfExport::rotation_transformed(x, y, elem_pos_x, elem_pos_y, -text_turn);
x = transformed_point.x() - ydir * fontSize * 0.5;
y = transformed_point.y() - xdir * fontSize * 0.5;
QStringList lines = text->text().split('\n');
qreal offset = fontSize * 1.6;
for (QString line : lines)
{
if (line.size() > 0 && line != "_" ) {
Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360 - angle, 0, 0.72);
}
x += offset * xdir;
y -= offset * ydir;
}
}
Createdxf::layer = Layer::Symbols;
for (QLineF line : primitives.m_lines)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QLineF l = t.map(line);
Createdxf::drawLine(file_path, l, 0);
}
for (QRectF rect : primitives.m_rectangles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QRectF r = t.mapRect(rect);
Createdxf::drawRectangle(file_path,r,0);
}
for (QRectF circle_rect : primitives.m_circles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QPointF c = t.map(QPointF(circle_rect.center().x(),circle_rect.center().y()));
Createdxf::drawCircle(file_path,c,circle_rect.width()/2,0);
}
for (QVector<QPointF> polygon : primitives.m_polygons)
{
if (polygon.size() == 0)
continue;
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
QPolygonF poly = t.map(polygon);
if(poly.isClosed())
Createdxf::drawPolygon(file_path,poly,0);
else
Createdxf::drawPolyline(file_path,poly,0);
}
// Draw arcs and ellipses
for (QVector<qreal> arc : primitives.m_arcs)
{
if (arc.size() == 0)
continue;
qreal x = (elem_pos_x + arc.at(0));
qreal y = (elem_pos_y + arc.at(1));
qreal w = arc.at(2);
qreal h = arc.at(3);
qreal startAngle = arc.at(4);
qreal spanAngle = arc .at(5);
//In a mirror the arc starts where it ended: an angle a
//becomes 180 - a in a horizontal one, -a in a vertical one
if (mirror_h)
{
x = elem_pos_x - arc.at(0) - w;
startAngle = 180 - startAngle - spanAngle;
}
if (mirror_v)
{
y = elem_pos_y - arc.at(1) - h;
startAngle = -startAngle - spanAngle;
}
QRectF r(x,y,w,h);
QPointF hotspot(elem_pos_x,elem_pos_y);
Createdxf::drawArcEllipse(file_path, r, startAngle, spanAngle, hotspot, rotation_angle, 0);
}
if (draw_terminals) {
Createdxf::layer = Layer::Terminals;
// Draw terminals
QList<Terminal *> list_terminals = elmt->terminals();
QColor col("red");
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle).scale(mirror_x, mirror_y);
foreach(Terminal *tp, list_terminals) {
QPointF c = t.map(QPointF(tp->dock_elmt_.x(),tp->dock_elmt_.y()));
Createdxf::drawCircle(file_path,c,3.0,Createdxf::dxfColor(col));
}
}
}
/// One line of a text as the export writes it: where it starts, in
/// scene coordinates, its size, and its angle (degrees, clockwise)
struct TextLine
{
QString text;
QPointF pos;
qreal font_size;
qreal angle;
};
/// The lines of @a dti that are written, one under the other as drawn
QList<TextLine> textLines(DiagramTextItem *dti)
{
qreal fontSize = dti -> font().pointSizeF();
if (fontSize < 0)
fontSize = dti -> font().pixelSize();
qreal angle = dti -> rotation();
QGraphicsItem *parent = dti->parentItem();
while (parent) {
angle += parent->rotation();
parent = parent->parentItem();
}
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
qreal x = (dti->scenePos().x())
+ xdir * fontSize * 1.8
- ydir * fontSize;
qreal y = dti->scenePos().y()
- ydir * fontSize * 1.8
- xdir * fontSize * 0.9;
//As drawn: a text with a width is wrapped
QStringList lines = TextLines::layoutLines(dti -> document());
qreal offset = fontSize * 1.6;
QList<TextLine> result;
foreach (QString line, lines) {
if (line.size() > 0 && line != "_" )
result << TextLine{line, QPointF(x, y), fontSize, angle};
x += offset * xdir;
y -= offset * ydir;
}
return result;
}
/// @a tag, or @a tag with _2, _3... if @a used has it, at most 31
/// characters
QString uniqueTag(const QString &tag, const QSet<QString> &used)
{
QString unique = tag;
for (int i = 2 ; used.contains(unique) ; ++i) {
const QString suffix = QStringLiteral("_%1").arg(i);
unique = tag.left(31 - suffix.size()) + suffix;
}
return unique;
}
/// Nothing would be drawn for @a elmt (its definition is missing)
bool drawsNothing(Element *elmt, bool draw_terminals)
{
const ElementPictureFactory::primitives p =
ElementPictureFactory::instance()->getPrimitives(elmt->location());
return p.m_lines.isEmpty() && p.m_rectangles.isEmpty()
&& p.m_circles.isEmpty() && p.m_polygons.isEmpty()
&& p.m_arcs.isEmpty() && p.m_texts.isEmpty()
&& (!draw_terminals || elmt->terminals().isEmpty());
}
}
/**
@brief DxfExport::folioSize
@return the size of @a diagram as exported with @a properties: its border
@@ -78,21 +306,13 @@ void DxfExport::write(Diagram *diagram, int width, int height,
width -= 2*Diagram::margin;
height -= 2*Diagram::margin;
Createdxf::xScale = Createdxf::sheetWidth / double(width);
Createdxf::yScale = Createdxf::sheetHeight / double(height);
Createdxf::dxfBegin(file_path, Layer::all());
//Each kind of content on its own layer (discussion #1071). The
//border's title block switches to Layer::TitleBlock itself, see
//BorderTitleBlock::drawDxf().
Createdxf::layer = Layer::Border;
//Add project elements (lines, rectangles, circles, texts) to dxf file
if (properties.draw_border) {
QRectF rect(Diagram::margin,Diagram::margin,width,height);
Createdxf::drawRectangle(file_path,rect,0);
}
diagram -> border_and_titleblock.drawDxf(file_path, 0);
//One scale for both axes, the largest that fits the sheet: two
//scales stretched the drawing and turned every circle into an oval
//(issue #1339).
const double scale = qMin(Createdxf::sheetWidth / double(width),
Createdxf::sheetHeight / double(height));
Createdxf::xScale = scale;
Createdxf::yScale = scale;
// Build the lists of elements.
QList<Element *> list_elements;
@@ -135,6 +355,10 @@ void DxfExport::write(Diagram *diagram, int width, int height,
}
}
for (QGraphicsItem *qgi : std::as_const(stacked_items)) {
//Left out like on screen and in print (View > Show, #301)
if (ShownKinds::isHidden(qgi)) {
continue;
}
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
list_elements << elmt;
} else if (Conductor *f = qgraphicsitem_cast<Conductor *>(qgi)) {
@@ -147,7 +371,8 @@ void DxfExport::write(Diagram *diagram, int width, int height,
list_shapes << dii;
} else if (DynamicElementTextItem *deti = qgraphicsitem_cast<DynamicElementTextItem *>(qgi)) {
list_texts << deti;
if (QGraphicsTextItem *xref = deti->slaveXrefItem()) {
QGraphicsTextItem *xref = deti->slaveXrefItem();
if (xref && !ShownKinds::isHidden(xref)) {
list_xref_texts << xref;
}
} else if (QetGraphicsTableItem *gti = qgraphicsitem_cast<QetGraphicsTableItem *>(qgi)) {
@@ -157,6 +382,186 @@ void DxfExport::write(Diagram *diagram, int width, int height,
}
}
//Symbols as blocks: one block per symbol definition the folio
//uses, named after its file and numbered in the order first met,
//so a repeat export is identical. A block has to be written before
//any entity, so they are all collected first.
QHash<QString, QString> block_names; // location -> block name
QList<Element *> block_models; // one placed symbol per block
if (properties.dxf_blocks) {
QSet<QString> used;
for (Element *elmt : std::as_const(list_elements)) {
const QString key = elmt -> location().toString();
if (!elmt -> symbolTextsTransform().isIdentity()
|| block_names.contains(key)
|| drawsNothing(elmt, properties.draw_terminals))
continue;
QString file_name = elmt -> location().fileName();
if (file_name.endsWith(QLatin1String(".elmt")))
file_name.chop(5);
const QString base = Createdxf::blockName(
QStringLiteral("QET_") + file_name);
QString name = base;
for (int i = 2 ; used.contains(name) ; ++i) {
const QString suffix = QStringLiteral("_%1").arg(i);
name = base.left(31 - suffix.size()) + suffix;
}
used << name;
block_names.insert(key, name);
block_models << elmt;
}
}
//A mirrored symbol, or a turned one whose texts stay horizontal,
//is drawn in full: an INSERT would mirror or turn its texts too,
//which QElectroTech keeps readable
const auto blockOf = [&block_names](Element *elmt) {
return !elmt -> symbolTextsTransform().isIdentity()
? QString()
: block_names.value(elmt -> location().toString());
};
//With dxf_attributes, a symbol's own texts (its label, its
//function...) become attributes of its INSERT, written where the
//text would be, so a CAD program moves them with the symbol and
//lists them. Off by default: LibreCAD does not show attributes,
//so the labels would vanish there. Tagged
//with the information they show (LABEL, FUNCTION...), or TEXT1,
//TEXT2... for a typed text; a second line gets _2. The block
//defines each tag once, where the first symbol has it.
QHash<Element *, QList<Createdxf::Attribute>> attributes;
QHash<QString, QList<Createdxf::Attribute>> attribute_definitions; // block -> ATTDEFs
QSet<DiagramTextItem *> attribute_texts;
{
QHash<Element *, QSet<QString>> used_tags;
QHash<Element *, int> typed_texts;
QHash<QString, QSet<QString>> defined_tags;
for (DiagramTextItem *dti : std::as_const(list_texts)) {
if (!properties.dxf_attributes)
break;
auto *deti = qgraphicsitem_cast<DynamicElementTextItem *>(dti);
Element *elmt = deti ? deti -> parentElement() : nullptr;
const QString block = elmt ? blockOf(elmt) : QString();
if (block.isEmpty())
continue;
const QString base =
deti -> textFrom() == DynamicElementTextItem::ElementInfo
&& !deti -> infoName().isEmpty()
? Createdxf::blockName(deti -> infoName())
: QStringLiteral("TEXT%1").arg(++typed_texts[elmt]);
//From the folio into the block: undo the INSERT
const QPointF insert(elmt -> pos().x() * Createdxf::xScale,
Createdxf::sheetHeight - elmt -> pos().y() * Createdxf::yScale);
const double turn = std::fmod(360 - elmt -> orientation() * 90, 360);
const QTransform to_block = QTransform()
.translate(0, Createdxf::sheetHeight)
.rotate(-turn)
.translate(-insert.x(), -insert.y());
const QList<TextLine> lines = textLines(dti);
for (int i = 0 ; i < lines.size() ; ++i) {
const TextLine &line = lines.at(i);
const QString tag = uniqueTag(
i == 0 ? base : base + QStringLiteral("_%1").arg(i + 1),
used_tags.value(elmt));
used_tags[elmt] << tag;
Createdxf::Attribute attribute;
attribute.tag = tag;
attribute.text = line.text;
attribute.x = line.pos.x() * Createdxf::xScale;
attribute.y = Createdxf::sheetHeight - line.pos.y() * Createdxf::yScale;
attribute.height = line.font_size * Createdxf::yScale;
attribute.rotation = 360 - line.angle;
attribute.xScaleW = 0.72;
attribute.colour = Createdxf::dxfColor(dti -> color());
attribute.layer = Layer::SymbolTexts;
attributes[elmt] << attribute;
if (!defined_tags[block].contains(tag)) {
defined_tags[block] << tag;
Createdxf::Attribute definition = attribute;
const QPointF in_block = to_block.map(QPointF(attribute.x, attribute.y));
definition.x = in_block.x();
definition.y = in_block.y();
definition.rotation = std::fmod(attribute.rotation - turn + 720, 360);
definition.text.clear();
attribute_definitions[block] << definition;
}
}
attribute_texts << dti;
}
//The rest of what is known about a symbol (manufacturer,
//reference, supplier, quantity...) as hidden attributes, so a
//CAD program can list the parts from the drawing. A field
//already written as a visible attribute (dxf_attributes) is not
//repeated; the label formula is how the label is made, not
//part data.
for (Element *elmt : std::as_const(list_elements)) {
const QString block = blockOf(elmt);
if (block.isEmpty())
continue;
const DiagramContext information = elmt -> elementInformations();
const QPointF insert(elmt -> pos().x() * Createdxf::xScale,
Createdxf::sheetHeight - elmt -> pos().y() * Createdxf::yScale);
for (const QString &key : QETInformation::elementInfoKeys()) {
if (key == QETInformation::ELMT_FORMULA)
continue;
const QString value = information.value(key).toString();
const QString tag = Createdxf::blockName(key);
if (value.isEmpty() || used_tags.value(elmt).contains(tag))
continue;
used_tags[elmt] << tag;
Createdxf::Attribute attribute;
attribute.tag = tag;
attribute.text = value;
attribute.x = insert.x();
attribute.y = insert.y();
attribute.height = 9 * Createdxf::yScale;
attribute.invisible = true;
attribute.layer = Layer::SymbolTexts;
attributes[elmt] << attribute;
if (!defined_tags[block].contains(tag)) {
defined_tags[block] << tag;
Createdxf::Attribute definition = attribute;
definition.x = 0;
definition.y = Createdxf::sheetHeight;
definition.text.clear();
attribute_definitions[block] << definition;
}
}
}
}
//A block is drawn as if its symbol sat unturned at the folio's
//origin, which is DXF (0, sheetHeight); that is its base point.
Createdxf::dxfBegin(file_path, Layer::all(), [&]() {
for (Element *model : std::as_const(block_models)) {
const QString block = block_names.value(model -> location().toString());
const QList<Createdxf::Attribute> definitions = attribute_definitions.value(block);
Createdxf::dxfBlockBegin(file_path, block, 0, Createdxf::sheetHeight,
!definitions.isEmpty());
drawSymbol(file_path, model, 0, 0, 0, properties.draw_terminals);
for (const Createdxf::Attribute &definition : definitions)
Createdxf::drawAttdef(file_path, definition);
Createdxf::dxfBlockEnd(file_path);
}
});
//Each kind of content on its own layer (discussion #1071). The
//border's title block switches to Layer::TitleBlock itself, see
//BorderTitleBlock::drawDxf().
Createdxf::layer = Layer::Border;
//Add project elements (lines, rectangles, circles, texts) to dxf file
if (properties.draw_border) {
QRectF rect(Diagram::margin,Diagram::margin,width,height);
Createdxf::drawRectangle(file_path,rect,0);
}
diagram -> border_and_titleblock.drawDxf(file_path, 0);
// Draw shapes
Createdxf::layer = Layer::Shapes;
foreach (QetShapeItem *qsi, list_shapes) qsi->toDXF(file_path, qsi->pen());
@@ -167,105 +572,25 @@ void DxfExport::write(Diagram *diagram, int width, int height,
gti->toDXF(file_path);
}
//Draw elements
//Draw elements: each one in full, or as an INSERT of its block
foreach(Element *elmt, list_elements)
{
double rotation_angle = elmt -> orientation() * 90;
const double rotation_angle = elmt -> orientation() * 90;
const qreal elem_pos_x = elmt -> pos().x();
const qreal elem_pos_y = elmt -> pos().y();
qreal elem_pos_x = elmt -> pos().x();
qreal elem_pos_y = elmt -> pos().y();// - (diagram -> margin / 2);
ElementPictureFactory::primitives primitives = ElementPictureFactory::instance()->getPrimitives(elmt->location());
Createdxf::layer = Layer::SymbolTexts;
for(QGraphicsSimpleTextItem *text : primitives.m_texts)
{
qreal fontSize = text->font().pointSizeF();
if (fontSize < 0)
fontSize = text->font().pixelSize();
qreal x = elem_pos_x + text->pos().x();
qreal y = elem_pos_y + text->pos().y();
qreal angle = text -> rotation() + rotation_angle;
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
QPointF transformed_point = rotation_transformed(x, y, elem_pos_x, elem_pos_y, -rotation_angle);
x = transformed_point.x() - ydir * fontSize * 0.5;
y = transformed_point.y() - xdir * fontSize * 0.5;
QStringList lines = text->text().split('\n');
qreal offset = fontSize * 1.6;
for (QString line : lines)
{
if (line.size() > 0 && line != "_" ) {
Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360 - angle, 0, 0.72);
}
x += offset * xdir;
y -= offset * ydir;
}
}
Createdxf::layer = Layer::Symbols;
for (QLineF line : primitives.m_lines)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QLineF l = t.map(line);
Createdxf::drawLine(file_path, l, 0);
}
for (QRectF rect : primitives.m_rectangles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QRectF r = t.mapRect(rect);
Createdxf::drawRectangle(file_path,r,0);
}
for (QRectF circle_rect : primitives.m_circles)
{
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QPointF c = t.map(QPointF(circle_rect.center().x(),circle_rect.center().y()));
Createdxf::drawCircle(file_path,c,circle_rect.width()/2,0);
}
for (QVector<QPointF> polygon : primitives.m_polygons)
{
if (polygon.size() == 0)
continue;
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
QPolygonF poly = t.map(polygon);
if(poly.isClosed())
Createdxf::drawPolygon(file_path,poly,0);
else
Createdxf::drawPolyline(file_path,poly,0);
}
// Draw arcs and ellipses
for (QVector<qreal> arc : primitives.m_arcs)
{
if (arc.size() == 0)
continue;
qreal x = (elem_pos_x + arc.at(0));
qreal y = (elem_pos_y + arc.at(1));
qreal w = arc.at(2);
qreal h = arc.at(3);
qreal startAngle = arc.at(4);
qreal spanAngle = arc .at(5);
QRectF r(x,y,w,h);
QPointF hotspot(elem_pos_x,elem_pos_y);
Createdxf::drawArcEllipse(file_path, r, startAngle, spanAngle, hotspot, rotation_angle, 0);
}
if (properties.draw_terminals) {
Createdxf::layer = Layer::Terminals;
// Draw terminals
QList<Terminal *> list_terminals = elmt->terminals();
QColor col("red");
QTransform t = QTransform().translate(elem_pos_x,elem_pos_y).rotate(rotation_angle);
foreach(Terminal *tp, list_terminals) {
QPointF c = t.map(QPointF(tp->dock_elmt_.x(),tp->dock_elmt_.y()));
Createdxf::drawCircle(file_path,c,3.0,Createdxf::dxfColor(col));
}
const QString block = blockOf(elmt);
if (block.isEmpty()) {
drawSymbol(file_path, elmt, elem_pos_x, elem_pos_y,
rotation_angle, properties.draw_terminals, true);
} else {
Createdxf::layer = Layer::Symbols;
//QElectroTech turns a symbol clockwise, DXF counter-clockwise
Createdxf::drawInsert(file_path, block,
elem_pos_x * Createdxf::xScale,
Createdxf::sheetHeight - elem_pos_y * Createdxf::yScale,
std::fmod(360 - rotation_angle, 360),
attributes.value(elmt));
}
}
@@ -290,7 +615,7 @@ void DxfExport::write(Diagram *diagram, int width, int height,
Createdxf::layer = Layer::WireNumbers;
ConductorTextItem *textItem = cond -> textItem();
if (textItem) {
if (textItem && !ShownKinds::isHidden(textItem)) {
qreal fontSize = textItem -> font().pointSizeF();
if (fontSize < 0)
fontSize = textItem -> font().pixelSize();
@@ -328,38 +653,14 @@ void DxfExport::write(Diagram *diagram, int width, int height,
//Draw text items
foreach(DiagramTextItem *dti, list_texts) {
//Already written as an attribute of its symbol's INSERT
if (attribute_texts.contains(dti))
continue;
//A free text, or a symbol's own text (its label and the like)
Createdxf::layer = qgraphicsitem_cast<IndependentTextItem *>(dti)
? Layer::Texts : Layer::SymbolTexts;
qreal fontSize = dti -> font().pointSizeF();
if (fontSize < 0)
fontSize = dti -> font().pixelSize();
qreal angle = dti -> rotation();
QGraphicsItem *parent = dti->parentItem();
while (parent) {
angle += parent->rotation();
parent = parent->parentItem();
}
qreal angler = angle * M_PI/180;
int xdir = -sin(angler);
int ydir = -cos(angler);
qreal x = (dti->scenePos().x())
+ xdir * fontSize * 1.8
- ydir * fontSize;
qreal y = dti->scenePos().y()
- ydir * fontSize * 1.8
- xdir * fontSize * 0.9;
QStringList lines = dti -> toPlainText().split('\n');
qreal offset = fontSize * 1.6;
foreach (QString line, lines) {
if (line.size() > 0 && line != "_" )
Createdxf::drawText(file_path, line, QPointF(x, y), fontSize, 360-angle, Createdxf::dxfColor(dti->color()), 0.72 );
x += offset * xdir;
y -= offset * ydir;
}
for (const TextLine &line : textLines(dti))
Createdxf::drawText(file_path, line.text, line.pos, line.font_size, 360-line.angle, Createdxf::dxfColor(dti->color()), 0.72 );
}
//Draw the slave cross-reference labels
+3 -3
View File
@@ -172,7 +172,7 @@ void ArcEditor::updateArcS()
if (value != arc->property("startAngle"))
{
QPropertyUndoCommand *undo= new QPropertyUndoCommand(arc, "startAngle", arc->property("startAngle"), value);
undo->setText("Modifier l'angle de depart d'un arc");
undo->setText(tr("Modifier l'angle de depart d'un arc"));
undo->enableAnimation();
elementScene()->undoStack().push(undo);
}
@@ -197,7 +197,7 @@ void ArcEditor::updateArcA()
if (value != arc->property("spanAngle"))
{
QPropertyUndoCommand *undo= new QPropertyUndoCommand(arc, "spanAngle", arc->property("spanAngle"), value);
undo->setText("Modifier l'angle d'un arc");
undo->setText(tr("Modifier l'angle d'un arc"));
undo->enableAnimation();
elementScene()->undoStack().push(undo);
}
@@ -240,7 +240,7 @@ void ArcEditor::updateRect()
if (rect != part->property("rect"))
{
auto undo= new QPropertyUndoCommand(ellipse, "rect", ellipse->property("rect"), rect);
undo->setText("Modifier un arc");
undo->setText(tr("Modifier un arc"));
undo->enableAnimation();
elementScene()->undoStack().push(undo);
}
+95
View File
@@ -17,6 +17,7 @@
*/
#include "editorcommands.h"
#include "../diagram.h"
#include "symbolscale.h"
/**
@brief ElementEditionCommand::ElementEditionCommand
@@ -785,3 +786,97 @@ void FlipElementsCommand::undo()
{
redo();
}
/**
@brief ScaleElementCommand::ScaleElementCommand
@param scene : the element to scale
@param factor : scale factor, applied about the hotspot
@param scale_text : also scale font sizes
@param parent : parent undo command
*/
ScaleElementCommand::ScaleElementCommand(ElementScene *scene,
qreal factor,
bool scale_text,
QUndoCommand *parent) :
ElementEditionCommand(QObject::tr("mise à l'échelle de l'élément", "undo caption"),
scene, nullptr, parent),
m_factor(factor)
{
const auto scaledFont = [factor, scale_text](QFont font) {
if (scale_text) {
if (font.pointSizeF() > 0) {
font.setPointSize(SymbolScale::scaledFontSize(font.pointSizeF(), factor));
} else if (font.pixelSize() > 0) {
font.setPixelSize(SymbolScale::scaledFontSize(font.pixelSize(), factor));
}
}
return font;
};
const auto parts = scene->primitives();
for (CustomElementPart *part : parts)
{
QGraphicsItem *item = part->toItem();
//Font before position: changing the font can move a text
//to keep its alignment.
if (auto text = qgraphicsitem_cast<PartText *>(item)) {
addChange(text, "font", scaledFont(text->font()));
addChange(text, "pos", text->pos() * factor);
}
else if (auto field = qgraphicsitem_cast<PartDynamicTextField *>(item)) {
addChange(field, "font", scaledFont(field->font()));
if (field->textWidth() > 0) {
addChange(field, "textWidth", field->textWidth() * factor);
}
addChange(field, "pos", field->pos() * factor);
}
else if (auto terminal = qgraphicsitem_cast<PartTerminal *>(item)) {
addChange(terminal, "label_font", scaledFont(terminal->labelFont()));
addChange(terminal, "label_pos", terminal->labelPos() * factor);
addChange(terminal, "pos", terminal->pos() * factor);
}
else {
m_geometry_parts << part;
if (auto line = qgraphicsitem_cast<PartLine *>(item)) {
addChange(line, "length1", line->firstEndLength() * factor);
addChange(line, "length2", line->secondEndLength() * factor);
}
}
}
}
void ScaleElementCommand::addChange(QObject *object, const char *name, const QVariant &after)
{
m_changes << PropertyChange{object, name, object->property(name), after};
}
/**
@brief ScaleElementCommand::scaleGeometry
Scale the drawn parts about the hotspot, from \a from times their
original size to \a to times it.
*/
void ScaleElementCommand::scaleGeometry(qreal from, qreal to)
{
const QRectF before(0, 0, from, from);
const QRectF after(0, 0, to, to);
for (CustomElementPart *part : std::as_const(m_geometry_parts)) {
part->startUserTransformation(before);
part->handleUserTransformation(before, after);
}
}
void ScaleElementCommand::undo()
{
scaleGeometry(m_factor, 1);
for (const PropertyChange &change : std::as_const(m_changes)) {
change.object->setProperty(change.name.constData(), change.before);
}
}
void ScaleElementCommand::redo()
{
scaleGeometry(1, m_factor);
for (const PropertyChange &change : std::as_const(m_changes)) {
change.object->setProperty(change.name.constData(), change.after);
}
}
+33
View File
@@ -315,4 +315,37 @@ private:
qreal m_axis_y = 0;
};
/**
@brief The ScaleElementCommand class
Scales the whole element about its hotspot by a fixed factor, as one
undo step. Drawn parts are scaled the way the resize handles scale them;
texts, dynamic text fields and terminals are set explicitly, so their
font sizes, terminal name offsets and line end sizes follow too, and a
terminal lands exactly on its new position.
*/
class ScaleElementCommand : public ElementEditionCommand
{
public:
ScaleElementCommand(ElementScene *scene, qreal factor,
bool scale_text, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
struct PropertyChange {
QObject *object;
QByteArray name;
QVariant before;
QVariant after;
};
void addChange(QObject *object, const char *name, const QVariant &after);
void scaleGeometry(qreal from, qreal to);
qreal m_factor;
/// Parts scaled through handleUserTransformation()
QList<CustomElementPart *> m_geometry_parts;
/// Values set directly, in the order they are applied
QList<PropertyChange> m_changes;
};
#endif
+7 -1
View File
@@ -206,7 +206,13 @@ void ElementPrimitiveDecorator::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
// When moving the selection, consider the position of the first selected item
QPointF current_position = scene_pos - mouse_offset_;
QPointF rounded_current_position = snapConstPointToGrid(current_position);
// Snap to the editor's grid, as a single part does (bugtracker
// #112); Ctrl keeps the fine 1-unit step.
ElementScene *element_scene = qobject_cast<ElementScene *>(scene());
QPointF rounded_current_position =
(element_scene && !(event->modifiers() & Qt::ControlModifier))
? element_scene->snapToGrid(current_position)
: snapConstPointToGrid(current_position);
movement = rounded_current_position - decorated_items_.at(0) -> toItem() -> scenePos();
QRectF bounding_rect = modified_bounding_rect_;
+41
View File
@@ -23,6 +23,7 @@
#include "../NameList/ui/namelistwidget.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "../QetGraphicsItemModeler/textresizehandles.h"
#include "editorcommands.h"
#include "elementcontent.h"
#include "elementprimitivedecorator.h"
@@ -1498,9 +1499,49 @@ void ElementScene::managePrimitivesGroups()
m_decorator -> setPos(0, 0);
m_decorator -> setItems(selected_items);
}
manageTextResizeHandles(selected_items);
m_decorator_lock -> unlock();
}
/**
@brief ElementScene::manageTextResizeHandles
Show the corner handles that change the width of a text field when it
is the only selected item.
Called again when the undo stack changes, including from the push of a
resize itself: the handles of the same text are then kept, not deleted
while they are still emitting.
@param selected_items
*/
void ElementScene::manageTextResizeHandles(const QList<QGraphicsItem *> &selected_items)
{
QGraphicsTextItem *text = nullptr;
if (selected_items.size() == 1 &&
selected_items.first()->type() == PartDynamicTextField::Type)
{
text = static_cast<PartDynamicTextField *>(selected_items.first());
}
if (m_text_resize_handles && m_text_resize_handles->parentItem() == text) {
m_text_resize_handles->updateHandlesPos();
return;
}
if (m_text_resize_handles) {
m_text_resize_handles->hide();
m_text_resize_handles->deleteLater();
m_text_resize_handles.clear();
}
if (text)
{
m_text_resize_handles = new TextResizeHandles(text);
connect(m_text_resize_handles, &TextResizeHandles::resizeFinished, this,
[this, text](qreal old_width, qreal new_width, QPointF old_pos, QPointF new_pos)
{
m_undo_stack.push(new TextResizeCommand(text, old_width, new_width, old_pos, new_pos));
});
}
}
/**
@brief ElementScene::stackAction
Push the provided \a command on the undo stack.
+3
View File
@@ -33,6 +33,7 @@ class QETElementEditor;
class ESEventInterface;
class QKeyEvent;
class CustomElementGraphicPart;
class TextResizeHandles;
/**
@brief The ElementScene class
This class is the canvas allowing the visual edition of an electrical element.
@@ -90,6 +91,7 @@ class ElementScene : public QGraphicsScene
m_y_grid;
QPointer<CustomElementGraphicPart> m_single_selected_item;
QPointer<TextResizeHandles> m_text_resize_handles;
bool m_hotspot_visible = true;
bool m_background_frame_visible = false;
@@ -180,6 +182,7 @@ class ElementScene : public QGraphicsScene
void slot_editAuthorInformations();
void slot_editProperties();
void managePrimitivesGroups();
void manageTextResizeHandles(const QList<QGraphicsItem *> &selected_items);
void stackAction(ElementEditionCommand *);
signals:
+11 -26
View File
@@ -17,6 +17,8 @@
*/
#include "elementview.h"
#include "elementviewgrid.h"
#include "../qetapp.h"
#include "UndoCommand/pastepartscommand.h"
#include "ui/qetelementeditor.h"
@@ -535,29 +537,12 @@ void ElementView::drawBackground(QPainter *p, const QRectF &r) {
// choisit la granularite de la grille en fonction du zoom en cours
// selects the grid granularity according to the current zoom level
int drawn_x_grid = 1;//scene_ -> xGrid();
int drawn_y_grid = 1;//scene_ -> yGrid();
bool draw_grid = true;
bool draw_cross = false;
const ElementViewGrid grid = ElementViewGrid::forZoom(zoom_factor);
const int grid_step = grid.step;
const bool draw_grid = grid.draw_grid;
const bool draw_cross = grid.draw_cross;
if (zoom_factor < 1.0) { //< no grid
draw_grid = false;
} else if (zoom_factor < 4.0) { //< grid 10*10
drawn_x_grid *= 10;
drawn_y_grid *= 10;
}else if (zoom_factor < 8.0) { //< grid 5*5
drawn_x_grid *= 5;
drawn_y_grid *= 5;
draw_cross = true;
} else if (zoom_factor < 10.0) { //< grid 2*2
drawn_x_grid *= 2;
drawn_y_grid *= 2;
draw_cross = true;
} else { //< grid 1*1
draw_cross = true;
}
m_scene->setGrid(drawn_x_grid, drawn_y_grid);
m_scene->setGrid(grid_step, grid_step);
if (draw_grid) {
// draw the dots of the grid
@@ -587,12 +572,12 @@ void ElementView::drawBackground(QPainter *p, const QRectF &r) {
qreal limit_y = r.y() + r.height();
int g_x = (int)ceil(r.x());
while (g_x % drawn_x_grid) ++ g_x;
while (g_x % grid_step) ++ g_x;
int g_y = (int)ceil(r.y());
while (g_y % drawn_y_grid) ++ g_y;
while (g_y % grid_step) ++ g_y;
for (int gx = g_x ; gx < limit_x ; gx += drawn_x_grid) {
for (int gy = g_y ; gy < limit_y ; gy += drawn_y_grid) {
for (int gx = g_x ; gx < limit_x ; gx += grid_step) {
for (int gy = g_y ; gy < limit_y ; gy += grid_step) {
if (draw_cross) {
if (!(gx % 10) && !(gy % 10)) {
p -> drawLine(QLineF(gx - (pen.width()/4.0), gy, gx + (pen.width()/4.0), gy));
+60
View File
@@ -0,0 +1,60 @@
/*
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 ELEMENTVIEWGRID_H
#define ELEMENTVIEWGRID_H
#include <QtGlobal>
/**
The symbol editor's grid at a given zoom: the step parts snap to, and
how the grid is drawn. The step gets finer as the view zooms in, so a
small detail can be placed precisely.
Below 100 % the grid is not drawn (its dots would merge into a grey
wash), but parts still snap every 10 units, as at 100 %. Snapping to 1
unit there left everything dragged while zoomed out off the grid, with
nothing on screen to show it (bugtracker #112).
*/
struct ElementViewGrid
{
int step = 10;
bool draw_grid = true;
bool draw_cross = false;
static ElementViewGrid forZoom(qreal zoom_factor)
{
ElementViewGrid grid;
if (zoom_factor < 1.0) {
grid.draw_grid = false;
} else if (zoom_factor < 4.0) {
grid.step = 10;
} else if (zoom_factor < 8.0) {
grid.step = 5;
grid.draw_cross = true;
} else if (zoom_factor < 10.0) {
grid.step = 2;
grid.draw_cross = true;
} else {
grid.step = 1;
grid.draw_cross = true;
}
return grid;
}
};
#endif // ELEMENTVIEWGRID_H
@@ -235,7 +235,10 @@ void PartDynamicTextField::fromXml(const QDomElement &dom_elmt) {
m_uuid = QUuid(dom_elmt.attribute("uuid", QUuid::createUuid().toString()));
setFrame(dom_elmt.attribute("frame", "false") == "true"? true : false);
//The saved position is the one to keep
m_block_alignment = true;
setTextWidth(dom_elmt.attribute("text_width", QString::number(-1)).toDouble());
m_block_alignment = false;
QMetaEnum me = DynamicElementTextItem::textFromMetaEnum();
m_text_from = DynamicElementTextItem::TextFrom(
@@ -431,13 +434,23 @@ bool PartDynamicTextField::frame() const
return m_frame;
}
/**
@brief PartDynamicTextField::setTextWidth
Set the width of this text (-1 = automatic width). The point of the text
chosen by its alignment stays in place, as when the text changes: a
right-aligned or centred field no longer grows to the right only, and
undoing a width change puts it back where it was.
@param width
*/
void PartDynamicTextField::setTextWidth(qreal width) {
prepareAlignment();
this -> document() -> setTextWidth(width);
//Adjust the width, to ideal width if needed
if(width > 0 && document() -> size().width() > width)
document() -> setTextWidth(document() -> idealWidth());
finishAlignment();
m_text_width = document() -> textWidth();
emit textWidthChanged(m_text_width);
}
+1 -1
View File
@@ -127,6 +127,6 @@ class PartLine : public CustomElementGraphicPart
int m_vector_index = -1;
QPropertyUndoCommand *m_undo_command;
QVector<QetGraphicsHandlerItem *> m_handler_vector;
qreal m_rot;
qreal m_rot = 0;
};
#endif
+1 -1
View File
@@ -668,7 +668,7 @@ void PartPlcTable::handlerMouseReleaseEvent(QetGraphicsHandlerItem *qghi, QGraph
Q_UNUSED(qghi)
Q_UNUSED(event)
QUndoCommand *undo = new QUndoCommand("Modifier une table PLC");
QUndoCommand *undo = new QUndoCommand(tr("Modifier une table PLC"));
if (m_old_rect != m_rect) {
QPropertyUndoCommand *u = new QPropertyUndoCommand(this, "rect", QVariant(m_old_rect.normalized()), QVariant(m_rect.normalized()), undo);
u->setAnimated(true, false);
+1 -1
View File
@@ -119,6 +119,6 @@ class PartPolygon : public CustomElementGraphicPart
QAction *m_insert_point,
*m_remove_point;
QPointF m_context_menu_pos;
qreal m_rot;
qreal m_rot = 0;
};
#endif
@@ -516,7 +516,7 @@ void PartRectangle::handlerMouseReleaseEvent(QetGraphicsHandlerItem *qghi, QGrap
m_modifie_radius_equaly = false;
QUndoCommand *undo = new QUndoCommand("Modifier un rectangle");
QUndoCommand *undo = new QUndoCommand(tr("Modifier un rectangle"));
if (m_old_rect != m_rect) {
QPropertyUndoCommand *u = new QPropertyUndoCommand(this, "rect", QVariant(m_old_rect.normalized()), QVariant(m_rect.normalized()), undo);
u->setAnimated(true, false);
+12 -2
View File
@@ -57,7 +57,17 @@ PartText::PartText(QETElementEditor *editor, QGraphicsItem *parent) :
connect(document(),
&QTextDocument::contentsChanged,
this,
[this]() { adjustItemPosition(); });
[this]() {
adjustItemPosition();
// During inline edition, keep the aligned point in place after
// every keystroke instead of only when the edition ends (#1252).
// setPlainText() and setFont() re-anchor on their own.
if (!previous_text.isNull()) {
applyLineAlignment();
finishAlignment();
prepareAlignment();
}
});
}
/// Destructeur
@@ -552,8 +562,8 @@ void PartText::endEdition()
QPropertyUndoCommand *undo = new QPropertyUndoCommand(this, "text", previous_text, new_text);
undo -> setText(tr("Modifier un champ texte"));
undoStack().push(undo);
previous_text = QString();
}
previous_text = QString();
}
// deselectionne le texte
+107
View File
@@ -0,0 +1,107 @@
/*
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 SYMBOLSCALE_H
#define SYMBOLSCALE_H
#include <QList>
#include <QPointF>
#include <QtMath>
/**
Which factors an element can be scaled by, in the element editor, without
taking its terminals off the grid of the folio.
A terminal's position is its wire end, relative to the hotspot, and the
hotspot of a placed element sits on a grid point. So the wires of an
element line up with the grid when every terminal coordinate is a
multiple of the grid size. The element is scaled about its hotspot, so a
factor is safe when every coordinate times the factor is still a
multiple of the grid size.
The same rule covers an element whose terminals are off the grid today:
the factors offered are the ones that bring them all on to it.
*/
namespace SymbolScale
{
/// The folio grid the collection is drawn to. Fixed rather than read
/// from the user's settings: elements are shared between users.
constexpr int grid = 10;
/// The factors offered, in the order shown.
inline QList<qreal> candidates()
{
return {0.5, 1.5, 2.0, 2.5, 3.0, 4.0};
}
/// True if \a value is a multiple of \a grid_size, ignoring the
/// rounding left by saving with two decimals.
inline bool onGrid(qreal value, int grid_size = grid)
{
const qreal steps = value / grid_size;
return qAbs(steps - qRound(steps)) * grid_size < 0.005;
}
/// True if every terminal in \a terminals is on the grid.
inline bool allOnGrid(const QList<QPointF> &terminals, int grid_size = grid)
{
for (const QPointF &p : terminals) {
if (!onGrid(p.x(), grid_size) || !onGrid(p.y(), grid_size)) {
return false;
}
}
return true;
}
/// How many of \a terminals are off the grid.
inline int offGridCount(const QList<QPointF> &terminals, int grid_size = grid)
{
int count = 0;
for (const QPointF &p : terminals) {
if (!onGrid(p.x(), grid_size) || !onGrid(p.y(), grid_size)) {
++count;
}
}
return count;
}
/// The candidate factors that leave every terminal of \a terminals on
/// the grid. An element with no terminal can take any of them.
inline QList<qreal> safeFactors(const QList<QPointF> &terminals, int grid_size = grid)
{
QList<qreal> safe;
for (const qreal factor : candidates()) {
QList<QPointF> scaled;
for (const QPointF &p : terminals) {
scaled << p * factor;
}
if (allOnGrid(scaled, grid_size)) {
safe << factor;
}
}
return safe;
}
/// Font size after scaling by \a factor: whole points, and never
/// below 4 pt, so a halved element keeps readable text.
inline int scaledFontSize(qreal size, qreal factor)
{
return qMax(4, qRound(size * factor));
}
}
#endif // SYMBOLSCALE_H
+6 -2
View File
@@ -157,7 +157,9 @@ void DynamicTextFieldEditor::updateForm()
m_color_kpb -> setColor(m_text_field.data() -> color());
}
#endif
ui -> m_width_sb -> setValue(m_text_field.data() -> textWidth());
//Rounded as on_m_width_sb_editingFinished() compares it;
//setValue(int) would truncate 61.7 to 61
ui -> m_width_sb -> setValue(qRound(m_text_field.data() -> textWidth()));
ui -> m_font_pb -> setText(m_text_field -> font().family());
switch (m_text_field.data() -> textFrom()) {
@@ -363,7 +365,9 @@ void DynamicTextFieldEditor::on_m_width_sb_editingFinished()
qreal width = (qreal)ui -> m_width_sb -> value();
for (int i = 0; i < m_parts.length(); i++) {
if(width != m_parts[i] -> textWidth()) {
//The box shows whole pixels: a width fitted to the text is not
//rounded just by leaving the box.
if(ui -> m_width_sb -> value() != qRound(m_parts[i] -> textWidth())) {
QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_parts[i], "textWidth", m_parts[i] -> textWidth(), width);
undo -> setText(tr("Modifier la largeur d'un texte"));
undoStack().push(undo);
+7 -1
View File
@@ -101,11 +101,17 @@
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="m_width_sb">
<property name="specialValueText">
<string>Auto</string>
</property>
<property name="suffix">
<string>px</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum">
<number>500</number>
<number>10000</number>
</property>
</widget>
</item>
@@ -48,42 +48,28 @@
#include <QMenu>
#include <QRegularExpressionValidator>
/**
@brief The EditorDelegate class
This delegate is only use for disable the edition of the first
column of the information tree widget
*/
class EditorDelegate : public QItemDelegate
QWidget* EditorDelegate::createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
public:
EditorDelegate(QObject *parent) :
QItemDelegate(parent)
{}
QWidget* createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override
if(index.column() == 1)
{
if(index.column() == 1)
{
const QString key = index.sibling(index.row(), 0)
.data(Qt::UserRole).toString();
const QString key = index.sibling(index.row(), 0)
.data(Qt::UserRole).toString();
if (key == QETInformation::ELMT_WIDTH || key == QETInformation::ELMT_HEIGHT || key == QETInformation::ELMT_DEPTH)
{
auto *line_edit = new QLineEdit(parent);
auto *validator = new QETInformation::NumericInfoValidator(line_edit);
line_edit->setValidator(validator);
line_edit->setPlaceholderText(tr("ex. 80.5"));
line_edit->setToolTip(tr("Nombre décimal avec un point comme séparateur (ex. 80.5)"));
return line_edit;
}
return QItemDelegate::createEditor(parent, option, index);
{
auto *line_edit = new QLineEdit(parent);
auto *validator = new QETInformation::NumericInfoValidator(line_edit);
line_edit->setValidator(validator);
line_edit->setPlaceholderText(tr("ex. 80.5"));
line_edit->setToolTip(tr("Nombre décimal avec un point comme séparateur (ex. 80.5)"));
return line_edit;
}
return nullptr;
return QItemDelegate::createEditor(parent, option, index);
}
};
return nullptr;
}
/**
* @brief ElementPropertiesEditorWidget::ElementPropertiesEditorWidget
@@ -23,6 +23,7 @@
#include <QAbstractButton>
#include <QDialog>
#include <QItemDelegate>
class QTableWidget;
class QSpinBox;
@@ -36,6 +37,24 @@ namespace Ui {
class ElementPropertiesEditorWidget;
}
/**
@brief The EditorDelegate class
This delegate is only use for disable the edition of the first
column of the information tree widget
*/
class EditorDelegate : public QItemDelegate
{
Q_OBJECT
public:
EditorDelegate(QObject *parent) :
QItemDelegate(parent)
{}
QWidget* createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
/**
@brief The ElementPropertiesEditorWidget class
This class provide a dialog for edit various property of element, like
+1 -1
View File
@@ -149,7 +149,7 @@ void EllipseEditor::updateRect()
if (rect != part->property("rect"))
{
auto undo= new QPropertyUndoCommand(ellipse, "rect", ellipse->property("rect"), rect);
undo->setText("Modifier une ellipse");
undo->setText(tr("Modifier une ellipse"));
undo->enableAnimation();
elementScene()->undoStack().push(undo);
}
+2 -4
View File
@@ -48,8 +48,7 @@
</layout>
<action name="m_add_point_action">
<property name="icon">
<iconset theme="list-add" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-add.png</normaloff>:/ico/16x16/list-add.png</iconset>
<iconset theme="list-add"/>
</property>
<property name="text">
<string>Ajouter un point</string>
@@ -57,8 +56,7 @@
</action>
<action name="m_remove_point_action">
<property name="icon">
<iconset theme="list-remove" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
<iconset theme="list-remove"/>
</property>
<property name="text">
<string>Supprimer ce point</string>
+36 -1
View File
@@ -54,6 +54,8 @@
#include "../../dxf/dxftoelmt.h"
#include "../../qet_elementscaler/qet_elementscaler.h"
#include "../UndoCommand/openelmtcommand.h"
#include "scaleelementdialog.h"
#include "../../toolbarsettings.h"
#include <QSettings>
#include <QActionGroup>
@@ -89,6 +91,7 @@ QETElementEditor::QETElementEditor(QWidget *parent) :
readSettings(); // restoreGeometry before show()
show();
readSettingsState(); // restoreState() must be called after show() in Qt6
ToolbarSettings::applyTo(this);
}
/**
@@ -765,6 +768,9 @@ void QETElementEditor::updateSelectionFromPartsList()
}
m_parts_list -> blockSignals(false);
m_elmt_scene -> blockSignals(false);
//selectionChanged was blocked above, so the selection decorator must be
//updated by hand, otherwise dragging moves only the part under the cursor
m_elmt_scene -> managePrimitivesGroups();
updateInformations();
updateAction();
}
@@ -1149,6 +1155,12 @@ void QETElementEditor::setupActions()
ShortcutManager::instance().registerAction(ui->m_mirror_action, "elementeditor.mirror", tr("Éditeur d'élément"), Qt::Key_M);
connect(ui->m_mirror_action, &QAction::triggered, [this]() {this -> elementScene() -> undoStack().push(new MirrorElementsCommand(this->elementScene()));});
//Scale the whole element by a factor that keeps its terminals on the grid
m_scale_element_action = new QAction(tr("Mettre l'élément à l'échelle..."), this);
ui->m_edit_menu->addAction(m_scale_element_action);
ShortcutManager::instance().registerAction(m_scale_element_action, "elementeditor.scale_element", tr("Éditeur d'élément"), QKeySequence());
connect(m_scale_element_action, &QAction::triggered, this, &QETElementEditor::scaleElement);
//Zoom action
ShortcutManager::instance().registerAction(ui->m_zoom_in_action, "elementeditor.zoom_in", tr("Éditeur d'élément"), QKeySequence::ZoomIn);
@@ -1242,7 +1254,8 @@ void QETElementEditor::updateAction()
//Action disabled if read only
auto ro_list = m_add_part_action_grp->actions();
ro_list << ui->m_paste_from_file_action
<< ui->m_paste_from_element_action;
<< ui->m_paste_from_element_action
<< m_scale_element_action;
for (auto action : std::as_const(ro_list)) {
action->setDisabled(m_read_only);
}
@@ -1798,6 +1811,28 @@ void QETElementEditor::on_m_import_dxf_triggered()
}
}
/**
@brief QETElementEditor::scaleElement
Ask for a factor that keeps the terminals on the grid,
then scale the whole element by it.
*/
void QETElementEditor::scaleElement()
{
QList<QPointF> terminals;
for (CustomElementPart *part : m_elmt_scene->primitives()) {
if (auto terminal = qgraphicsitem_cast<PartTerminal *>(part->toItem())) {
terminals << terminal->scenePos();
}
}
ScaleElementDialog dialog(terminals, this);
if (dialog.exec() != QDialog::Accepted || dialog.factor() == 1.0) {
return;
}
m_elmt_scene->undoStack().push(
new ScaleElementCommand(m_elmt_scene, dialog.factor(), dialog.scaleText()));
}
void QETElementEditor::on_m_import_scaled_element_triggered()
{
if (ElementScalerIsPresent(true, this))
+2
View File
@@ -112,6 +112,7 @@ class QETElementEditor : public QMainWindow
void on_m_about_qt_action_triggered();
void on_m_import_dxf_triggered();
void on_m_import_scaled_element_triggered();
void scaleElement();
private:
bool canClose();
@@ -142,6 +143,7 @@ class QETElementEditor : public QMainWindow
QList<QAction *> m_context_menu_action_list;
QAction
*m_scale_element_action = nullptr,
*m_undo_action = nullptr,
*m_redo_action = nullptr;
+31 -62
View File
@@ -14,8 +14,7 @@
<string>QElectroTech - Éditeur d'élément</string>
</property>
<property name="windowIcon">
<iconset theme="qet" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/qet.png</normaloff>:/ico/16x16/qet.png</iconset>
<iconset theme="qet"/>
</property>
<widget class="QWidget" name="centralwidget"/>
<widget class="QStatusBar" name="statusbar"/>
@@ -55,8 +54,7 @@
<string>Coller depuis...</string>
</property>
<property name="icon">
<iconset theme="edit-paste" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-paste.png</normaloff>:/ico/16x16/edit-paste.png</iconset>
<iconset theme="edit-paste"/>
</property>
<addaction name="m_paste_from_file_action"/>
<addaction name="m_paste_from_element_action"/>
@@ -202,8 +200,7 @@
</widget>
<action name="m_select_all_act">
<property name="icon">
<iconset theme="edit-select-all" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-select-all.png</normaloff>:/ico/16x16/edit-select-all.png</iconset>
<iconset theme="edit-select-all"/>
</property>
<property name="text">
<string>Tout sélectionner</string>
@@ -211,8 +208,7 @@
</action>
<action name="m_new_action">
<property name="icon">
<iconset theme="document-new" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/document-new.png</normaloff>:/ico/22x22/document-new.png</iconset>
<iconset theme="document-new"/>
</property>
<property name="text">
<string>&amp;Nouveau</string>
@@ -220,8 +216,7 @@
</action>
<action name="m_open_action">
<property name="icon">
<iconset theme="folder-open" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/folder-open.png</normaloff>:/ico/22x22/folder-open.png</iconset>
<iconset theme="folder-open"/>
</property>
<property name="text">
<string>&amp;Ouvrir</string>
@@ -229,8 +224,7 @@
</action>
<action name="m_open_from_file_action">
<property name="icon">
<iconset theme="folder-open" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/folder-open.png</normaloff>:/ico/22x22/folder-open.png</iconset>
<iconset theme="folder-open"/>
</property>
<property name="text">
<string>&amp;Ouvrir depuis un fichier</string>
@@ -238,8 +232,7 @@
</action>
<action name="m_save_action">
<property name="icon">
<iconset theme="document-save" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/document-save.png</normaloff>:/ico/22x22/document-save.png</iconset>
<iconset theme="document-save"/>
</property>
<property name="text">
<string>&amp;Enregistrer</string>
@@ -247,8 +240,7 @@
</action>
<action name="m_save_as_action">
<property name="icon">
<iconset theme="document-save-as" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/document-save-as.png</normaloff>:/ico/22x22/document-save-as.png</iconset>
<iconset theme="document-save-as"/>
</property>
<property name="text">
<string>Enregistrer sous</string>
@@ -256,8 +248,7 @@
</action>
<action name="m_save_as_file_action">
<property name="icon">
<iconset theme="document-save" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/document-save.png</normaloff>:/ico/22x22/document-save.png</iconset>
<iconset theme="document-save"/>
</property>
<property name="text">
<string>Enregistrer dans un fichier</string>
@@ -265,8 +256,7 @@
</action>
<action name="m_export_svg_action">
<property name="icon">
<iconset theme="document-export" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/document-export.png</normaloff>:/ico/22x22/document-export.png</iconset>
<iconset theme="document-export"/>
</property>
<property name="text">
<string>Exporter en SVG</string>
@@ -274,8 +264,7 @@
</action>
<action name="m_reload_action">
<property name="icon">
<iconset theme="view-refresh" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/view-refresh.png</normaloff>:/ico/22x22/view-refresh.png</iconset>
<iconset theme="view-refresh"/>
</property>
<property name="text">
<string>Recharger</string>
@@ -283,8 +272,7 @@
</action>
<action name="m_quit_action">
<property name="icon">
<iconset theme="application-exit" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/application-exit.png</normaloff>:/ico/16x16/application-exit.png</iconset>
<iconset theme="application-exit"/>
</property>
<property name="text">
<string>&amp;Fermer cet éditeur</string>
@@ -295,8 +283,7 @@
</action>
<action name="m_deselect_all_action">
<property name="icon">
<iconset theme="edit-select-none" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-select-none.png</normaloff>:/ico/16x16/edit-select-none.png</iconset>
<iconset theme="edit-select-none"/>
</property>
<property name="text">
<string>Désélectionner tout</string>
@@ -304,8 +291,7 @@
</action>
<action name="m_cut_action">
<property name="icon">
<iconset theme="edit-cut" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-cut.png</normaloff>:/ico/16x16/edit-cut.png</iconset>
<iconset theme="edit-cut"/>
</property>
<property name="text">
<string>Co&amp;uper</string>
@@ -313,8 +299,7 @@
</action>
<action name="m_copy_action">
<property name="icon">
<iconset theme="edit-copy" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-copy.png</normaloff>:/ico/16x16/edit-copy.png</iconset>
<iconset theme="edit-copy"/>
</property>
<property name="text">
<string>Cop&amp;ier</string>
@@ -322,8 +307,7 @@
</action>
<action name="m_paste_action">
<property name="icon">
<iconset theme="edit-paste" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-paste.png</normaloff>:/ico/16x16/edit-paste.png</iconset>
<iconset theme="edit-paste"/>
</property>
<property name="text">
<string>C&amp;oller</string>
@@ -331,8 +315,7 @@
</action>
<action name="m_paste_in_area_action">
<property name="icon">
<iconset theme="edit-paste" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-paste.png</normaloff>:/ico/16x16/edit-paste.png</iconset>
<iconset theme="edit-paste"/>
</property>
<property name="text">
<string>C&amp;oller dans la zone</string>
@@ -348,8 +331,7 @@
</action>
<action name="m_paste_from_element_action">
<property name="icon">
<iconset theme="element" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/element.png</normaloff>:/ico/16x16/element.png</iconset>
<iconset theme="element"/>
</property>
<property name="text">
<string>Un élément</string>
@@ -357,8 +339,7 @@
</action>
<action name="m_revert_selection_action">
<property name="icon">
<iconset theme="edit-select-invert" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-select-invert.png</normaloff>:/ico/16x16/edit-select-invert.png</iconset>
<iconset theme="edit-select-invert"/>
</property>
<property name="text">
<string>Inverser la sélection</string>
@@ -366,8 +347,7 @@
</action>
<action name="m_delete_action">
<property name="icon">
<iconset theme="edit-delete" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/edit-delete.png</normaloff>:/ico/22x22/edit-delete.png</iconset>
<iconset theme="edit-delete"/>
</property>
<property name="text">
<string>&amp;Supprimer</string>
@@ -383,8 +363,7 @@
</action>
<action name="m_edit_author_action">
<property name="icon">
<iconset theme="preferences-desktop-user" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/preferences-desktop-user.png</normaloff>:/ico/16x16/preferences-desktop-user.png</iconset>
<iconset theme="preferences-desktop-user"/>
</property>
<property name="text">
<string>Éditer les informations sur l'auteur</string>
@@ -400,8 +379,7 @@
</action>
<action name="m_zoom_in_action">
<property name="icon">
<iconset theme="zoom-in" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/zoom-in.png</normaloff>:/ico/22x22/zoom-in.png</iconset>
<iconset theme="zoom-in"/>
</property>
<property name="text">
<string>Zoom avant</string>
@@ -409,8 +387,7 @@
</action>
<action name="m_zoom_out_action">
<property name="icon">
<iconset theme="zoom-out" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/zoom-out.png</normaloff>:/ico/22x22/zoom-out.png</iconset>
<iconset theme="zoom-out"/>
</property>
<property name="text">
<string>Zoom arrière</string>
@@ -434,8 +411,7 @@
</action>
<action name="m_about_qet_action">
<property name="icon">
<iconset theme="qet" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/qet.png</normaloff>:/ico/16x16/qet.png</iconset>
<iconset theme="qet"/>
</property>
<property name="text">
<string>À &amp;propos de QElectroTech</string>
@@ -446,8 +422,7 @@
</action>
<action name="m_online_manual">
<property name="icon">
<iconset theme="help-contents" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/help-contents.png</normaloff>:/ico/16x16/help-contents.png</iconset>
<iconset theme="help-contents"/>
</property>
<property name="text">
<string>Manuel en ligne</string>
@@ -458,8 +433,7 @@
</action>
<action name="m_youtube_action">
<property name="icon">
<iconset theme="kdenlive-show-video" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/kdenlive-show-video.png</normaloff>:/ico/16x16/kdenlive-show-video.png</iconset>
<iconset theme="kdenlive-show-video"/>
</property>
<property name="text">
<string>Chaine Youtube</string>
@@ -470,8 +444,7 @@
</action>
<action name="m_donate_action">
<property name="icon">
<iconset theme="help-donate" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/help-donate.png</normaloff>:/ico/16x16/help-donate.png</iconset>
<iconset theme="help-donate"/>
</property>
<property name="text">
<string>Soutenir le projet par un don</string>
@@ -482,8 +455,7 @@
</action>
<action name="m_about_qt_action">
<property name="icon">
<iconset theme="qt" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/qt.png</normaloff>:/ico/16x16/qt.png</iconset>
<iconset theme="qt"/>
</property>
<property name="text">
<string>À propos de &amp;Qt</string>
@@ -510,8 +482,7 @@
</action>
<action name="m_mirror_action">
<property name="icon">
<iconset theme="mirror" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/mirror.png</normaloff>:/ico/16x16/mirror.png</iconset>
<iconset theme="mirror"/>
</property>
<property name="text">
<string>Mirror</string>
@@ -519,8 +490,7 @@
</action>
<action name="m_flip_action">
<property name="icon">
<iconset theme="flip" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/16x16/flip.png</normaloff>:/ico/16x16/flip.png</iconset>
<iconset theme="flip"/>
</property>
<property name="text">
<string>Flip</string>
@@ -536,8 +506,7 @@
</action>
<action name="m_import_scaled_element">
<property name="icon">
<iconset theme="folder-open" resource="../../../qelectrotech.qrc">
<normaloff>:/ico/22x22/folder-open.png</normaloff>:/ico/22x22/folder-open.png</iconset>
<iconset theme="folder-open"/>
</property>
<property name="text">
<string>importer un élément à redimensionner</string>

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