diff --git a/lang/qet_de.ts b/lang/qet_de.ts index 108d3b071..6eefdced3 100644 --- a/lang/qet_de.ts +++ b/lang/qet_de.ts @@ -826,7 +826,7 @@ Auto Numbering - Automatische Nummerierung + Autom. Nummerierung @@ -1512,7 +1512,7 @@ Bemerkung: diese Optionen verhindern NICHT das automatische Nummerieren. Auto Numbering - Automatische Nummerierung: + Autom. Nummerierung: @@ -3468,7 +3468,7 @@ Der angezeigte Name des Elements lässt sich separat in den Eigenschaften des El Exclude from the bill of materials - Aus der Materialliste ausschließen + Aus Materialliste ausschließen @@ -3504,7 +3504,7 @@ Soll sie erstellt werden? Automatic numbering - Automatische Nummerierung + Autom. Nummerierung @@ -3565,7 +3565,7 @@ Soll sie erstellt werden? None (name entered by hand) - Keine (von Hand eingegebener Name) + Keine (von Hand vergeben) @@ -3702,7 +3702,7 @@ Fortfahren? Exclude from the bill of materials - Aus der Materialliste ausschließen + Aus Materialliste ausschließen diff --git a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.cpp b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.cpp index 2d672f9c4..e11ede4a3 100644 --- a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.cpp +++ b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.cpp @@ -18,6 +18,8 @@ #include "terminalstriplayoutpattern.h" #include "../../../utils/qetutils.h" +#include + TerminalStripLayoutPattern::TerminalStripLayoutPattern() { m_font.setPixelSize(15); @@ -116,3 +118,176 @@ void TerminalStripLayoutPattern::updateTerminalsTextOption() m_xref_text_option.setAlignment(m_xref_text_alignment); m_xref_text_option.setWrapMode(QTextOption::WordWrap); } + +namespace +{ + void rectToXml(QDomElement &element, const QString &name, const QRectF &rect) + { + element.setAttribute(name + QStringLiteral("_x"), rect.x()); + element.setAttribute(name + QStringLiteral("_y"), rect.y()); + element.setAttribute(name + QStringLiteral("_w"), rect.width()); + element.setAttribute(name + QStringLiteral("_h"), rect.height()); + } + + QRectF rectFromXml(const QDomElement &element, const QString &name, const QRectF &default_rect) + { + auto value = [&element, &name](const QString &suffix, qreal default_value) + { + bool ok = false; + const auto v = element.attribute(name + suffix).toDouble(&ok); + return ok ? v : default_value; + }; + + return QRectF(value(QStringLiteral("_x"), default_rect.x()), + value(QStringLiteral("_y"), default_rect.y()), + value(QStringLiteral("_w"), default_rect.width()), + value(QStringLiteral("_h"), default_rect.height())); + } + + qreal realFromXml(const QDomElement &element, const QString &name, qreal default_value) + { + bool ok = false; + const auto v = element.attribute(name).toDouble(&ok); + return ok ? v : default_value; + } + + QString orientationToString(Qt::Orientation orientation) { + return orientation == Qt::Horizontal ? QStringLiteral("horizontal") : QStringLiteral("vertical"); + } + + Qt::Orientation orientationFromXml(const QDomElement &element, const QString &name, Qt::Orientation default_value) + { + const auto v = element.attribute(name); + if (v == QLatin1String("horizontal")) return Qt::Horizontal; + if (v == QLatin1String("vertical")) return Qt::Vertical; + return default_value; + } + + Qt::Alignment alignmentFromXml(const QDomElement &element, const QString &name, Qt::Alignment default_value) + { + bool ok = false; + const auto v = element.attribute(name).toInt(&ok); + return ok && v ? Qt::Alignment(static_cast(v)) : default_value; + } +} + +/** + * @brief TerminalStripLayoutPattern::xmlTagName + * @return the tag name of the xml element used by toXml / fromXml + */ +QString TerminalStripLayoutPattern::xmlTagName() { + return QStringLiteral("terminal_strip_layout"); +} + +/** + * @brief TerminalStripLayoutPattern::toXml + * Save all the values of this layout, including the font. + * The font is saved by hand (family, pixel size, bold, italic) and not with + * QFont::toString() because the format of this string is different between Qt5 and Qt6. + * @param document + * @return the xml element + */ +QDomElement TerminalStripLayoutPattern::toXml(QDomDocument &document) const +{ + auto element = document.createElement(xmlTagName()); + + rectToXml(element, QStringLiteral("header"), m_header_rect); + element.setAttribute(QStringLiteral("header_text_orientation"), orientationToString(m_header_text_orientation)); + element.setAttribute(QStringLiteral("header_text_alignment"), static_cast(m_header_text_alignment)); + + rectToXml(element, QStringLiteral("spacer"), m_spacer_rect); + + element.setAttribute(QStringLiteral("font_family"), m_font.family()); + element.setAttribute(QStringLiteral("font_pixel_size"), m_font.pixelSize()); + element.setAttribute(QStringLiteral("font_bold"), m_font.bold() ? 1 : 0); + element.setAttribute(QStringLiteral("font_italic"), m_font.italic() ? 1 : 0); + + for (auto i = 0; i < m_terminal_rect.size(); ++i) { + rectToXml(element, QStringLiteral("terminal_%1").arg(i), m_terminal_rect.at(i)); + } + + element.setAttribute(QStringLiteral("terminals_text_height"), m_terminals_text_height); + element.setAttribute(QStringLiteral("terminals_text_y"), m_terminals_text_y); + element.setAttribute(QStringLiteral("terminals_text_orientation"), orientationToString(m_terminals_text_orientation)); + element.setAttribute(QStringLiteral("terminals_text_alignment"), static_cast(m_terminals_text_alignment)); + + element.setAttribute(QStringLiteral("xref_text_height"), m_xref_text_height); + element.setAttribute(QStringLiteral("xref_text_y"), m_xref_text_y); + element.setAttribute(QStringLiteral("xref_text_orientation"), orientationToString(m_xref_text_orientation)); + element.setAttribute(QStringLiteral("xref_text_alignment"), static_cast(m_xref_text_alignment)); + + element.setAttribute(QStringLiteral("type_symbol_y"), m_type_symbol_y); + element.setAttribute(QStringLiteral("type_symbol_height"), m_type_symbol_height); + element.setAttribute(QStringLiteral("connection_length"), m_connection_length); + element.setAttribute(QStringLiteral("cable_wire_length"), m_cable_wire_length); + element.setAttribute(QStringLiteral("cable_length"), m_cable_length); + element.setAttribute(QStringLiteral("cable_end_length"), m_cable_end_length); + + element.setAttribute(QStringLiteral("bridge_point_d"), m_bridge_point_d); + for (auto i = 0; i < m_bridge_point_y_offset.size(); ++i) { + element.setAttribute(QStringLiteral("bridge_point_y_offset_%1").arg(i), m_bridge_point_y_offset.at(i)); + } + + return element; +} + +/** + * @brief TerminalStripLayoutPattern::fromXml + * Load the values saved by toXml. A value missing in @a layout_element + * (project saved with an older version) keep the current value of this layout. + * @param layout_element + */ +void TerminalStripLayoutPattern::fromXml(const QDomElement &layout_element) +{ + if (layout_element.isNull()) { + return; + } + + m_header_rect = rectFromXml(layout_element, QStringLiteral("header"), m_header_rect); + m_header_text_orientation = orientationFromXml(layout_element, QStringLiteral("header_text_orientation"), m_header_text_orientation); + setHeaderTextAlignment(alignmentFromXml(layout_element, QStringLiteral("header_text_alignment"), m_header_text_alignment)); + + m_spacer_rect = rectFromXml(layout_element, QStringLiteral("spacer"), m_spacer_rect); + + auto font_ = m_font; + if (layout_element.hasAttribute(QStringLiteral("font_family"))) { + font_.setFamily(layout_element.attribute(QStringLiteral("font_family"))); + } + const auto pixel_size = layout_element.attribute(QStringLiteral("font_pixel_size")).toInt(); + if (pixel_size > 0) { + font_.setPixelSize(pixel_size); + } + if (layout_element.hasAttribute(QStringLiteral("font_bold"))) { + font_.setBold(layout_element.attribute(QStringLiteral("font_bold")).toInt() != 0); + } + if (layout_element.hasAttribute(QStringLiteral("font_italic"))) { + font_.setItalic(layout_element.attribute(QStringLiteral("font_italic")).toInt() != 0); + } + setFont(font_); + + for (auto i = 0; i < m_terminal_rect.size(); ++i) { + m_terminal_rect[i] = rectFromXml(layout_element, QStringLiteral("terminal_%1").arg(i), m_terminal_rect.at(i)); + } + + m_terminals_text_height = realFromXml(layout_element, QStringLiteral("terminals_text_height"), m_terminals_text_height); + m_terminals_text_y = realFromXml(layout_element, QStringLiteral("terminals_text_y"), m_terminals_text_y); + m_terminals_text_orientation = orientationFromXml(layout_element, QStringLiteral("terminals_text_orientation"), m_terminals_text_orientation); + setTerminalsTextAlignment(alignmentFromXml(layout_element, QStringLiteral("terminals_text_alignment"), m_terminals_text_alignment)); + + m_xref_text_height = realFromXml(layout_element, QStringLiteral("xref_text_height"), m_xref_text_height); + m_xref_text_y = realFromXml(layout_element, QStringLiteral("xref_text_y"), m_xref_text_y); + m_xref_text_orientation = orientationFromXml(layout_element, QStringLiteral("xref_text_orientation"), m_xref_text_orientation); + setXrefTextAlignment(alignmentFromXml(layout_element, QStringLiteral("xref_text_alignment"), m_xref_text_alignment)); + + m_type_symbol_y = realFromXml(layout_element, QStringLiteral("type_symbol_y"), m_type_symbol_y); + m_type_symbol_height = realFromXml(layout_element, QStringLiteral("type_symbol_height"), m_type_symbol_height); + m_connection_length = realFromXml(layout_element, QStringLiteral("connection_length"), m_connection_length); + m_cable_wire_length = realFromXml(layout_element, QStringLiteral("cable_wire_length"), m_cable_wire_length); + m_cable_length = realFromXml(layout_element, QStringLiteral("cable_length"), m_cable_length); + m_cable_end_length = realFromXml(layout_element, QStringLiteral("cable_end_length"), m_cable_end_length); + + m_bridge_point_d = realFromXml(layout_element, QStringLiteral("bridge_point_d"), m_bridge_point_d); + for (auto i = 0; i < m_bridge_point_y_offset.size(); ++i) { + m_bridge_point_y_offset[i] = realFromXml(layout_element, QStringLiteral("bridge_point_y_offset_%1").arg(i), m_bridge_point_y_offset.at(i)); + } +} diff --git a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h index ee5e4e8ee..b432783b2 100644 --- a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h +++ b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h @@ -18,6 +18,7 @@ #ifndef TERMINALSTRIPLAYOUTPATTERN_H #define TERMINALSTRIPLAYOUTPATTERN_H +#include #include #include #include @@ -103,6 +104,11 @@ class TerminalStripLayoutPattern QUuid m_uuid{QUuid::createUuid()}; QString m_name; + //Save / load of the layout in the project xml + QDomElement toXml(QDomDocument &document) const; + void fromXml(const QDomElement &layout_element); + static QString xmlTagName(); + private: void updateHeaderTextOption(); void updateTerminalsTextOption(); diff --git a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.cpp b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.cpp index 66a0bc92f..1dc905d73 100644 --- a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.cpp +++ b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.cpp @@ -17,6 +17,7 @@ */ #include "terminalstriplayoutshandler.h" +#include #include TerminalStripLayoutsHandler::TerminalStripLayoutsHandler() @@ -30,3 +31,27 @@ QSharedPointer TerminalStripLayoutsHandler::defaultL { return m_default_layout; } + +/** + * @brief TerminalStripLayoutsHandler::toXml + * Append the default layout (font included) as a child of @a parent_element + * @param parent_element + */ +void TerminalStripLayoutsHandler::toXml(QDomElement &parent_element) const +{ + auto document = parent_element.ownerDocument(); + parent_element.appendChild(m_default_layout->toXml(document)); +} + +/** + * @brief TerminalStripLayoutsHandler::fromXml + * Load the default layout from the child of @a parent_element, if any. + * The existing layout object is modified in place because the terminal + * strip items share it. + * @param parent_element + */ +void TerminalStripLayoutsHandler::fromXml(const QDomElement &parent_element) +{ + m_default_layout->fromXml( + parent_element.firstChildElement(TerminalStripLayoutPattern::xmlTagName())); +} diff --git a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.h b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.h index cddb842f2..d9275c4da 100644 --- a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.h +++ b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutshandler.h @@ -18,6 +18,7 @@ #ifndef TERMINALSTRIPLAYOUTSHANDLER_H #define TERMINALSTRIPLAYOUTSHANDLER_H +#include #include #include @@ -33,6 +34,9 @@ class TerminalStripLayoutsHandler TerminalStripLayoutsHandler(); QSharedPointer defaultLayout(); + void toXml(QDomElement &parent_element) const; + void fromXml(const QDomElement &parent_element); + private: QSet> m_layout_set; QSharedPointer m_default_layout; diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index be933ab63..8eccb75df 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -90,6 +90,31 @@ void drawGroundSymbol(QPainter *painter) painter->drawLine(QPointF{-1, 4}, QPointF{1, 4}); } + //Two diodes side by side, wired in anti-parallel between the top and + //the bottom of the terminal (current flows through the left one from + //the top to the bottom and through the right one the other way) +void drawAntiParallelDiodesSymbol(QPainter *painter) +{ + painter->drawLine(QPointF{0, -7}, QPointF{0, -5}); + painter->drawLine(QPointF{-5, -5}, QPointF{5, -5}); + painter->drawLine(QPointF{0, 5}, QPointF{0, 7}); + painter->drawLine(QPointF{-5, 5}, QPointF{5, 5}); + + //Left diode, anode on top + const QPointF left_diode[3] { {-8, -3}, {-2, -3}, {-5, 2} }; + painter->drawLine(QPointF{-5, -5}, QPointF{-5, -3}); + painter->drawPolygon(left_diode, 3); + painter->drawLine(QPointF{-8, 2}, QPointF{-2, 2}); + painter->drawLine(QPointF{-5, 2}, QPointF{-5, 5}); + + //Right diode, anode at the bottom + const QPointF right_diode[3] { {2, 2}, {8, 2}, {5, -3} }; + painter->drawLine(QPointF{5, -5}, QPointF{5, -3}); + painter->drawPolygon(right_diode, 3); + painter->drawLine(QPointF{2, -3}, QPointF{8, -3}); + painter->drawLine(QPointF{5, 2}, QPointF{5, 5}); +} + /** * @brief drawScaled * Call @a draw with the painter moved by @a dx and scaled by @a factor. @@ -147,6 +172,7 @@ void drawTypeSymbol(QPainter *painter, const QRectF &box, ElementData::TerminalT case ElementData::TTSectional : drawSectionalSymbol(p); break; case ElementData::TTDiode : drawDiodeSymbol(p); break; case ElementData::TTGround : drawGroundSymbol(p); break; + case ElementData::TTAntiParallelDiodes : drawAntiParallelDiodesSymbol(p); break; default: break; } }; diff --git a/sources/TerminalStrip/ui/freeterminaleditor.cpp b/sources/TerminalStrip/ui/freeterminaleditor.cpp index 50e006d7b..3f08e39d6 100644 --- a/sources/TerminalStrip/ui/freeterminaleditor.cpp +++ b/sources/TerminalStrip/ui/freeterminaleditor.cpp @@ -191,6 +191,8 @@ void FreeTerminalEditor::on_m_type_cb_activated(int index) override_type = ElementData::TTDiode; break; case 4: override_type = ElementData::TTGround; break; + case 5: + override_type = ElementData::TTAntiParallelDiodes; break; default: override_type = ElementData::TTGeneric; break; } diff --git a/sources/TerminalStrip/ui/freeterminaleditor.ui b/sources/TerminalStrip/ui/freeterminaleditor.ui index 56f6c4c2a..736c2b12d 100644 --- a/sources/TerminalStrip/ui/freeterminaleditor.ui +++ b/sources/TerminalStrip/ui/freeterminaleditor.ui @@ -122,6 +122,11 @@ Ground + + + Anti-parallel diodes + + diff --git a/sources/TerminalStrip/ui/freeterminalmodel.cpp b/sources/TerminalStrip/ui/freeterminalmodel.cpp index 27183100e..7eb2eb1f6 100644 --- a/sources/TerminalStrip/ui/freeterminalmodel.cpp +++ b/sources/TerminalStrip/ui/freeterminalmodel.cpp @@ -429,6 +429,7 @@ QWidget *FreeTerminalModelDelegate::createEditor(QWidget *parent, const QStyleOp qcb->addItem(ElementData::translatedTerminalType(ElementData::TTSectional), ElementData::TTSectional); qcb->addItem(ElementData::translatedTerminalType(ElementData::TTDiode), ElementData::TTDiode); qcb->addItem(ElementData::translatedTerminalType(ElementData::TTGround), ElementData::TTGround); + qcb->addItem(ElementData::translatedTerminalType(ElementData::TTAntiParallelDiodes), ElementData::TTAntiParallelDiodes); return qcb; } diff --git a/sources/TerminalStrip/ui/terminalstripeditor.cpp b/sources/TerminalStrip/ui/terminalstripeditor.cpp index 9a56339b0..ba3964f1e 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.cpp +++ b/sources/TerminalStrip/ui/terminalstripeditor.cpp @@ -569,6 +569,8 @@ void TerminalStripEditor::on_m_type_cb_activated(int index) override_type = ElementData::TTDiode; break; case 4: override_type = ElementData::TTGround; break; + case 5: + override_type = ElementData::TTAntiParallelDiodes; break; default: override_type = ElementData::TTGeneric; break; } diff --git a/sources/TerminalStrip/ui/terminalstripeditor.ui b/sources/TerminalStrip/ui/terminalstripeditor.ui index 35fbb9f5d..46998a04e 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.ui +++ b/sources/TerminalStrip/ui/terminalstripeditor.ui @@ -127,6 +127,11 @@ Ground + + + Anti-parallel diodes + + diff --git a/sources/TerminalStrip/ui/terminalstripmodel.cpp b/sources/TerminalStrip/ui/terminalstripmodel.cpp index c6e3fac46..2f363ba1c 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.cpp +++ b/sources/TerminalStrip/ui/terminalstripmodel.cpp @@ -833,6 +833,7 @@ QWidget *TerminalStripModelDelegate::createEditor(QWidget *parent, const QStyleO qcb->addItem(ElementData::translatedTerminalType(ElementData::TTSectional), ElementData::TTSectional); qcb->addItem(ElementData::translatedTerminalType(ElementData::TTDiode), ElementData::TTDiode); qcb->addItem(ElementData::translatedTerminalType(ElementData::TTGround), ElementData::TTGround); + qcb->addItem(ElementData::translatedTerminalType(ElementData::TTAntiParallelDiodes), ElementData::TTAntiParallelDiodes); return qcb; } diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 15a10ce7b..52c7d5b87 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -75,18 +75,7 @@ projectDataBase::projectDataBase(QETProject *project, QObject *parent) : connect(m_project, &QETProject::projectDiagramsOrderChanged, [this]() { m_content_changed = true; - for (auto diagram : m_project->diagrams()) - { - m_diagram_order_changed.bindValue(":pos", m_project->folioIndex(diagram)+1); - m_diagram_order_changed.bindValue(":uuid", diagram->uuid()); - m_diagram_order_changed.exec(); - - - m_diagram_info_order_changed.bindValue(":folio", diagram->border_and_titleblock.titleblockInformation().value("folio")); - m_diagram_info_order_changed.bindValue(":uuid", diagram->uuid()); - m_diagram_info_order_changed.exec(); - - } + updateFolioPositions(); emit dataBaseUpdated(); }); } @@ -122,6 +111,7 @@ void projectDataBase::updateDB() { flushDrawingItems(); flushLinks(); + flushElementPositions(); emit dataBaseUpdated(); return; } @@ -649,6 +639,7 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString query.exec(QStringLiteral("DELETE FROM ") + table); } m_dirty_link_elements.clear(); + m_moved_elements.clear(); for (int i = 0 ; i < diagram_uuids.size() ; ++i) { @@ -824,9 +815,11 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) { //Drawing-item rows are rewritten lazily, see drawingItemChanged(). //Every read from outside comes through here, so this is the one //place the queue has to be emptied for a reader to see current rows. - //The same goes for link rows, see linksChanged(). + //The same goes for link rows and elements' folio cells, see + //linksChanged() and elementMoved(). flushDrawingItems(); flushLinks(); + flushElementPositions(); // First gate: which kind of statement is acceptable here at all. A // textual check is the right tool for that and the wrong tool for @@ -920,6 +913,8 @@ void projectDataBase::addElement(Element *element) } connect(element, &Element::linkedElementChanged, this, &projectDataBase::linksChanged, Qt::UniqueConnection); + connect(element, &QGraphicsObject::xChanged, this, &projectDataBase::elementMoved, Qt::UniqueConnection); + connect(element, &QGraphicsObject::yChanged, this, &projectDataBase::elementMoved, Qt::UniqueConnection); bindElementInfoValues(m_insert_element_info_query, element); if (!m_insert_element_info_query.exec()) { @@ -1022,18 +1017,78 @@ void projectDataBase::addDiagram(Diagram *diagram) addDrawingItem(item); } - //The information "folio" of other existing diagram can have the variable %total, - //so when a new diagram is added this variable change. - //We need to update this information in the database. - for (auto diagram : project()->diagrams()) + //The folios after the new one moved down, and a folio number made + //from %id or %total changed on every folio. + updateFolioPositions(); + emit dataBaseUpdated(); +} + +/** + @brief projectDataBase::elementMoved + The sender() element moved: its folio cell is written again by the + next flushElementPositions(). +*/ +void projectDataBase::elementMoved() +{ + auto *element = qobject_cast(sender()); + if (element && !m_moved_elements.contains(element)) { + m_moved_elements << element; + } +} + +/** + @brief projectDataBase::flushElementPositions + Write the folio cell of every element queued by elementMoved() +*/ +void projectDataBase::flushElementPositions() +{ + if (m_moved_elements.isEmpty()) { + return; + } + + const auto moved = m_moved_elements; + m_moved_elements.clear(); + const bool own_transaction = m_data_base.transaction(); + QSqlQuery update(m_data_base); + update.prepare(QStringLiteral("UPDATE element SET pos = :pos WHERE uuid = :uuid")); + for (const QPointer &element : moved) { - m_diagram_info_order_changed.bindValue(":folio", diagram->border_and_titleblock.titleblockInformation().value("folio")); - m_diagram_info_order_changed.bindValue(":uuid", diagram->uuid()); - if (!m_diagram_info_order_changed.exec()) { - qDebug() << "projectDataBase::addDiagram update diagram infp order error : " << m_diagram_info_order_changed.lastError(); + if (!element || !element->diagram()) { + continue; + } + update.bindValue(QStringLiteral(":pos"), + element->diagram()->convertPosition(element->scenePos()).toString()); + update.bindValue(QStringLiteral(":uuid"), element->uuid().toString()); + if (!update.exec()) { + qDebug() << "projectDataBase::flushElementPositions error : " << update.lastError(); + } + } + if (own_transaction) { + m_data_base.commit(); + } +} + +/** + @brief projectDataBase::updateFolioPositions + Write every folio's position and folio number again, after a folio was + added, removed or moved. +*/ +void projectDataBase::updateFolioPositions() +{ + for (auto diagram : m_project->diagrams()) + { + m_diagram_order_changed.bindValue(":pos", m_project->folioIndex(diagram)+1); + m_diagram_order_changed.bindValue(":uuid", diagram->uuid().toString()); + if (!m_diagram_order_changed.exec()) { + qDebug() << "projectDataBase::updateFolioPositions position error : " << m_diagram_order_changed.lastError(); + } + + m_diagram_info_order_changed.bindValue(":folio", diagram->border_and_titleblock.titleblockInformation().value("folio")); + m_diagram_info_order_changed.bindValue(":uuid", diagram->uuid().toString()); + if (!m_diagram_info_order_changed.exec()) { + qDebug() << "projectDataBase::updateFolioPositions folio error : " << m_diagram_info_order_changed.lastError(); } } - emit dataBaseUpdated(); } void projectDataBase::removeDiagram(Diagram *diagram) @@ -1118,6 +1173,15 @@ void projectDataBase::removeDiagram(Diagram *diagram) } } + QSqlQuery remove_info(m_data_base); + remove_info.prepare(QStringLiteral("DELETE FROM diagram_info WHERE diagram_uuid = :uuid")); + remove_info.bindValue(QStringLiteral(":uuid"), uuid_str); + if (!remove_info.exec()) { + qDebug() << "projectDataBase::removeDiagram diagram_info delete error : " << remove_info.lastError(); + m_data_base.rollback(); + return; + } + m_remove_diagram_query.bindValue(":uuid", uuid_str); if (!m_remove_diagram_query.exec()) { qDebug() << "projectDataBase::removeDiagram delete error : " << m_remove_diagram_query.lastError(); @@ -1126,6 +1190,8 @@ void projectDataBase::removeDiagram(Diagram *diagram) } m_data_base.commit(); + //The folios after it moved up, and %id / %total changed. + updateFolioPositions(); emit dataBaseUpdated(); } @@ -1187,12 +1253,39 @@ void projectDataBase::addConductor(Conductor *conductor) void projectDataBase::removeConductor(Conductor *conductor) { m_content_changed = true; + //The terminal table lists the terminals a conductor ends on: the + //conductor's two ends go with it unless another conductor ends there. + QList> ends; + QSqlQuery read_ends(m_data_base); + read_ends.prepare(QStringLiteral("SELECT terminal1_uuid, terminal1_element_uuid, " + "terminal2_uuid, terminal2_element_uuid " + "FROM conductor WHERE uuid = :uuid")); + read_ends.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString()); + if (read_ends.exec() && read_ends.next()) { + ends << qMakePair(read_ends.value(0).toString(), read_ends.value(1).toString()) + << qMakePair(read_ends.value(2).toString(), read_ends.value(3).toString()); + } + m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString()); if (!m_remove_conductor_query.exec()) { qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError(); - } else { - emit dataBaseUpdated(); + return; } + + QSqlQuery remove_end(m_data_base); + remove_end.prepare(QStringLiteral( + "DELETE FROM terminal WHERE uuid = :uuid AND element_uuid = :element_uuid " + "AND NOT EXISTS (SELECT 1 FROM conductor WHERE " + "(terminal1_uuid = :uuid AND terminal1_element_uuid = :element_uuid) OR " + "(terminal2_uuid = :uuid AND terminal2_element_uuid = :element_uuid))")); + for (const auto &end : std::as_const(ends)) { + remove_end.bindValue(QStringLiteral(":uuid"), end.first); + remove_end.bindValue(QStringLiteral(":element_uuid"), end.second); + if (!remove_end.exec()) { + qDebug() << "projectDataBase::removeConductor terminal delete error : " << remove_end.lastError(); + } + } + emit dataBaseUpdated(); } /** @@ -2061,6 +2154,7 @@ static ElementData::Types allElementTypes() */ void projectDataBase::populateElementTable() { + m_moved_elements.clear(); QSqlQuery query_(m_data_base); query_.exec("DELETE FROM element"); @@ -2597,6 +2691,7 @@ void projectDataBase::exportDb(projectDataBase *db, const auto escaped_path = path_.replace("'", "''"); db->flushDrawingItems(); db->flushLinks(); + db->flushElementPositions(); QSqlQuery query(db->m_data_base); if (!query.exec("VACUUM INTO '" % escaped_path % "'")) { qWarning() << "Unable to export project database:" << query.lastError().text(); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index b15cd9286..9f3779c7c 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -126,6 +126,8 @@ class projectDataBase : public QObject void drawingItemDestroyed(QObject *object); //Queue the sender()'s link rows for rewriting. void linksChanged(); + //Queue the sender() element's folio cell for rewriting. + void elementMoved(); public: @@ -139,6 +141,8 @@ class projectDataBase : public QObject void createWiringListView(); void createDrawingItemView(); void populateDiagramTable(); + void updateFolioPositions(); + void flushElementPositions(); void populateElementTable(); void populateElementInfoTable(); void populateDiagramInfoTable(); @@ -214,6 +218,10 @@ class projectDataBase : public QObject //written: a link is made first and its contact group set after, //so the rows are written when next read, as drawing items are. QList> m_dirty_link_elements; + //Elements moved since their folio cell was last written: a move + //sends xChanged and yChanged for every step of a drag, so the + //cell is written when next read, as drawing items are. + QList> m_moved_elements; #ifdef QET_EXPORT_PROJECT_DB public: diff --git a/sources/editor/ui/elementpropertieseditorwidget.cpp b/sources/editor/ui/elementpropertieseditorwidget.cpp index 178d08a21..d45477483 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.cpp +++ b/sources/editor/ui/elementpropertieseditorwidget.cpp @@ -225,6 +225,7 @@ void ElementPropertiesEditorWidget::setUpInterface() ui->m_terminal_type_cb->addItem(tr("Sectionable"), ElementData::TTSectional); ui->m_terminal_type_cb->addItem(tr("Diode"), ElementData::TTDiode); ui->m_terminal_type_cb->addItem(tr("Ground"), ElementData::TTGround); + ui->m_terminal_type_cb->addItem(tr("Anti-parallel diodes"), ElementData::TTAntiParallelDiodes); ui->m_terminal_func_cb->addItem(tr("Generic"), ElementData::TFGeneric); ui->m_terminal_func_cb->addItem(tr("Phase"), ElementData::TFPhase); diff --git a/sources/imagedrop.cpp b/sources/imagedrop.cpp index 06aa5ed85..76ac259d6 100644 --- a/sources/imagedrop.cpp +++ b/sources/imagedrop.cpp @@ -35,11 +35,6 @@ namespace const QString suffix = QFileInfo(url.toLocalFile()).suffix().toLower(); return ImageDrop::supportedSuffixes().contains(suffix); } - - QString tr(const char *text) - { - return QCoreApplication::translate("ImageDrop", text); - } } QStringList ImageDrop::supportedSuffixes() @@ -74,18 +69,18 @@ QImage ImageDrop::load(const QString &path, QString *error) const QFileInfo info(path); if (!info.isFile()) - return fail(tr("not a file")); + return fail(QCoreApplication::translate("ImageDrop", "not a file")); if (info.size() > maxFileBytes) - return fail(tr("the file is larger than 10 MB")); + return fail(QCoreApplication::translate("ImageDrop", "the file is larger than 10 MB")); QImageReader reader(path); const QSize size = reader.size(); if (size.isValid() && qint64(size.width()) * size.height() > maxPixels) - return fail(tr("the image has too many pixels")); + return fail(QCoreApplication::translate("ImageDrop", "the image has too many pixels")); const QImage image = reader.read(); if (image.isNull()) - return fail(tr("unable to read the image")); + return fail(QCoreApplication::translate("ImageDrop", "unable to read the image")); return image; } diff --git a/sources/properties/elementdata.cpp b/sources/properties/elementdata.cpp index 8586c243e..2804295ed 100644 --- a/sources/properties/elementdata.cpp +++ b/sources/properties/elementdata.cpp @@ -840,6 +840,8 @@ QString ElementData::terminalTypeToString(ElementData::TerminalType type) return QStringLiteral("diode"); case ElementData::TTGround: return QStringLiteral("ground"); + case ElementData::TTAntiParallelDiodes: + return QStringLiteral("antiparallel_diodes"); } return QStringLiteral("generic"); } @@ -856,6 +858,8 @@ ElementData::TerminalType ElementData::terminalTypeFromString(const QString &str return ElementData::TTDiode; } else if (string == QLatin1String("ground")) { return ElementData::TTGround; + } else if (string == QLatin1String("antiparallel_diodes")) { + return ElementData::TTAntiParallelDiodes; } qDebug() << "ElementData::terminalTypeFromString : string : " @@ -877,6 +881,8 @@ QString ElementData::translatedTerminalType(ElementData::TerminalType type) return QObject::tr("Diode", "diode terminal element type"); case ElementData::TTGround: return QObject::tr("Ground", "ground terminal element type"); + case ElementData::TTAntiParallelDiodes: + return QObject::tr("Anti-parallel diodes", "anti-parallel diodes terminal element type"); } return QObject::tr("Generic", "generic terminal element type"); } diff --git a/sources/properties/elementdata.h b/sources/properties/elementdata.h index 51b9c9678..97a834840 100644 --- a/sources/properties/elementdata.h +++ b/sources/properties/elementdata.h @@ -175,7 +175,8 @@ class ElementData : public PropertiesInterface TTFuse, TTSectional, TTDiode, - TTGround + TTGround, + TTAntiParallelDiodes }; Q_ENUM(TerminalType) diff --git a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp index 75660104a..df090c36d 100644 --- a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp +++ b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp @@ -448,7 +448,7 @@ void QetGraphicsTableItem::setPreviousTable(QetGraphicsTableItem *table) m_previous_table->setNextTable(this); setModel(m_previous_table->m_model); } - else //Copie the model of old previous table + else if (old_previous_table->model()) //Copie the model of old previous table { setModel(new ProjectDBModel(*static_cast(old_previous_table->model()))); } @@ -596,11 +596,23 @@ QDomElement QetGraphicsTableItem::toXml(QDomDocument &dom_document) const //Add the header xml dom_table.appendChild(m_header_item->toXml(dom_document)); - //Add previous table, the model is save by the previous table - if (m_previous_table) + //Add previous table, the model is save by the previous table. + //A folio deleted with undo still possible keeps its tables in the + //chain, but it is not saved: name the nearest previous table that + //is, or save the model if none is. + const auto project_ = diagram() ? diagram()->project() : nullptr; + auto in_project = [project_](const QetGraphicsTableItem *table) { + return project_ && table->diagram() + && project_->diagrams().contains(table->diagram()); + }; + QetGraphicsTableItem *previous_ = m_previous_table; + while (previous_ && !in_project(previous_)) { + previous_ = previous_->previousTable(); + } + if (previous_) { auto dom_previous_table = dom_document.createElement("previous_table"); - dom_previous_table.setAttribute("uuid", m_previous_table->m_uuid.toString()); + dom_previous_table.setAttribute("uuid", previous_->m_uuid.toString()); dom_table.appendChild(dom_previous_table); } else if (m_model) //There is not a previous table, we need to save the model diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index d6b070f49..829ffd3b6 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -1587,6 +1587,8 @@ QDomDocument QETProject::toXml() //Write terminal strip to xml if (m_terminal_strip_vector.count()) { + m_project_properties_handler.terminalStripLayoutHandler().toXml(project_root); + auto xml_strip = xml_doc.createElement(QStringLiteral("terminal_strips")); for (auto &strip : m_terminal_strip_vector) { xml_strip.appendChild(strip->toXml(xml_doc)); @@ -2092,6 +2094,10 @@ void QETProject::readProjectXml(QDomDocument &xml_project) readElementsCollectionXml(xml_project); const qint64 elements_ms = phase_timer.restart(); + //Load the layout of the terminal strip (font, size of cells...), + //before the diagrams because the terminal strip items use it + m_project_properties_handler.terminalStripLayoutHandler().fromXml(xml_project.documentElement()); + //Load the diagrams readDiagramsXml(xml_project); const qint64 diagrams_ms = phase_timer.restart(); diff --git a/tests/qttest/tst_databasefromdocument.cpp b/tests/qttest/tst_databasefromdocument.cpp index 1660803d8..9fba39225 100644 --- a/tests/qttest/tst_databasefromdocument.cpp +++ b/tests/qttest/tst_databasefromdocument.cpp @@ -56,6 +56,12 @@ class tst_databasefromdocument : public QObject { const QString out = run({QStringLiteral("--run"), m_dir.filePath(QStringLiteral("dump.js")), project}, from_folios); + return tablesIn(out, how); + } + + // The DUMP line of a --run output, each table as a sorted list of rows + static QJsonObject tablesIn(const QString &out, QString *how) + { QJsonObject tables; for (const QString &line : out.split(QLatin1Char('\n'))) { if (line.contains(QStringLiteral("Project database filled"))) @@ -345,6 +351,105 @@ private slots: "a link is not one the folios would make as saved")); } + // After a folio is added and another removed, the folio tables hold + // what a fresh open of the saved project gives: every folio's position + // and folio number, and no row for the removed folio. Positions only + // changed when folios were reordered, and the removed folio's + // diagram_info row stayed. + void folioAddedAndRemoved() + { + const QString saved = m_dir.filePath(QStringLiteral("folios.qet")); + const QString edited = m_dir.filePath(QStringLiteral("folios-edited.qet")); + run({QStringLiteral("--resave"), QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), saved}); + QVERIFY(QFile::exists(saved)); + QFile js(m_dir.filePath(QStringLiteral("folios.js"))); + QVERIFY(js.open(QIODevice::WriteOnly)); + js.write(QStringLiteral( + "qet.log('ADDED ' + qet.insertFolio(0));\n" + "qet.log('REMOVED ' + qet.removeFolio(2));\n" + "var out = {};\n" + "['diagram', 'diagram_info'].forEach(function (t) { out[t] = qet.query('SELECT * FROM ' + t); });\n" + "qet.log('DUMP ' + JSON.stringify(out));\n" + "qet.log('SAVED ' + qet.save('%1'));\n").arg(edited).toUtf8()); + js.close(); + const QString log = run({QStringLiteral("--run"), js.fileName(), saved}); + QVERIFY2(log.contains(QStringLiteral("ADDED 0")) && log.contains(QStringLiteral("REMOVED true")) + && log.contains(QStringLiteral("SAVED true")), qPrintable(log.right(400))); + QString unused; + const QJsonObject after_edits = tablesIn(log, &unused); + const QJsonObject reopened = dump(edited, false, &unused); + for (const QString &table : {QStringLiteral("diagram"), QStringLiteral("diagram_info")}) { + const QJsonArray a = after_edits.value(table).toArray(), b = reopened.value(table).toArray(); + QVERIFY2(!a.isEmpty(), qPrintable(table)); + QVERIFY2(a == b, qPrintable(QStringLiteral("%1: %2 rows after the edits, %3 on reopening, or different") + .arg(table).arg(a.size()).arg(b.size()))); + } + } + + // After an element is moved several columns, its folio cell in the + // element table is the one a fresh open of the saved project gives. + // It used to stay where the element was added. + void movedElementCell() + { + const QString saved = m_dir.filePath(QStringLiteral("moved.qet")); + const QString edited = m_dir.filePath(QStringLiteral("moved-edited.qet")); + run({QStringLiteral("--resave"), QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), saved}); + QVERIFY(QFile::exists(saved)); + QFile js(m_dir.filePath(QStringLiteral("moved.js"))); + QVERIFY(js.open(QIODevice::WriteOnly)); + js.write(QStringLiteral( + "var e = qet.elementUuids(0)[0];\n" + "var before = qet.query(\"SELECT pos FROM element WHERE uuid = '\" + e + \"'\")[0].pos;\n" + "qet.log('MOVED ' + qet.moveElement(0, e, 300, 0));\n" + "var after = qet.query(\"SELECT pos FROM element WHERE uuid = '\" + e + \"'\")[0].pos;\n" + "qet.log('CELL ' + before + ' ' + after);\n" + "qet.log('DUMP ' + JSON.stringify({element: qet.query('SELECT * FROM element')}));\n" + "qet.log('SAVED ' + qet.save('%1'));\n").arg(edited).toUtf8()); + js.close(); + const QString log = run({QStringLiteral("--run"), js.fileName(), saved}); + QVERIFY2(log.contains(QStringLiteral("MOVED true")) && log.contains(QStringLiteral("SAVED true")), + qPrintable(log.right(400))); + QString unused; + const QJsonArray a = tablesIn(log, &unused).value(QStringLiteral("element")).toArray(); + const QJsonArray b = dump(edited, false, &unused).value(QStringLiteral("element")).toArray(); + QVERIFY(!a.isEmpty()); + QVERIFY2(a == b, qPrintable(log.mid(log.indexOf(QStringLiteral("CELL")), 40))); + } + + // After every conductor on a folio is deleted, the terminal table + // holds what a fresh open of the saved project gives: a terminal is + // listed while a conductor ends on it. The ends of a deleted conductor + // used to stay. + void deletedWireTerminals() + { + const QString saved = m_dir.filePath(QStringLiteral("unwired.qet")); + const QString edited = m_dir.filePath(QStringLiteral("unwired-edited.qet")); + run({QStringLiteral("--resave"), QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet"), saved}); + QVERIFY(QFile::exists(saved)); + QFile js(m_dir.filePath(QStringLiteral("unwired.js"))); + QVERIFY(js.open(QIODevice::WriteOnly)); + js.write(QStringLiteral( + "var n = 0;\n" + "qet.conductorUuids(0).forEach(function (c) {\n" + " var end = qet.conductorEnds(0, c)[0];\n" + " if (!end) return;\n" + " var m = end.match(/^(\\{[^}]+\\}) terminal (\\d+)$/);\n" + " if (m && qet.deleteConductor(0, m[1], parseInt(m[2]))) ++n;\n" + "});\n" + "qet.log('DELETED ' + n);\n" + "qet.log('DUMP ' + JSON.stringify({terminal: qet.query('SELECT * FROM terminal')}));\n" + "qet.log('SAVED ' + qet.save('%1'));\n").arg(edited).toUtf8()); + js.close(); + const QString log = run({QStringLiteral("--run"), js.fileName(), saved}); + QVERIFY2(!log.contains(QStringLiteral("DELETED 0")) && log.contains(QStringLiteral("SAVED true")), + qPrintable(log.right(400))); + QString unused; + const QJsonArray a = tablesIn(log, &unused).value(QStringLiteral("terminal")).toArray(); + const QJsonArray b = dump(edited, false, &unused).value(QStringLiteral("terminal")).toArray(); + QVERIFY2(a == b, qPrintable(QStringLiteral("%1 terminal rows after the edits, %2 on reopening") + .arg(a.size()).arg(b.size()))); + } + // A file whose items carry no saved uuid is filled from the folios, // which work those uuids out as they are built, and says why. void olderFileFallsBack() diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp index 9198bf850..f372f99d7 100644 --- a/tests/qttest/tst_resaveunchanged.cpp +++ b/tests/qttest/tst_resaveunchanged.cpp @@ -160,6 +160,73 @@ private slots: QVERIFY2(sb < sa, "the coil's contacts were saved in another order than the file's"); } + // A table split over several folios whose part names a previous part + // that is not in the file: opening and closing it crashed (the part + // has no data of its own and handed its missing data on when it was + // destroyed). industrial.qet's third is changed. + void missingPreviousTableOpens() + { + QByteArray xml = read(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet")); + int at = -1; + for (int n = 0 ; n < 3 ; ++n) { + at = xml.indexOf(" 0); + } + at += int(qstrlen(" in the file names a table that is in + // it. It used to name the deleted part, so every part after it opened + // with no data. industrial.qet's folio 46 (index 45) holds such a part. + void deletedFolioKeepsTableChain() + { +#ifndef QET_HAS_SCRIPTING + QSKIP("needs --run: this QElectroTech is built without Qt Qml"); +#endif + const QString out = m_dir.filePath(QStringLiteral("deleted_folio.qet")); + const QString script = m_dir.filePath(QStringLiteral("deleted_folio.js")); + QFile js(script); + QVERIFY(js.open(QIODevice::WriteOnly)); + js.write(QStringLiteral("qet.log('REMOVED ' + qet.removeFolio(45));\n" + "qet.log('SAVED ' + qet.save('%1'));\n").arg(out).toUtf8()); + js.close(); + const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++)); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); + env.insert(QStringLiteral("HOME"), home); + env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config")); + env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + {QStringLiteral("--run"), script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet")}); + QVERIFY(proc.waitForFinished(180000)); + const QString log = QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError()); + QVERIFY2(log.contains(QStringLiteral("REMOVED true")) && log.contains(QStringLiteral("SAVED true")), + qPrintable(log.right(400))); + + const QString saved = QString::fromUtf8(read(out)); + QSet tables; + for (const auto &m : QRegularExpression(QStringLiteral("]*uuid=\"([^\"]+)\"")).globalMatch(saved)) + tables.insert(m.captured(1)); + int references = 0; + for (const auto &m : QRegularExpression(QStringLiteral(" 0); + } + // A folio report arrow linked to one that has no terminal -- such as // ref_voyant_2_h.elmt from the collection -- crashed QElectroTech on // opening: the arrow's text looked for a wire on the other arrow's