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