Merge pull request #1550 from Kellermorph/auto-terminalstrip

Terminal strip: create the strips from the free terminals' labels
This commit is contained in:
Laurent Trinques
2026-10-19 18:14:13 +02:00
committed by GitHub
7 changed files with 509 additions and 2 deletions
+2
View File
@@ -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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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 <QCoreApplication>
#include <QMap>
#include <QUndoStack>
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<QSharedPointer<RealTerminal>> free_terminals;
QVector<QPair<QSharedPointer<RealTerminal>, TerminalStrip *>> placed_terminals;
QString name, plant, location;
};
QString elementInfo(const QSharedPointer<RealTerminal> &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<QString, StripGroup> groups_;
const auto collect_ = [&groups_](const QSharedPointer<RealTerminal> &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<RealTerminal> &a_,
const QSharedPointer<RealTerminal> &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<Plan> 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_;
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef TERMINALSTRIPAUTOIMPORT_H
#define TERMINALSTRIPAUTOIMPORT_H
#include <QString>
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
+2 -2
View File
@@ -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,
@@ -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 <QApplication>
#include <QElapsedTimer>
#include <QMessageBox>
#include <QProgressDialog>
#include <QThread>
#include <QWindow>
@@ -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);
}
@@ -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};
@@ -51,6 +51,8 @@
<addaction name="m_add_terminal_strip"/>
<addaction name="m_remove_terminal"/>
<addaction name="m_reload"/>
<addaction name="m_import_free_terminals"/>
<addaction name="m_regroup_terminals"/>
</widget>
<action name="m_add_terminal_strip">
<property name="icon">
@@ -85,6 +87,28 @@
<string>Reload the terminals</string>
</property>
</action>
<action name="m_import_free_terminals">
<property name="icon">
<iconset theme="list-add"/>
</property>
<property name="text">
<string>Assign free terminals</string>
</property>
<property name="toolTip">
<string>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.</string>
</property>
</action>
<action name="m_regroup_terminals">
<property name="icon">
<iconset theme="start"/>
</property>
<property name="text">
<string>Re-group all terminals</string>
</property>
<property name="toolTip">
<string>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.</string>
</property>
</action>
</widget>
<resources>
<include location="../../../qelectrotech.qrc"/>