mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-10 06:04:12 +02:00
Merge pull request #1418 from ispyisail/feature/generic-device
Add a generic device: a box symbol with terminals on any side
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