mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-10 06:04:12 +02:00
Add a generic device: a box symbol with terminals on any side
Edit > Add > Generic device (also on the Add toolbar, and found by the
command search as diagrameditor.add_generic_device) opens a three-page
wizard for devices used too rarely to draw a symbol for, such as a VSD
or a power supply:
1. Layout: terminals per side (0 for none), a gap every N terminals,
divider marks on gaps, groups lined up across opposite sides, pitch
and extra size.
2. Terminals: a table per side and the same list as one line, where
In{4} is In1..In4, Q{z4} is Q0..Q3, X{3-6} is X3..X6 and an empty
entry is a gap; number pins 1..n counter-clockwise; paste rows of
side, name, function from a spreadsheet or a CSV file.
3. Information: name, label, manufacturer, reference, description.
A preview beside every page shows the box as it will be placed. Finish,
available from the first page, writes an ordinary symbol to the folder
"Generic devices" of the project's collection and puts it on the cursor.
The box sizes itself to its terminals and their names; every terminal
lands on the grid and gets a uuid. No new XML: any QElectroTech reads it.
The layout is pure (sources/genericdevice/genericdevice.cpp) and tested
by tst_genericdevice. ElementPictureFactory gains pictureFromDefinition()
to draw the preview with the same code as a placed element.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -244,6 +244,10 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/exportpropertieswidget.h
|
||||
${QET_DIR}/sources/genericpanel.cpp
|
||||
${QET_DIR}/sources/genericpanel.h
|
||||
${QET_DIR}/sources/genericdevice/genericdevice.cpp
|
||||
${QET_DIR}/sources/genericdevice/genericdevice.h
|
||||
${QET_DIR}/sources/genericdevice/genericdevicewizard.cpp
|
||||
${QET_DIR}/sources/genericdevice/genericdevicewizard.h
|
||||
${QET_DIR}/sources/itemgroups.cpp
|
||||
${QET_DIR}/sources/itemgroups.h
|
||||
${QET_DIR}/sources/shownkinds.cpp
|
||||
|
||||
@@ -149,6 +149,7 @@ Comment:
|
||||
ico/qet/traced/*/circle.svg, ellipse.svg, view-fit-window.svg by ispyisail from the QElectroTech team, 16 pixel versions by the QElectroTech team
|
||||
ico/qet/traced/*/ellipse-to-bezier.svg, rect-to-bezier.svg, rect-to-polyline.svg by the QElectroTech team, redrawn from Andre Rummler's drawings, with permission
|
||||
ico/qet/traced/* other than the above by the QElectroTech team, traced from QElectroTech's PNG icons of the same names
|
||||
ico/qet/scalable/generic-device.svg by ispyisail from the QElectroTech team, a box with terminals and a divider on each side
|
||||
ico/qet/scalable/16/* by the QElectroTech team, 16 pixel versions of the ico/qet/scalable/ icons of the same names
|
||||
ico/colors/* by Arnaud, contributed in 2020
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<file alias="themes/qet/scalable/folio-delete.svg">qet/scalable/folio-delete.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-new.svg">qet/scalable/folio-new.svg</file>
|
||||
<file alias="themes/qet/scalable/folio-properties.svg">qet/scalable/folio-properties.svg</file>
|
||||
<file alias="themes/qet/scalable/generic-device.svg">qet/scalable/generic-device.svg</file>
|
||||
<file alias="themes/qet/scalable/label.svg">qet/scalable/label.svg</file>
|
||||
<file alias="themes/qet/scalable/pdf-import.svg">qet/scalable/pdf-import.svg</file>
|
||||
<file alias="themes/qet/scalable/snap-to-grid.svg">qet/scalable/snap-to-grid.svg</file>
|
||||
@@ -1332,6 +1333,18 @@
|
||||
<file alias="themes/qet/48x48@2/format-text-underline.svg">themes/generated/breeze-24/format-text-underline.svg</file>
|
||||
<file alias="themes/qet/64x64@2/format-text-underline.svg">breeze/16/format-text-underline.svg</file>
|
||||
<file alias="themes/qet/128x128@2/format-text-underline.svg">breeze/16/format-text-underline.svg</file>
|
||||
<file alias="themes/qet/16x16/generic-device.svg">qet/scalable/16/generic-device.svg</file>
|
||||
<file alias="themes/qet/22x22/generic-device.svg">themes/generated/scalable-22/generic-device.svg</file>
|
||||
<file alias="themes/qet/32x32/generic-device.svg">qet/scalable/16/generic-device.svg</file>
|
||||
<file alias="themes/qet/48x48/generic-device.svg">qet/scalable/generic-device.svg</file>
|
||||
<file alias="themes/qet/64x64/generic-device.svg">qet/scalable/16/generic-device.svg</file>
|
||||
<file alias="themes/qet/128x128/generic-device.svg">qet/scalable/16/generic-device.svg</file>
|
||||
<file alias="themes/qet/16x16@2/generic-device.svg">qet/scalable/16/generic-device.svg</file>
|
||||
<file alias="themes/qet/22x22@2/generic-device.svg">themes/generated/scalable-22/generic-device.svg</file>
|
||||
<file alias="themes/qet/32x32@2/generic-device.svg">qet/scalable/16/generic-device.svg</file>
|
||||
<file alias="themes/qet/48x48@2/generic-device.svg">qet/scalable/generic-device.svg</file>
|
||||
<file alias="themes/qet/64x64@2/generic-device.svg">qet/scalable/16/generic-device.svg</file>
|
||||
<file alias="themes/qet/128x128@2/generic-device.svg">qet/scalable/16/generic-device.svg</file>
|
||||
<file alias="themes/qet/16x16/go-bottom.svg">breeze/16/go-bottom.svg</file>
|
||||
<file alias="themes/qet/22x22/go-bottom.svg">breeze/22/go-bottom.svg</file>
|
||||
<file alias="themes/qet/scalable/go-bottom.svg">themes/generated/breeze-24/go-bottom.svg</file>
|
||||
@@ -2623,6 +2636,7 @@
|
||||
<file>themes/qet-dark/scalable/folio-delete.svg</file>
|
||||
<file>themes/qet-dark/scalable/folio-new.svg</file>
|
||||
<file>themes/qet-dark/scalable/folio-properties.svg</file>
|
||||
<file>themes/qet-dark/scalable/generic-device.svg</file>
|
||||
<file>themes/qet-dark/scalable/label.svg</file>
|
||||
<file>themes/qet-dark/scalable/pdf-import.svg</file>
|
||||
<file>themes/qet-dark/scalable/snap-to-grid.svg</file>
|
||||
@@ -2994,6 +3008,8 @@
|
||||
<file>themes/qet-dark/breeze/16/format-text-underline.svg</file>
|
||||
<file>themes/qet-dark/breeze/22/format-text-underline.svg</file>
|
||||
<file>themes/qet-dark/breeze-24/format-text-underline.svg</file>
|
||||
<file>themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file>themes/qet-dark/scalable-22/generic-device.svg</file>
|
||||
<file>themes/qet-dark/breeze/16/go-bottom.svg</file>
|
||||
<file>themes/qet-dark/breeze/22/go-bottom.svg</file>
|
||||
<file>themes/qet-dark/breeze-24/go-bottom.svg</file>
|
||||
@@ -4520,6 +4536,18 @@
|
||||
<file alias="themes/qet-dark/48x48@2/format-text-underline.svg">themes/qet-dark/breeze-24/format-text-underline.svg</file>
|
||||
<file alias="themes/qet-dark/64x64@2/format-text-underline.svg">themes/qet-dark/breeze/16/format-text-underline.svg</file>
|
||||
<file alias="themes/qet-dark/128x128@2/format-text-underline.svg">themes/qet-dark/breeze/16/format-text-underline.svg</file>
|
||||
<file alias="themes/qet-dark/16x16/generic-device.svg">themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/22x22/generic-device.svg">themes/qet-dark/scalable-22/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/32x32/generic-device.svg">themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/48x48/generic-device.svg">themes/qet-dark/scalable/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/64x64/generic-device.svg">themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/128x128/generic-device.svg">themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/16x16@2/generic-device.svg">themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/22x22@2/generic-device.svg">themes/qet-dark/scalable-22/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/32x32@2/generic-device.svg">themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/48x48@2/generic-device.svg">themes/qet-dark/scalable/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/64x64@2/generic-device.svg">themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/128x128@2/generic-device.svg">themes/qet-dark/scalable-16/generic-device.svg</file>
|
||||
<file alias="themes/qet-dark/16x16/go-bottom.svg">themes/qet-dark/breeze/16/go-bottom.svg</file>
|
||||
<file alias="themes/qet-dark/22x22/go-bottom.svg">themes/qet-dark/breeze/22/go-bottom.svg</file>
|
||||
<file alias="themes/qet-dark/scalable/go-bottom.svg">themes/qet-dark/breeze-24/go-bottom.svg</file>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1" d="M 5.5 1.5 H 10.5 V 14.5 H 5.5 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1" d="M 1 4.5 H 5.5 M 1 7.5 H 5.5 M 10.5 4.5 H 15 M 10.5 7.5 H 15 M 1 12.5 H 5.5 M 10.5 12.5 H 15" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1" d="M 5.5 10 H 7 M 10.5 10 H 9" class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 697 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 7.75 3.75 H 16.25 V 20.25 H 7.75 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 2 7.5 H 7.75 M 2 11.5 H 7.75 M 16.25 7.5 H 22 M 16.25 11.5 H 22" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1.5" d="M 7.75 15.5 H 10 M 16.25 15.5 H 14" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 2 18.5 H 7.75 M 16.25 18.5 H 22" class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 818 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 1 22 22" width="22" height="22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#4d4d4d;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 7.75 3.75 H 16.25 V 20.25 H 7.75 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 2 7.5 H 7.75 M 2 11.5 H 7.75 M 16.25 7.5 H 22 M 16.25 11.5 H 22" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1.5" d="M 7.75 15.5 H 10 M 16.25 15.5 H 14" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 2 18.5 H 7.75 M 16.25 18.5 H 22" class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 818 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1" d="M 5.5 1.5 H 10.5 V 14.5 H 5.5 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1" d="M 1 4.5 H 5.5 M 1 7.5 H 5.5 M 10.5 4.5 H 15 M 10.5 7.5 H 15 M 1 12.5 H 5.5 M 10.5 12.5 H 15" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1" d="M 5.5 10 H 7 M 10.5 10 H 9" class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 697 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 1 22 22" width="22" height="22">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 7.75 3.75 H 16.25 V 20.25 H 7.75 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 2 7.5 H 7.75 M 2 11.5 H 7.75 M 16.25 7.5 H 22 M 16.25 11.5 H 22" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1.5" d="M 7.75 15.5 H 10 M 16.25 15.5 H 14" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 2 18.5 H 7.75 M 16.25 18.5 H 22" class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 818 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#dcdcdc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 7.75 3.75 H 16.25 V 20.25 H 7.75 Z" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 2 7.5 H 7.75 M 2 11.5 H 7.75 M 16.25 7.5 H 22 M 16.25 11.5 H 22" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-opacity:0.35;stroke-width:1.5" d="M 7.75 15.5 H 10 M 16.25 15.5 H 14" class="ColorScheme-Text"/>
|
||||
<path style="fill:none;stroke:currentColor;stroke-width:1.5" d="M 2 18.5 H 7.75 M 16.25 18.5 H 22" class="ColorScheme-Text"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 818 B |
@@ -183,6 +183,7 @@ SVGS = [
|
||||
"qet/scalable/folio-delete.svg",
|
||||
"qet/scalable/folio-new.svg",
|
||||
"qet/scalable/folio-properties.svg",
|
||||
"qet/scalable/generic-device.svg",
|
||||
"qet/scalable/label.svg",
|
||||
"qet/scalable/pdf-import.svg",
|
||||
"qet/scalable/snap-to-grid.svg",
|
||||
|
||||
@@ -207,7 +207,8 @@ QStringList DiagramToolbarSettings::defaultIds(const QString &name)
|
||||
QStringLiteral("diagrameditor.add_polyline"),
|
||||
QStringLiteral("diagrameditor.add_path"),
|
||||
QStringLiteral("diagrameditor.add_fillet"),
|
||||
QStringLiteral("diagrameditor.add_terminal_strip")};
|
||||
QStringLiteral("diagrameditor.add_terminal_strip"),
|
||||
QStringLiteral("diagrameditor.add_generic_device")};
|
||||
}
|
||||
if (name == QLatin1String("diagram_depth_toolbar")) {
|
||||
return {QStringLiteral("depth.forward"),
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "ElementsCollection/xmlelementcollection.h"
|
||||
#include "NameList/nameslist.h"
|
||||
#include "elementdialog.h"
|
||||
#include "genericdevice/genericdevicewizard.h"
|
||||
#include "qetapp.h"
|
||||
#include "qetgraphicsitem/element.h"
|
||||
#include "qetinformation.h"
|
||||
@@ -2416,6 +2417,64 @@ void DiagramView::generateCabinetThumbnails()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramView::addGenericDevice
|
||||
Edit > Add > Generic device. Open the wizard; on Finish, write the box
|
||||
it describes as an ordinary symbol into the folder "Generic devices" of
|
||||
this project's embedded collection and put it on the cursor, ready to
|
||||
be placed. The file name carries a short uuid, so two devices given the
|
||||
same name never overwrite each other.
|
||||
*/
|
||||
void DiagramView::addGenericDevice()
|
||||
{
|
||||
QETProject *project = m_diagram ? m_diagram->project() : nullptr;
|
||||
if (!project || project->isReadOnly() || m_diagram->isReadOnly()) {
|
||||
return;
|
||||
}
|
||||
XmlElementCollection *collection = project->embeddedElementCollection();
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Measured with the fonts the folio draws with, so names fit
|
||||
const GenericDevice::Fonts fonts{QETApp::diagramTextsFont(7),
|
||||
QETApp::dynamicTextsItemFont(9)};
|
||||
GenericDeviceWizard wizard(fonts, this);
|
||||
if (wizard.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
const GenericDevice::Spec spec = wizard.spec();
|
||||
|
||||
const QString dir_name = QStringLiteral("Generic devices");
|
||||
const QString dir_path = QStringLiteral("import/") + dir_name;
|
||||
if (!collection->exist(dir_path))
|
||||
{
|
||||
NamesList dir_names;
|
||||
dir_names.addName(QStringLiteral("en"), dir_name);
|
||||
//Stored per language, like an element's names: not tr(), which
|
||||
//would store the interface's language under "en"
|
||||
dir_names.addName(QStringLiteral("fr"), QStringLiteral("Appareils génériques"));
|
||||
if (!collection->createDir(QStringLiteral("import"), dir_name, dir_names)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const QString file_name = QET::stringToFileName(spec.name)
|
||||
+ QLatin1Char('-')
|
||||
+ QUuid::createUuid().toString(QUuid::Id128).left(8)
|
||||
+ QStringLiteral(".elmt");
|
||||
QDomDocument doc;
|
||||
const QDomElement definition = GenericDevice::toDefinition(
|
||||
spec, fonts, doc, QetVersion::currentVersion().toString());
|
||||
if (!collection->addElementDefinition(dir_path, file_name, definition)) {
|
||||
return;
|
||||
}
|
||||
project->setModified(true);
|
||||
|
||||
const ElementsLocation location(dir_path + QLatin1Char('/') + file_name, project);
|
||||
startElementPlacement(location, defaultPlacementPos());
|
||||
}
|
||||
|
||||
/**
|
||||
@return l'editeur de schemas parent ou 0
|
||||
*/
|
||||
|
||||
@@ -196,6 +196,7 @@ class DiagramView : public PaletteGraphicsView
|
||||
void adjustSceneRect();
|
||||
void updateWindowTitle();
|
||||
void resetConductors();
|
||||
void addGenericDevice();
|
||||
|
||||
private slots:
|
||||
void adjustGridToZoom();
|
||||
|
||||
@@ -257,6 +257,29 @@ ElementPictureFactory::~ElementPictureFactory()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementPictureFactory::pictureFromDefinition
|
||||
@return the drawing of an element @a definition that is in no
|
||||
collection yet, such as one being made by a dialog, drawn by the same
|
||||
code as a placed element. Not cached.
|
||||
*/
|
||||
QPicture ElementPictureFactory::pictureFromDefinition(const QDomElement &definition) const
|
||||
{
|
||||
QPicture picture;
|
||||
QPainter painter(&picture);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setRenderHint(QPainter::TextAntialiasing, true);
|
||||
primitives primitives_;
|
||||
const QDomElement description = definition.firstChildElement(QStringLiteral("description"));
|
||||
for (QDomElement part = description.firstChildElement() ; !part.isNull() ;
|
||||
part = part.nextSiblingElement()) {
|
||||
parseElement(part, painter, primitives_);
|
||||
}
|
||||
painter.end();
|
||||
qDeleteAll(primitives_.m_texts);
|
||||
return picture;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementPictureFactory::build
|
||||
Build the picture from location.
|
||||
|
||||
@@ -90,6 +90,7 @@ class ElementPictureFactory
|
||||
QPixmap pixmap(const ElementsLocation &location);
|
||||
ElementPictureFactory::primitives getPrimitives(const ElementsLocation &location);
|
||||
void dropCache(const ElementsLocation &location);
|
||||
QPicture pictureFromDefinition(const QDomElement &definition) const;
|
||||
|
||||
private:
|
||||
ElementPictureFactory() {}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
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 GENERICDEVICE_H
|
||||
#define GENERICDEVICE_H
|
||||
|
||||
#include <QDomElement>
|
||||
#include <QFont>
|
||||
#include <QLineF>
|
||||
#include <QList>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
|
||||
/**
|
||||
@brief The GenericDevice namespace
|
||||
A generic device is a plain box symbol with terminals on any side, made
|
||||
by the generic device wizard for a device used too rarely to draw a
|
||||
symbol by hand (a VSD, a power supply). Everything here is pure: a
|
||||
description of the device (Spec) goes in, an ordinary element
|
||||
definition comes out. No widget, no scene, no project.
|
||||
|
||||
Coordinates are the element's own, with (0, 0) at the top-left corner
|
||||
of the box body. Every terminal, stub and the body size are multiples
|
||||
of the grid step, so a device placed on the grid puts every terminal on
|
||||
a grid point and wires to it run straight.
|
||||
*/
|
||||
namespace GenericDevice
|
||||
{
|
||||
enum Side { Left = 0, Right, Top, Bottom };
|
||||
constexpr int SideCount = 4;
|
||||
|
||||
constexpr int Grid = 10; ///< grid step of the folio
|
||||
constexpr int Stub = 10; ///< terminal point is this far outside the body
|
||||
constexpr int MinWidth = 40;
|
||||
constexpr int MinHeight = 30;
|
||||
constexpr int Inset = 3; ///< names sit this far inside the body edge
|
||||
constexpr int MaxPatternCount = 200; ///< largest {n} or {a-b} a pattern may expand
|
||||
|
||||
/**
|
||||
@brief The Slot struct
|
||||
One position along a side: a terminal, a gap the user asked for, or
|
||||
padding added to line groups up across opposite sides. Gaps and
|
||||
padding take up a pitch of space and hold no terminal; a gap may
|
||||
carry a divider mark, padding never does.
|
||||
*/
|
||||
struct Slot
|
||||
{
|
||||
enum Kind { Terminal, Gap, Padding };
|
||||
Kind kind = Terminal;
|
||||
QString name;
|
||||
QString function;
|
||||
QString type = QStringLiteral("Generic"); ///< Generic, Inner or Outer
|
||||
QUuid uuid;
|
||||
|
||||
bool isTerminal() const { return kind == Terminal; }
|
||||
static Slot terminal(const QString &name) { Slot s; s.name = name; return s; }
|
||||
static Slot gap() { Slot s; s.kind = Gap; return s; }
|
||||
static Slot padding() { Slot s; s.kind = Padding; return s; }
|
||||
/// The text drawn beside the terminal: "name function", or the name.
|
||||
QString shownText() const;
|
||||
};
|
||||
using Slots = QList<Slot>;
|
||||
|
||||
/**
|
||||
@brief The Spec struct
|
||||
Everything the wizard collects. sides[] are ordered top to bottom
|
||||
(left, right) and left to right (top, bottom).
|
||||
*/
|
||||
struct Spec
|
||||
{
|
||||
QString name;
|
||||
QString label;
|
||||
QString manufacturer;
|
||||
QString reference;
|
||||
QString description;
|
||||
bool show_reference = true;
|
||||
|
||||
Slots sides[SideCount];
|
||||
int pitch = 20; ///< 10 to 50, a multiple of the grid
|
||||
int extra_width = 0; ///< grid squares added to the automatic size
|
||||
int extra_height = 0;
|
||||
bool mark_gaps = true; ///< draw a divider mark on each gap
|
||||
bool line_up = true; ///< pad groups so dividers meet across the box
|
||||
|
||||
int terminalCount() const;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The Fonts struct
|
||||
What the names and the label are drawn with. Measuring with the font
|
||||
the folio will draw with is what keeps names inside the box.
|
||||
*/
|
||||
struct Fonts
|
||||
{
|
||||
QFont names;
|
||||
QFont label;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The Layout struct
|
||||
Where everything goes, computed from a Spec. Pure geometry.
|
||||
*/
|
||||
struct Layout
|
||||
{
|
||||
struct PlacedTerminal {
|
||||
Slot slot;
|
||||
Side side;
|
||||
QPointF point; ///< the terminal point, outside the body
|
||||
QPointF edge; ///< where its stub meets the body
|
||||
};
|
||||
struct Name {
|
||||
QPointF pos; ///< baseline-left, as a static text part is anchored
|
||||
QString text;
|
||||
int rotation = 0;
|
||||
};
|
||||
|
||||
int body_width = 0;
|
||||
int body_height = 0;
|
||||
int auto_width = 0; ///< the size before the extra width/height
|
||||
int auto_height = 0;
|
||||
Slots sides[SideCount]; ///< the spec's list after line-up padding
|
||||
QList<PlacedTerminal> terminals;
|
||||
QList<Name> names;
|
||||
QList<QLineF> dividers;
|
||||
QRectF label_box; ///< x, y, width and the height of one line
|
||||
QStringList warnings;
|
||||
};
|
||||
|
||||
struct PatternResult
|
||||
{
|
||||
Slots list;
|
||||
QString error; ///< empty when the pattern was read
|
||||
bool ok() const { return error.isEmpty(); }
|
||||
};
|
||||
|
||||
struct PasteResult
|
||||
{
|
||||
Slots sides[SideCount];
|
||||
bool has_side[SideCount] = {false, false, false, false};
|
||||
int rows = 0; ///< rows read
|
||||
QStringList bad_rows; ///< rows that could not be read, as typed
|
||||
};
|
||||
|
||||
QString sideName(Side side);
|
||||
|
||||
PatternResult expandPattern(const QString &pattern);
|
||||
QString toPattern(const Slots &list, bool *expressible = nullptr);
|
||||
Slots applyPattern(const Slots ¤t, const Slots &expanded);
|
||||
Slots groupEvery(const Slots &list, int n);
|
||||
Slots resize(const Slots &list, int count, int group_every);
|
||||
void lineUpGroups(Slots &a, Slots &b);
|
||||
void numberPins(Spec &spec);
|
||||
bool hasNumberedPins(const Spec &spec);
|
||||
void nameUnnamed(Spec &spec);
|
||||
PasteResult parsePastedTable(const QString &text);
|
||||
|
||||
Layout layout(const Spec &spec, const Fonts &fonts);
|
||||
QDomElement toDefinition(const Spec &spec,
|
||||
const Fonts &fonts,
|
||||
QDomDocument &document,
|
||||
const QString &version = QString());
|
||||
bool fromDefinition(const QDomElement &definition,
|
||||
const Fonts &fonts,
|
||||
Spec *spec);
|
||||
}
|
||||
|
||||
#endif // GENERICDEVICE_H
|
||||
@@ -0,0 +1,760 @@
|
||||
/*
|
||||
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 "genericdevicewizard.h"
|
||||
|
||||
#include "../editor/terminalnamecheck.h"
|
||||
#include "../factory/elementpicturefactory.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPicture>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
using namespace GenericDevice;
|
||||
|
||||
namespace {
|
||||
|
||||
/// After terminals were added: renumber every pin if the names were
|
||||
/// still the automatic ones, otherwise only name the new, unnamed ones.
|
||||
void nameNewTerminals(Spec &spec, bool were_numbered)
|
||||
{
|
||||
if (were_numbered)
|
||||
numberPins(spec);
|
||||
else
|
||||
nameUnnamed(spec);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
@brief The GenericDevicePreview class
|
||||
The box as it will be placed, drawn by ElementPictureFactory like any
|
||||
placed element, with its label and reference, size and warnings.
|
||||
*/
|
||||
class GenericDevicePreview : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit GenericDevicePreview(QWidget *parent = nullptr) :
|
||||
QWidget(parent)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_canvas = new Canvas(this);
|
||||
m_size = new QLabel(this);
|
||||
m_size->setAlignment(Qt::AlignHCenter);
|
||||
m_warnings = new QLabel(this);
|
||||
m_warnings->setWordWrap(true);
|
||||
layout->addWidget(m_canvas, 1);
|
||||
layout->addWidget(m_size);
|
||||
layout->addWidget(m_warnings);
|
||||
setMinimumWidth(280);
|
||||
}
|
||||
|
||||
void setSpec(const Spec &spec, const Fonts &fonts)
|
||||
{
|
||||
QDomDocument document;
|
||||
const QDomElement definition = toDefinition(spec, fonts, document);
|
||||
m_canvas->picture = ElementPictureFactory::instance()->pictureFromDefinition(definition);
|
||||
m_canvas->layout = GenericDevice::layout(spec, fonts);
|
||||
m_canvas->label = spec.label.trimmed();
|
||||
m_canvas->reference = spec.show_reference ? spec.reference.trimmed() : QString();
|
||||
m_canvas->fonts = fonts;
|
||||
m_canvas->update();
|
||||
|
||||
const Layout &l = m_canvas->layout;
|
||||
m_size->setText(QCoreApplication::translate("GenericDeviceWizard", "Box: %1 × %2 grid squares")
|
||||
.arg(l.body_width / Grid).arg(l.body_height / Grid));
|
||||
m_warnings->setText(l.warnings.join(QLatin1Char('\n')));
|
||||
m_warnings->setVisible(!l.warnings.isEmpty());
|
||||
}
|
||||
|
||||
private:
|
||||
class Canvas : public QWidget
|
||||
{
|
||||
public:
|
||||
using QWidget::QWidget;
|
||||
QPicture picture;
|
||||
Layout layout;
|
||||
Fonts fonts;
|
||||
QString label;
|
||||
QString reference;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), Qt::white);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QRectF bounds(-Stub, -Stub, layout.body_width + 2 * Stub,
|
||||
layout.body_height + 2 * Stub);
|
||||
bounds = bounds.united(layout.label_box).adjusted(-10, -10, 10, 10);
|
||||
const qreal scale = std::min({width() / bounds.width(),
|
||||
height() / bounds.height(), 3.0});
|
||||
painter.translate(width() / 2.0, height() / 2.0);
|
||||
painter.scale(scale, scale);
|
||||
painter.translate(-bounds.center());
|
||||
|
||||
painter.drawPicture(0, 0, picture);
|
||||
|
||||
//Terminals are items on a folio, not drawing: mark the
|
||||
//point each wire will reach
|
||||
painter.setPen(QPen(QColor(0, 102, 204), 1.5 / scale));
|
||||
for (const auto &t : layout.terminals) {
|
||||
painter.drawLine(t.point + QPointF(-2, 0), t.point + QPointF(2, 0));
|
||||
painter.drawLine(t.point + QPointF(0, -2), t.point + QPointF(0, 2));
|
||||
}
|
||||
|
||||
painter.setPen(Qt::black);
|
||||
const QRectF box = layout.label_box;
|
||||
painter.setFont(fonts.label);
|
||||
painter.drawText(QRectF(box.x(), box.y(), box.width(), box.height()),
|
||||
Qt::AlignHCenter | Qt::AlignTop, label);
|
||||
painter.setFont(fonts.names);
|
||||
painter.drawText(QRectF(box.x(), box.bottom(), box.width(), box.height()),
|
||||
Qt::AlignHCenter | Qt::AlignTop, reference);
|
||||
}
|
||||
};
|
||||
|
||||
Canvas *m_canvas = nullptr;
|
||||
QLabel *m_size = nullptr;
|
||||
QLabel *m_warnings = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The GenericDeviceLayoutPage class
|
||||
Page 1: how many terminals on each side, how they are grouped, the
|
||||
pitch and any extra size.
|
||||
*/
|
||||
class GenericDeviceLayoutPage : public QWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GenericDeviceLayoutPage(GenericDeviceWizard *wizard) :
|
||||
QWizardPage(wizard),
|
||||
m_wizard(wizard)
|
||||
{
|
||||
setTitle(tr("Layout"));
|
||||
//Finish from here makes the device with what is set so far
|
||||
setFinalPage(true);
|
||||
setSubTitle(tr("How many terminals on each side, 0 for none. "
|
||||
"They are numbered 1…n; name them on the next page."));
|
||||
|
||||
auto *grid = new QGridLayout();
|
||||
grid->addWidget(new QLabel(tr("Terminals")), 0, 1);
|
||||
grid->addWidget(new QLabel(tr("Group every")), 0, 2);
|
||||
for (int side = 0 ; side < SideCount ; ++side)
|
||||
{
|
||||
grid->addWidget(new QLabel(sideName(Side(side))), side + 1, 0);
|
||||
m_count[side] = new QSpinBox();
|
||||
m_count[side]->setRange(0, MaxPatternCount);
|
||||
m_group[side] = new QSpinBox();
|
||||
m_group[side]->setRange(0, MaxPatternCount);
|
||||
m_group[side]->setSpecialValueText(tr("no groups"));
|
||||
m_group[side]->setToolTip(tr("Leave a gap after every this many terminals"));
|
||||
grid->addWidget(m_count[side], side + 1, 1);
|
||||
grid->addWidget(m_group[side], side + 1, 2);
|
||||
connect(m_count[side], qOverload<int>(&QSpinBox::valueChanged),
|
||||
this, [this, side](int value) { countChanged(Side(side), value); });
|
||||
connect(m_group[side], qOverload<int>(&QSpinBox::valueChanged),
|
||||
this, [this, side](int value) { groupChanged(Side(side), value); });
|
||||
}
|
||||
|
||||
m_mark_gaps = new QCheckBox(tr("Mark gaps with a divider"));
|
||||
m_line_up = new QCheckBox(tr("Line up groups across opposite sides"));
|
||||
m_line_up->setToolTip(tr("Adds blank rows to the shorter group, so that "
|
||||
"the gaps on facing sides fall on the same row"));
|
||||
|
||||
m_pitch = new QSpinBox();
|
||||
m_pitch->setRange(1, 5);
|
||||
m_pitch->setSuffix(tr(" grid squares"));
|
||||
m_extra_width = new QSpinBox();
|
||||
m_extra_width->setRange(0, 100);
|
||||
m_extra_width->setSuffix(tr(" grid squares"));
|
||||
m_extra_height = new QSpinBox();
|
||||
m_extra_height->setRange(0, 100);
|
||||
m_extra_height->setSuffix(tr(" grid squares"));
|
||||
|
||||
auto *form = new QFormLayout();
|
||||
form->addRow(tr("Space between terminals:"), m_pitch);
|
||||
form->addRow(tr("Extra width:"), m_extra_width);
|
||||
form->addRow(tr("Extra height:"), m_extra_height);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(grid);
|
||||
layout->addWidget(m_mark_gaps);
|
||||
layout->addWidget(m_line_up);
|
||||
layout->addLayout(form);
|
||||
layout->addStretch();
|
||||
|
||||
connect(m_mark_gaps, &QCheckBox::toggled, this, [this](bool on) {
|
||||
m_wizard->editableSpec().mark_gaps = on; changed(); });
|
||||
connect(m_line_up, &QCheckBox::toggled, this, [this](bool on) {
|
||||
m_wizard->editableSpec().line_up = on; changed(); });
|
||||
connect(m_pitch, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
||||
m_wizard->editableSpec().pitch = v * Grid; changed(); });
|
||||
connect(m_extra_width, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
||||
m_wizard->editableSpec().extra_width = v; changed(); });
|
||||
connect(m_extra_height, qOverload<int>(&QSpinBox::valueChanged), this, [this](int v) {
|
||||
m_wizard->editableSpec().extra_height = v; changed(); });
|
||||
}
|
||||
|
||||
void initializePage() override
|
||||
{
|
||||
//Back from page 2 shows what was done there
|
||||
const Spec &spec = m_wizard->editableSpec();
|
||||
for (int side = 0 ; side < SideCount ; ++side) {
|
||||
QSignalBlocker blocker(m_count[side]);
|
||||
int count = 0;
|
||||
for (const Slot &s : spec.sides[side])
|
||||
if (s.isTerminal()) ++count;
|
||||
m_count[side]->setValue(count);
|
||||
}
|
||||
QSignalBlocker b1(m_mark_gaps), b2(m_line_up), b3(m_pitch), b4(m_extra_width), b5(m_extra_height);
|
||||
m_mark_gaps->setChecked(spec.mark_gaps);
|
||||
m_line_up->setChecked(spec.line_up);
|
||||
m_pitch->setValue(spec.pitch / Grid);
|
||||
m_extra_width->setValue(spec.extra_width);
|
||||
m_extra_height->setValue(spec.extra_height);
|
||||
}
|
||||
|
||||
/// Going Back must never change anything (scope §8)
|
||||
void cleanupPage() override {}
|
||||
|
||||
bool isComplete() const override
|
||||
{
|
||||
return m_wizard->editableSpec().terminalCount() > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
void countChanged(Side side, int value)
|
||||
{
|
||||
Spec &spec = m_wizard->editableSpec();
|
||||
const bool numbered = hasNumberedPins(spec);
|
||||
spec.sides[side] = GenericDevice::resize(spec.sides[side], value, m_group[side]->value());
|
||||
nameNewTerminals(spec, numbered);
|
||||
changed();
|
||||
}
|
||||
|
||||
void groupChanged(Side side, int value)
|
||||
{
|
||||
Spec &spec = m_wizard->editableSpec();
|
||||
const Slots regrouped = groupEvery(spec.sides[side], value);
|
||||
const Slots expected = groupEvery(spec.sides[side], m_group_value[side]);
|
||||
auto same = [](const Slots &a, const Slots &b) {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (int i = 0 ; i < a.size() ; ++i)
|
||||
if (a[i].kind != b[i].kind) return false;
|
||||
return true;
|
||||
};
|
||||
//Gaps typed on page 2 would be replaced: say so first
|
||||
if (!same(spec.sides[side], expected)
|
||||
&& QMessageBox::question(this, tr("Group terminals"),
|
||||
tr("Grouping replaces the gaps you made on the %1 side. Continue?")
|
||||
.arg(sideName(side).toLower())) != QMessageBox::Yes)
|
||||
{
|
||||
QSignalBlocker blocker(m_group[side]);
|
||||
m_group[side]->setValue(m_group_value[side]);
|
||||
return;
|
||||
}
|
||||
m_group_value[side] = value;
|
||||
spec.sides[side] = regrouped;
|
||||
changed();
|
||||
}
|
||||
|
||||
void changed()
|
||||
{
|
||||
m_wizard->specChanged();
|
||||
emit completeChanged();
|
||||
}
|
||||
|
||||
GenericDeviceWizard *m_wizard;
|
||||
QSpinBox *m_count[SideCount];
|
||||
QSpinBox *m_group[SideCount];
|
||||
int m_group_value[SideCount] = {0, 0, 0, 0};
|
||||
QCheckBox *m_mark_gaps;
|
||||
QCheckBox *m_line_up;
|
||||
QSpinBox *m_pitch;
|
||||
QSpinBox *m_extra_width;
|
||||
QSpinBox *m_extra_height;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The GenericDeviceTerminalsPage class
|
||||
Page 2: one table per side, and the same list as a pattern line above
|
||||
it. Editing either rewrites the other.
|
||||
*/
|
||||
class GenericDeviceTerminalsPage : public QWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GenericDeviceTerminalsPage(GenericDeviceWizard *wizard) :
|
||||
QWizardPage(wizard),
|
||||
m_wizard(wizard)
|
||||
{
|
||||
setTitle(tr("Terminals"));
|
||||
//Finish from here makes the device with what is set so far
|
||||
setFinalPage(true);
|
||||
setSubTitle(tr("Name the terminals. A side can be typed as one line: "
|
||||
"In{4} gives In1 to In4, Q{z4} Q0 to Q3, X{3-6} X3 to X6, "
|
||||
"and an empty entry is a gap."));
|
||||
|
||||
m_tabs = new QTabWidget();
|
||||
for (int side = 0 ; side < SideCount ; ++side)
|
||||
{
|
||||
auto *tab = new QWidget();
|
||||
auto *layout = new QVBoxLayout(tab);
|
||||
auto *line = new QHBoxLayout();
|
||||
line->addWidget(new QLabel(tr("As a line:")));
|
||||
m_pattern[side] = new QLineEdit();
|
||||
m_pattern[side]->setPlaceholderText(tr("e.g. L1,L2,L3,,PE"));
|
||||
line->addWidget(m_pattern[side], 1);
|
||||
layout->addLayout(line);
|
||||
m_error[side] = new QLabel();
|
||||
m_error[side]->setWordWrap(true);
|
||||
m_error[side]->setStyleSheet(QStringLiteral("color: #c0392b"));
|
||||
m_error[side]->hide();
|
||||
layout->addWidget(m_error[side]);
|
||||
|
||||
m_table[side] = new QTableWidget(0, 3);
|
||||
m_table[side]->setHorizontalHeaderLabels({tr("Name"), tr("Function"), tr("Type")});
|
||||
m_table[side]->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
m_table[side]->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
layout->addWidget(m_table[side], 1);
|
||||
m_tabs->addTab(tab, sideName(Side(side)));
|
||||
|
||||
connect(m_pattern[side], &QLineEdit::textEdited, this,
|
||||
[this, side](const QString &text) { patternEdited(Side(side), text); });
|
||||
connect(m_table[side], &QTableWidget::itemChanged, this,
|
||||
[this, side](QTableWidgetItem *item) { itemChanged(Side(side), item); });
|
||||
}
|
||||
|
||||
auto *add = new QPushButton(tr("Add terminal"));
|
||||
auto *gap = new QPushButton(tr("Add gap"));
|
||||
auto *remove = new QPushButton(tr("Delete"));
|
||||
auto *up = new QPushButton(tr("Up"));
|
||||
auto *down = new QPushButton(tr("Down"));
|
||||
auto *buttons = new QHBoxLayout();
|
||||
for (QPushButton *b : {add, gap, remove, up, down})
|
||||
buttons->addWidget(b);
|
||||
buttons->addStretch();
|
||||
|
||||
auto *number = new QPushButton(tr("Number pins 1…n"));
|
||||
number->setToolTip(tr("Rename every terminal by its pin number, "
|
||||
"counter-clockwise from the top of the left side"));
|
||||
auto *paste = new QPushButton(tr("Paste table"));
|
||||
paste->setToolTip(tr("Rows of side, name, function and type, copied from "
|
||||
"a spreadsheet or a CSV file. Side is left, right, top, "
|
||||
"bottom or L, R, T, B; an empty name is a gap."));
|
||||
auto *global = new QHBoxLayout();
|
||||
global->addWidget(number);
|
||||
global->addWidget(paste);
|
||||
global->addStretch();
|
||||
|
||||
m_paste_report = new QLabel();
|
||||
m_paste_report->setWordWrap(true);
|
||||
m_paste_report->hide();
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(m_tabs, 1);
|
||||
layout->addLayout(buttons);
|
||||
layout->addLayout(global);
|
||||
layout->addWidget(m_paste_report);
|
||||
|
||||
connect(add, &QPushButton::clicked, this, [this] { insert(Slot::terminal(QString())); });
|
||||
connect(gap, &QPushButton::clicked, this, [this] { insert(Slot::gap()); });
|
||||
connect(remove, &QPushButton::clicked, this, &GenericDeviceTerminalsPage::removeRows);
|
||||
connect(up, &QPushButton::clicked, this, [this] { move(-1); });
|
||||
connect(down, &QPushButton::clicked, this, [this] { move(1); });
|
||||
connect(number, &QPushButton::clicked, this, [this] {
|
||||
numberPins(m_wizard->editableSpec());
|
||||
rebuildAll();
|
||||
changed();
|
||||
});
|
||||
connect(paste, &QPushButton::clicked, this, &GenericDeviceTerminalsPage::pasteTable);
|
||||
}
|
||||
|
||||
void initializePage() override
|
||||
{
|
||||
rebuildAll();
|
||||
//Open on the first side that has terminals
|
||||
const Spec &spec = m_wizard->editableSpec();
|
||||
for (int side = 0 ; side < SideCount ; ++side)
|
||||
if (!spec.sides[side].isEmpty()) { m_tabs->setCurrentIndex(side); break; }
|
||||
}
|
||||
|
||||
void cleanupPage() override {}
|
||||
|
||||
bool isComplete() const override
|
||||
{
|
||||
return m_wizard->editableSpec().terminalCount() > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
Side currentSide() const { return Side(m_tabs->currentIndex()); }
|
||||
|
||||
void rebuildAll()
|
||||
{
|
||||
for (int side = 0 ; side < SideCount ; ++side) {
|
||||
rebuildTable(Side(side));
|
||||
updatePattern(Side(side));
|
||||
}
|
||||
}
|
||||
|
||||
void rebuildTable(Side side)
|
||||
{
|
||||
QTableWidget *table = m_table[side];
|
||||
const Slots &list = m_wizard->editableSpec().sides[side];
|
||||
QSignalBlocker blocker(table);
|
||||
table->clearSpans();
|
||||
table->setRowCount(int(list.size()));
|
||||
for (int row = 0 ; row < list.size() ; ++row)
|
||||
{
|
||||
const Slot &slot = list.at(row);
|
||||
table->removeCellWidget(row, 2);
|
||||
if (!slot.isTerminal())
|
||||
{
|
||||
auto *item = new QTableWidgetItem(tr("gap"));
|
||||
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
item->setForeground(palette().color(QPalette::PlaceholderText));
|
||||
item->setTextAlignment(Qt::AlignCenter);
|
||||
table->setItem(row, 0, item);
|
||||
delete table->takeItem(row, 1);
|
||||
table->setSpan(row, 0, 1, 3);
|
||||
continue;
|
||||
}
|
||||
table->setItem(row, 0, new QTableWidgetItem(slot.name));
|
||||
table->setItem(row, 1, new QTableWidgetItem(slot.function));
|
||||
auto *type = new QComboBox();
|
||||
type->addItem(tr("Generic"), QStringLiteral("Generic"));
|
||||
type->addItem(tr("Inner"), QStringLiteral("Inner"));
|
||||
type->addItem(tr("Outer"), QStringLiteral("Outer"));
|
||||
type->setCurrentIndex(std::max(0, type->findData(slot.type)));
|
||||
connect(type, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
[this, side, type, row](int) {
|
||||
Slots &l = m_wizard->editableSpec().sides[side];
|
||||
if (row < l.size()) l[row].type = type->currentData().toString();
|
||||
changed();
|
||||
});
|
||||
table->setCellWidget(row, 2, type);
|
||||
}
|
||||
markRepeatedNames();
|
||||
}
|
||||
|
||||
/// A repeated name gets a warning icon on its row, never a block
|
||||
void markRepeatedNames()
|
||||
{
|
||||
const Spec &spec = m_wizard->editableSpec();
|
||||
QStringList all;
|
||||
for (const Slots &side : spec.sides)
|
||||
for (const Slot &s : side)
|
||||
if (s.isTerminal()) all << s.name.trimmed();
|
||||
QSet<QString> repeated;
|
||||
for (const auto &pair : TerminalNameCheck::repeatedNames(all))
|
||||
repeated << pair.first;
|
||||
|
||||
for (int side = 0 ; side < SideCount ; ++side)
|
||||
{
|
||||
QSignalBlocker blocker(m_table[side]);
|
||||
const Slots &list = spec.sides[side];
|
||||
for (int row = 0 ; row < list.size() ; ++row)
|
||||
{
|
||||
QTableWidgetItem *item = m_table[side]->item(row, 0);
|
||||
if (!item || !list.at(row).isTerminal()) continue;
|
||||
const bool twice = repeated.contains(list.at(row).name.trimmed());
|
||||
item->setIcon(twice ? QIcon::fromTheme(QStringLiteral("dialog-warning")) : QIcon());
|
||||
item->setToolTip(twice ? tr("This name is used more than once. "
|
||||
"Fine if the device repeats it, as N or PE often are.")
|
||||
: QString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updatePattern(Side side)
|
||||
{
|
||||
bool expressible = true;
|
||||
const QString text = toPattern(m_wizard->editableSpec().sides[side], &expressible);
|
||||
if (!m_pattern[side]->hasFocus())
|
||||
m_pattern[side]->setText(text);
|
||||
m_pattern[side]->setReadOnly(!expressible);
|
||||
m_pattern[side]->setToolTip(expressible ? QString()
|
||||
: tr("A name holds a comma or a brace, which a line "
|
||||
"cannot say: edit this side in the table."));
|
||||
m_error[side]->hide();
|
||||
}
|
||||
|
||||
void patternEdited(Side side, const QString &text)
|
||||
{
|
||||
const PatternResult result = expandPattern(text);
|
||||
if (!result.ok()) {
|
||||
m_error[side]->setText(result.error);
|
||||
m_error[side]->show();
|
||||
return;
|
||||
}
|
||||
m_error[side]->hide();
|
||||
Slots &list = m_wizard->editableSpec().sides[side];
|
||||
list = applyPattern(list, result.list);
|
||||
rebuildTable(side);
|
||||
changed();
|
||||
}
|
||||
|
||||
void itemChanged(Side side, QTableWidgetItem *item)
|
||||
{
|
||||
Slots &list = m_wizard->editableSpec().sides[side];
|
||||
if (item->row() >= list.size() || !list[item->row()].isTerminal())
|
||||
return;
|
||||
if (item->column() == 0)
|
||||
list[item->row()].name = item->text().trimmed();
|
||||
else if (item->column() == 1)
|
||||
list[item->row()].function = item->text().trimmed();
|
||||
updatePattern(side);
|
||||
markRepeatedNames();
|
||||
changed();
|
||||
}
|
||||
|
||||
void insert(const Slot &slot)
|
||||
{
|
||||
const Side side = currentSide();
|
||||
Spec &spec = m_wizard->editableSpec();
|
||||
const bool numbered = hasNumberedPins(spec);
|
||||
const int current = m_table[side]->currentRow();
|
||||
const int row = current < 0 ? int(spec.sides[side].size()) : current + 1;
|
||||
spec.sides[side].insert(row, slot);
|
||||
if (slot.isTerminal())
|
||||
nameNewTerminals(spec, numbered);
|
||||
rebuildAll();
|
||||
m_table[side]->setCurrentCell(row, 0);
|
||||
changed();
|
||||
}
|
||||
|
||||
void removeRows()
|
||||
{
|
||||
const Side side = currentSide();
|
||||
QList<int> rows;
|
||||
for (const QModelIndex &index : m_table[side]->selectionModel()->selectedRows())
|
||||
rows << index.row();
|
||||
std::sort(rows.rbegin(), rows.rend());
|
||||
Spec &spec = m_wizard->editableSpec();
|
||||
const bool numbered = hasNumberedPins(spec);
|
||||
for (int row : rows)
|
||||
spec.sides[side].removeAt(row);
|
||||
if (numbered) numberPins(spec);
|
||||
rebuildAll();
|
||||
changed();
|
||||
}
|
||||
|
||||
void move(int step)
|
||||
{
|
||||
const Side side = currentSide();
|
||||
Slots &list = m_wizard->editableSpec().sides[side];
|
||||
const int row = m_table[side]->currentRow();
|
||||
const int to = row + step;
|
||||
if (row < 0 || to < 0 || to >= list.size())
|
||||
return;
|
||||
list.swapItemsAt(row, to);
|
||||
rebuildTable(side);
|
||||
updatePattern(side);
|
||||
m_table[side]->setCurrentCell(to, 0);
|
||||
changed();
|
||||
}
|
||||
|
||||
void pasteTable()
|
||||
{
|
||||
const PasteResult result = parsePastedTable(QApplication::clipboard()->text());
|
||||
if (!result.rows)
|
||||
{
|
||||
m_paste_report->setText(tr("Nothing to paste: copy rows of side, name, function "
|
||||
"from a spreadsheet or a CSV file first."));
|
||||
m_paste_report->show();
|
||||
return;
|
||||
}
|
||||
Spec &spec = m_wizard->editableSpec();
|
||||
for (int side = 0 ; side < SideCount ; ++side)
|
||||
if (result.has_side[side]) spec.sides[side] = result.sides[side];
|
||||
rebuildAll();
|
||||
changed();
|
||||
if (result.bad_rows.isEmpty()) {
|
||||
m_paste_report->setText(tr("Pasted %n row(s).", "", result.rows));
|
||||
} else {
|
||||
m_paste_report->setText(tr("Pasted %1 rows; skipped these, which could not be read: %2")
|
||||
.arg(result.rows)
|
||||
.arg(result.bad_rows.join(QStringLiteral(" · "))));
|
||||
}
|
||||
m_paste_report->show();
|
||||
}
|
||||
|
||||
void changed()
|
||||
{
|
||||
m_wizard->specChanged();
|
||||
emit completeChanged();
|
||||
}
|
||||
|
||||
GenericDeviceWizard *m_wizard;
|
||||
QTabWidget *m_tabs;
|
||||
QLineEdit *m_pattern[SideCount];
|
||||
QLabel *m_error[SideCount];
|
||||
QTableWidget *m_table[SideCount];
|
||||
QLabel *m_paste_report;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The GenericDeviceInformationPage class
|
||||
Page 3: the name of the symbol and the element information.
|
||||
*/
|
||||
class GenericDeviceInformationPage : public QWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GenericDeviceInformationPage(GenericDeviceWizard *wizard) :
|
||||
QWizardPage(wizard),
|
||||
m_wizard(wizard)
|
||||
{
|
||||
setTitle(tr("Information"));
|
||||
setSubTitle(tr("The name is how the symbol is listed in the project's "
|
||||
"collection; the rest can be changed later on the placed device."));
|
||||
|
||||
m_name = new QLineEdit();
|
||||
m_name->setPlaceholderText(tr("Generic device"));
|
||||
m_label = new QLineEdit();
|
||||
m_label->setPlaceholderText(tr("e.g. -U1"));
|
||||
m_manufacturer = new QLineEdit();
|
||||
m_reference = new QLineEdit();
|
||||
m_description = new QLineEdit();
|
||||
m_show_reference = new QCheckBox(tr("Show the manufacturer reference in the box"));
|
||||
|
||||
auto *form = new QFormLayout(this);
|
||||
form->addRow(tr("Name:"), m_name);
|
||||
form->addRow(tr("Label:"), m_label);
|
||||
form->addRow(tr("Manufacturer:"), m_manufacturer);
|
||||
form->addRow(tr("Manufacturer reference:"), m_reference);
|
||||
form->addRow(tr("Description:"), m_description);
|
||||
form->addRow(m_show_reference);
|
||||
|
||||
auto bind = [this](QLineEdit *edit, QString Spec::*field) {
|
||||
connect(edit, &QLineEdit::textChanged, this, [this, field](const QString &text) {
|
||||
m_wizard->editableSpec().*field = text;
|
||||
m_wizard->specChanged();
|
||||
});
|
||||
};
|
||||
bind(m_name, &Spec::name);
|
||||
bind(m_label, &Spec::label);
|
||||
bind(m_manufacturer, &Spec::manufacturer);
|
||||
bind(m_reference, &Spec::reference);
|
||||
bind(m_description, &Spec::description);
|
||||
connect(m_show_reference, &QCheckBox::toggled, this, [this](bool on) {
|
||||
m_wizard->editableSpec().show_reference = on;
|
||||
m_wizard->specChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void initializePage() override
|
||||
{
|
||||
const Spec &spec = m_wizard->editableSpec();
|
||||
for (auto pair : {qMakePair(m_name, spec.name), qMakePair(m_label, spec.label),
|
||||
qMakePair(m_manufacturer, spec.manufacturer),
|
||||
qMakePair(m_reference, spec.reference),
|
||||
qMakePair(m_description, spec.description)}) {
|
||||
QSignalBlocker blocker(pair.first);
|
||||
pair.first->setText(pair.second);
|
||||
}
|
||||
QSignalBlocker blocker(m_show_reference);
|
||||
m_show_reference->setChecked(spec.show_reference);
|
||||
}
|
||||
|
||||
void cleanupPage() override {}
|
||||
|
||||
bool isComplete() const override
|
||||
{
|
||||
return m_wizard->editableSpec().terminalCount() > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
GenericDeviceWizard *m_wizard;
|
||||
QLineEdit *m_name;
|
||||
QLineEdit *m_label;
|
||||
QLineEdit *m_manufacturer;
|
||||
QLineEdit *m_reference;
|
||||
QLineEdit *m_description;
|
||||
QCheckBox *m_show_reference;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief GenericDeviceWizard::GenericDeviceWizard
|
||||
@param fonts : the fonts the folio draws names and labels with, so the
|
||||
box is sized for them
|
||||
*/
|
||||
GenericDeviceWizard::GenericDeviceWizard(const Fonts &fonts, QWidget *parent) :
|
||||
QWizard(parent),
|
||||
m_fonts(fonts)
|
||||
{
|
||||
setWindowTitle(tr("Generic device"));
|
||||
setOption(QWizard::HaveFinishButtonOnEarlyPages);
|
||||
setOption(QWizard::NoBackButtonOnStartPage);
|
||||
|
||||
//Something to see on opening: two numbered terminals left and right
|
||||
m_spec.sides[Left] = GenericDevice::resize({}, 2, 0);
|
||||
m_spec.sides[Right] = GenericDevice::resize({}, 2, 0);
|
||||
numberPins(m_spec);
|
||||
|
||||
m_preview = new GenericDevicePreview(this);
|
||||
setSideWidget(m_preview);
|
||||
|
||||
setPage(LayoutPageId, new GenericDeviceLayoutPage(this));
|
||||
setPage(TerminalsPageId, new GenericDeviceTerminalsPage(this));
|
||||
setPage(InformationPageId, new GenericDeviceInformationPage(this));
|
||||
resize(960, 600);
|
||||
specChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
@return the device as filled in, with a name even if none was typed
|
||||
*/
|
||||
Spec GenericDeviceWizard::spec() const
|
||||
{
|
||||
Spec spec = m_spec;
|
||||
spec.name = spec.name.trimmed();
|
||||
if (spec.name.isEmpty())
|
||||
spec.name = tr("Generic device");
|
||||
return spec;
|
||||
}
|
||||
|
||||
void GenericDeviceWizard::specChanged()
|
||||
{
|
||||
m_preview->setSpec(m_spec, m_fonts);
|
||||
emit specUpdated();
|
||||
}
|
||||
|
||||
#include "genericdevicewizard.moc"
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#ifndef GENERICDEVICEWIZARD_H
|
||||
#define GENERICDEVICEWIZARD_H
|
||||
|
||||
#include "genericdevice.h"
|
||||
|
||||
#include <QWizard>
|
||||
|
||||
class GenericDevicePreview;
|
||||
|
||||
/**
|
||||
@brief The GenericDeviceWizard class
|
||||
Three pages, layout, terminals and information, that fill one
|
||||
GenericDevice::Spec, with a preview of the box beside every page.
|
||||
Finish is offered from the first page: a box with numbered pins needs
|
||||
nothing more.
|
||||
*/
|
||||
class GenericDeviceWizard : public QWizard
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum { LayoutPageId, TerminalsPageId, InformationPageId };
|
||||
|
||||
GenericDeviceWizard(const GenericDevice::Fonts &fonts,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
GenericDevice::Spec spec() const;
|
||||
GenericDevice::Spec &editableSpec() { return m_spec; }
|
||||
const GenericDevice::Fonts &fonts() const { return m_fonts; }
|
||||
|
||||
/// Redraw the preview and tell the pages the spec changed.
|
||||
void specChanged();
|
||||
|
||||
signals:
|
||||
void specUpdated();
|
||||
|
||||
private:
|
||||
GenericDevice::Spec m_spec;
|
||||
GenericDevice::Fonts m_fonts;
|
||||
GenericDevicePreview *m_preview = nullptr;
|
||||
};
|
||||
|
||||
#endif // GENERICDEVICEWIZARD_H
|
||||
@@ -1242,6 +1242,7 @@ void QETDiagramEditor::setUpActions()
|
||||
QAction *add_path = m_add_item_actions_group.addAction(QET::Icons::PartBezier, tr("Add a curve"));
|
||||
QAction *add_fillet = m_add_item_actions_group.addAction(QET::Icons::DrawFillet, tr("Add a fillet"));
|
||||
QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Add a terminal plan"));
|
||||
QAction *add_generic_device = m_add_item_actions_group.addAction(QET::Icons::GenericDevice, tr("Add a generic device…"));
|
||||
|
||||
add_text ->setStatusTip(tr("Adds a text field to the current sheet"));
|
||||
add_image ->setStatusTip(tr("Add an image to the current sheet"));
|
||||
@@ -1256,6 +1257,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_path ->setStatusTip(tr("Adds a Bézier curve to the current sheet"));
|
||||
add_fillet ->setStatusTip(tr("Rounds the corner between two lines of the current sheet"));
|
||||
add_terminal_strip->setStatusTip(tr("Adds a terminal plan to the current sheet"));
|
||||
add_generic_device->setStatusTip(tr("Makes a box symbol with terminals on any side and places it on the current sheet"));
|
||||
|
||||
add_text ->setData(QStringLiteral("text"));
|
||||
add_image ->setData(QStringLiteral("image"));
|
||||
@@ -1270,6 +1272,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_path ->setData(QStringLiteral("path"));
|
||||
add_fillet ->setData(QStringLiteral("fillet"));
|
||||
add_terminal_strip->setData(QStringLiteral("terminal_strip"));
|
||||
add_generic_device->setData(QStringLiteral("generic_device"));
|
||||
|
||||
add_text->setCheckable(true);
|
||||
add_line->setCheckable(true);
|
||||
@@ -2321,6 +2324,10 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
{
|
||||
diagram_event = new DiagramEventAddText(d);
|
||||
}
|
||||
else if (value == QLatin1String("generic_device"))
|
||||
{
|
||||
currentDiagramView()->addGenericDevice();
|
||||
}
|
||||
else if (value == QLatin1String("terminal_strip"))
|
||||
{
|
||||
const auto diagram_view{currentDiagramView()};
|
||||
|
||||
@@ -232,6 +232,7 @@ namespace QET {
|
||||
QIcon listDrawings;
|
||||
QIcon AutoNum;
|
||||
QIcon TerminalStrip;
|
||||
QIcon GenericDevice;
|
||||
QIcon QETManual;
|
||||
QIcon QETDonate;
|
||||
QIcon QETDownload;
|
||||
@@ -592,6 +593,7 @@ void QET::Icons::initIcons()
|
||||
TableOfContent = QIcon::fromTheme("table-of-content");
|
||||
Terminal = QIcon::fromTheme("terminal");
|
||||
TerminalStrip = QIcon::fromTheme("terminalstrip");
|
||||
GenericDevice = QIcon::fromTheme("generic-device");
|
||||
TitleBlock = QIcon::fromTheme("label");
|
||||
TitleBlockBottom = QIcon::fromTheme("titleblock-bottom");
|
||||
TitleBlockRight = QIcon::fromTheme("titleblock-right");
|
||||
|
||||
@@ -240,6 +240,7 @@ namespace QET {
|
||||
extern QIcon listDrawings;
|
||||
extern QIcon AutoNum;
|
||||
extern QIcon TerminalStrip;
|
||||
extern QIcon GenericDevice;
|
||||
extern QIcon QETManual;
|
||||
extern QIcon QETDonate;
|
||||
extern QIcon QETDownload;
|
||||
|
||||
@@ -185,6 +185,15 @@ add_test(NAME tst_symbolscale COMMAND tst_symbolscale)
|
||||
target_include_directories(tst_symbolscale PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_symbolscale PRIVATE Qt::Test)
|
||||
|
||||
# genericdevice.cpp is pure: the generic device wizard's layout, pattern
|
||||
# lines, groups and pin numbers, and the symbol it writes.
|
||||
add_executable(tst_genericdevice tst_genericdevice.cpp
|
||||
${QET_DIR}/sources/genericdevice/genericdevice.cpp)
|
||||
add_test(NAME tst_genericdevice COMMAND tst_genericdevice)
|
||||
set_tests_properties(tst_genericdevice PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_genericdevice PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_genericdevice PRIVATE Qt::Test Qt::Gui Qt::Xml)
|
||||
|
||||
# configprofile.h is header-only: which settings a saved configuration
|
||||
# profile carries, and how loading one replaces the live settings.
|
||||
# borderproperties.cpp: the real writer of the folio size defaults, to check
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include "genericdevice/genericdevice.h"
|
||||
|
||||
using namespace GenericDevice;
|
||||
|
||||
namespace {
|
||||
|
||||
/// QElectroTech's default diagram font, at the sizes the wizard uses
|
||||
Fonts fonts()
|
||||
{
|
||||
QFont names(QStringLiteral("Liberation Sans"));
|
||||
names.setPointSizeF(7);
|
||||
QFont label(QStringLiteral("Liberation Sans"));
|
||||
label.setPointSizeF(9);
|
||||
return {names, label};
|
||||
}
|
||||
|
||||
QStringList names(const Slots &slot_list)
|
||||
{
|
||||
QStringList list;
|
||||
for (const Slot &s : slot_list)
|
||||
list << (s.kind == Slot::Terminal ? s.name : s.kind == Slot::Gap ? QStringLiteral("_")
|
||||
: QStringLiteral("~"));
|
||||
return list;
|
||||
}
|
||||
|
||||
Slots parse(const QString &pattern)
|
||||
{
|
||||
const PatternResult r = expandPattern(pattern);
|
||||
Q_ASSERT(r.ok());
|
||||
return r.list;
|
||||
}
|
||||
|
||||
/// The VSD of the scope document, §3
|
||||
Spec vsd()
|
||||
{
|
||||
Spec spec;
|
||||
spec.name = QStringLiteral("VSD");
|
||||
spec.label = QStringLiteral("-U1");
|
||||
spec.manufacturer = QStringLiteral("ACME");
|
||||
spec.reference = QStringLiteral("VF-400");
|
||||
spec.sides[Left] = parse(QStringLiteral("L1,L2,L3,,PE"));
|
||||
spec.sides[Right] = parse(QStringLiteral("U,V,W,,PE"));
|
||||
spec.sides[Bottom] = parse(QStringLiteral("DI{4},COM,+24V"));
|
||||
return spec;
|
||||
}
|
||||
|
||||
QString toText(const QDomElement &e)
|
||||
{
|
||||
QString out;
|
||||
QTextStream stream(&out);
|
||||
e.save(stream, 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class tst_genericdevice : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void expandPattern_data()
|
||||
{
|
||||
QTest::addColumn<QString>("pattern");
|
||||
QTest::addColumn<QStringList>("expected");
|
||||
QTest::newRow("plain") << "L1" << QStringList{"L1"};
|
||||
QTest::newRow("gap") << "L3,,PE" << QStringList{"L3", "_", "PE"};
|
||||
QTest::newRow("count") << "In{5}" << QStringList{"In1", "In2", "In3", "In4", "In5"};
|
||||
QTest::newRow("from zero") << "Q{z5}" << QStringList{"Q0", "Q1", "Q2", "Q3", "Q4"};
|
||||
QTest::newRow("range") << "X{3-8}" << QStringList{"X3", "X4", "X5", "X6", "X7", "X8"};
|
||||
QTest::newRow("prefix") << "X1:{4}" << QStringList{"X1:1", "X1:2", "X1:3", "X1:4"};
|
||||
QTest::newRow("suffix") << "{2}.1" << QStringList{"1.1", "2.1"};
|
||||
QTest::newRow("spaces") << " L1 , L2 " << QStringList{"L1", "L2"};
|
||||
QTest::newRow("empty") << "" << QStringList{};
|
||||
QTest::newRow("vsd bottom") << "DI{4},COM,+24V"
|
||||
<< QStringList{"DI1", "DI2", "DI3", "DI4", "COM", "+24V"};
|
||||
}
|
||||
void expandPattern()
|
||||
{
|
||||
QFETCH(QString, pattern);
|
||||
QFETCH(QStringList, expected);
|
||||
const PatternResult r = GenericDevice::expandPattern(pattern);
|
||||
QVERIFY2(r.ok(), qPrintable(r.error));
|
||||
QCOMPARE(names(r.list), expected);
|
||||
}
|
||||
|
||||
void patternErrors_data()
|
||||
{
|
||||
QTest::addColumn<QString>("pattern");
|
||||
QTest::newRow("two braces") << "X{2}{3}";
|
||||
QTest::newRow("unclosed") << "X{2";
|
||||
QTest::newRow("letters") << "X{a}";
|
||||
QTest::newRow("z with range") << "X{z1-3}";
|
||||
QTest::newRow("backwards range") << "X{5-3}";
|
||||
QTest::newRow("zero count") << "X{0}";
|
||||
QTest::newRow("over the cap") << "X{201}";
|
||||
}
|
||||
void patternErrors()
|
||||
{
|
||||
QFETCH(QString, pattern);
|
||||
const PatternResult r = GenericDevice::expandPattern(pattern);
|
||||
QVERIFY(!r.ok());
|
||||
QVERIFY(r.list.isEmpty());
|
||||
}
|
||||
|
||||
void capIsInclusive()
|
||||
{
|
||||
QCOMPARE(GenericDevice::expandPattern(QStringLiteral("X{200}")).list.size(), 200);
|
||||
}
|
||||
|
||||
void toPatternFoldsRuns()
|
||||
{
|
||||
bool ok = false;
|
||||
QCOMPARE(toPattern(parse(QStringLiteral("In{5},COM")), &ok), QStringLiteral("In{5},COM"));
|
||||
QVERIFY(ok);
|
||||
QCOMPARE(toPattern(parse(QStringLiteral("Q{z4}"))), QStringLiteral("Q{z4}"));
|
||||
QCOMPARE(toPattern(parse(QStringLiteral("X{3-8}"))), QStringLiteral("X{3-8}"));
|
||||
QCOMPARE(toPattern(parse(QStringLiteral("L1,L2,L3,,PE"))), QStringLiteral("L{3},,PE"));
|
||||
//Two in a row stay as written
|
||||
QCOMPARE(toPattern(parse(QStringLiteral("A1,A2"))), QStringLiteral("A1,A2"));
|
||||
//Leading zeros are not numbers to fold
|
||||
QCOMPARE(toPattern(parse(QStringLiteral("01,02,03"))), QStringLiteral("01,02,03"));
|
||||
}
|
||||
|
||||
void toPatternRoundTrips_data()
|
||||
{
|
||||
QTest::addColumn<QString>("pattern");
|
||||
for (const char *p : {"L1,L2,L3,,PE", "DI{4},COM,+24V", "Q{z5}", "X1:{4}", ",A,,B,",
|
||||
"1,2,3,4,5", "X9,X10,X11", "A,A,A"})
|
||||
QTest::newRow(p) << QString::fromLatin1(p);
|
||||
}
|
||||
void toPatternRoundTrips()
|
||||
{
|
||||
QFETCH(QString, pattern);
|
||||
const Slots original = parse(pattern);
|
||||
QCOMPARE(names(parse(toPattern(original))), names(original));
|
||||
}
|
||||
|
||||
void toPatternFlagsWhatItCannotSay()
|
||||
{
|
||||
bool ok = true;
|
||||
Slots s{Slot::terminal(QStringLiteral("A,B"))};
|
||||
toPattern(s, &ok);
|
||||
QVERIFY(!ok);
|
||||
ok = true;
|
||||
toPattern({Slot::terminal(QStringLiteral("{x}"))}, &ok);
|
||||
QVERIFY(!ok);
|
||||
}
|
||||
|
||||
void applyPatternKeepsWhatTheTableHolds()
|
||||
{
|
||||
Slots current = parse(QStringLiteral("1,2"));
|
||||
current[0].function = QStringLiteral("OUT1");
|
||||
current[0].uuid = QUuid::createUuid();
|
||||
current[1].type = QStringLiteral("Inner");
|
||||
const Slots result = applyPattern(current, parse(QStringLiteral("A,,B,C")));
|
||||
QCOMPARE(names(result), (QStringList{"A", "_", "B", "C"}));
|
||||
QCOMPARE(result[0].function, QStringLiteral("OUT1"));
|
||||
QCOMPARE(result[0].uuid, current[0].uuid);
|
||||
QCOMPARE(result[2].type, QStringLiteral("Inner"));
|
||||
QVERIFY(result[3].uuid.isNull());
|
||||
}
|
||||
|
||||
void groupEvery_data()
|
||||
{
|
||||
QTest::addColumn<int>("count");
|
||||
QTest::addColumn<int>("n");
|
||||
QTest::addColumn<QString>("expected");
|
||||
QTest::newRow("9 by 4") << 9 << 4 << "1 2 3 4 _ 5 6 7 8 _ 9";
|
||||
QTest::newRow("8 by 4") << 8 << 4 << "1 2 3 4 _ 5 6 7 8";
|
||||
QTest::newRow("none") << 3 << 0 << "1 2 3";
|
||||
QTest::newRow("by 1") << 3 << 1 << "1 _ 2 _ 3";
|
||||
QTest::newRow("n = count") << 4 << 4 << "1 2 3 4";
|
||||
QTest::newRow("n > count") << 3 << 8 << "1 2 3";
|
||||
}
|
||||
void groupEvery()
|
||||
{
|
||||
QFETCH(int, count);
|
||||
QFETCH(int, n);
|
||||
QFETCH(QString, expected);
|
||||
const Slots s = parse(QStringLiteral("{%1}").arg(count));
|
||||
QCOMPARE(names(GenericDevice::groupEvery(s, n)).join(' '), expected);
|
||||
}
|
||||
|
||||
void groupEveryReplacesHandGaps()
|
||||
{
|
||||
QCOMPARE(names(GenericDevice::groupEvery(parse(QStringLiteral("1,,2,3,,,4")), 2)).join(' '),
|
||||
QStringLiteral("1 2 _ 3 4"));
|
||||
}
|
||||
|
||||
void resizeKeepsTypedNames()
|
||||
{
|
||||
const Slots s = parse(QStringLiteral("L1,L2,,PE"));
|
||||
QCOMPARE(names(resize(s, 5, 0)).join(' '), QStringLiteral("L1 L2 _ PE "));
|
||||
QCOMPARE(names(resize(s, 2, 0)).join(' '), QStringLiteral("L1 L2"));
|
||||
QCOMPARE(names(resize(s, 0, 0)), QStringList());
|
||||
QCOMPARE(names(resize(s, 5, 2)).join(' '), QStringLiteral("L1 L2 _ PE _ "));
|
||||
}
|
||||
|
||||
void lineUpGroups()
|
||||
{
|
||||
Slots a = parse(QStringLiteral("L1,L2,L3,N,,PE"));
|
||||
Slots b = parse(QStringLiteral("U,V,W,,PE"));
|
||||
GenericDevice::lineUpGroups(a, b);
|
||||
QCOMPARE(names(a).join(' '), QStringLiteral("L1 L2 L3 N _ PE"));
|
||||
QCOMPARE(names(b).join(' '), QStringLiteral("U V W ~ _ PE"));
|
||||
QCOMPARE(a.size(), b.size());
|
||||
}
|
||||
|
||||
void lineUpLeavesEqualAndUngroupedSidesAlone()
|
||||
{
|
||||
Slots a = parse(QStringLiteral("L1,L2,L3,,PE"));
|
||||
Slots b = parse(QStringLiteral("U,V,W,,PE"));
|
||||
GenericDevice::lineUpGroups(a, b);
|
||||
QCOMPARE(names(b).join(' '), QStringLiteral("U V W _ PE"));
|
||||
|
||||
//No gap on one side: nothing faces the other's groups
|
||||
Slots c = parse(QStringLiteral("1,2,3,4,5"));
|
||||
Slots d = parse(QStringLiteral("A,,B"));
|
||||
GenericDevice::lineUpGroups(c, d);
|
||||
QCOMPARE(names(c).join(' '), QStringLiteral("1 2 3 4 5"));
|
||||
QCOMPARE(names(d).join(' '), QStringLiteral("A _ B"));
|
||||
|
||||
//More groups on one side: the extra ones simply follow
|
||||
Slots e = parse(QStringLiteral("A,,B,B2,,C"));
|
||||
Slots f = parse(QStringLiteral("X,X2,,Y"));
|
||||
GenericDevice::lineUpGroups(e, f);
|
||||
QCOMPARE(names(e).join(' '), QStringLiteral("A ~ _ B B2 _ C"));
|
||||
QCOMPARE(names(f).join(' '), QStringLiteral("X X2 _ Y"));
|
||||
}
|
||||
|
||||
void numberPinsCounterClockwise()
|
||||
{
|
||||
Spec spec;
|
||||
spec.sides[Left] = resize({}, 3, 0);
|
||||
spec.sides[Bottom] = {Slot::terminal({}), Slot::gap(), Slot::terminal({})};
|
||||
spec.sides[Right] = resize({}, 2, 0);
|
||||
spec.sides[Top] = resize({}, 2, 0);
|
||||
numberPins(spec);
|
||||
QCOMPARE(names(spec.sides[Left]).join(' '), QStringLiteral("1 2 3"));
|
||||
QCOMPARE(names(spec.sides[Bottom]).join(' '), QStringLiteral("4 _ 5"));
|
||||
QCOMPARE(names(spec.sides[Right]).join(' '), QStringLiteral("7 6"));
|
||||
QCOMPARE(names(spec.sides[Top]).join(' '), QStringLiteral("9 8"));
|
||||
QVERIFY(hasNumberedPins(spec));
|
||||
spec.sides[Top][0].name = QStringLiteral("PE");
|
||||
QVERIFY(!hasNumberedPins(spec));
|
||||
}
|
||||
|
||||
void nameUnnamedLeavesTypedNames()
|
||||
{
|
||||
Spec spec;
|
||||
spec.sides[Left] = {Slot::terminal(QStringLiteral("L1")), Slot::terminal({}), Slot::gap(),
|
||||
Slot::terminal({})};
|
||||
nameUnnamed(spec);
|
||||
QCOMPARE(names(spec.sides[Left]).join(' '), QStringLiteral("L1 2 _ 3"));
|
||||
}
|
||||
|
||||
void pasteTabsAndCommasReadAlike()
|
||||
{
|
||||
const QString tabs = QStringLiteral(
|
||||
"side\tname\tfunction\n"
|
||||
"L\tL1\n" "L\tL2\n" "L\tL3\n" "L\t\n" "L\tPE\n"
|
||||
"right\tU\tmotor\n" "R\tV\n" "R\tW\n" "R\t\n" "R\tPE\n"
|
||||
"bottom\tDI1\t\tInner\n");
|
||||
QString commas = tabs;
|
||||
commas.replace('\t', ',');
|
||||
for (const QString &text : {tabs, commas})
|
||||
{
|
||||
const PasteResult r = parsePastedTable(text);
|
||||
QVERIFY2(r.bad_rows.isEmpty(), qPrintable(r.bad_rows.join('|')));
|
||||
QCOMPARE(r.rows, 11);
|
||||
QCOMPARE(names(r.sides[Left]).join(' '), QStringLiteral("L1 L2 L3 _ PE"));
|
||||
QCOMPARE(r.sides[Right][0].function, QStringLiteral("motor"));
|
||||
QCOMPARE(r.sides[Bottom][0].type, QStringLiteral("Inner"));
|
||||
QVERIFY(r.has_side[Left] && r.has_side[Right] && r.has_side[Bottom]);
|
||||
QVERIFY(!r.has_side[Top]);
|
||||
}
|
||||
}
|
||||
|
||||
void pasteListsBadRows()
|
||||
{
|
||||
const PasteResult r = parsePastedTable(QStringLiteral(
|
||||
"left,A\r\nmiddle,B\r\ntop,C,,Sideways\r\n\r\nbottom,D,f,Generic,extra\r\nT,E"));
|
||||
QCOMPARE(r.rows, 2);
|
||||
QCOMPARE(r.bad_rows, (QStringList{"middle,B", "top,C,,Sideways", "bottom,D,f,Generic,extra"}));
|
||||
QCOMPARE(names(r.sides[Top]), QStringList{"E"});
|
||||
}
|
||||
|
||||
/// Scope §3: the VSD body is 160 × 140 with the terminals packed
|
||||
/// from the top and clear of the corners
|
||||
void vsdSize()
|
||||
{
|
||||
const Layout l = layout(vsd(), fonts());
|
||||
QCOMPARE(l.body_width, 160);
|
||||
QCOMPARE(l.body_height, 140);
|
||||
QCOMPARE(l.terminals.size(), 14);
|
||||
QCOMPARE(l.dividers.size(), 2);
|
||||
QVERIFY(l.warnings.join(' ').contains(QStringLiteral("PE")));
|
||||
}
|
||||
|
||||
void everythingOnTheGrid()
|
||||
{
|
||||
Spec spec = vsd();
|
||||
spec.sides[Top] = parse(QStringLiteral("X{3}"));
|
||||
spec.extra_width = 3;
|
||||
QDomDocument doc;
|
||||
const QDomElement def = toDefinition(spec, fonts(), doc);
|
||||
for (const char *a : {"width", "height", "hotspot_x", "hotspot_y"})
|
||||
QCOMPARE(def.attribute(a).toInt() % Grid, 0);
|
||||
const QDomNodeList terminals = def.elementsByTagName(QStringLiteral("terminal"));
|
||||
QCOMPARE(terminals.size(), 17);
|
||||
for (int i = 0 ; i < terminals.size() ; ++i) {
|
||||
const QDomElement t = terminals.at(i).toElement();
|
||||
QCOMPARE(qRound(t.attribute("x").toDouble()) % Grid, 0);
|
||||
QCOMPARE(qRound(t.attribute("y").toDouble()) % Grid, 0);
|
||||
QVERIFY(!QUuid(t.attribute("uuid")).isNull());
|
||||
}
|
||||
}
|
||||
|
||||
void lineUpPutsPeOnOneRow()
|
||||
{
|
||||
Spec spec;
|
||||
spec.sides[Left] = parse(QStringLiteral("L1,L2,L3,N,,PE"));
|
||||
spec.sides[Right] = parse(QStringLiteral("U,V,W,,PE"));
|
||||
Layout l = layout(spec, fonts());
|
||||
QList<qreal> pe;
|
||||
for (const auto &t : l.terminals)
|
||||
if (t.slot.name == QLatin1String("PE")) pe << t.point.y();
|
||||
QCOMPARE(pe.size(), 2);
|
||||
QCOMPARE(pe[0], pe[1]);
|
||||
//Both gaps marked, the padding not
|
||||
QCOMPARE(l.dividers.size(), 2);
|
||||
QCOMPARE(l.dividers[0].y1(), l.dividers[1].y1());
|
||||
|
||||
spec.line_up = false;
|
||||
l = layout(spec, fonts());
|
||||
pe.clear();
|
||||
for (const auto &t : l.terminals)
|
||||
if (t.slot.name == QLatin1String("PE")) pe << t.point.y();
|
||||
QVERIFY(pe[0] != pe[1]);
|
||||
|
||||
spec.mark_gaps = false;
|
||||
QVERIFY(layout(spec, fonts()).dividers.isEmpty());
|
||||
}
|
||||
|
||||
void edgeCases()
|
||||
{
|
||||
Spec empty;
|
||||
Layout l = layout(empty, fonts());
|
||||
//No terminals: the minimum, or as wide as the label needs
|
||||
QVERIFY(l.body_width >= MinWidth);
|
||||
QCOMPARE(l.body_width % Grid, 0);
|
||||
QCOMPARE(l.body_height, MinHeight);
|
||||
QVERIFY(l.terminals.isEmpty());
|
||||
|
||||
Spec one;
|
||||
one.sides[Top] = parse(QStringLiteral("A"));
|
||||
l = layout(one, fonts());
|
||||
QCOMPARE(l.terminals.size(), 1);
|
||||
QCOMPARE(l.terminals[0].point.y(), qreal(-Stub));
|
||||
|
||||
Spec many;
|
||||
many.sides[Left] = parse(QStringLiteral("{50}"));
|
||||
l = layout(many, fonts());
|
||||
QCOMPARE(l.terminals.size(), 50);
|
||||
QVERIFY(l.body_height >= 50 * many.pitch);
|
||||
}
|
||||
|
||||
void extraSizeOnlyAdds()
|
||||
{
|
||||
Spec spec = vsd();
|
||||
const Layout base = layout(spec, fonts());
|
||||
spec.extra_width = 2;
|
||||
spec.extra_height = 1;
|
||||
const Layout grown = layout(spec, fonts());
|
||||
QCOMPARE(grown.body_width, base.body_width + 20);
|
||||
QCOMPARE(grown.body_height, base.body_height + 10);
|
||||
spec.extra_width = -5;
|
||||
QCOMPARE(layout(spec, fonts()).body_width, base.body_width);
|
||||
}
|
||||
|
||||
void definitionRoundTrip()
|
||||
{
|
||||
Spec spec = vsd();
|
||||
spec.sides[Right][0].function = QStringLiteral("motor");
|
||||
spec.sides[Left][1].type = QStringLiteral("Inner");
|
||||
spec.sides[Top] = parse(QStringLiteral("A,B"));
|
||||
spec.extra_width = 2;
|
||||
spec.extra_height = 1;
|
||||
spec.pitch = 30;
|
||||
|
||||
QDomDocument doc;
|
||||
const QDomElement def = toDefinition(spec, fonts(), doc, QStringLiteral("0.100.0"));
|
||||
Spec back;
|
||||
QVERIFY(fromDefinition(def, fonts(), &back));
|
||||
|
||||
QCOMPARE(back.name, spec.name);
|
||||
QCOMPARE(back.label, spec.label);
|
||||
QCOMPARE(back.manufacturer, spec.manufacturer);
|
||||
QCOMPARE(back.reference, spec.reference);
|
||||
QCOMPARE(back.show_reference, true);
|
||||
QCOMPARE(back.pitch, 30);
|
||||
QCOMPARE(back.extra_width, 2);
|
||||
QCOMPARE(back.extra_height, 1);
|
||||
QVERIFY(back.mark_gaps);
|
||||
QVERIFY(back.line_up);
|
||||
for (int s = 0 ; s < SideCount ; ++s)
|
||||
QCOMPARE(names(back.sides[s]), names(spec.sides[s]));
|
||||
QCOMPARE(back.sides[Right][0].function, QStringLiteral("motor"));
|
||||
QCOMPARE(back.sides[Left][1].type, QStringLiteral("Inner"));
|
||||
|
||||
//Written again, the terminals keep their uuids and positions
|
||||
QDomDocument doc2;
|
||||
const QDomElement def2 = toDefinition(back, fonts(), doc2, QStringLiteral("0.100.0"));
|
||||
const QDomNodeList t1 = def.elementsByTagName(QStringLiteral("terminal"));
|
||||
const QDomNodeList t2 = def2.elementsByTagName(QStringLiteral("terminal"));
|
||||
QCOMPARE(t2.size(), t1.size());
|
||||
for (int i = 0 ; i < t1.size() ; ++i)
|
||||
for (const char *a : {"x", "y", "uuid", "name", "orientation", "type"})
|
||||
QCOMPARE(t2.at(i).toElement().attribute(a), t1.at(i).toElement().attribute(a));
|
||||
for (const char *a : {"width", "height", "hotspot_x", "hotspot_y"})
|
||||
QCOMPARE(def2.attribute(a), def.attribute(a));
|
||||
}
|
||||
|
||||
void roundTripWithPaddingAndNoMarks()
|
||||
{
|
||||
Spec spec;
|
||||
spec.name = QStringLiteral("X");
|
||||
spec.sides[Left] = parse(QStringLiteral("L1,L2,L3,N,,PE"));
|
||||
spec.sides[Right] = parse(QStringLiteral("U,V,W,,PE"));
|
||||
for (bool marks : {true, false})
|
||||
{
|
||||
spec.mark_gaps = marks;
|
||||
QDomDocument doc;
|
||||
const QDomElement def = toDefinition(spec, fonts(), doc);
|
||||
Spec back;
|
||||
QVERIFY(fromDefinition(def, fonts(), &back));
|
||||
QDomDocument doc2;
|
||||
const QDomElement def2 = toDefinition(back, fonts(), doc2);
|
||||
const QDomNodeList t1 = def.elementsByTagName(QStringLiteral("terminal"));
|
||||
const QDomNodeList t2 = def2.elementsByTagName(QStringLiteral("terminal"));
|
||||
QCOMPARE(t2.size(), t1.size());
|
||||
for (int i = 0 ; i < t1.size() ; ++i)
|
||||
QCOMPARE(t2.at(i).toElement().attribute("y"), t1.at(i).toElement().attribute("y"));
|
||||
QCOMPARE(def2.elementsByTagName("line").size(), def.elementsByTagName("line").size());
|
||||
}
|
||||
}
|
||||
|
||||
void foreignDefinitionsAreRefused()
|
||||
{
|
||||
QDomDocument doc;
|
||||
QDomElement def = toDefinition(vsd(), fonts(), doc);
|
||||
Spec back;
|
||||
QVERIFY(fromDefinition(def, fonts(), &back));
|
||||
|
||||
//A circle drawn on it in the symbol editor
|
||||
QDomElement description = def.firstChildElement(QStringLiteral("description"));
|
||||
QDomElement extra = doc.createElement(QStringLiteral("ellipse"));
|
||||
description.appendChild(extra);
|
||||
QVERIFY(!fromDefinition(def, fonts(), &back));
|
||||
description.removeChild(extra);
|
||||
|
||||
//A terminal moved off its edge
|
||||
QDomElement t = description.firstChildElement(QStringLiteral("terminal"));
|
||||
t.setAttribute(QStringLiteral("x"), 5);
|
||||
QVERIFY(!fromDefinition(def, fonts(), &back));
|
||||
}
|
||||
|
||||
/// The output is stable apart from the uuids
|
||||
void definitionIsDeterministic()
|
||||
{
|
||||
Spec spec = vsd();
|
||||
for (Slots &side : spec.sides)
|
||||
for (Slot &s : side)
|
||||
s.uuid = QUuid::createUuid();
|
||||
QDomDocument a, b;
|
||||
QString one = toText(toDefinition(spec, fonts(), a));
|
||||
QString two = toText(toDefinition(spec, fonts(), b));
|
||||
static const QRegularExpression uuid(QStringLiteral("uuid=\"\\{[^}]+\\}\""));
|
||||
static const QRegularExpression part_uuid(QStringLiteral("<uuid uuid=\"[^\"]+\"/>"));
|
||||
//Only parts and the definition get fresh uuids; the terminals keep theirs
|
||||
QCOMPARE(one.count(QStringLiteral("<terminal ")), 14);
|
||||
for (QString *s : {&one, &two}) {
|
||||
const QStringList lines = s->split('\n');
|
||||
QStringList kept;
|
||||
for (QString line : lines) {
|
||||
if (!line.contains(QLatin1String("<terminal")))
|
||||
line.replace(uuid, QStringLiteral("uuid=\"\""));
|
||||
kept << line;
|
||||
}
|
||||
*s = kept.join('\n');
|
||||
}
|
||||
QCOMPARE(one, two);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_genericdevice)
|
||||
|
||||
#include "tst_genericdevice.moc"
|
||||
Reference in New Issue
Block a user