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:
ispyisail
2026-10-09 23:27:37 +13:00
parent ff35d0dc69
commit a3594c6633
24 changed files with 2729 additions and 1 deletions
+4
View File
@@ -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
+1
View File
@@ -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
+28
View File
@@ -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>
+12
View 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

+13
View File
@@ -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

+1
View File
@@ -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",
+2 -1
View File
@@ -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"),
+59
View File
@@ -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
*/
+1
View File
@@ -196,6 +196,7 @@ class DiagramView : public PaletteGraphicsView
void adjustSceneRect();
void updateWindowTitle();
void resetConductors();
void addGenericDevice();
private slots:
void adjustGridToZoom();
+23
View File
@@ -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.
+1
View File
@@ -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
+185
View File
@@ -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 &current, 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
+7
View File
@@ -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()};
+2
View File
@@ -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");
+1
View File
@@ -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;
+9
View File
@@ -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
+502
View File
@@ -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"