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
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