mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-11 22:54:12 +02:00
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:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¢re, 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"));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¢er,
|
||||
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);
|
||||
|
||||
@@ -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 ¢er, const QList<QAction *> &actions);
|
||||
void showAt(const QPoint ¢er, 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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>&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>&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>&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>&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>&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&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&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&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&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>&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>À &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 &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
Reference in New Issue
Block a user