/* 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 . */ // SPDX-License-Identifier: GPL-2.0-or-later #include "genericdevicewizard.h" #include "../editor/terminalnamecheck.h" #include "../factory/elementpicturefactory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(&QSpinBox::valueChanged), this, [this, side](int value) { countChanged(Side(side), value); }); connect(m_group[side], qOverload(&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(&QSpinBox::valueChanged), this, [this](int v) { m_wizard->editableSpec().pitch = v * Grid; changed(); }); connect(m_extra_width, qOverload(&QSpinBox::valueChanged), this, [this](int v) { m_wizard->editableSpec().extra_width = v; changed(); }); connect(m_extra_height, qOverload(&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(&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 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 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"