From 3e2ad8c4bd34e02c6c817f6224d70ce88bead090 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Mon, 19 Oct 2026 07:42:22 +0200 Subject: [PATCH] Terminal strip: create the strips from the free terminals' labels Add an automatic import which groups the free terminals of a project by the part of their label before the first colon ("X1:2" goes to the strip "X1"), creates the strips missing and sorts the terminals in natural order, as one undoable step. The import runs when the terminal block manager opens on a project and can be run again from a new toolbar action. It only adds : the strips already known keep their terminals and their order, and terminals without label are left free. --- cmake/qet_compilation_vars.cmake | 2 + .../TerminalStrip/terminalstripautoimport.cpp | 293 ++++++++++++++++++ .../TerminalStrip/terminalstripautoimport.h | 103 ++++++ sources/TerminalStrip/terminalstripdata.h | 4 +- .../ui/terminalstripeditorwindow.cpp | 82 +++++ .../ui/terminalstripeditorwindow.h | 3 + .../ui/terminalstripeditorwindow.ui | 24 ++ 7 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 sources/TerminalStrip/terminalstripautoimport.cpp create mode 100644 sources/TerminalStrip/terminalstripautoimport.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index ac71e8106..eebec6489 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -716,6 +716,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/TerminalStrip/terminalstripbridge.h ${QET_DIR}/sources/TerminalStrip/terminalstrip.cpp ${QET_DIR}/sources/TerminalStrip/terminalstrip.h + ${QET_DIR}/sources/TerminalStrip/terminalstripautoimport.cpp + ${QET_DIR}/sources/TerminalStrip/terminalstripautoimport.h ${QET_DIR}/sources/TerminalStrip/terminalstripdata.cpp ${QET_DIR}/sources/TerminalStrip/terminalstripdata.h ${QET_DIR}/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp diff --git a/sources/TerminalStrip/terminalstripautoimport.cpp b/sources/TerminalStrip/terminalstripautoimport.cpp new file mode 100644 index 000000000..6e9508dda --- /dev/null +++ b/sources/TerminalStrip/terminalstripautoimport.cpp @@ -0,0 +1,293 @@ +/* + 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 . +*/ +#include "terminalstripautoimport.h" + +#include "../elementprovider.h" +#include "../qetgraphicsitem/terminalelement.h" +#include "../qetinformation.h" +#include "../qetproject.h" +#include "../utils/qetutils.h" +#include "UndoCommand/addterminalstripcommand.h" +#include "UndoCommand/addterminaltostripcommand.h" +#include "UndoCommand/sortterminalstripcommand.h" +#include "physicalterminal.h" +#include "realterminal.h" +#include "terminalstrip.h" + +#include +#include +#include + +namespace +{ + /** + * Terminals going to one strip : a label prefix plus the plant and + * the location typed on the terminals. A plant or location set on + * the terminals must match the strip exactly, an empty one accepts + * every strip of the same name (in most projects the plant and the + * location live on the strip, not on the terminals). + */ + struct StripGroup + { + QVector> free_terminals; + QVector, TerminalStrip *>> placed_terminals; + QString name, plant, location; + }; + + QString elementInfo(const QSharedPointer &terminal, const QString &key) + { + auto element_ = terminal->element(); + if (!element_) { + return QString(); + } + return element_->elementData().m_informations.value(key).toString().trimmed(); + } + + //An empty plant is saved and read back as "=" and an empty + //location as "+" (the placeholders of the title block) : all + //three spellings mean "no plant" / "no location" + QString normalizedPlant(const QString &plant_) + { + const auto value_ = plant_.trimmed(); + return (value_.isEmpty() || value_ == QLatin1String("=")) + ? QString() : value_; + } + + QString normalizedLocation(const QString &location_) + { + const auto value_ = location_.trimmed(); + return (value_.isEmpty() || value_ == QLatin1String("+")) + ? QString() : value_; + } + + QString groupKey(const QString &name, const QString &plant, const QString &location) { + return name + QChar(0x1f) + plant + QChar(0x1f) + location; + } + + /** + * @brief stripOf + * The strip of the group : same name, and same plant and location + * when the terminals carry some. Empty plant and location on the + * terminals mean "no opinion" : any strip of the name is reused, + * the one already holding terminals winning over an empty one. + * @return the strip to work on, null when none has to exist yet. + */ + TerminalStrip *stripOf(QETProject *project, const QString &name, + const QString &plant, const QString &location) + { + const bool strict_ = !plant.isEmpty() || !location.isEmpty(); + TerminalStrip *best_ = nullptr; + int best_score_ = -1; + for (auto strip_ : project->terminalStrip()) + { + if (!strip_ || strip_->name() != name) { + continue; + } + const bool same_ = + normalizedPlant(strip_->installation()) == plant && + normalizedLocation(strip_->location()) == location; + if (strict_ && !same_) { + continue; + } + //Same plant and location first, then the strip already + //holding terminals over an empty one + const int score_ = (same_ ? 4 : 0) + + (strip_->realTerminals().isEmpty() ? 0 : 1); + if (score_ > best_score_) { + best_ = strip_; + best_score_ = score_; + } + } + return best_; + } +} + +/** + * @brief TerminalStripAutoImport::importFreeTerminals + * Put the terminals of @a project in the strip named after their label + * prefix ("X1:2" goes to the strip "X1"), creating the strip when it + * does not exist yet. The whole run is one undoable step. + * + * @a move_placed picks the action : false only places free terminals, + * placed ones are never touched. True also moves a placed terminal + * whose label prefix or plant and location no longer match its strip. + * + * In both cases the order the user gave to a strip is never changed, + * only a strip created by the run itself is sorted. + */ +TerminalStripAutoImport::ImportResult TerminalStripAutoImport::importFreeTerminals( + QETProject *project, bool move_placed) +{ + if (!project) { + return ImportResult{}; + } + + //Group every real terminal of the project, the free ones and + //the ones already in a strip alike + QMap groups_; + const auto collect_ = [&groups_](const QSharedPointer &real_, + TerminalStrip *current_) + { + if (!real_ || !real_->element()) { + return; //Spare terminals (no element) are left alone + } + const auto name_ = stripNameForLabel(real_->label()); + if (name_.isEmpty()) { + return; + } + const auto plant_ = normalizedPlant( + elementInfo(real_, QETInformation::ELMT_PLANT)); + const auto location_ = normalizedLocation( + elementInfo(real_, QETInformation::ELMT_LOCATION)); + + auto &group_ = groups_[groupKey(name_, plant_, location_)]; + group_.name = name_; + group_.plant = plant_; + group_.location = location_; + if (current_) { + group_.placed_terminals.append({real_, current_}); + } else { + group_.free_terminals.append(real_); + } + }; + + const ElementProvider provider(project); + for (auto terminal_ : provider.freeTerminal()) { + if (terminal_) { + collect_(terminal_->realTerminal(), nullptr); + } + } + for (auto strip_ : project->terminalStrip()) + { + if (!strip_) { + continue; + } + for (const auto &real_ : strip_->realTerminals()) { + collect_(real_, strip_); + } + } + if (groups_.isEmpty()) { + return ImportResult{}; + } + + for (auto it_ = groups_.begin(); it_ != groups_.end(); ++it_) + { + auto by_label_ = [](const QSharedPointer &a_, + const QSharedPointer &b_) { + return QETUtils::naturalLessThan(a_->label(), b_->label()); + }; + std::sort(it_.value().free_terminals.begin(), + it_.value().free_terminals.end(), by_label_); + std::sort(it_.value().placed_terminals.begin(), + it_.value().placed_terminals.end(), + [&by_label_](const auto &a_, const auto &b_) { + return by_label_(a_.first, b_.first); + }); + } + + auto names_ = groups_.keys(); + std::sort(names_.begin(), names_.end(), QETUtils::naturalLessThan); + + //First phase : work out what has to change, without touching the + //project. When there is nothing to do, nothing is pushed : no + //undo step is recorded and the project stays untouched. + struct Plan + { + const StripGroup *group{nullptr}; + TerminalStrip *strip{nullptr}; //Target strip, null : to create + }; + QVector plans; + + for (const auto &key_ : std::as_const(names_)) + { + auto &group_ = groups_[key_]; + Plan plan_; + plan_.group = &group_; + plan_.strip = stripOf(project, group_.name, group_.plant, group_.location); + + //Free terminals always, placed ones only when the action + //is allowed to re-group them + bool changed_ = !group_.free_terminals.isEmpty(); + if (move_placed) { + for (const auto &entry_ : std::as_const(group_.placed_terminals)) { + changed_ = changed_ || entry_.second != plan_.strip; + } + } + if (changed_) { + plans.append(plan_); + } + } + if (plans.isEmpty()) { + return ImportResult{}; + } + + //Second phase : the changes, as one undoable step + int added_ = 0, moved_ = 0, created_strips_ = 0; + auto undo_ = project->undoStack(); + undo_->beginMacro(move_placed + ? QCoreApplication::translate("TerminalStripAutoImport", + "Re-group all terminals") + : QCoreApplication::translate("TerminalStripAutoImport", + "Assign free terminals")); + + for (const auto &plan_ : std::as_const(plans)) + { + const auto &group_ = *plan_.group; + + bool created_ = false; + auto strip_ = plan_.strip; + if (!strip_) { + strip_ = project->newTerminalStrip( + group_.plant, group_.location, group_.name); + undo_->push(new AddTerminalStripCommand(strip_, project)); + ++created_strips_; + created_ = true; + } + + if (!group_.free_terminals.isEmpty()) { + undo_->push(new AddTerminalToStripCommand(group_.free_terminals, strip_)); + added_ += group_.free_terminals.size(); + } + //A terminal whose label prefix or plant and location no + //longer match its strip moves to the right one, but only + //when the action is allowed to re-group placed terminals + if (move_placed) + { + for (const auto &entry_ : std::as_const(group_.placed_terminals)) + { + if (entry_.second == strip_) { + continue; + } + undo_->push(new MoveTerminalCommand( + entry_.first->physicalTerminal(), entry_.second, strip_)); + ++moved_; + } + } + //A strip just created is sorted, a strip already known + //keeps the order its user gave it + if (created_) { + undo_->push(new SortTerminalStripCommand(strip_)); + } + } + + undo_->endMacro(); + ImportResult result_; + result_.terminals = added_ + moved_; + result_.strips = created_strips_; + return result_; +} diff --git a/sources/TerminalStrip/terminalstripautoimport.h b/sources/TerminalStrip/terminalstripautoimport.h new file mode 100644 index 000000000..5d3336388 --- /dev/null +++ b/sources/TerminalStrip/terminalstripautoimport.h @@ -0,0 +1,103 @@ +/* + 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 . +*/ +#ifndef TERMINALSTRIPAUTOIMPORT_H +#define TERMINALSTRIPAUTOIMPORT_H + +#include + +class QETProject; + +/** + * The TerminalStripAutoImport namespace works out the terminal strips of a + * project from the labels of its free terminals, so that the user does not + * have to create the strips and move the terminals one by one. + * + * The rule : the name of a strip is the label of its terminals up to the + * first colon, "X1:2" giving the strip "X1", as numbered by the terminal + * numbering dialog. Terminals whose label holds no colon use the whole + * label, terminals with no label are left alone. + * + * The plant and the location work as follows : they are read from the + * terminal and only decide which strip to pick when they are set. An + * existing strip with the same name is reused as soon as they are empty + * (in most projects the plant and the location live on the strip, not on + * the terminals), while a set plant or location must match exactly, a + * strip being created when no one matches. That way the import never + * builds a second strip for a name that already exists. + * + * Two actions run this import : + * - the default one places free terminals only, it never moves or + * re-sorts a terminal the user already assigned to a strip; + * - "re-group all terminals" also moves a placed terminal whose label + * prefix or plant and location no longer match its strip, for the case + * where the user corrects a terminal afterwards. Existing strips are + * never re-sorted. + * + * The import is idempotent : a second run adds nothing, the terminals + * of a strip the user ordered or completed by hand are never re-ordered, + * only new terminals are added (at the end of the strip). Only a strip + * created by the import itself is sorted. When nothing has to change, + * nothing is pushed to the undo stack : the project stays untouched. + */ +namespace TerminalStripAutoImport +{ + struct ImportResult + { + int terminals = 0; ///< Terminals added to a strip, moved ones included + int strips = 0; ///< Terminal strips created + }; + + /** + * @brief stripNameForLabel + * @param label : the label of a terminal, for example "X1:2" + * @return the name of the strip holding this terminal, "X1", or an + * empty string when the label holds nothing usable. + */ + inline QString stripNameForLabel(const QString &label) + { + const auto label_ = label.trimmed(); + if (label_.isEmpty()) { + return QString(); + } + const auto colon_ = label_.indexOf(QLatin1Char(':')); + if (colon_ > 0) { + return label_.left(colon_).trimmed(); + } + if (colon_ == 0) { + return QString(); //Label starts with ":" : no name before it + } + return label_; //No colon : the whole label names the strip + } + + /** + * @brief importFreeTerminals + * Add every free terminal of @a project to the terminal strip named + * after its label prefix, creating the strip when it does not exist + * yet. Everything happens on the undo stack, as one undoable step. + * @param move_placed : false (default, the "assign free terminals" + * action) only places free terminals, placed ones are never touched. + * True (the "re-group all terminals" action) also moves a placed + * terminal whose label prefix or plant and location no longer match + * its strip. Existing strips are never re-sorted. + * @return What has been done. Empty : nothing was pushed, the + * project is untouched. + */ + ImportResult importFreeTerminals(QETProject *project, bool move_placed = false); +} + +#endif // TERMINALSTRIPAUTOIMPORT_H diff --git a/sources/TerminalStrip/terminalstripdata.h b/sources/TerminalStrip/terminalstripdata.h index 191cccadc..b1189507b 100644 --- a/sources/TerminalStrip/terminalstripdata.h +++ b/sources/TerminalStrip/terminalstripdata.h @@ -44,8 +44,8 @@ class TerminalStripData : public PropertiesInterface private : static QDomElement infoToXml(QDomDocument &xml_doc, const QString &name, const QString &value); - QString m_installation = QStringLiteral("="), - m_location = QStringLiteral("+"), + QString m_installation, + m_location, m_name, m_comment, m_description, diff --git a/sources/TerminalStrip/ui/terminalstripeditorwindow.cpp b/sources/TerminalStrip/ui/terminalstripeditorwindow.cpp index ef7538c49..2d9a7a2eb 100644 --- a/sources/TerminalStrip/ui/terminalstripeditorwindow.cpp +++ b/sources/TerminalStrip/ui/terminalstripeditorwindow.cpp @@ -24,6 +24,7 @@ #include "../../qetproject.h" #include "../realterminal.h" #include "../terminalstrip.h" +#include "../terminalstripautoimport.h" #include "terminalstripcreatordialog.h" #include "terminalstripeditor.h" #include "terminalstripeditorwindow.h" @@ -31,6 +32,7 @@ #include #include +#include #include #include #include @@ -154,6 +156,11 @@ TerminalStripEditorWindow::TerminalStripEditorWindow(QETProject *project, QWidge ui->m_stacked_widget->insertWidget(EMPTY_PAGE, new QWidget(ui->m_stacked_widget)); ui->m_stacked_widget->insertWidget(FREE_TERMINAL_PAGE, m_free_terminal_editor); ui->m_stacked_widget->insertWidget(TERMINAL_STRIP_PAGE, m_terminal_strip_editor); + + //The strips are worked out from the labels of the free terminals, + //so that the user does not create them one by one : only the + //toolbar actions run the import, opening the window changes + //nothing } /** @@ -176,6 +183,81 @@ void TerminalStripEditorWindow::setProject(QETProject *project) m_terminal_strip_editor->setProject(project); } +/** + * @brief TerminalStripEditorWindow::importFreeTerminals + * Run one of the two import actions of the window, after the user has + * confirmed it : @a move_placed false assigns the free terminals only, + * true re-groups every terminal and also moves placed ones. Both run as + * one undoable step and say what has been done. + */ +void TerminalStripEditorWindow::importFreeTerminals(bool move_placed) +{ + if (!m_project) { + return; + } + //A reload of the free terminal table would lose its unapplied edits + if (m_free_terminal_editor && m_free_terminal_editor->hasPendingEdits()) + { + QMessageBox::information( + this, tr("Unapplied edits"), + tr("Apply or discard the pending edits of the free terminals " + "table first.")); + return; + } + + const auto title_ = move_placed ? tr("Re-group all terminals") + : tr("Assign free terminals"); + const auto text_ = move_placed + ? tr("Free terminals will be added to the terminal strips named " + "after their labels, and every terminal whose label prefix " + "or plant and location no longer match its strip will be " + "moved to the right strip.\n\n" + "Terminals you placed by hand in a strip whose name does " + "not match their label will be moved too. Existing strips " + "are never re-sorted.\n\n" + "This action can be undone with a single Undo.") + : tr("Every free terminal will be added to the terminal strip " + "named after its label (\"X1:2\" goes to the strip \"X1\"), " + "creating the strips when needed.\n\n" + "Terminals that already belong to a strip are not touched " + "and no strip is re-sorted.\n\n" + "This action can be undone with a single Undo."); + + if (QMessageBox::question(this, title_, text_, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) != QMessageBox::Yes) { + return; + } + + const auto result_ = TerminalStripAutoImport::importFreeTerminals( + m_project, move_placed); + if (result_.terminals == 0 && result_.strips == 0) { + return; + } + + m_tree_dock->reload(); + + if (move_placed) { + statusBar()->showMessage( + tr("%1 terminal(s) assigned or moved to a terminal strip.", + "", result_.terminals).arg(result_.terminals), + 10000); + } else { + statusBar()->showMessage( + tr("%1 free terminal(s) added to the terminal strips.", + "", result_.terminals).arg(result_.terminals), + 10000); + } +} + +void TerminalStripEditorWindow::on_m_import_free_terminals_triggered() { + importFreeTerminals(false); +} + +void TerminalStripEditorWindow::on_m_regroup_terminals_triggered() { + importFreeTerminals(true); +} + void TerminalStripEditorWindow::setCurrentStrip(TerminalStrip *strip) { m_tree_dock->setSelectedStrip(strip); } diff --git a/sources/TerminalStrip/ui/terminalstripeditorwindow.h b/sources/TerminalStrip/ui/terminalstripeditorwindow.h index 8786104dc..d8cc75f4b 100644 --- a/sources/TerminalStrip/ui/terminalstripeditorwindow.h +++ b/sources/TerminalStrip/ui/terminalstripeditorwindow.h @@ -69,6 +69,8 @@ class TerminalStripEditorWindow : public QMainWindow void on_m_add_terminal_strip_triggered(); void on_m_remove_terminal_triggered(); void on_m_reload_triggered(); + void on_m_import_free_terminals_triggered(); + void on_m_regroup_terminals_triggered(); void on_m_button_box_clicked(QAbstractButton *button); void on_m_stacked_widget_currentChanged(int arg1); @@ -76,6 +78,7 @@ class TerminalStripEditorWindow : public QMainWindow void addTreeDockWidget(); void currentStripChanged(TerminalStrip *strip); void updateUi(); + void importFreeTerminals(bool move_placed); private: Ui::TerminalStripEditorWindow *ui{nullptr}; diff --git a/sources/TerminalStrip/ui/terminalstripeditorwindow.ui b/sources/TerminalStrip/ui/terminalstripeditorwindow.ui index 710f106b6..a57a129ad 100644 --- a/sources/TerminalStrip/ui/terminalstripeditorwindow.ui +++ b/sources/TerminalStrip/ui/terminalstripeditorwindow.ui @@ -51,6 +51,8 @@ + + @@ -85,6 +87,28 @@ Reload the terminals + + + + + + Assign free terminals + + + Add the free terminals to the terminal strips named after their labels ("X1:2" goes to the strip "X1"), creating the strips when needed. Terminals already assigned to a strip are never touched. Asks for confirmation. + + + + + + + + Re-group all terminals + + + Assign the free terminals and also move every terminal whose label prefix or plant and location no longer match its strip. Existing strips are never re-sorted. Asks for confirmation. + +