diff --git a/CMakeLists.txt b/CMakeLists.txt
index 99973095d..bd685a45d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -345,6 +345,7 @@ target_include_directories(
${QET_DIR}/sources/print
${QET_DIR}/sources/svg
${QET_DIR}/sources/materiallist
+ ${QET_DIR}/sources/cablelist
)
if(NOT BUILD_WITH_KF)
diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 2a46f4e8e..8e4cca867 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -387,6 +387,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/diagramevent/diagrameventfillet.h
${QET_DIR}/sources/diagramevent/diagrameventaddpath.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddpath.h
+ ${QET_DIR}/sources/diagramevent/diagrameventaddcable.cpp
+ ${QET_DIR}/sources/diagramevent/diagrameventaddcable.h
${QET_DIR}/sources/diagramevent/diagrameventaddtext.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddtext.h
${QET_DIR}/sources/diagramevent/diagrameventinterface.cpp
@@ -812,6 +814,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/elementinfowidget.h
${QET_DIR}/sources/ui/terminalnumberingdialog.cpp
${QET_DIR}/sources/ui/terminalnumberingdialog.h
+ ${QET_DIR}/sources/ui/cablenumberingdialog.cpp
+ ${QET_DIR}/sources/ui/cablenumberingdialog.h
${QET_DIR}/sources/ui/elementpropertieswidget.cpp
${QET_DIR}/sources/ui/elementpropertieswidget.h
${QET_DIR}/sources/ui/formulaassistantdialog.cpp
@@ -937,6 +941,44 @@ set(QET_SRC_FILES
${QET_DIR}/sources/materiallist/materialselectiondialog.cpp
${QET_DIR}/sources/materiallist/materialselectiondialog.h
+ ${QET_DIR}/sources/cablelist/cabletypelist.cpp
+ ${QET_DIR}/sources/cablelist/cabletypelist.h
+ ${QET_DIR}/sources/cablelist/cableexportdialog.cpp
+ ${QET_DIR}/sources/cablelist/cableexportdialog.h
+ ${QET_DIR}/sources/cablelist/cablelistmodel.cpp
+ ${QET_DIR}/sources/cablelist/cablelistmodel.h
+ ${QET_DIR}/sources/cablelist/cablelistmodelpropertieswidget.cpp
+ ${QET_DIR}/sources/cablelist/cablelistmodelpropertieswidget.h
+ ${QET_DIR}/sources/cablelist/cablelistrows.cpp
+ ${QET_DIR}/sources/cablelist/cablelistrows.h
+ ${QET_DIR}/sources/cablelist/cablequerywidget.cpp
+ ${QET_DIR}/sources/cablelist/cablequerywidget.h
+
+ ${QET_DIR}/sources/cable/addcablecommand.cpp
+ ${QET_DIR}/sources/cable/addcablecommand.h
+ ${QET_DIR}/sources/cable/cable.cpp
+ ${QET_DIR}/sources/cable/cable.h
+ ${QET_DIR}/sources/cable/cablecopy.cpp
+ ${QET_DIR}/sources/cable/cablecopy.h
+ ${QET_DIR}/sources/cable/cablecreatedialog.cpp
+ ${QET_DIR}/sources/cable/cablecreatedialog.h
+ ${QET_DIR}/sources/cable/cablemanager.cpp
+ ${QET_DIR}/sources/cable/cablemanager.h
+ ${QET_DIR}/sources/cable/cablepart.cpp
+ ${QET_DIR}/sources/cable/cablepart.h
+ ${QET_DIR}/sources/cable/cablepropertiesdialog.cpp
+ ${QET_DIR}/sources/cable/cablepropertiesdialog.h
+ ${QET_DIR}/sources/cable/cablepropertieseditorwidget.cpp
+ ${QET_DIR}/sources/cable/cablepropertieseditorwidget.h
+ ${QET_DIR}/sources/cable/cablepropertieswidget.cpp
+ ${QET_DIR}/sources/cable/cablepropertieswidget.h
+ ${QET_DIR}/sources/cable/cablerenumber.cpp
+ ${QET_DIR}/sources/cable/cablerenumber.h
+ ${QET_DIR}/sources/cable/cabletextformatdialog.cpp
+ ${QET_DIR}/sources/cable/cabletextformatdialog.h
+ ${QET_DIR}/sources/cable/editcablecommand.cpp
+ ${QET_DIR}/sources/cable/editcablecommand.h
+
${QET_DIR}/sources/xml/terminalstripitemxml.cpp
${QET_DIR}/sources/xml/terminalstripitemxml.h
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.cpp
diff --git a/ico/icon-themes.qrc b/ico/icon-themes.qrc
index 30d2b9de1..47e98b7c6 100644
--- a/ico/icon-themes.qrc
+++ b/ico/icon-themes.qrc
@@ -257,6 +257,21 @@
themes/generated/traced-24/bring_forward.svgthemes/generated/traced-large-24/bring_forward.svgqet/traced/large22/bring_forward.svg
+ qet/traced/16/cable.svg
+ qet/traced/large16/cable.svg
+ qet/traced/22/cable.svg
+ qet/traced/large22/cable.svg
+ qet/traced/large16/cable.svg
+ qet/traced/large16/cable.svg
+ themes/generated/traced-large-24/cable.svg
+ themes/generated/traced-large-24/cable.svg
+ qet/traced/large16/cable.svg
+ qet/traced/large16/cable.svg
+ qet/traced/large16/cable.svg
+ qet/traced/large16/cable.svg
+ themes/generated/traced-24/cable.svg
+ themes/generated/traced-large-24/cable.svg
+ qet/traced/large22/cable.svgqet/traced/16/circle.svgqet/traced/large16/circle.svgqet/traced/22/circle.svg
@@ -2699,6 +2714,12 @@
themes/qet-dark/traced/large22/bring_forward.svgthemes/qet-dark/traced-large-24/bring_forward.svgthemes/qet-dark/traced-24/bring_forward.svg
+ themes/qet-dark/traced/16/cable.svg
+ themes/qet-dark/traced/large16/cable.svg
+ themes/qet-dark/traced/22/cable.svg
+ themes/qet-dark/traced/large22/cable.svg
+ themes/qet-dark/traced-large-24/cable.svg
+ themes/qet-dark/traced-24/cable.svgthemes/qet-dark/traced/16/circle.svgthemes/qet-dark/traced/large16/circle.svgthemes/qet-dark/traced/22/circle.svg
@@ -3516,6 +3537,21 @@
themes/qet-dark/traced-24/bring_forward.svgthemes/qet-dark/traced-large-24/bring_forward.svgthemes/qet-dark/traced/large22/bring_forward.svg
+ themes/qet-dark/traced/16/cable.svg
+ themes/qet-dark/traced/large16/cable.svg
+ themes/qet-dark/traced/22/cable.svg
+ themes/qet-dark/traced/large22/cable.svg
+ themes/qet-dark/traced/large16/cable.svg
+ themes/qet-dark/traced/large16/cable.svg
+ themes/qet-dark/traced-large-24/cable.svg
+ themes/qet-dark/traced-large-24/cable.svg
+ themes/qet-dark/traced/large16/cable.svg
+ themes/qet-dark/traced/large16/cable.svg
+ themes/qet-dark/traced/large16/cable.svg
+ themes/qet-dark/traced/large16/cable.svg
+ themes/qet-dark/traced-24/cable.svg
+ themes/qet-dark/traced-large-24/cable.svg
+ themes/qet-dark/traced/large22/cable.svgthemes/qet-dark/traced/16/circle.svgthemes/qet-dark/traced/large16/circle.svgthemes/qet-dark/traced/22/circle.svg
diff --git a/ico/qet/traced/16/cable.svg b/ico/qet/traced/16/cable.svg
new file mode 100644
index 000000000..7c619123c
--- /dev/null
+++ b/ico/qet/traced/16/cable.svg
@@ -0,0 +1,6 @@
+
diff --git a/ico/qet/traced/22/cable.svg b/ico/qet/traced/22/cable.svg
new file mode 100644
index 000000000..e0a389dea
--- /dev/null
+++ b/ico/qet/traced/22/cable.svg
@@ -0,0 +1,6 @@
+
diff --git a/ico/qet/traced/large16/cable.svg b/ico/qet/traced/large16/cable.svg
new file mode 100644
index 000000000..623488647
--- /dev/null
+++ b/ico/qet/traced/large16/cable.svg
@@ -0,0 +1,6 @@
+
diff --git a/ico/qet/traced/large22/cable.svg b/ico/qet/traced/large22/cable.svg
new file mode 100644
index 000000000..97f52785c
--- /dev/null
+++ b/ico/qet/traced/large22/cable.svg
@@ -0,0 +1,6 @@
+
diff --git a/ico/themes/generated/traced-24/cable.svg b/ico/themes/generated/traced-24/cable.svg
new file mode 100644
index 000000000..ff2411462
--- /dev/null
+++ b/ico/themes/generated/traced-24/cable.svg
@@ -0,0 +1,8 @@
+
diff --git a/ico/themes/generated/traced-large-24/cable.svg b/ico/themes/generated/traced-large-24/cable.svg
new file mode 100644
index 000000000..778188781
--- /dev/null
+++ b/ico/themes/generated/traced-large-24/cable.svg
@@ -0,0 +1,8 @@
+
diff --git a/ico/themes/qet-dark/traced-24/cable.svg b/ico/themes/qet-dark/traced-24/cable.svg
new file mode 100644
index 000000000..6cf88f6c5
--- /dev/null
+++ b/ico/themes/qet-dark/traced-24/cable.svg
@@ -0,0 +1,8 @@
+
diff --git a/ico/themes/qet-dark/traced-large-24/cable.svg b/ico/themes/qet-dark/traced-large-24/cable.svg
new file mode 100644
index 000000000..3aae9ea45
--- /dev/null
+++ b/ico/themes/qet-dark/traced-large-24/cable.svg
@@ -0,0 +1,8 @@
+
diff --git a/ico/themes/qet-dark/traced/16/cable.svg b/ico/themes/qet-dark/traced/16/cable.svg
new file mode 100644
index 000000000..b21070f7f
--- /dev/null
+++ b/ico/themes/qet-dark/traced/16/cable.svg
@@ -0,0 +1,6 @@
+
diff --git a/ico/themes/qet-dark/traced/22/cable.svg b/ico/themes/qet-dark/traced/22/cable.svg
new file mode 100644
index 000000000..608b450e4
--- /dev/null
+++ b/ico/themes/qet-dark/traced/22/cable.svg
@@ -0,0 +1,6 @@
+
diff --git a/ico/themes/qet-dark/traced/large16/cable.svg b/ico/themes/qet-dark/traced/large16/cable.svg
new file mode 100644
index 000000000..8c9386972
--- /dev/null
+++ b/ico/themes/qet-dark/traced/large16/cable.svg
@@ -0,0 +1,6 @@
+
diff --git a/ico/themes/qet-dark/traced/large22/cable.svg b/ico/themes/qet-dark/traced/large22/cable.svg
new file mode 100644
index 000000000..104596625
--- /dev/null
+++ b/ico/themes/qet-dark/traced/large22/cable.svg
@@ -0,0 +1,6 @@
+
diff --git a/sources/autoNum/assignvariables.cpp b/sources/autoNum/assignvariables.cpp
index 633fe7da3..40563956c 100644
--- a/sources/autoNum/assignvariables.cpp
+++ b/sources/autoNum/assignvariables.cpp
@@ -649,6 +649,74 @@ namespace autonum
}
}
+ /**
+ @brief resetFolioCounters
+ @param nc the rule to read
+ @return the same rule, with the parts whose counter belongs to a
+ single folio put back to the beginning that folio is numbered
+ from -- the initial value the rule carries for it -- while the
+ parts counting over the whole project are left alone
+ */
+ NumerotationContext resetFolioCounters(const NumerotationContext &nc)
+ {
+ NumerotationContext out = nc;
+ for (int i = 0; i < out.size(); ++i)
+ {
+ const QStringList parts = out.itemAt(i);
+ if (parts.isEmpty()) continue;
+
+ const QString &type = parts.at(0);
+ if (type == QLatin1String("unitfolio")
+ || type == QLatin1String("tenfolio")
+ || type == QLatin1String("hundredfolio"))
+ {
+ //The beginning a folio is numbered from is the one
+ //written into the rule, never a hard 1: a counter
+ //starting at 5 starts every folio at 5.
+ const bool has_start = parts.size() > 3
+ && !parts.at(3).isEmpty();
+ out.replaceValue(i, has_start ? parts.at(3)
+ : QStringLiteral("1"));
+ }
+ }
+ return out;
+ }
+
+ /**
+ @brief resetContextCounters
+ @param nc the rule to read
+ @return the same rule, with every part which counts set back to
+ the value it started from
+ */
+ NumerotationContext resetContextCounters(const NumerotationContext &nc)
+ {
+ NumerotationContext out = resetFolioCounters(nc);
+ for (int i = 0; i < out.size(); ++i)
+ {
+ const QStringList parts = out.itemAt(i);
+ if (parts.isEmpty()) continue;
+
+ const QString &type = parts.at(0);
+ if (type == QLatin1String("unit")
+ || type == QLatin1String("ten")
+ || type == QLatin1String("hundred"))
+ {
+ out.replaceValue(i, QStringLiteral("1"));
+ }
+ else if (type == QLatin1String("wrap"))
+ {
+ //A cyclic part walks over [0, modulus), so it goes
+ //back to 0 rather than to 1
+ out.replaceValue(i, QStringLiteral("0"));
+ }
+ else if (type == QLatin1String("alpha"))
+ {
+ out.replaceValue(i, QStringLiteral("a"));
+ }
+ }
+ return out;
+ }
+
/**
@brief numerotationContextToFormula
@param nc
diff --git a/sources/autoNum/assignvariables.h b/sources/autoNum/assignvariables.h
index 255ad6003..04e353274 100644
--- a/sources/autoNum/assignvariables.h
+++ b/sources/autoNum/assignvariables.h
@@ -117,6 +117,31 @@ namespace autonum
void setFolioSequentialToHash(QStringList &list, QHash &hash, const QString& autoNumName);
void setSequential(const QString& label, autonum::sequentialNumbers &seqStruct, NumerotationContext &context, Diagram *diagram, const QString& hashKey);
QString numerotationContextToFormula(const NumerotationContext &nc);
+ /**
+ Put back every part of a numbering rule whose counter belongs to
+ one single folio -- "Chiffre 1 - Folio" and its two longer forms
+ -- to the beginning that folio is numbered from, which is the
+ initial value written into the rule itself. The parts which count
+ over the whole project are left exactly as they are: a folio
+ starts again, the project does not.
+ @param nc the rule to read
+ @return the same rule, with its folio-bound counters restarted
+ */
+ NumerotationContext resetFolioCounters(const NumerotationContext &nc);
+ /**
+ Set every part of a numbering rule which counts back to its very
+ first value, so that numbering starts at the beginning again --
+ which is what numbering a whole project over has to do, whatever
+ the last number handed out happened to be. A part which counts
+ from an initial value of its own goes back to that value, a plain
+ number goes back to 1, a cyclic one to 0 and an alphabetic one to
+ "a". The parts which do not count -- a fixed text, the number of
+ the folio, the plant -- are left exactly as they are: they say
+ what a number is made of, they are not the part which goes on.
+ @param nc the rule to read
+ @return the same rule, with its counters at their beginning
+ */
+ NumerotationContext resetContextCounters(const NumerotationContext &nc);
QString elementPrefixForLocation(const ElementsLocation &location);
}
diff --git a/sources/autoNum/ui/numparteditorw.cpp b/sources/autoNum/ui/numparteditorw.cpp
index 9f45289ad..7b1d25908 100644
--- a/sources/autoNum/ui/numparteditorw.cpp
+++ b/sources/autoNum/ui/numparteditorw.cpp
@@ -105,6 +105,16 @@ NumPartEditorW::NumPartEditorW (NumerotationContext &context,
if (strl.at(0)=="wrap" && strl.size() > 4)
ui -> modulus_spinBox -> setValue(strl.at(4).toInt());
ui -> format_le -> setText(NumerotationContext::formatOf(strl));
+ //A counter bound to the folio keeps the start it was
+ //configured with: value_field shows the live counter, and
+ //writing that back as the start would make every folio
+ //begin at the last number the previous numbering gave out
+ if ((strl.at(0)==QLatin1String("unitfolio")
+ || strl.at(0)==QLatin1String("tenfolio")
+ || strl.at(0)==QLatin1String("hundredfolio"))
+ && strl.size() > 3 && !strl.at(3).isEmpty()) {
+ m_initial_value = strl.at(3).toInt();
+ }
}
}
@@ -131,8 +141,12 @@ void NumPartEditorW::setVisibleItems()
<< tr("Alphabetical")
<< tr("Text");
}
- else if (m_edited_type == 1)
+ else if (m_edited_type == 1 || m_edited_type == 3)
{
+ //Type 3 is a cable: it is drawn along a folio just like a
+ //conductor is, so it takes the same kinds of variable and
+ //none of the ones an element has and a cable has not (its
+ //line, its column, its prefix).
items << tr("number format 1")
<< tr("number format 1 - Sheet")
<< tr("number format 01")
@@ -232,7 +246,8 @@ NumerotationContext NumPartEditorW::toNumContext()
nc.addValue(type_str,
ui -> value_field -> displayText(),
ui -> increase_spinBox -> value(),
- ui->value_field->displayText().toInt(),
+ m_initial_value >= 0 ? m_initial_value
+ : ui->value_field->displayText().toInt(),
0,
number_format);
else if (type_str == "wrap")
@@ -315,6 +330,10 @@ void NumPartEditorW::on_type_cb_activated(int) {
*/
void NumPartEditorW::on_value_field_textEdited()
{
+ //What the user types in is a deliberate start for a counter
+ //bound to the folio; merely showing the live counter (as
+ //setContext() does) never changes the start.
+ m_initial_value = ui -> value_field -> text().toInt();
emit changed();
}
diff --git a/sources/autoNum/ui/numparteditorw.h b/sources/autoNum/ui/numparteditorw.h
index ce7d3ffae..b9ee8aafb 100644
--- a/sources/autoNum/ui/numparteditorw.h
+++ b/sources/autoNum/ui/numparteditorw.h
@@ -77,6 +77,14 @@ class NumPartEditorW : public QWidget
QValidator *intValidator;
QValidator *alphaValidator;
int m_edited_type = -1; ///<0 == element : 1 == conductor : 2 == folio
+ /**
+ The value a counter bound to the folio counts each folio
+ from (-1 while the row carries none): value_field shows
+ the live counter, which numbering a project over leaves
+ at the last number given out, so the start has to be
+ kept apart from what is on show.
+ */
+ int m_initial_value = -1;
diff --git a/sources/autoNum/ui/pastenumberingimport.cpp b/sources/autoNum/ui/pastenumberingimport.cpp
index 0e4ca0476..3a1bc9f2f 100644
--- a/sources/autoNum/ui/pastenumberingimport.cpp
+++ b/sources/autoNum/ui/pastenumberingimport.cpp
@@ -37,22 +37,41 @@ QList PasteNumberingImport::copiedBy(const
elements follow and the project does not have.
@param parent : parent of the question
@param copied : the numberings the copy carries
+ @param extra : commands the copy brings beside the paste itself
+ (the cable lines of a fragment, for example); they are pushed inside
+ the same macro, so one Ctrl+Z undoes all of it at once.
*/
void PasteNumberingImport::push(QWidget *parent,
Diagram *diagram,
const DiagramContent &content,
- const QList &copied)
+ const QList &copied,
+ const QList &extra)
{
QETProject *project = diagram->project();
auto *paste = new PasteDiagramCommand(diagram, content);
+ //The paste and the commands beside it are one step: only the
+ //macros really need more than the single push.
+ const auto push_all = [&]() {
+ if (extra.isEmpty()) {
+ diagram->undoStack().push(paste);
+ return;
+ }
+ diagram->undoStack().beginMacro(paste->text());
+ diagram->undoStack().push(paste);
+ for (QUndoCommand *cmd : extra) {
+ diagram->undoStack().push(cmd);
+ }
+ diagram->undoStack().endMacro();
+ };
+
QList missing;
if (project && !copied.isEmpty()
&& QSettings().value("diagramcommands/autonumber-pasted-elements", true).toBool()) {
missing = ElementAutoNumSchemeCommand::missingForPaste(project, copied, content.m_elements);
}
if (missing.isEmpty()) {
- diagram->undoStack().push(paste);
+ push_all();
return;
}
@@ -71,7 +90,7 @@ void PasteNumberingImport::push(QWidget *parent,
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
- diagram->undoStack().push(paste);
+ push_all();
return;
}
@@ -91,5 +110,8 @@ void PasteNumberingImport::push(QWidget *parent,
}
}
diagram->undoStack().push(paste);
+ for (QUndoCommand *cmd : extra) {
+ diagram->undoStack().push(cmd);
+ }
diagram->undoStack().endMacro();
}
diff --git a/sources/autoNum/ui/pastenumberingimport.h b/sources/autoNum/ui/pastenumberingimport.h
index 0889ad813..8e03c3b0d 100644
--- a/sources/autoNum/ui/pastenumberingimport.h
+++ b/sources/autoNum/ui/pastenumberingimport.h
@@ -23,6 +23,7 @@
#include
#include
+#include
class Diagram;
class QWidget;
@@ -40,6 +41,11 @@ class QWidget;
without a numbering.
The import and the paste are one undo step.
+
+ A paste which brings more than itself (the cable lines of the copy,
+ for instance, which are no part of a DiagramContent) hands those
+ commands over through @p extra: they are pushed inside the very
+ same macro, so one Ctrl+Z still takes the whole of it back.
*/
class PasteNumberingImport
{
@@ -49,7 +55,8 @@ class PasteNumberingImport
static void push(QWidget *parent,
Diagram *diagram,
const DiagramContent &content,
- const QList &copied);
+ const QList &copied,
+ const QList &extra = QList());
static QList copiedBy(const QDomDocument &clipboard);
};
diff --git a/sources/autoNum/ui/selectautonumw.cpp b/sources/autoNum/ui/selectautonumw.cpp
index ff32c7b31..dc215651e 100644
--- a/sources/autoNum/ui/selectautonumw.cpp
+++ b/sources/autoNum/ui/selectautonumw.cpp
@@ -138,6 +138,9 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
updatePartButtons();
applyEnable(false);
+ //Everything the rule held before is read now, so nothing of it
+ //is left to write: what is on show is what there is.
+ m_dirty = false;
}
/**
@@ -147,7 +150,11 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
*/
void SelectAutonumW::insertPartRow(NumPartEditorW *part)
{
- connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
+ connect(part, &NumPartEditorW::changed, this, [this]() {
+ m_dirty = true;
+ applyEnable();
+ emit contextEdited();
+ });
PartRow r;
r.part = part;
@@ -263,7 +270,10 @@ NumerotationContext SelectAutonumW::toNumContext() const
void SelectAutonumW::on_add_button_clicked()
{
insertPartRow(new NumPartEditorW(m_edited_type, this));
+ m_dirty = true;
+ ui -> remove_button -> setEnabled(true);
applyEnable();
+ emit contextEdited();
}
/**
@@ -272,9 +282,16 @@ void SelectAutonumW::on_add_button_clicked()
*/
void SelectAutonumW::on_remove_button_clicked()
{
- if (!m_rows.isEmpty()) {
+ //remove if @num_part_list contains more than one item
+ if (num_part_list_.size() > 1) {
+ m_dirty = true;
removePartRow(m_rows.last().row);
+ if (num_part_list_.size() == 1) {
+ ui -> remove_button -> setDisabled(true);
+ }
}
+ applyEnable();
+ emit contextEdited();
}
/**
@@ -296,6 +313,78 @@ QComboBox *SelectAutonumW::contextComboBox() const
return ui->m_comboBox;
}
+/**
+ @brief SelectAutonumW::setSingleRuleMode
+ @param single true to show this editor as the one and only rule of
+ its kind.
+
+ The row which lists the available numberings -- its label, its combo
+ box and the button which deletes one -- is hidden, so there is
+ nothing to name a rule with and nothing to choose between: the rule
+ being edited is all there is. The combo box itself stays where it is
+ and keeps holding the rule's name, so everything which reads or
+ writes the rule through contextComboBox() carries on untouched; he
+ just cannot see it.
+*/
+void SelectAutonumW::setSingleRuleMode(bool single)
+{
+ m_single_rule = single;
+ ui->label->setVisible(!single);
+ ui->m_comboBox->setVisible(!single);
+ ui->m_remove_pb->setVisible(!single);
+
+ //A single rule has no list to be deleted from, so it needs a
+ //button of its own to get out of the numbering again. It is
+ //only shown when there is really a rule to take away -- see
+ //setRuleRemovable.
+ if (single && !m_rule_remove_pb) {
+ m_rule_remove_pb = new QPushButton(tr("Delete the rule"), this);
+ m_rule_remove_pb->setToolTip(tr(
+ "Delete the cable numbering rule: cables are "
+ "numbered again as before, and the question of "
+ "defining one comes back on the next drawing."));
+ m_rule_remove_pb->setObjectName(QStringLiteral("m_rule_remove_pb"));
+ if (ui->m_definition_groupe && ui->m_definition_groupe->layout()) {
+ ui->m_definition_groupe->layout()->addWidget(m_rule_remove_pb);
+ }
+ connect(m_rule_remove_pb, &QPushButton::clicked,
+ this, &SelectAutonumW::removeClicked);
+ }
+ if (m_rule_remove_pb) {
+ m_rule_remove_pb->setVisible(m_single_rule && m_rule_removable);
+ }
+}
+
+/**
+ @brief SelectAutonumW::setRuleRemovable
+ Whether the button which takes the whole rule away is worth showing:
+ it has nothing to do while this editor holds no rule at all, and it
+ would only be a button which does nothing.
+ @param can_remove true when a rule is there to be taken away
+*/
+void SelectAutonumW::setRuleRemovable(bool can_remove)
+{
+ m_rule_removable = can_remove;
+ if (m_rule_remove_pb) {
+ m_rule_remove_pb->setVisible(m_single_rule && can_remove);
+ }
+}
+
+/**
+ @brief SelectAutonumW::isValid
+ @return true when there is at least one variable in the editor and
+ every one of them holds what it needs. An editor which has only just
+ been opened holds an empty rule, and an empty rule is not one.
+*/
+bool SelectAutonumW::isValid()
+{
+ if (num_part_list_.isEmpty()) return false;
+ for (NumPartEditorW *npe : num_part_list_) {
+ if (!npe || !npe->isValid()) return false;
+ }
+ return true;
+}
+
/**
@brief SelectAutonumW::on_buttonBox_clicked
Action on button clicked
@@ -364,6 +453,32 @@ void SelectAutonumW::on_buttonBox_clicked(QAbstractButton *button)
));
break;
}
+ else if (m_edited_type == 3)
+ {
+ QMessageBox::information (
+ this,
+ tr("Cable Auto Numbering",
+ "title window"),
+ tr("This is where you can define how new cables will be numbered.\n"
+ "-A numbering is composed of a minimum variable.\n"
+ "-You can add or delete a dialing variable through the - and + buttons.\n"
+ "-A numbering variable includes: a type, a value and an increment.\n"
+
+ "\n-the \"Digit 1\", \"Digit 01\" and \"Digit 001\" types represent a numeric type defined in the \"Value\" field, "
+ "which is incremented to each new cable by the value of the \"Increment\" field.\n"
+ "-\"Digit 01\" and \"Digit 001\", are respectively represented on the diagram by two and three digits minimum.\n"
+ "If the digit defined in the Value field has fewer digits than the chosen type, it will be preceded by one or two 0s in "
+ "order to respect its type.\n"
+
+ "\n-Type \"Text\", represents a fixed text.\nThe \"Increment\" field is not used.\n"
+
+ "\n-The \"Sheet no.\" type represents the number of the current sheet.\nThe other fields are not used.\n"
+
+ "\n-The \"Sheet\" type represents the name of the current sheet.\nThe other fields are not used.",
+ "help dialog about the cable autonumerotation"
+ ));
+ break;
+ }
else
{
QMessageBox::information (
@@ -397,6 +512,9 @@ void SelectAutonumW::on_buttonBox_clicked(QAbstractButton *button)
case QDialogButtonBox::ApplyRole:
applyEnable(false);
emit applyPressed();
+ //Whatever was edited is written now, so there is
+ //nothing of it left for a later save to write again.
+ m_dirty = false;
break;
};
}
diff --git a/sources/autoNum/ui/selectautonumw.h b/sources/autoNum/ui/selectautonumw.h
index 77055b764..b43c47fa2 100644
--- a/sources/autoNum/ui/selectautonumw.h
+++ b/sources/autoNum/ui/selectautonumw.h
@@ -27,6 +27,7 @@ class NumPartEditorW;
class QAbstractButton;
class FormulaAutonumberingW;
class QComboBox;
+class QPushButton;
class QToolButton;
class QHBoxLayout;
@@ -54,11 +55,55 @@ class SelectAutonumW : public QWidget
void contextToFormula ();
QString formula();
QComboBox *contextComboBox() const;
+ /**
+ Show this editor as the only numbering rule there is: the
+ row which lets several named numberings be kept and chosen
+ between is hidden, so the rule being edited is the one and
+ only. Everything else -- the definition itself, the buttons
+ around it -- stays exactly as it is for the other kinds.
+ */
+ void setSingleRuleMode(bool single);
+ bool isSingleRuleMode() const { return m_single_rule; }
+ /**
+ True while everything the editor holds is filled in well
+ enough to be a rule -- no variable left with nothing in it.
+ The Apply button says the same thing; this is so that a page
+ which saves on its own way knows it too, rather than writing
+ a rule nobody has finished filling in.
+ */
+ bool isValid();
+ /**
+ True while the rule on show has been touched since it was
+ read or since it was last applied: a page which saves on
+ its own way (the cable rule does) writes it only when there
+ is something to write, so that confirming a window with
+ nothing changed in it leaves the project alone.
+ */
+ bool isModified() const { return m_dirty; }
+ /**
+ Show the button which takes the rule itself away, which is
+ the only way to stop numbering cables automatically again:
+ the row the other numberings delete from is hidden here, so
+ a single rule would have no way out at all. The page behind
+ the editor says whether there is a rule to take away at the
+ moment -- without one the button has nothing to do and is
+ kept out of sight.
+ @param can_remove true when a rule is really there
+ */
+ void setRuleRemovable(bool can_remove);
void setExplicitNaming();
signals:
void applyPressed();
void removeClicked();
+ /**
+ Sent whenever what the rule holds is edited -- a
+ field of a part changed, a part added, a part taken
+ away -- so that a window holding this editor can let
+ its own buttons say whether there is something to
+ write now.
+ */
+ void contextEdited();
/// Only with setExplicitNaming()
void newClicked();
void renameClicked();
@@ -96,7 +141,19 @@ class SelectAutonumW : public QWidget
NumerotationContext m_context;
FormulaAutonumberingW *m_feaw;
FormulaAutonumberingW *m_fcaw;
- int m_edited_type = -1; ///<0 == element : 1 == conductor : 2 == folio
+ int m_edited_type = -1; ///<0 == element : 1 == conductor : 2 == folio : 3 == cable
+ ///< when true, the row of available numberings is hidden and
+ ///< this editor holds the one rule there is (see setSingleRuleMode)
+ bool m_single_rule = false;
+ ///< true from the moment the rule on show is touched until
+ ///< it is read again or applied (see isModified)
+ bool m_dirty = false;
+ ///< the button which takes the whole rule away, built for
+ ///< the one kind of numbering which has no list to delete
+ ///< it from (see setRuleRemovable)
+ QPushButton *m_rule_remove_pb = nullptr;
+ ///< whether such a rule is there to be taken away
+ bool m_rule_removable = false;
};
#endif // SELECTAUTONUMW_H
diff --git a/sources/bomexport.cpp b/sources/bomexport.cpp
index 97ecdff61..768dd78c0 100644
--- a/sources/bomexport.cpp
+++ b/sources/bomexport.cpp
@@ -29,7 +29,9 @@ QString csvField(QString value)
return QLatin1Char('"') + value + QLatin1Char('"');
}
-QByteArray csvRecord(const QStringList &values)
+}
+
+QByteArray BomExport::csvRecord(const QStringList &values)
{
QStringList escaped;
for (const auto &value : values) {
@@ -37,7 +39,6 @@ QByteArray csvRecord(const QStringList &values)
}
return (escaped.join(QLatin1Char(';')) + QLatin1Char('\n')).toUtf8();
}
-}
QStringList BomExport::defaultColumns()
{
diff --git a/sources/bomexport.h b/sources/bomexport.h
index 5305d378e..a12ba5789 100644
--- a/sources/bomexport.h
+++ b/sources/bomexport.h
@@ -30,6 +30,11 @@ namespace BomExport
QString defaultQuery(bool include_slaves = true,
bool include_junctions = true);
QString junctionFilter();
+ /// One CSV record: every field quoted, ';' between them, '\n'
+ /// after -- the format every CSV export of this program writes.
+ /// Public so a list which is not read from the project database
+ /// (the cable list) writes the same bytes.
+ QByteArray csvRecord(const QStringList &values);
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers = true, int *row_count = nullptr);
bool writeCsv(const QString &file_path, const QByteArray &csv,
diff --git a/sources/cable/addcablecommand.cpp b/sources/cable/addcablecommand.cpp
new file mode 100644
index 000000000..d3d81b41b
--- /dev/null
+++ b/sources/cable/addcablecommand.cpp
@@ -0,0 +1,221 @@
+/*
+ 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 "addcablecommand.h"
+
+#include "cablemanager.h"
+#include "cablepart.h"
+#include "../autoNum/numerotationcontextcommands.h"
+#include "../diagram.h"
+#include "../qetproject.h"
+
+#include
+
+/**
+ @brief AddCableCommand::AddCableCommand
+ @param cable the cable the line was just given to
+ @param part the line which was just drawn
+ @param diagram the folio it was drawn on
+ @param own_cable true when the cable was created by this very action
+ @param taken the cores this line took away from other cables, so
+ undo can give them back
+*/
+AddCableCommand::AddCableCommand(Cable *cable,
+ CablePart *part,
+ Diagram *diagram,
+ bool own_cable,
+ const QList &taken) :
+ m_cable(cable),
+ m_part(part),
+ m_diagram(diagram),
+ m_taken(taken),
+ m_own_cable(own_cable)
+{
+ if (!m_cable || !m_part) return;
+
+ m_part_data.uuid = m_part->partUuid();
+ m_part_data.diagram = m_diagram ? m_diagram->uuid() : QUuid();
+ m_part_data.p1 = m_part->firstPoint();
+ m_part_data.p2 = m_part->secondPoint();
+
+ for (const CableCore &core : m_cable->usedCores()) {
+ if (core.part == m_part_data.uuid) {
+ m_cores.append(core);
+ }
+ }
+
+ //A cable this action brought into being takes the next number
+ //the project's numbering rule hands out. The number itself was
+ //worked out before this command existed; what has to happen here
+ //is moving the rule on, and moving it is undoable just like the
+ //cable is -- so undoing gives the number back and the cable
+ //drawn after that takes it again instead of skipping over it.
+ //
+ //Only the rule as it stands now is taken down. Where the rule
+ //stands after this cable is worked out when the cable really
+ //comes into being, because several cables pasted together are
+ //all made before any of them is added, and each of them has to
+ //move the rule on from wherever the one before it left it.
+ if (m_own_cable && m_diagram && project() && project()->hasCableAutoNum())
+ {
+ const QString key = project()->cableCurrentAutoNum();
+ const NumerotationContext current = project()->cableAutoNum(key);
+ if (!current.isEmpty())
+ {
+ m_num_key = key;
+ m_num_before = current;
+ }
+ }
+}
+
+/**
+ @brief AddCableCommand::~AddCableCommand
+ Whatever the stack leaves behind has to be cleaned up here: a cable
+ which is not held by the project any more belongs to this command,
+ and so does a line which never made it onto a folio.
+
+ The cable is a QPointer, so if another command took it away first it
+ is already null here and there is nothing left to do -- which is
+ exactly the point, since following a raw pointer here meant freeing
+ the same cable a second time.
+*/
+AddCableCommand::~AddCableCommand()
+{
+ if (m_cable && m_own_cable) {
+ QETProject *proj = project();
+ if (!proj || !proj->cables().contains(m_cable.data())) {
+ delete m_cable.data();
+ }
+ }
+ if (m_part && !m_part->scene()) {
+ delete m_part;
+ }
+}
+
+/**
+ @brief AddCableCommand::project
+ @return the project this command works in, or nullptr
+*/
+QETProject *AddCableCommand::project() const
+{
+ return m_diagram ? m_diagram->project() : nullptr;
+}
+
+/**
+ @brief AddCableCommand::redo
+ Put the line on the folio, together with everything the cable holds
+ for it.
+*/
+void AddCableCommand::redo()
+{
+ QETProject *proj = project();
+ if (!proj || !m_cable || !m_part || !m_diagram) return;
+
+ if (m_own_cable) {
+ proj->addCable(m_cable);
+ } else {
+ if (!m_cable->hasPart(m_part_data.uuid)) {
+ m_cable->addPart(m_part_data);
+ }
+ for (const CableCore &core : m_cores) {
+ m_cable->setCore(core);
+ }
+ }
+
+ if (m_part->scene() != m_diagram) {
+ m_diagram->addItem(m_part);
+ }
+
+ //Take the cores away from the cables which held them before this
+ //line claimed those conductors. On the very first run they are
+ //already gone, so nothing happens then; redoing after an undo
+ //has to take them again.
+ for (const CableCoreTaken &taken : std::as_const(m_taken))
+ {
+ if (taken.cable && taken.cable != m_cable) {
+ taken.cable->removeCore(taken.core.core);
+ }
+ }
+ //The cable field of every conductor is worked out again either
+ //way: redoing after an undo puts the cable back into the project
+ //without it having said anything, and the wires it feeds would go
+ //on showing a blank cable field although the cable which
+ //generates that field is right there again.
+ //
+ //And the number this cable was given is spent now: the rule
+ //moves on to the one the next cable will be handed. Worked out
+ //here rather than when the command was made, because a run of
+ //cables pasted together is made from end to end before any of
+ //it is added -- each of them has to move the rule on from where
+ //the one before it left it.
+ if (!m_num_key.isEmpty()) {
+ NumerotationContextCommands ncc(proj->cableAutoNum(m_num_key),
+ m_diagram.data());
+ proj->addCableAutoNum(m_num_key, ncc.next());
+ proj->setCurrentCableAutoNum(m_num_key);
+ }
+ CableManager::refreshLabels(proj);
+}
+
+/**
+ @brief AddCableCommand::undo
+ Take the line back off the folio. An existing cable loses the
+ section and the cores it was carrying on it -- and with them the
+ cable field of the conductors, which CableManager works out again as
+ soon as the cable says it changed.
+
+ The cores this line had taken away from other cables go back to
+ those cables afterwards: without them the conductors they belong to
+ would be left with a blank cable field although the cable which
+ generates that field is still there.
+*/
+void AddCableCommand::undo()
+{
+ QETProject *proj = project();
+ if (!proj || !m_cable || !m_part || !m_diagram) return;
+
+ if (m_part->scene()) {
+ m_diagram->removeItem(m_part);
+ }
+
+ if (m_own_cable) {
+ //The cable keeps everything it knows: it is only taken out of
+ //the project, so redo finds it exactly as it was.
+ proj->removeCable(m_cable);
+ } else {
+ m_cable->removePart(m_part_data.uuid);
+ }
+
+ for (const CableCoreTaken &taken : std::as_const(m_taken))
+ {
+ if (taken.cable && taken.cable != m_cable) {
+ taken.cable->setCore(taken.core);
+ }
+ }
+ //The number this cable was given goes back into the rule, so
+ //the cable drawn after an undo takes that number rather than
+ //the one after it: nothing of it is left spent.
+ if (!m_num_key.isEmpty()) {
+ proj->addCableAutoNum(m_num_key, m_num_before);
+ proj->setCurrentCableAutoNum(m_num_key);
+ }
+
+ //Same as in redo(): worked out again whatever happened above,
+ //so a cable section or a whole cable taken away never leaves
+ //the wires it used to feed with a cable field it still writes.
+ CableManager::refreshLabels(proj);
+}
diff --git a/sources/cable/addcablecommand.h b/sources/cable/addcablecommand.h
new file mode 100644
index 000000000..2980ece12
--- /dev/null
+++ b/sources/cable/addcablecommand.h
@@ -0,0 +1,95 @@
+/*
+ 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 ADDCABLECOMMAND_H
+#define ADDCABLECOMMAND_H
+
+#include "cable.h"
+
+#include "../autoNum/numerotationcontext.h"
+
+#include
+#include
+#include
+
+class CablePart;
+class Diagram;
+class QETProject;
+
+/**
+ @brief Puts a drawn cable line on the folio, or takes it back off.
+
+ Both halves are one single step, because a cable line without its
+ cable is of no use to anybody: undoing has to take the line away and
+ leave nothing behind, and redoing has to put both back exactly as
+ they were.
+*/
+class AddCableCommand : public QUndoCommand
+{
+ public:
+ AddCableCommand(Cable *cable,
+ CablePart *part,
+ Diagram *diagram,
+ bool own_cable,
+ const QList &taken = QList());
+ ~AddCableCommand() override;
+
+ void undo() override;
+ void redo() override;
+
+ private:
+ QETProject *project() const;
+
+ private:
+ /**
+ The cable the line belongs to.
+
+ A pointer, not a raw one: RemoveCableCommand deletes the
+ cable when it goes, and both of them are torn down together
+ when the stack takes a new command. Whoever of the two runs
+ first frees the cable, so the other one must not follow it
+ into the freed memory -- QPointer goes null instead, and
+ this command then simply has nothing left to clean up.
+ */
+ QPointer m_cable;
+ ///The line itself, owned by the folio once it is drawn
+ QPointer m_part;
+ QPointer m_diagram;
+ ///Which section of the cable this line is
+ CablePartData m_part_data;
+ ///The cores the line was giving to the cable, put back by redo
+ QList m_cores;
+ ///The cores this line took away from other cables: redo takes
+ ///them again, undo gives them back
+ QList m_taken;
+ ///True when this command brought the cable into existence, so it
+ ///is the one which has to take it away again
+ bool m_own_cable = false;
+ /**
+ When this cable took a number out of the project's numbering
+ rule, the rule as it was before it did and as it was after:
+ an empty key means the project numbers its cables by no rule
+ and nothing here applies. Handing the number out is part of
+ bringing the cable into being, so undo has to hand it back --
+ otherwise undoing a cable and drawing it once more would
+ leave a gap in the numbering for good.
+ */
+ QString m_num_key;
+ NumerotationContext m_num_before;
+};
+
+#endif // ADDCABLECOMMAND_H
diff --git a/sources/cable/cable.cpp b/sources/cable/cable.cpp
new file mode 100644
index 000000000..79810c5e7
--- /dev/null
+++ b/sources/cable/cable.cpp
@@ -0,0 +1,921 @@
+/*
+ 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 "cable.h"
+
+#include
+#include
+
+#include
+
+namespace {
+ /**
+ @brief The name an alignment is written under in the file, an
+ empty string when the alignment is the plain default (the text
+ ends where it always did, before the line).
+ @param alignment Qt::AlignLeft, Qt::AlignHCenter or Qt::AlignRight
+ @return left, center or right
+ */
+ QString alignmentName(int alignment)
+ {
+ switch (alignment)
+ {
+ case int(Qt::AlignLeft): return QStringLiteral("left");
+ case int(Qt::AlignHCenter): return QStringLiteral("center");
+ case int(Qt::AlignRight): return QStringLiteral("right");
+ default: return QString();
+ }
+ }
+
+ /**
+ @brief The alignment a name written in the file stands for.
+ @param name left, center or right
+ @return the Qt flags, 0 when the name says nothing
+ */
+ int alignmentFromName(const QString &name)
+ {
+ if (name == QLatin1String("left")) return int(Qt::AlignLeft);
+ if (name == QLatin1String("center")) return int(Qt::AlignHCenter);
+ if (name == QLatin1String("right")) return int(Qt::AlignRight);
+ return 0;
+ }
+}
+
+/**
+ @brief Cable::Cable
+ @param parent
+*/
+Cable::Cable(QObject *parent) :
+ QObject(parent),
+ m_uuid(QUuid::createUuid())
+{
+}
+
+/**
+ @brief Cable::uuid
+ @return the identity of the cable, never empty once constructed
+*/
+QUuid Cable::uuid() const
+{
+ return m_uuid;
+}
+
+/**
+ @brief Cable::setUuid
+ Only used while reading a project: a cable keeps the identity it was
+ saved with, so the conductors it feeds stay bound to it.
+ @param uuid
+*/
+void Cable::setUuid(const QUuid &uuid)
+{
+ if (uuid.isNull() || uuid == m_uuid) {
+ return;
+ }
+ m_uuid = uuid;
+ emit changed();
+}
+
+/**
+ @brief Cable::designation
+ @return the cable number, e.g. 15W1, which heads the label drawn next
+ to the trunk line and opens every core label
+*/
+QString Cable::designation() const
+{
+ return m_designation;
+}
+
+/**
+ @brief Cable::setDesignation
+ @param designation
+*/
+void Cable::setDesignation(const QString &designation)
+{
+ if (m_designation == designation) return;
+ m_designation = designation;
+ emit changed();
+}
+
+/**
+ @brief Cable::designationByHand
+ @return true when the name of this cable was typed in rather than
+ worked out of the numbering rule
+*/
+bool Cable::designationByHand() const
+{
+ return m_designation_by_hand;
+}
+
+/**
+ @brief Cable::setDesignationByHand
+ Says where the name of this cable comes from. Nothing of it is
+ drawn, so it changes no label and asks for no redraw.
+ @param by_hand
+*/
+void Cable::setDesignationByHand(bool by_hand)
+{
+ m_designation_by_hand = by_hand;
+}
+
+/**
+ @brief Cable::bmk
+ @return the BMK of the cable
+*/
+QString Cable::bmk() const
+{
+ return m_bmk;
+}
+
+/**
+ @brief Cable::setBmk
+ @param bmk
+*/
+void Cable::setBmk(const QString &bmk)
+{
+ if (m_bmk == bmk) return;
+ m_bmk = bmk;
+ emit changed();
+}
+
+/**
+ @brief Cable::installation
+ @return the installation (Anlage) the cable belongs to
+*/
+QString Cable::installation() const
+{
+ return m_installation;
+}
+
+/**
+ @brief Cable::setInstallation
+ @param installation
+*/
+void Cable::setInstallation(const QString &installation)
+{
+ if (m_installation == installation) return;
+ m_installation = installation;
+ emit changed();
+}
+
+/**
+ @brief Cable::location
+ @return the location (Ort) the cable runs through
+*/
+QString Cable::location() const
+{
+ return m_location;
+}
+
+/**
+ @brief Cable::setLocation
+ @param location
+*/
+void Cable::setLocation(const QString &location)
+{
+ if (m_location == location) return;
+ m_location = location;
+ emit changed();
+}
+
+/**
+ @brief Cable::length
+ @return the length of the cable as it is written down: a free text,
+ because it may come from the drawing or be typed in by hand
+*/
+QString Cable::length() const
+{
+ return m_length;
+}
+
+/**
+ @brief Cable::setLength
+ @param length
+*/
+void Cable::setLength(const QString &length)
+{
+ if (m_length == length) return;
+ m_length = length;
+ emit changed();
+}
+
+/**
+ @brief Cable::properties
+ @return every field the user fills in, together with what of it he
+ want to see drawn next to the trunk line
+*/
+CableProperties Cable::properties() const
+{
+ CableProperties properties;
+ properties.designation = m_designation;
+ properties.designation_by_hand = m_designation_by_hand;
+ properties.type = m_type;
+ properties.installation = m_installation;
+ properties.location = m_location;
+ properties.length = m_length;
+ properties.show_designation = m_show_designation;
+ properties.show_type = m_show_type;
+ properties.show_installation = m_show_installation;
+ properties.show_location = m_show_location;
+ properties.show_length = m_show_length;
+ properties.texts = m_texts;
+ properties.text_offset = m_text_offset;
+ properties.core_offset = m_core_offset;
+ return properties;
+}
+
+/**
+ @brief Cable::setProperties
+ Write a whole set of fields at once -- one undo step covers it, and
+ the label next to the trunk line and the conductors it feeds are
+ worked out again once, not ten times.
+ @param properties
+*/
+void Cable::setProperties(const CableProperties &properties)
+{
+ if (properties == this->properties()) return;
+
+ m_designation = properties.designation;
+ m_designation_by_hand = properties.designation_by_hand;
+ m_type = properties.type;
+ m_installation = properties.installation;
+ m_location = properties.location;
+ m_length = properties.length;
+ m_show_designation = properties.show_designation;
+ m_show_type = properties.show_type;
+ m_show_installation = properties.show_installation;
+ m_show_location = properties.show_location;
+ m_show_length = properties.show_length;
+ m_texts = properties.texts;
+ m_text_offset = properties.text_offset;
+ m_core_offset = properties.core_offset;
+ emit changed();
+}
+
+/**
+ @brief Cable::type
+ @return the type of the cable, e.g. H07RN-F 4G1,5. The type is copied
+ from the cable type file when the cable is created, so a project
+ opens even when that file is gone or has changed since.
+*/
+QString Cable::type() const
+{
+ return m_type;
+}
+
+/**
+ @brief Cable::setType
+ @param type
+*/
+void Cable::setType(const QString &type)
+{
+ if (m_type == type) return;
+ m_type = type;
+ emit changed();
+}
+
+/**
+ @brief Cable::coreCount
+ @return how many cores the type of this cable has. This is only the
+ size of the type, not the number of cores actually wired: see
+ usedCoreCount().
+*/
+int Cable::coreCount() const
+{
+ return m_core_count;
+}
+
+/**
+ @brief Cable::setCoreCount
+ @param count
+*/
+void Cable::setCoreCount(int count)
+{
+ count = qMax(0, count);
+ if (m_core_count == count) return;
+ m_core_count = count;
+
+ //A type with fewer cores takes the cores it no longer has away
+ //from the conductors which showed them: the label of a core the
+ //cable doesn't own would be a lie.
+ for (int i = m_cores.size() - 1; i >= 0; --i) {
+ if (m_cores.at(i).core >= m_core_count) {
+ m_cores.removeAt(i);
+ }
+ }
+ emit changed();
+}
+
+/**
+ @brief Cable::coreColors
+ @return the colour of each core, in the order of the cable type file
+*/
+QStringList Cable::coreColors() const
+{
+ return m_core_colors;
+}
+
+/**
+ @brief Cable::setCoreColors
+ @param colors
+*/
+void Cable::setCoreColors(const QStringList &colors)
+{
+ if (m_core_colors == colors) return;
+ m_core_colors = colors;
+ emit changed();
+}
+
+/**
+ @brief Cable::typeState
+ @return everything the type of this cable decides, taken in one
+ block: changing the type works on that block, and undo puts the very
+ same block back -- name, size, colours and the cores which go with
+ them, so no mark can be left standing on a core the cable lost.
+*/
+CableTypeState Cable::typeState() const
+{
+ CableTypeState state;
+ state.type = m_type;
+ state.core_count = m_core_count;
+ state.core_colors = m_core_colors;
+ state.cores = m_cores;
+ return state;
+}
+
+/**
+ @brief Cable::applyTypeState
+ Put back a state taken with typeState().
+
+ The count comes before the cores: a cable which has fewer cores than
+ it had cannot hold the others, and setCores() drops what stands
+ beyond the count -- which is exactly what a type with fewer cores
+ means, and exactly what undo has to take back.
+ @param state
+*/
+void Cable::applyTypeState(const CableTypeState &state)
+{
+ setType(state.type);
+ setCoreColors(state.core_colors);
+ setCoreCount(state.core_count);
+ setCores(state.cores);
+}
+
+/**
+ @brief Cable::usedCores
+ @return every core wired to a conductor, ordered by core number
+*/
+QList Cable::usedCores() const
+{
+ QList cores = m_cores;
+ std::sort(cores.begin(), cores.end(),
+ [](const CableCore &a, const CableCore &b) {return a.core < b.core;});
+ return cores;
+}
+
+/**
+ @brief Cable::hasCore
+ @param core
+ @return true when that core is wired to a conductor
+*/
+bool Cable::hasCore(int core) const
+{
+ for (const CableCore &c : m_cores) {
+ if (c.core == core) return true;
+ }
+ return false;
+}
+
+/**
+ @brief Cable::core
+ @param core
+ @return the assignment of that core, an empty one when it is free
+*/
+CableCore Cable::core(int core) const
+{
+ for (const CableCore &c : m_cores) {
+ if (c.core == core) return c;
+ }
+ return CableCore();
+}
+
+/**
+ @brief Cable::setCore
+ Wire a core to a conductor, or move the slash of an already wired
+ core. A core number may only appear once, so the previous holder of
+ that number, if any, is replaced.
+ @param core
+*/
+void Cable::setCore(const CableCore &core)
+{
+ if (core.core < 0 || core.core >= m_core_count) return;
+
+ for (int i = 0; i < m_cores.size(); ++i) {
+ if (m_cores.at(i).core == core.core) {
+ if (m_cores.at(i).conductor == core.conductor
+ && m_cores.at(i).part == core.part
+ && m_cores.at(i).position == core.position) {
+ return;
+ }
+ m_cores[i] = core;
+ emit changed();
+ return;
+ }
+ }
+ m_cores.append(core);
+ emit changed();
+}
+
+/**
+ @brief Cable::setCores
+ Write a whole set of assignments in place of the current one, the way
+ undoing a dragged slash has to: every core which was moved, swapped
+ or dropped comes back at once.
+ @param cores
+*/
+void Cable::setCores(const QList &cores)
+{
+ m_cores = cores;
+ for (int i = m_cores.size() - 1; i >= 0; --i) {
+ if (m_cores.at(i).core < 0 || m_cores.at(i).core >= m_core_count) {
+ m_cores.removeAt(i);
+ }
+ }
+ emit changed();
+}
+
+/**
+ @brief Cable::removeCore
+ Wire a core back to nothing: its conductor stops showing the label
+ this cable gave it.
+ @param core
+*/
+void Cable::removeCore(int core)
+{
+ for (int i = 0; i < m_cores.size(); ++i) {
+ if (m_cores.at(i).core == core) {
+ m_cores.removeAt(i);
+ emit changed();
+ return;
+ }
+ }
+}
+
+/**
+ @brief Cable::usedCoreCount
+ @return how many cores of this cable are wired to a conductor
+*/
+int Cable::usedCoreCount() const
+{
+ return m_cores.size();
+}
+
+/**
+ @brief Cable::freeCoreCount
+ @return how many cores the type offers and no conductor shows yet
+*/
+int Cable::freeCoreCount() const
+{
+ return qMax(0, m_core_count - m_cores.size());
+}
+
+/**
+ @brief Cable::firstFreeCore
+ @return the lowest core number which is free, -1 when every core of
+ the type is already wired
+*/
+int Cable::firstFreeCore() const
+{
+ for (int i = 0; i < m_core_count; ++i) {
+ if (!hasCore(i)) return i;
+ }
+ return -1;
+}
+
+/**
+ @brief Cable::colorOfCore
+ @param core
+ @return the colour this core has in the cable type file, an empty
+ string when the type doesn't describe that many cores
+*/
+QString Cable::colorOfCore(int core) const
+{
+ return m_core_colors.value(core);
+}
+
+/**
+ @brief Cable::labelOfCore
+ The text written into the cable field of the conductor this core is
+ wired to. It is never typed by hand: it is worked out here, from the
+ cable and the core, so renaming the cable or changing its type
+ updates every conductor at once.
+ @param core
+ @return e.g. "15W1:br"
+*/
+QString Cable::labelOfCore(int core) const
+{
+ if (m_designation.isEmpty()) {
+ return QString();
+ }
+ const QString color = colorOfCore(core);
+ if (color.isEmpty()) {
+ return m_designation;
+ }
+ return m_designation + QLatin1Char(':') + color;
+}
+
+/**
+ @brief Cable::parts
+ @return every drawn section of this cable, whatever folio it sits on
+*/
+QList Cable::parts() const
+{
+ return m_parts;
+}
+
+/**
+ @brief Cable::hasPart
+ @param part
+ @return true when the cable has a section with that identity
+*/
+bool Cable::hasPart(const QUuid &part) const
+{
+ if (part.isNull()) return false;
+ for (const CablePartData &p : m_parts) {
+ if (p.uuid == part) return true;
+ }
+ return false;
+}
+
+/**
+ @brief Cable::part
+ @param part
+ @return the section with that identity, an invalid one when there is none
+*/
+CablePartData Cable::part(const QUuid &part) const
+{
+ for (const CablePartData &p : m_parts) {
+ if (p.uuid == part) return p;
+ }
+ return CablePartData();
+}
+
+/**
+ @brief Cable::addPart
+ @param part
+*/
+void Cable::addPart(const CablePartData &part)
+{
+ if (part.uuid.isNull() || hasPart(part.uuid)) return;
+ m_parts.append(part);
+ emit changed();
+}
+
+/**
+ @brief Cable::removePart
+ Also drops the cores drawn on that section, since their slash and
+ their colour label live with it.
+ @param part
+*/
+void Cable::removePart(const QUuid &part)
+{
+ bool touched = false;
+ for (int i = m_parts.size() - 1; i >= 0; --i) {
+ if (m_parts.at(i).uuid == part) {
+ m_parts.removeAt(i);
+ touched = true;
+ }
+ }
+ for (int i = m_cores.size() - 1; i >= 0; --i) {
+ if (m_cores.at(i).part == part) {
+ m_cores.removeAt(i);
+ touched = true;
+ }
+ }
+ if (touched) {
+ emit changed();
+ }
+}
+
+/**
+ @brief Cable::setPartGeometry
+ Moving or lengthening the trunk line is a purely optical change: no
+ core is added, none is removed, nothing is reassigned. Only the line
+ itself moves -- which is why this never emits changed(): the labels
+ the cable writes into the conductors do not depend on it.
+ @param part
+ @param p1
+ @param p2
+*/
+void Cable::setPartGeometry(const QUuid &part, const QPointF &p1, const QPointF &p2)
+{
+ for (int i = 0; i < m_parts.size(); ++i) {
+ if (m_parts.at(i).uuid != part) continue;
+ if (m_parts.at(i).p1 == p1 && m_parts.at(i).p2 == p2) return;
+ m_parts[i].p1 = p1;
+ m_parts[i].p2 = p2;
+ return;
+ }
+}
+
+/**
+ @brief Cable::setPartDiagram
+ @param part
+ @param diagram
+*/
+void Cable::setPartDiagram(const QUuid &part, const QUuid &diagram)
+{
+ for (int i = 0; i < m_parts.size(); ++i) {
+ if (m_parts.at(i).uuid != part) continue;
+ if (m_parts.at(i).diagram == diagram) return;
+ m_parts[i].diagram = diagram;
+ emit changed();
+ return;
+ }
+}
+
+/**
+ @brief Cable::movePartCores
+ Carry the colour labels of one section along when that section is
+ moved as a whole: when a copy is put down, when it is stamped a grid
+ way beside the original, and when the line travels with the rest of
+ a selection rather than on its own.
+
+ Quiet on purpose. Where a slash stands says nothing about the text
+ a conductor shows, so there is nothing to work out again -- and this
+ runs while items travel under the mouse, where one full pass over
+ every wire of the project would cost more than the movement itself.
+ @param part the section which moved
+ @param delta how far it came, in scene coordinates
+*/
+void Cable::movePartCores(const QUuid &part, const QPointF &delta)
+{
+ if (part.isNull() || delta.isNull()) return;
+
+ for (CableCore &core : m_cores)
+ {
+ if (core.part == part) {
+ core.position += delta;
+ }
+ }
+}
+
+/**
+ @brief Cable::toXml
+ @param document
+ @return the <cable> element holding every part and every wired core
+*/
+QDomElement Cable::toXml(QDomDocument &document) const
+{
+ QDomElement root = document.createElement(QLatin1String("cable"));
+ root.setAttribute(QLatin1String("uuid"), m_uuid.toString());
+ if (!m_designation.isEmpty()) {
+ root.setAttribute(QLatin1String("designation"), m_designation);
+ }
+ //Only a name which was typed in says so: the ones the numbering
+ //rule handed out are the ordinary case and need no marking
+ if (m_designation_by_hand) {
+ root.setAttribute(QLatin1String("designation_by_hand"), QLatin1String("1"));
+ }
+ if (!m_bmk.isEmpty()) {
+ root.setAttribute(QLatin1String("bmk"), m_bmk);
+ }
+ if (!m_installation.isEmpty()) {
+ root.setAttribute(QLatin1String("installation"), m_installation);
+ }
+ if (!m_location.isEmpty()) {
+ root.setAttribute(QLatin1String("location"), m_location);
+ }
+ if (!m_length.isEmpty()) {
+ root.setAttribute(QLatin1String("length"), m_length);
+ }
+ //Only what was switched off is written: a cable written before
+ //these existed shows everything, like the ones written from now
+ if (!m_show_designation) {
+ root.setAttribute(QLatin1String("show_designation"), QLatin1String("0"));
+ }
+ if (!m_show_type) {
+ root.setAttribute(QLatin1String("show_type"), QLatin1String("0"));
+ }
+ if (!m_show_installation) {
+ root.setAttribute(QLatin1String("show_installation"), QLatin1String("0"));
+ }
+ if (!m_show_location) {
+ root.setAttribute(QLatin1String("show_location"), QLatin1String("0"));
+ }
+ if (!m_show_length) {
+ root.setAttribute(QLatin1String("show_length"), QLatin1String("0"));
+ }
+ if (!m_type.isEmpty()) {
+ root.setAttribute(QLatin1String("type"), m_type);
+ }
+ if (m_core_count > 0) {
+ root.setAttribute(QLatin1String("cores"), QString::number(m_core_count));
+ }
+
+ if (!m_core_colors.isEmpty())
+ {
+ QDomElement colors = document.createElement(QLatin1String("colors"));
+ for (const QString &color : m_core_colors) {
+ QDomElement e = document.createElement(QLatin1String("color"));
+ e.appendChild(document.createTextNode(color));
+ colors.appendChild(e);
+ }
+ root.appendChild(colors);
+ }
+
+ //Only the texts this cable does not write the way the preferences
+ //say are stored, so a cable stays silent as long as it merely
+ //follows the default -- and a later change of that default still
+ //reaches it. The nudges are kept in the same element: they say
+ //where those texts stand rather than what the cable is.
+ if (!m_texts.isEmpty() || !m_text_offset.isNull() || !m_core_offset.isNull())
+ {
+ QDomElement texts = document.createElement(QLatin1String("texts"));
+ bool stored = false;
+ for (auto it = m_texts.cbegin(); it != m_texts.cend(); ++it)
+ {
+ const CableTextFormat &format = it.value();
+ if (format.font.isEmpty() && format.alignment == 0) {
+ continue;
+ }
+ QDomElement e = document.createElement(QLatin1String("text"));
+ e.setAttribute(QLatin1String("key"), it.key());
+ if (!format.font.isEmpty()) {
+ e.setAttribute(QLatin1String("font"), format.font);
+ }
+ const QString align = alignmentName(format.alignment);
+ if (!align.isEmpty()) {
+ e.setAttribute(QLatin1String("align"), align);
+ }
+ texts.appendChild(e);
+ stored = true;
+ }
+ if (!m_text_offset.isNull())
+ {
+ texts.setAttribute(QLatin1String("text_dx"),
+ QString::number(m_text_offset.x(), 'f', 2));
+ texts.setAttribute(QLatin1String("text_dy"),
+ QString::number(m_text_offset.y(), 'f', 2));
+ stored = true;
+ }
+ if (!m_core_offset.isNull())
+ {
+ texts.setAttribute(QLatin1String("core_dx"),
+ QString::number(m_core_offset.x(), 'f', 2));
+ texts.setAttribute(QLatin1String("core_dy"),
+ QString::number(m_core_offset.y(), 'f', 2));
+ stored = true;
+ }
+ if (stored) {
+ root.appendChild(texts);
+ }
+ }
+
+ for (const CablePartData &part : m_parts)
+ {
+ QDomElement e = document.createElement(QLatin1String("part"));
+ e.setAttribute(QLatin1String("uuid"), part.uuid.toString());
+ e.setAttribute(QLatin1String("diagram"), part.diagram.toString());
+ e.setAttribute(QLatin1String("x1"), QString::number(part.p1.x(), 'f', 2));
+ e.setAttribute(QLatin1String("y1"), QString::number(part.p1.y(), 'f', 2));
+ e.setAttribute(QLatin1String("x2"), QString::number(part.p2.x(), 'f', 2));
+ e.setAttribute(QLatin1String("y2"), QString::number(part.p2.y(), 'f', 2));
+ root.appendChild(e);
+ }
+
+ for (const CableCore &core : usedCores())
+ {
+ QDomElement e = document.createElement(QLatin1String("core"));
+ e.setAttribute(QLatin1String("index"), QString::number(core.core));
+ if (!core.part.isNull()) {
+ e.setAttribute(QLatin1String("part"), core.part.toString());
+ }
+ if (!core.conductor.isNull()) {
+ e.setAttribute(QLatin1String("conductor"), core.conductor.toString());
+ }
+ e.setAttribute(QLatin1String("x"), QString::number(core.position.x(), 'f', 2));
+ e.setAttribute(QLatin1String("y"), QString::number(core.position.y(), 'f', 2));
+ root.appendChild(e);
+ }
+
+ return root;
+}
+
+/**
+ @brief Cable::fromXml
+ @param element a <cable> element
+*/
+void Cable::fromXml(const QDomElement &element)
+{
+ m_uuid = QUuid(element.attribute(QLatin1String("uuid")));
+ if (m_uuid.isNull()) {
+ m_uuid = QUuid::createUuid();
+ }
+ m_designation = element.attribute(QLatin1String("designation"));
+ //A file written before this existed holds names the program
+ //gave, which is what not marking them means
+ m_designation_by_hand = element.attribute(
+ QLatin1String("designation_by_hand")) == QLatin1String("1");
+ m_bmk = element.attribute(QLatin1String("bmk"));
+ m_installation = element.attribute(QLatin1String("installation"));
+ m_location = element.attribute(QLatin1String("location"));
+ m_length = element.attribute(QLatin1String("length"));
+ m_show_designation = element.attribute(
+ QLatin1String("show_designation"), QLatin1String("1")).toInt() != 0;
+ m_show_type = element.attribute(
+ QLatin1String("show_type"), QLatin1String("1")).toInt() != 0;
+ m_show_installation = element.attribute(
+ QLatin1String("show_installation"), QLatin1String("1")).toInt() != 0;
+ m_show_location = element.attribute(
+ QLatin1String("show_location"), QLatin1String("1")).toInt() != 0;
+ m_show_length = element.attribute(
+ QLatin1String("show_length"), QLatin1String("1")).toInt() != 0;
+ m_type = element.attribute(QLatin1String("type"));
+ m_core_count = qMax(0, element.attribute(QLatin1String("cores"), QLatin1String("0")).toInt());
+
+ m_core_colors.clear();
+ const QDomElement colors = element.firstChildElement(QLatin1String("colors"));
+ for (QDomElement e = colors.firstChildElement(QLatin1String("color"));
+ !e.isNull();
+ e = e.nextSiblingElement(QLatin1String("color"))) {
+ m_core_colors.append(e.text());
+ }
+
+ //The texts this cable writes its own way; everything not in here
+ //keeps following what the preferences say.
+ m_texts.clear();
+ m_text_offset = QPointF();
+ m_core_offset = QPointF();
+ const QDomElement texts = element.firstChildElement(QLatin1String("texts"));
+ for (QDomElement e = texts.firstChildElement(QLatin1String("text"));
+ !e.isNull();
+ e = e.nextSiblingElement(QLatin1String("text")))
+ {
+ const QString key = e.attribute(QLatin1String("key"));
+ if (key.isEmpty()) continue;
+ CableTextFormat format;
+ format.font = e.attribute(QLatin1String("font"));
+ format.alignment = alignmentFromName(e.attribute(QLatin1String("align")));
+ if (format.font.isEmpty() && format.alignment == 0) continue;
+ m_texts.insert(key, format);
+ }
+ if (!texts.isNull())
+ {
+ m_text_offset.setX(texts.attribute(QLatin1String("text_dx")).toDouble());
+ m_text_offset.setY(texts.attribute(QLatin1String("text_dy")).toDouble());
+ m_core_offset.setX(texts.attribute(QLatin1String("core_dx")).toDouble());
+ m_core_offset.setY(texts.attribute(QLatin1String("core_dy")).toDouble());
+ }
+
+ m_parts.clear();
+ for (QDomElement e = element.firstChildElement(QLatin1String("part"));
+ !e.isNull();
+ e = e.nextSiblingElement(QLatin1String("part")))
+ {
+ CablePartData part;
+ part.uuid = QUuid(e.attribute(QLatin1String("uuid")));
+ if (part.uuid.isNull()) {
+ continue;
+ }
+ part.diagram = QUuid(e.attribute(QLatin1String("diagram")));
+ part.p1 = QPointF(e.attribute(QLatin1String("x1")).toDouble(),
+ e.attribute(QLatin1String("y1")).toDouble());
+ part.p2 = QPointF(e.attribute(QLatin1String("x2")).toDouble(),
+ e.attribute(QLatin1String("y2")).toDouble());
+ m_parts.append(part);
+ }
+
+ m_cores.clear();
+ for (QDomElement e = element.firstChildElement(QLatin1String("core"));
+ !e.isNull();
+ e = e.nextSiblingElement(QLatin1String("core")))
+ {
+ CableCore core;
+ core.core = e.attribute(QLatin1String("index"), QLatin1String("-1")).toInt();
+ if (core.core < 0) {
+ continue;
+ }
+ core.part = QUuid(e.attribute(QLatin1String("part")));
+ core.conductor = QUuid(e.attribute(QLatin1String("conductor")));
+ core.position = QPointF(e.attribute(QLatin1String("x")).toDouble(),
+ e.attribute(QLatin1String("y")).toDouble());
+ if (hasCore(core.core)) {
+ continue;
+ }
+ m_cores.append(core);
+ }
+}
diff --git a/sources/cable/cable.h b/sources/cable/cable.h
new file mode 100644
index 000000000..29a0c9d55
--- /dev/null
+++ b/sources/cable/cable.h
@@ -0,0 +1,376 @@
+/*
+ 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 CABLE_H
+#define CABLE_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class QDomDocument;
+class QDomElement;
+
+/**
+ @brief One core of a cable which is wired to a conductor of the drawing.
+
+ A cable owns its assignment: this is the only place in QElectroTech
+ which knows that core number 3 of cable 15W1 lands on such a
+ conductor. The conductor itself only carries the text the assignment
+ generates (15W1:br), written back by CableManager::refreshLabels(),
+ so the text can never drift away from the cable it comes from.
+*/
+struct CableCore
+{
+ /// Core number inside the cable, 0 based, from 0 to coreCount()-1
+ int core = -1;
+ /// The part carrying the slash drawn for this core
+ QUuid part;
+ /// The conductor this core lands on, null when the core is free
+ QUuid conductor;
+ /// Where the slash sits, in the coordinates of its own part
+ QPointF position;
+
+ ///Two assignments are the same when they say the same thing, so
+ ///undo can tell a gesture which changed something from one which
+ ///merely put the slash back where it already was
+ friend bool operator==(const CableCore &a, const CableCore &b)
+ {
+ return a.core == b.core && a.part == b.part
+ && a.conductor == b.conductor && a.position == b.position;
+ }
+ friend bool operator!=(const CableCore &a, const CableCore &b)
+ {
+ return !(a == b);
+ }
+};
+
+/**
+ @brief One drawn section of a cable: the trunk line of one folio.
+
+ The geometry is kept as plain data rather than as a pointer to the
+ graphics item, because the folio owns the item and deletes it when the
+ folio is deleted, while the cable outlives it (it may have another
+ part on another folio).
+*/
+struct CablePartData
+{
+ QUuid uuid;
+ QUuid diagram;
+ QPointF p1;
+ QPointF p2;
+
+ bool isHorizontal() const {return qFuzzyCompare(p1.y(), p2.y());}
+};
+
+/**
+ @brief How one of the texts of a cable is written.
+
+ Both fields may say nothing at all, and that is the normal case:
+ the cable then writes that text the way the preferences offer it
+ (QETApp::cableTextsFont() and QETApp::cableCoreFont()), so picking
+ another default in the preferences reaches every cable which has
+ not been given a font of its own.
+
+ The texts of a cable are kept in one hash rather than one pair of
+ members each, so that a new text can be added without dragging a
+ new field through the whole code. They are named the way the file
+ writes them:
+ "designation", "type", "installation", "location", "length" for the
+ five texts written next to the trunk line, and "cores" for the
+ colour labels of the cores -- which have no alignment, being turned
+ by a quarter along their own line.
+*/
+struct CableTextFormat
+{
+ ///The font, in the stable description QETUtils::fontToString()
+ ///writes; empty when this cable follows the preferences
+ QString font;
+ ///Qt::AlignLeft, Qt::AlignHCenter or Qt::AlignRight; 0 when the
+ ///text goes on ending where it always did, before the line
+ int alignment = 0;
+
+ bool operator==(const CableTextFormat &other) const
+ {
+ return font == other.font && alignment == other.alignment;
+ }
+ bool operator!=(const CableTextFormat &other) const
+ {
+ return !(*this == other);
+ }
+};
+
+/**
+ @brief Everything about a cable the user may type in or tick, and
+ whether what he typed is written on the sheet at all.
+
+ It is kept as a whole so that one undo step can put a whole edit
+ back, and so the selection panel and the dialog always offer exactly
+ the same fields in the same order.
+*/
+struct CableProperties
+{
+ ///Name of the cable, e.g. 15W1 -- the head of the label
+ QString designation;
+ /**
+ Where that name comes from: false while the program worked
+ it out of the numbering rule, true from the moment it was
+ typed in. Nothing shows it and no field edits it -- it
+ travels with the fields so that undo puts it back with them,
+ and so that numbering the whole project over again knows
+ which names were never the rule's to begin with and may be
+ left alone.
+ */
+ bool designation_by_hand = false;
+ ///Kind of cable, e.g. H07RN-F 4G1,5
+ QString type;
+ ///Installation the cable belongs to
+ QString installation;
+ ///Place the cable runs through
+ QString location;
+ ///Length, typed in by hand
+ QString length;
+
+ ///What of that is written next to the trunk line of the drawing
+ bool show_designation = true;
+ bool show_type = true;
+ bool show_installation = true;
+ bool show_location = true;
+ bool show_length = true;
+
+ ///How each text is written -- font and alignment, keyed by the
+ ///names listed on CableTextFormat. A text which is not in here
+ ///follows what the preferences say, which is where a cable
+ ///freshly drawn stands for all of them.
+ QHash texts;
+
+ ///How far the label at the left of the trunk line is nudged from
+ ///where it has always stood. It is a nudge on the drawing only,
+ ///and it moves the whole label as one thing: installation, place
+ ///and designation over the line, the type and the length under
+ ///it -- while the line, the slashes and the wiring keep standing
+ ///exactly where they were.
+ QPointF text_offset;
+ ///How far the row of colours is nudged from the slashes it names.
+ ///Again on the drawing only: the slashes, the entries they mark
+ ///and every wire behind them stay where they are, and so does the
+ ///row itself when the trunk line is moved.
+ QPointF core_offset;
+
+ bool operator==(const CableProperties &other) const
+ {
+ return designation == other.designation
+ && designation_by_hand == other.designation_by_hand
+ && type == other.type
+ && installation == other.installation
+ && location == other.location
+ && length == other.length
+ && show_designation == other.show_designation
+ && show_type == other.show_type
+ && show_installation == other.show_installation
+ && show_location == other.show_location
+ && show_length == other.show_length
+ && texts == other.texts
+ && text_offset == other.text_offset
+ && core_offset == other.core_offset;
+ }
+ bool operator!=(const CableProperties &other) const
+ {
+ return !(*this == other);
+ }
+};
+
+/**
+ @brief Everything about a cable which its type decides, taken as one
+ block so that changing the type is one single step.
+
+ The cores go with the type rather than with the text fields: taking a
+ type with fewer cores takes those cores off the drawing, and undoing
+ has to bring their marks back along with the name and the colours.
+*/
+struct CableTypeState
+{
+ ///Kind of cable, e.g. H07RN-F 4G1,5
+ QString type;
+ ///How many cores that type has
+ int core_count = 0;
+ ///The colour of each of them, in core order
+ QStringList core_colors;
+ ///The cores the cable really holds at this moment
+ QList cores;
+
+ bool operator==(const CableTypeState &other) const
+ {
+ return type == other.type
+ && core_count == other.core_count
+ && core_colors == other.core_colors
+ && cores == other.cores;
+ }
+ bool operator!=(const CableTypeState &other) const
+ {
+ return !(*this == other);
+ }
+};
+
+/**
+ @brief The Cable class is the project wide object a drawn cable line
+ stands for.
+
+ It carries what the user fills in -- the type, the number, BMK,
+ installation, location -- together with what QElectroTech works out:
+ how many cores the type has, which colour each core has, which cores
+ are wired to which conductor, and which folios the cable runs on.
+
+ Properties belong to the cable, never to one of its parts: editing
+ the type or the number anywhere changes it for every part.
+*/
+class Cable : public QObject
+{
+ Q_OBJECT
+
+ public:
+ explicit Cable(QObject *parent = nullptr);
+
+ //Identity
+ QUuid uuid() const;
+ void setUuid(const QUuid &uuid);
+
+ //Properties the user fills in
+ QString designation() const;
+ void setDesignation(const QString &designation);
+ /**
+ True when the name of this cable was typed in rather
+ than worked out of the numbering rule: numbering the
+ whole project over asks before it takes such a name.
+ */
+ bool designationByHand() const;
+ void setDesignationByHand(bool by_hand);
+ QString bmk() const;
+ void setBmk(const QString &bmk);
+ QString installation() const;
+ void setInstallation(const QString &installation);
+ QString location() const;
+ void setLocation(const QString &location);
+ QString length() const;
+ void setLength(const QString &length);
+
+ ///Every field of the cable, and what of it is drawn on the sheet
+ CableProperties properties() const;
+ void setProperties(const CableProperties &properties);
+
+ //Type, copied from the cable type file when the cable is created
+ QString type() const;
+ void setType(const QString &type);
+ int coreCount() const;
+ void setCoreCount(int count);
+ QStringList coreColors() const;
+ void setCoreColors(const QStringList &colors);
+ ///Everything the type of this cable decides, taken as one block
+ CableTypeState typeState() const;
+ ///Put back a state taken with typeState(), in one single pass
+ void applyTypeState(const CableTypeState &state);
+
+ //Cores
+ QList usedCores() const;
+ bool hasCore(int core) const;
+ CableCore core(int core) const;
+ void setCore(const CableCore &core);
+ ///Write a whole set of assignments in place of another
+ void setCores(const QList &cores);
+ void removeCore(int core);
+ int usedCoreCount() const;
+ int freeCoreCount() const;
+ ///The lowest core number which is free, -1 when none is
+ int firstFreeCore() const;
+ ///The colour of a core, as written in the cable type file
+ QString colorOfCore(int core) const;
+ ///The text a conductor of this core shows, e.g. "15W1:br"
+ QString labelOfCore(int core) const;
+
+ //Parts
+ QList parts() const;
+ bool hasPart(const QUuid &part) const;
+ CablePartData part(const QUuid &part) const;
+ void addPart(const CablePartData &part);
+ void removePart(const QUuid &part);
+ void setPartGeometry(const QUuid &part, const QPointF &p1, const QPointF &p2);
+ void setPartDiagram(const QUuid &part, const QUuid &diagram);
+ ///Carry the colour labels of one section along when that
+ ///section is moved as a whole (paste, duplicate, a selection
+ ///being moved with everything else on it)
+ void movePartCores(const QUuid &part, const QPointF &delta);
+
+ //Serialisation
+ QDomElement toXml(QDomDocument &document) const;
+ void fromXml(const QDomElement &element);
+
+ signals:
+ void changed();
+
+ private:
+ QUuid m_uuid;
+ QString m_designation;
+ ///< where that name comes from (see designationByHand)
+ bool m_designation_by_hand = false;
+ QString m_bmk;
+ QString m_installation;
+ QString m_location;
+ QString m_length;
+ QString m_type;
+ int m_core_count = 0;
+ QStringList m_core_colors;
+ QList m_cores;
+ QList m_parts;
+ ///What the label next to the trunk line is allowed to show
+ bool m_show_designation = true;
+ bool m_show_type = true;
+ bool m_show_installation = true;
+ bool m_show_location = true;
+ bool m_show_length = true;
+ ///The texts this cable writes its own way, by the names
+ ///listed on CableTextFormat; everything not in here follows
+ ///what the preferences say
+ QHash m_texts;
+ ///How far the label at the left of the line and the row of
+ ///colours are nudged on the drawing: distances nothing else
+ ///in the project ever looks at
+ QPointF m_text_offset;
+ QPointF m_core_offset;
+};
+
+/**
+ @brief One core which was taken away from another cable.
+
+ A conductor belongs to one cable only, so drawing a second cable
+ across wires a first one already holds takes those cores away from
+ it. That has to be remembered where it is decided, so that undoing
+ the second cable gives the first one its cores back instead of
+ leaving those conductors with a blank cable field forever.
+*/
+struct CableCoreTaken
+{
+ ///The cable the core was taken from; null when it is gone
+ QPointer cable;
+ ///The core exactly as the other cable held it
+ CableCore core;
+};
+
+#endif // CABLE_H
diff --git a/sources/cable/cablecopy.cpp b/sources/cable/cablecopy.cpp
new file mode 100644
index 000000000..2ac97c893
--- /dev/null
+++ b/sources/cable/cablecopy.cpp
@@ -0,0 +1,454 @@
+/*
+ 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 "cablecopy.h"
+
+#include "cablemanager.h"
+#include "cablepart.h"
+
+#include "../autoNum/assignvariables.h"
+#include "../autoNum/numerotationcontext.h"
+#include "../diagram.h"
+#include "../qetgraphicsitem/conductor.h"
+#include "../qetproject.h"
+
+#include
+#include
+
+#include
+
+/**
+ @brief CableCopy::write
+ Puts the cable lines selected on @a diagram into @a document, so a
+ copy carries them the way it carries the elements and the texts of
+ the same selection.
+
+ Only the line which was picked is written, together with the whole
+ cable it belongs to: the copy needs everything the cable knows, but
+ only the one section it is standing on.
+ @param diagram the folio being copied
+ @param document the fragment the clipboard is about to hold
+*/
+void CableCopy::write(Diagram *diagram, QDomDocument &document)
+{
+ if (!diagram) return;
+ QDomElement root = document.documentElement();
+ if (root.isNull()) return;
+
+ QList parts;
+ for (QGraphicsItem *item : diagram->selectedItems()) {
+ if (auto *part = qgraphicsitem_cast(item)) {
+ parts << part;
+ }
+ }
+ if (parts.isEmpty()) return;
+
+ QDomElement block = document.createElement(QStringLiteral("cables"));
+ bool anything = false;
+ for (CablePart *part : std::as_const(parts))
+ {
+ Cable *cable = part->cable();
+ if (!cable) continue;
+
+ QDomElement element = cable->toXml(document);
+ const QPointF p1 = part->firstPoint();
+ const QPointF p2 = part->secondPoint();
+ element.setAttribute(QStringLiteral("line"), part->partUuid().toString());
+ element.setAttribute(QStringLiteral("x1"), QString::number(p1.x(), 'f', 2));
+ element.setAttribute(QStringLiteral("y1"), QString::number(p1.y(), 'f', 2));
+ element.setAttribute(QStringLiteral("x2"), QString::number(p2.x(), 'f', 2));
+ element.setAttribute(QStringLiteral("y2"), QString::number(p2.y(), 'f', 2));
+ block.appendChild(element);
+ anything = true;
+ }
+ if (anything) root.appendChild(block);
+}
+
+/**
+ @brief CableCopy::read
+ Builds a cable of its own for every line @a root carries, each of
+ them standing exactly where the fragment says it stood.
+
+ The cable keeps everything the source cable knows -- type,
+ installation, place, length, how many cores, which colours -- and it
+ also keeps where the colour labels of the copied section stand, so a
+ copy arrives as a whole: onto a folio which holds no wire at all,
+ into an empty part of a sheet, wherever. What it does not keep is
+ any wire: a copy may not take a wire away from the cable it was
+ copied from, not even for as long as it is being built, so every
+ core is let go while the cable still answers to the source's
+ identity -- no conductor of the source is ever repointed, not even
+ for the length of one signal.
+
+ Which wire each carried core belongs to is worked out later, in
+ CableCopy::wire(), because that depends on where the line has
+ finally been put -- and a copy put where there is no wire simply
+ keeps its labels, to be wired when the line is pulled into place.
+ @param diagram the folio which is about to hold those lines
+ @param root the <diagram> element of a copy fragment
+ @return the lines built, not yet wired
+*/
+QList CableCopy::read(Diagram *diagram, const QDomElement &root)
+{
+ QList built;
+ if (!diagram || !diagram->project()) return built;
+
+ const QDomElement block = root.firstChildElement(QStringLiteral("cables"));
+ if (block.isNull()) return built;
+
+ QETProject *project = diagram->project();
+ //One rule for the whole run. Every cable of a paste takes a
+ //number from it and steps it on, so several cables pasted at
+ //once do not all end up under the same number -- although the
+ //project's own rule does not move until those cables really
+ //exist, which is where the commands which add them take over.
+ //A counter bound to the folio starts the folio the copies are
+ //pasted onto at its own beginning, once for the whole run.
+ NumerotationContext rule = autonum::resetFolioCounters(
+ project->cableAutoNum(project->cableCurrentAutoNum()));
+ for (QDomElement element = block.firstChildElement(QStringLiteral("cable"));
+ !element.isNull();
+ element = element.nextSiblingElement(QStringLiteral("cable")))
+ {
+ if (element.attribute(QStringLiteral("x1")).isEmpty()) continue;
+
+ Cable *cable = project->newCable();
+ cable->fromXml(element);
+
+ //The section of the copy, standing where the section it was
+ //copied from stood: the caller moves it to its final place,
+ //and the colour labels come with it (CablePart::itemChange)
+ CablePartData own;
+ own.uuid = QUuid::createUuid();
+ own.diagram = diagram->uuid();
+ own.p1 = QPointF(element.attribute(QStringLiteral("x1")).toDouble(),
+ element.attribute(QStringLiteral("y1")).toDouble());
+ own.p2 = QPointF(element.attribute(QStringLiteral("x2")).toDouble(),
+ element.attribute(QStringLiteral("y2")).toDouble());
+
+ //The colour labels of that one section, carried over whole
+ //and with every wire taken away
+ const QUuid source_part(element.attribute(QStringLiteral("line")));
+ QList carried;
+ for (const CableCore &core : cable->usedCores())
+ {
+ if (core.part != source_part) continue;
+
+ CableCore copy = core;
+ copy.conductor = QUuid();
+ copy.part = own.uuid;
+ carried.append(copy);
+ }
+ //Goes first, while the copy still answers to the source's
+ //identity: that way nothing of the source is left pointing
+ //at a cable which is about to become somebody else
+ cable->setCores(carried);
+
+ //The copy is a cable of its own: nothing of the source's
+ //identity, nothing of its sections and nothing of the wires
+ //it wires may be taken over, only what the user filled in
+ for (const CablePartData &other : cable->parts()) {
+ cable->removePart(other.uuid);
+ }
+ cable->setUuid(QUuid::createUuid());
+ //The copy is numbered by the rule like every other cable.
+ //A project which has no rule gets no number invented for it,
+ //and then what the copy was copied from keeps standing there
+ //rather than being wiped out by an empty one.
+ const QString designation =
+ CableManager::nextDesignation(project, diagram, rule);
+ if (!designation.isEmpty()) {
+ cable->setDesignation(designation);
+ //The copy is numbered by the rule, so whatever the
+ //source was called by hand is not carried over
+ cable->setDesignationByHand(false);
+ }
+ cable->addPart(own);
+
+ auto *item = new CablePart(own);
+ item->setCable(cable);
+ diagram->addItem(item);
+ built << item;
+ }
+ return built;
+}
+
+/**
+ @brief CableCopy::wire
+ Give the cores of every line in @a parts the wire each of them
+ stands on now that the line is where it is going to stay.
+
+ A copy arrives whole: it carries the colour labels it was copied
+ with, and this is the moment they are matched against the wires of
+ the new place. Matching never moves a label: each one goes on
+ standing exactly where it was carried, and the wire it stands on --
+ within reach, as everywhere else -- is the one which takes the cable
+ field. A label standing on no wire at all simply goes on
+ standing there -- that is what makes a copy useful onto a folio
+ which holds no wire yet: the line is there, with its colours, ready
+ to be pulled into place, and pulling it there wires it
+ (CablePart::settleCores).
+
+ A piece of wire belongs to one cable only, and a copy never takes
+ one away: the wires of the folio it lands on which another cable
+ already describes are left to that cable, nothing is asked about
+ them and nothing is written for them. The wires which came along
+ with the copy itself are different -- they are new wires on this
+ folio, carrying the cable field they had back on the folio they
+ come from, where they used to be one of that cable's own wires. If
+ that were still believed here, every one of them would look like a
+ wire another cable holds and the copy would be refused all of them,
+ arriving without a single entry although it is standing on wires of
+ its own. So they are let go of first, exactly like read() lets go
+ of the wires the cores used to name.
+ @param diagram the folio the lines are on
+ @param parts the lines built by CableCopy::read()
+ @param fresh the wires which came with this copy
+ @return one entry per line which is going to stay, with what it
+ had to give back to other cables, ready to be put on the undo stack
+*/
+QList CableCopy::wire(Diagram *diagram,
+ const QList &parts,
+ const QList &fresh)
+{
+ QList wired;
+ if (!diagram || !diagram->project()) {
+ for (CablePart *part : parts) discard(diagram, part);
+ return wired;
+ }
+ QETProject *project = diagram->project();
+
+ //A wire which came with this copy is a new wire here: what it
+ //says about a cable describes the folio it was copied from.
+ //Done by identity rather than by position, because the wire it
+ //was copied from may well stand in this very place -- and that
+ //one goes on belonging to its own cable.
+ for (Conductor *conductor : fresh)
+ {
+ if (!conductor) continue;
+ if (conductor->properties().m_cable_uuid.isNull()) continue;
+
+
+ conductor->setCableReference(QString(), QUuid(), -1);
+ }
+
+ for (CablePart *part : parts)
+ {
+ Cable *cable = part ? part->cable() : nullptr;
+ if (!cable) {
+ discard(diagram, part);
+ continue;
+ }
+
+ //One wire a colour label is dropped on. Two labels may
+ //not be given the same wire, so each one is handed out
+ //once.
+ struct Offered
+ {
+ int core;
+ Conductor *conductor;
+ };
+ QList offered;
+ QSet wires;
+
+ const QLineF line(part->firstPoint(), part->secondPoint());
+ const QList crossings = CableManager::crossedConductors(diagram, line);
+ for (const CableCore &core : cable->usedCores())
+ {
+ if (core.part != part->partUuid()) continue;
+ if (!core.conductor.isNull()) continue;
+
+ //The wire this label stands on, worked out the way it
+ //is worked out everywhere else (CablePart::wireFor):
+ //the nearest place this line runs across a wire, close
+ //enough for the slash and the wire to read as one
+ //thing. The label itself is not moved to reach it --
+ //a copy arrives with its colours where they were
+ //carried, and no gesture moves one of them, so here
+ //too the entry follows the label rather than the
+ //label being sent off to find a wire. When that wire
+ //has already been handed to another label of the same
+ //copy, the next nearest one this label stands on wins,
+ //rather than leaving it with no wire although one is
+ //under it.
+ QList open = crossings;
+ Conductor *conductor = nullptr;
+ while (true)
+ {
+ Conductor *nearest = part->wireFor(open, core, core.position);
+ if (!nearest) break;
+ if (!wires.contains(nearest->uuid())) {
+ conductor = nearest;
+ break;
+ }
+ for (int i = open.size() - 1; i >= 0; --i) {
+ if (open.at(i).conductor == nearest) open.removeAt(i);
+ }
+ }
+
+ if (!conductor) continue;
+
+ //A wire another cable already describes stays with that
+ //cable. Taking it away would empty a cable behind the
+ //user's back, and asking about it on every paste turned
+ //putting a copy into an existing drawing into a question
+ //to answer first -- so a copy simply takes the wires
+ //nobody holds, and its colour labels standing on the
+ //others name nothing at all. An entry follows its label,
+ //and here it comes to a stop. He can give such a label
+ //one of those wires himself afterwards, by dragging it
+ //there, which is where the question is still asked.
+ const QUuid holder_uuid = conductor->properties().m_cable_uuid;
+ if (!holder_uuid.isNull() && holder_uuid != cable->uuid())
+ {
+ const Cable *previous = CableManager::cableByUuid(project, holder_uuid);
+ if (previous
+ && previous->hasCore(conductor->properties().m_cable_slot))
+ {
+ continue;
+ }
+ }
+
+ wires.insert(conductor->uuid());
+ Offered item;
+ item.core = core.core;
+ item.conductor = conductor;
+ offered.append(item);
+ }
+
+ QList bound;
+ for (const Offered &item : std::as_const(offered))
+ {
+ //A wire is one piece of wire: whichever cable held it
+ //before has to let it go.
+ CableManager::claimConductor(project, item.conductor, cable);
+
+ //A cable binds a conductor by its identity, so a
+ //conductor read back from a file written before it had a
+ //persistent one is given one to keep from now on.
+ item.conductor->setUuid(item.conductor->uuid());
+
+ CableCore core = cable->core(item.core);
+ core.conductor = item.conductor->uuid();
+ cable->setCore(core);
+
+ Bound bind;
+ bind.core = item.core;
+ bind.conductor = item.conductor;
+ bound.append(bind);
+ }
+
+ Wired wired_line;
+ wired_line.part = part;
+ wired_line.bound = bound;
+ wired.append(wired_line);
+ }
+ return wired;
+}
+
+/**
+ @brief CableCopy::repoint
+ Work out again which wire each core of @a wired is on, using the
+ identity the wire has right now.
+
+ A copy is put on the undo stack just after it was wired, and the
+ first run of that stack renews the identity of every conductor it
+ pasted (PasteDiagramCommand::redo): a copy must not keep the
+ identity of the conductor it was copied from. The cores of the copy
+ would then go on naming an identity which no longer exists, and the
+ next working out of the labels (CableManager::refreshLabels) would
+ find nothing there and take the cable field away from those wires.
+
+ So the cores are pointed at the very same wires again, with their
+ new identity, and the cable fields are written out from the cables
+ one more time afterwards: what the copy was given as it was put
+ down still names the identity its wires had a moment earlier, and
+ on a folio where those wires have their own new identity that
+ leaves them without an entry although the copy is standing on them.
+ Nothing moves and nothing is asked: an untouched copy is not
+ changed by any of this at all.
+ @param wired the lines CableCopy::wire() just gave wires to
+*/
+void CableCopy::repoint(const QList &wired)
+{
+ for (const Wired &line : wired)
+ {
+ Cable *cable = line.part ? line.part->cable() : nullptr;
+ if (!cable) continue;
+
+ for (const Bound &bind : line.bound)
+ {
+ if (!bind.conductor) continue;
+
+ const QUuid now = bind.conductor->uuid();
+ const CableCore core = cable->core(bind.core);
+ if (core.core != bind.core || core.conductor == now) continue;
+
+
+ CableCore updated = core;
+ updated.conductor = now;
+ cable->setCore(updated);
+ }
+ }
+
+ //With every core naming the identity its wire really has now,
+ //the cable fields are written out from the cables once more:
+ //this is what puts the entries a copy was given as it was put
+ //down onto the wires it has actually landed on.
+ Diagram *folio = nullptr;
+ for (const Wired &line : wired) {
+ if (line.part && line.part->diagram()) {
+ folio = line.part->diagram();
+ break;
+ }
+ }
+ if (folio && folio->project()) {
+ CableManager::refreshLabels(folio->project());
+ }
+}
+
+/**
+ @brief CableCopy::discard
+ Take a line away again whole -- off the folio, out of the project,
+ and deleted -- without putting anything on the undo stack, because
+ nothing really happened: the user said no to the question above.
+
+ Whatever the cable said while it was being built must not be left
+ behind either: the project has to go on claiming exactly what it
+ claimed before, or it would ask to be saved although nothing was
+ changed at all.
+ @param diagram the folio it was built on
+ @param part the line to take away, may be null
+*/
+void CableCopy::discard(Diagram *diagram, CablePart *part)
+{
+ if (!part) return;
+ Cable *cable = part->cable();
+ QETProject *project = diagram ? diagram->project() : nullptr;
+
+ if (part->scene()) part->scene()->removeItem(part);
+
+ if (cable && project && project->cables().contains(cable))
+ {
+ const bool options_modified = project->projectOptionsWereModified();
+ project->removeCable(cable);
+ project->setModified(options_modified);
+ }
+
+ delete part;
+ delete cable;
+}
diff --git a/sources/cable/cablecopy.h b/sources/cable/cablecopy.h
new file mode 100644
index 000000000..040e7c402
--- /dev/null
+++ b/sources/cable/cablecopy.h
@@ -0,0 +1,126 @@
+/*
+ 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 CABLECOPY_H
+#define CABLECOPY_H
+
+#include "cable.h"
+
+#include
+#include
+
+class CablePart;
+class Conductor;
+class Diagram;
+class QDomDocument;
+class QDomElement;
+class QUndoCommand;
+
+/**
+ @brief Carrying a drawn cable line over into a copy -- through the
+ clipboard (Ctrl+C then Ctrl+V) or as a duplicate (Ctrl+D).
+
+ What travels is the line and everything which made the cable what it
+ was: its type, its installation, the place it runs through, its
+ length, how many cores it has, which colours those have -- and where
+ the colour labels of the copied section stand, so a copy arrives as
+ a whole even onto a folio which holds no wire at all. What does not
+ travel is which wire each core is on: a copy is a cable of its own
+ with a number of its own, and it takes the wire each colour label
+ happens to stand on at the place it is put -- those of the folio it
+ lands on which no other cable describes. A wire another cable
+ already holds is left to that cable, and a label put where there is
+ no wire goes on standing there, to be wired when the line is pulled
+ into place.
+
+ A cable line is not one of the folio's items (the cable owns it),
+ so Diagram::toXml() never writes it: the block travels beside the
+ folio's own XML rather than inside it, and Diagram::fromXml() reads
+ it back only when its caller asks to be handed the lines it built.
+*/
+namespace CableCopy
+{
+ ///Put the cable lines selected on @a diagram into @a document
+ void write(Diagram *diagram, QDomDocument &document);
+
+ ///Build new cables of their own for the lines @a root holds,
+ ///at the coordinates the fragment carries, with the colour
+ ///labels of the copied section carried over as they were.
+ ///Nothing is asked and no wire is taken yet: that needs the
+ ///place the lines have really been put at, which only the
+ ///caller knows
+ QList read(Diagram *diagram, const QDomElement &root);
+
+ ///One core of a line, and the wire it was given
+ struct Bound
+ {
+ int core = -1;
+ Conductor *conductor = nullptr;
+ };
+
+ ///One line which was built, with what it had to take away
+ struct Wired
+ {
+ QPointer part;
+ QList taken;
+ ///The wires the colour labels of this line were given,
+ ///so they can be pointed at them again afterwards
+ QList bound;
+ };
+
+ ///Give each colour label of a line the wire it stands on now
+ ///that the line is where it will stay.
+ ///
+ ///@a fresh lists the wires which came along with the copy
+ ///itself. They are new wires on this folio: the cable field
+ ///they carry describes the folio they were copied from, where
+ ///they used to be one of that cable's own wires -- and carried
+ ///over, that reference would make every one of them look like
+ ///a wire some other cable holds, so the copy would be refused
+ ///all of them and arrive without a single entry although it is
+ ///standing on wires of its own. They are let go of first, the
+ ///same way read() lets go of the wires the cores used to name.
+ ///
+ ///A wire the folio already held, which another cable
+ ///describes, is never taken away from that cable: nothing is
+ ///asked and nothing is written for it, and the colour label
+ ///standing on it simply names nothing until the user drags it
+ ///onto a free wire himself, which is where the question is
+ ///still asked. A label standing on no wire at all goes on
+ ///standing where it is, to be wired when the line is pulled
+ ///into place.
+ QList wire(Diagram *diagram,
+ const QList &parts,
+ const QList &fresh);
+
+ ///Point the cores @a wired lists at the wires they were given,
+ ///again: putting the copy on the undo stack may have renewed
+ ///the identity of a pasted conductor (PasteDiagramCommand::redo),
+ ///and a cable binds a wire by its identity. Nothing is asked and
+ ///nothing moves here. The cable fields are written out once more
+ ///afterwards, from the cables themselves, so the entries a copy
+ ///was given as it was put down are on the wires it really
+ ///landed on rather than on the identity they had one moment
+ ///earlier.
+ void repoint(const QList &wired);
+
+ ///Take a built line back off the folio without leaving a trace
+ ///in the undo history, for a copy which was refused
+ void discard(Diagram *diagram, CablePart *part);
+}
+
+#endif // CABLECOPY_H
diff --git a/sources/cable/cablecreatedialog.cpp b/sources/cable/cablecreatedialog.cpp
new file mode 100644
index 000000000..dff345851
--- /dev/null
+++ b/sources/cable/cablecreatedialog.cpp
@@ -0,0 +1,1405 @@
+/*
+ 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 "cablecreatedialog.h"
+
+#include "editcablecommand.h"
+#include "../diagram.h"
+#include "../qetgraphicsitem/conductor.h"
+#include "../qetproject.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+/**
+ @brief CableFilterProxy::CableFilterProxy
+ @param parent
+*/
+CableFilterProxy::CableFilterProxy(QObject *parent) :
+ QSortFilterProxyModel(parent)
+{
+}
+
+/**
+ @brief CableFilterProxy::setTokens
+ @param tokens every word the user typed, all of which have to be found
+*/
+void CableFilterProxy::setTokens(const QStringList &tokens)
+{
+ if (m_tokens == tokens) return;
+ m_tokens = tokens;
+ invalidateFilter();
+}
+
+/**
+ @brief CableFilterProxy::setHideUnsuitable
+ @param hide true to keep the rows which are of no use out of sight,
+ false to show them -- struck through while a line was just drawn,
+ plain while a type is being changed, where the loss was explained
+ instead of being refused
+*/
+void CableFilterProxy::setHideUnsuitable(bool hide)
+{
+ if (m_hide_unsuitable == hide) return;
+ m_hide_unsuitable = hide;
+ invalidateFilter();
+}
+
+/**
+ @brief CableFilterProxy::filterAcceptsRow
+ @param source_row
+ @param source_parent
+ @return
+*/
+bool CableFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+{
+ const QModelIndex first = sourceModel()->index(source_row, 0, source_parent);
+
+ if (m_hide_unsuitable && first.data(UnsuitableRole).toBool()) {
+ return false;
+ }
+ if (m_tokens.isEmpty()) {
+ return true;
+ }
+
+ const int columns = sourceModel()->columnCount();
+ for (const QString &token : m_tokens)
+ {
+ bool found = false;
+ for (int column = 0; column < columns; ++column)
+ {
+ const QString value = sourceModel()->index(source_row, column, source_parent)
+ .data(Qt::DisplayRole).toString();
+ if (value.contains(token, Qt::CaseInsensitive))
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ @brief CableCreateDialog::CableCreateDialog
+ @param diagram the folio the line was drawn on
+ @param crossings the conductors that line runs across, in drawing order
+ @param part the section which will carry the trunk line and its slashes
+ @param parent
+*/
+CableCreateDialog::CableCreateDialog(Diagram *diagram,
+ const QList &crossings,
+ const CablePartData &part,
+ QWidget *parent) :
+ QDialog(parent),
+ m_diagram(diagram),
+ m_crossings(crossings),
+ m_part(part)
+{
+ build();
+}
+
+/**
+ @brief CableCreateDialog::CableCreateDialog
+ @param diagram the folio whose undo stack takes the change
+ @param cable the cable whose type is being changed
+ @param parent
+*/
+CableCreateDialog::CableCreateDialog(Diagram *diagram,
+ Cable *cable,
+ QWidget *parent) :
+ QDialog(parent),
+ m_diagram(diagram),
+ m_cable(cable),
+ m_change(cable)
+{
+ build();
+}
+
+/**
+ @brief CableCreateDialog::build
+ Everything the two modes share: the header saying what this window
+ is about, the tabs, the property fields and the buttons. What the
+ header says and which tabs are built is where they part company --
+ a window which settles a line just drawn knows how many conductors
+ that line crossed, one which changes a cable knows how many cores
+ that cable has.
+*/
+void CableCreateDialog::build()
+{
+ setWindowTitle(tr("Cable"));
+
+ auto *layout = new QVBoxLayout(this);
+
+ if (changeMode())
+ {
+ //What this window is for, and how many cores the cable has
+ //right now: the type he picks is measured against that number.
+ const QString name = m_change->type().isEmpty()
+ ? tr("(no type)") : m_change->type();
+ m_header = new QLabel(tr("Change the type: %1, %n core(s)",
+ "which type this window changes, and how many cores that cable has now",
+ m_change->coreCount()).arg(name), this);
+ }
+ else
+ {
+ m_header = new QLabel(tr("%n recognized core(s)",
+ "how many cores the line just crossed",
+ m_crossings.size()), this);
+ }
+ QFont header_font = m_header->font();
+ header_font.setBold(true);
+ m_header->setFont(header_font);
+ layout->addWidget(m_header);
+
+ m_tabs = new QTabWidget(this);
+ layout->addWidget(m_tabs);
+
+ buildNewTab();
+ //A window changing an existing cable has no line just drawn, so
+ //there is no other cable to hand that line over to: the second
+ //tab would only offer cables this one is to be replaced by,
+ //which is not what changing a type means.
+ if (!changeMode()) {
+ buildExistingTab();
+ }
+ buildPropertyRow();
+ layout->addWidget(m_property_row);
+
+ m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ connect(m_buttons, &QDialogButtonBox::accepted, this, &CableCreateDialog::accept);
+ connect(m_buttons, &QDialogButtonBox::rejected, this, &CableCreateDialog::reject);
+ layout->addWidget(m_buttons);
+
+ connect(m_tabs, &QTabWidget::currentChanged, this, [this]() {selectionChanged();});
+
+ loadTypeFile();
+ refreshExisting();
+ selectionChanged();
+
+ resize(780, 540);
+}
+
+/**
+ @brief CableCreateDialog::cell
+ One read only cell of one of the two tables. A row which cannot be
+ used is drawn struck through and greyed out, so the checkbox which
+ keeps such rows visible never leaves the user to guess why they are
+ there.
+ @param value
+ @param unsuitable
+ @return
+*/
+QStandardItem *CableCreateDialog::cell(const QString &value, bool unsuitable) const
+{
+ auto *item = new QStandardItem(value);
+ item->setEditable(false);
+ if (unsuitable)
+ {
+ QFont font = item->font();
+ font.setStrikeOut(true);
+ item->setFont(font);
+ item->setForeground(QBrush(palette().color(QPalette::Disabled, QPalette::Text)));
+ }
+ return item;
+}
+
+/**
+ @brief CableCreateDialog::buildNewTab
+ The tab choosing the type of a brand new cable, read from the cable
+ type file.
+*/
+void CableCreateDialog::buildNewTab()
+{
+ auto *page = new QWidget(m_tabs);
+ auto *layout = new QVBoxLayout(page);
+ layout->setContentsMargins(4, 4, 4, 4);
+
+ auto *search_row = new QHBoxLayout;
+ search_row->addWidget(new QLabel(tr("Search:"), page));
+ m_new_search = new QLineEdit(page);
+ m_new_search->setClearButtonEnabled(true);
+ m_new_search->setPlaceholderText(tr("Filter cable types"));
+ search_row->addWidget(m_new_search, 1);
+ layout->addLayout(search_row);
+
+ //What the checkbox hides is what cannot be taken: too few cores
+ //to carry the line which was just drawn, and -- when a type is
+ //being changed rather than drawn -- too few cores to keep this
+ //cable as it stands, since taking such a type takes cores off it.
+ m_new_hide = new QCheckBox(changeMode()
+ ? tr("Hide types which have fewer cores "
+ "than this cable")
+ : tr("Hide types which have fewer cores "
+ "than recognized conductors"), page);
+ m_new_hide->setChecked(true);
+ layout->addWidget(m_new_hide);
+
+ //Said up front rather than only when a struck through row is
+ //picked: the user sees the lines crossed out the moment the
+ //checkbox shows them, and asks why straight away.
+ auto *why = new QLabel(changeMode()
+ ? tr("Uncheck to also see the types which have "
+ "fewer cores: taking them makes the cores "
+ "beyond their number leave the cable.")
+ : tr("A struck through line is a type which has "
+ "fewer cores than recognized conductors: it "
+ "cannot carry this cable."), page);
+ why->setWordWrap(true);
+ layout->addWidget(why);
+
+ m_new_model = new QStandardItemModel(this);
+ m_new_proxy = new CableFilterProxy(this);
+ m_new_proxy->setSourceModel(m_new_model);
+
+ m_new_table = new QTableView(page);
+ m_new_table->setModel(m_new_proxy);
+ m_new_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_new_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_new_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_new_table->setAlternatingRowColors(true);
+ m_new_table->setSortingEnabled(true);
+ m_new_table->horizontalHeader()->setStretchLastSection(true);
+ m_new_table->verticalHeader()->setVisible(false);
+ layout->addWidget(m_new_table, 1);
+
+ m_new_note = new QLabel(page);
+ m_new_note->setWordWrap(true);
+ m_new_note->setVisible(false);
+ layout->addWidget(m_new_note);
+
+ //A new line for the cable type file, filled in the way the
+ //material window fills in one of its own articles: he adds the
+ //type right here rather than leaving the window to find out how.
+ //Beside it, the button rewriting the line he picked -- which
+ //stays out of reach until a line really is picked, since there
+ //would then be nothing to rewrite.
+ auto *add_row = new QHBoxLayout;
+ add_row->addStretch(1);
+ auto *add_button = new QPushButton(tr("New entry"), page);
+ add_button->setToolTip(tr("Add a new cable type to the list"));
+ connect(add_button, &QPushButton::clicked,
+ this, &CableCreateDialog::addTypeRecord);
+ add_row->addWidget(add_button);
+
+ m_edit_button = new QPushButton(tr("Edit entry"), page);
+ m_edit_button->setToolTip(tr("Edit the cable type selected in "
+ "the list."));
+ m_edit_button->setEnabled(false);
+ connect(m_edit_button, &QPushButton::clicked,
+ this, &CableCreateDialog::editTypeRecord);
+ add_row->addWidget(m_edit_button);
+
+ layout->addLayout(add_row);
+
+ m_tabs->addTab(page, tr("New cable"));
+
+ connect(m_new_search, &QLineEdit::textChanged, this, [this](const QString &text) {
+ m_new_proxy->setTokens(text.split(QLatin1Char(' '), Qt::SkipEmptyParts));
+ });
+ connect(m_new_hide, &QCheckBox::toggled, m_new_proxy, &CableFilterProxy::setHideUnsuitable);
+ connect(m_new_table->selectionModel(), &QItemSelectionModel::currentChanged,
+ this, [this]() {selectionChanged();});
+ //A double click settles the choice, exactly the way clicking
+ //OK would: he has come to pick that cable, and making him press
+ //OK after every pick is one click too many. A row which cannot
+ //carry this cable goes on being refused the same way it is
+ //refused by that button, greyed out as it is.
+ connect(m_new_table, &QTableView::doubleClicked, this, [this]() {
+ if (m_buttons->button(QDialogButtonBox::Ok)->isEnabled()) {
+ accept();
+ }
+ });
+}
+
+/**
+ @brief CableCreateDialog::buildExistingTab
+ The tab giving more cores to a cable the project already holds. That
+ cable keeps its type, its designation, its installation and its
+ location: choosing it is what inherits them.
+*/
+void CableCreateDialog::buildExistingTab()
+{
+ auto *page = new QWidget(m_tabs);
+ auto *layout = new QVBoxLayout(page);
+ layout->setContentsMargins(4, 4, 4, 4);
+
+ auto *search_row = new QHBoxLayout;
+ search_row->addWidget(new QLabel(tr("Search:"), page));
+ m_existing_search = new QLineEdit(page);
+ m_existing_search->setClearButtonEnabled(true);
+ m_existing_search->setPlaceholderText(tr("Filter existing cables"));
+ search_row->addWidget(m_existing_search, 1);
+ layout->addLayout(search_row);
+
+ m_existing_hide = new QCheckBox(tr("Hide cables which have fewer "
+ "free cores than recognized conductors"), page);
+ m_existing_hide->setChecked(true);
+ layout->addWidget(m_existing_hide);
+
+ //Same as on the first tab: the reason a line is crossed out is
+ //said where the crossed out lines appear, not only afterwards.
+ auto *why = new QLabel(tr("A struck through line is a cable which has "
+ "fewer free cores than recognized "
+ "conductors: it cannot receive them."), page);
+ why->setWordWrap(true);
+ layout->addWidget(why);
+
+ m_existing_model = new QStandardItemModel(this);
+ m_existing_proxy = new CableFilterProxy(this);
+ m_existing_proxy->setSourceModel(m_existing_model);
+
+ m_existing_table = new QTableView(page);
+ m_existing_table->setModel(m_existing_proxy);
+ m_existing_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_existing_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_existing_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_existing_table->setAlternatingRowColors(true);
+ m_existing_table->setSortingEnabled(true);
+ m_existing_table->horizontalHeader()->setStretchLastSection(true);
+ m_existing_table->verticalHeader()->setVisible(false);
+ layout->addWidget(m_existing_table, 1);
+
+ m_existing_note = new QLabel(page);
+ m_existing_note->setWordWrap(true);
+ m_existing_note->setVisible(false);
+ layout->addWidget(m_existing_note);
+
+ m_tabs->addTab(page, tr("Existing cables"));
+
+ connect(m_existing_search, &QLineEdit::textChanged, this, [this](const QString &text) {
+ m_existing_proxy->setTokens(text.split(QLatin1Char(' '), Qt::SkipEmptyParts));
+ });
+ connect(m_existing_hide, &QCheckBox::toggled, m_existing_proxy, &CableFilterProxy::setHideUnsuitable);
+ connect(m_existing_table->selectionModel(), &QItemSelectionModel::currentChanged,
+ this, [this]() {selectionChanged();});
+ //A double click settles the choice here too: the cable goes
+ //straight onto the line he has just drawn, without a stop at
+ //the OK button on the way -- and a cable with no room left for
+ //these cores goes on being refused, since that button is greyed
+ //out for it.
+ connect(m_existing_table, &QTableView::doubleClicked, this, [this]() {
+ if (m_buttons->button(QDialogButtonBox::Ok)->isEnabled()) {
+ accept();
+ }
+ });
+}
+
+/**
+ @brief CableCreateDialog::buildPropertyRow
+ The properties of the cable itself, which belong to it rather than to
+ one of its parts: whatever is typed here applies to the whole cable.
+
+ There is one single field for the number of the cable, not one per
+ name it goes by: the Désignation is the tag of the cable -- the thing
+ one is tempted to call its BMK -- and it is what gets drawn next to
+ the line and written into the conductors.
+*/
+void CableCreateDialog::buildPropertyRow()
+{
+ m_property_row = new QWidget(this);
+ auto *form = new QFormLayout(m_property_row);
+ form->setContentsMargins(0, 0, 0, 0);
+
+ m_designation = new QLineEdit(m_property_row);
+ m_installation = new QLineEdit(m_property_row);
+ m_location = new QLineEdit(m_property_row);
+
+ form->addRow(tr("Designation"), m_designation);
+ form->addRow(tr("Plant (=)"), m_installation);
+ form->addRow(tr("Location (+)"), m_location);
+}
+
+/**
+ @brief CableCreateDialog::loadTypeFile
+ Read the cable type file the same way the material file is read, and
+ fill the first tab with it. A file which is not there yet simply
+ leaves the tab empty and says so.
+*/
+void CableCreateDialog::loadTypeFile()
+{
+ m_new_model->clear();
+
+ QString path = CableTypeList::configuredPath();
+ if (path.isEmpty()) {
+ path = CableTypeList::defaultPath();
+ }
+ m_types = CableTypeListData();
+ CableTypeList::load(path, &m_types);
+
+ m_new_model->setHorizontalHeaderLabels(CableTypeList::translatedHeader(m_types.columns));
+
+ const int needed = neededCores();
+ int current_row = -1;
+ for (const CableTypeRecord &record : std::as_const(m_types.records))
+ {
+ const int cores = CableTypeList::coreCount(record);
+ //A type saying how many cores it has, but fewer than the line
+ //crosses, cannot carry the cable: too few cores were chosen.
+ //When a type is being changed, such a row is not refused --
+ //he may well want a cable with fewer cores -- but it is the
+ //one the checkbox hides, and the one which has something to
+ //say about what it takes off the cable.
+ const bool fewer = cores > 0 && cores < needed;
+ const bool unsuitable = fewer && !changeMode();
+ if (changeMode() && !m_change->type().isEmpty()
+ && CableTypeList::designation(record) == m_change->type()) {
+ current_row = m_types.records.indexOf(record);
+ }
+
+ QList items;
+ items.reserve(m_types.columns.size());
+ for (const QString &column : std::as_const(m_types.columns))
+ {
+ items.append(cell(record.value(column), unsuitable));
+ }
+ if (!items.isEmpty())
+ {
+ items.first()->setData(fewer, CableFilterProxy::UnsuitableRole);
+ m_new_model->appendRow(items);
+ }
+ }
+
+ //The type the cable already has is the one he came here to
+ //change, so it is the one standing under his finger from the
+ //start -- and OK is enabled with something to say yes to.
+ if (current_row >= 0 && m_new_proxy)
+ {
+ const QModelIndex source = m_new_model->index(current_row, 0);
+ m_new_table->setCurrentIndex(m_new_proxy->mapFromSource(source));
+ m_new_table->scrollTo(m_new_table->currentIndex());
+ }
+}
+
+/**
+ @brief CableCreateDialog::refreshExisting
+ Fill the second tab with the cables the project already holds,
+ saying how many of their cores are wired and how many are left.
+*/
+void CableCreateDialog::refreshExisting()
+{
+ //No second tab when a type is being changed, so nothing to fill
+ //-- and nothing was built which could be filled either.
+ if (changeMode()) return;
+
+ m_existing_model->clear();
+ m_existing.clear();
+
+ if (!m_diagram || !m_diagram->project()) return;
+
+ m_existing_model->setHorizontalHeaderLabels({
+ tr("Designation"),
+ tr("Type"),
+ tr("Cores"),
+ tr("Plant"),
+ tr("Location")
+ });
+
+ const int needed = m_crossings.size();
+ for (Cable *cable : m_diagram->project()->cables())
+ {
+ if (!cable) continue;
+
+ const bool unsuitable = cable->freeCoreCount() < needed;
+ QList items;
+ items << cell(cable->designation(), unsuitable)
+ << cell(cable->type(), unsuitable)
+ << cell(tr("%1 / %2")
+ .arg(cable->usedCoreCount())
+ .arg(cable->coreCount()), unsuitable)
+ << cell(cable->installation(), unsuitable)
+ << cell(cable->location(), unsuitable);
+ items.first()->setData(unsuitable, CableFilterProxy::UnsuitableRole);
+
+ m_existing_model->appendRow(items);
+ m_existing.append(cable);
+ }
+}
+
+/**
+ @brief CableCreateDialog::currentSourceIndex
+ @return the row currently chosen on whichever tab is in front, in the
+ coordinates of the model behind the filter
+*/
+QModelIndex CableCreateDialog::currentSourceIndex() const
+{
+ const bool on_new_tab = m_tabs->currentIndex() == 0;
+ QTableView *table = on_new_tab ? m_new_table : m_existing_table;
+ CableFilterProxy *proxy = on_new_tab ? m_new_proxy : m_existing_proxy;
+
+ const QModelIndex proxy_index = table->currentIndex();
+ if (!proxy_index.isValid()) return QModelIndex();
+ return proxy->mapToSource(proxy_index);
+}
+
+/**
+ @brief CableCreateDialog::currentSelectionIsUnsuitable
+ @return true when the chosen row is one the filter only shows when
+ the checkbox is off, that is one which cannot carry this cable
+*/
+bool CableCreateDialog::currentSelectionIsUnsuitable() const
+{
+ const QModelIndex index = currentSourceIndex();
+ if (!index.isValid()) return false;
+
+ const bool on_new_tab = m_tabs->currentIndex() == 0;
+ CableFilterProxy *proxy = on_new_tab ? m_new_proxy : m_existing_proxy;
+ return proxy->index(index.row(), 0).data(CableFilterProxy::UnsuitableRole).toBool();
+}
+
+/**
+ @brief CableCreateDialog::unsuitableNote
+ @return what has to be said about the struck through row which was
+ chosen, so the user learns why it cannot be taken
+*/
+QString CableCreateDialog::unsuitableNote() const
+{
+ const int needed = neededCores();
+ const QModelIndex index = currentSourceIndex();
+ if (!index.isValid()) return QString();
+
+ if (m_tabs->currentIndex() == 0)
+ {
+ const int cores = CableTypeList::coreCount(m_types.records.at(index.row()));
+ //Changing a type may well take a cable with fewer cores:
+ //nothing is refused, but what that costs has to be said out
+ //loud before he says yes to it.
+ if (changeMode())
+ {
+ return tr("This type has fewer cores than this cable: %1 instead of %2. "
+ "The cores beyond %1 leave the cable and their marks "
+ "disappear from the drawing.")
+ .arg(cores)
+ .arg(needed);
+ }
+
+ return tr("Too many cores selected: the type offers only %1, "
+ "%2 conductors are recognized")
+ .arg(cores)
+ .arg(needed);
+ }
+
+ const Cable *cable = m_existing.value(index.row());
+ return tr("Only %1 free cores, %2 requested")
+ .arg(cable ? cable->freeCoreCount() : 0)
+ .arg(needed);
+}
+
+/**
+ @brief CableCreateDialog::selectionChanged
+ Show the reason a struck through row cannot be taken, keep the OK
+ button in step with it, and fill the property fields from whatever is
+ chosen -- an existing cable hands its properties over, a new one gets
+ its number worked out.
+*/
+void CableCreateDialog::selectionChanged()
+{
+ for (QLabel *note : {m_new_note, m_existing_note})
+ {
+ if (!note) continue;
+ note->clear();
+ note->hide();
+ }
+
+ const bool on_new_tab = m_tabs->currentIndex() == 0;
+ QLabel *note = on_new_tab ? m_new_note : m_existing_note;
+ const QModelIndex index = currentSourceIndex();
+
+ bool acceptable = index.isValid();
+
+ if (on_new_tab && m_types.columns.isEmpty())
+ {
+ note->setText(tr("No cable type available: configure the cable "
+ "type file in the preferences."));
+ note->show();
+ acceptable = false;
+ }
+ else if (acceptable && currentSelectionIsUnsuitable())
+ {
+ note->setText(unsuitableNote());
+ note->show();
+ //Changing a type may well take one with fewer cores: what
+ //that costs is said out loud here, and he may still take it.
+ acceptable = !changeMode();
+ }
+ else if (!acceptable)
+ {
+ note->setText(on_new_tab ? tr("Choose a cable type.")
+ : tr("Choose an existing cable."));
+ note->show();
+ }
+
+ //A type with more cores than this cable holds now loses nothing:
+ //the extra ones wait in the list of cores which are not placed
+ //yet, and he brings them in one by one.
+ if (acceptable && on_new_tab && changeMode() && index.isValid()
+ && index.row() < m_types.records.size())
+ {
+ const int cores = CableTypeList::coreCount(m_types.records.at(index.row()));
+ if (cores > m_change->coreCount())
+ {
+ note->setText(tr("%1 cores instead of %2: the extra cores "
+ "stay unplaced.")
+ .arg(cores)
+ .arg(m_change->coreCount()));
+ note->show();
+ }
+ }
+
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(acceptable);
+
+ //The button rewriting a line of the file works on the line he
+ //picked and on nothing else: greyed out while his finger has
+ //chosen no line yet, and greyed out on the tab holding the
+ //cables of the project, which are not lines of that file.
+ if (m_edit_button)
+ {
+ m_edit_button->setEnabled(on_new_tab && index.isValid()
+ && index.row() < m_types.records.size());
+ }
+
+ if (!acceptable) return;
+
+ if (on_new_tab)
+ {
+ if (changeMode())
+ {
+ //The cable already has its number, its installation and
+ //its place: those are what it holds, not something this
+ //window works out again.
+ fillDesignation(m_change->designation(),
+ m_change->designationByHand());
+ m_installation->setText(m_change->installation());
+ m_location->setText(m_change->location());
+ return;
+ }
+
+ fillDesignation(m_diagram
+ ? CableManager::nextDesignation(m_diagram->project(), m_diagram.data())
+ : QString(),
+ false);
+ m_installation->clear();
+ m_location->clear();
+ return;
+ }
+
+ if (const Cable *cable = m_existing.value(index.row()))
+ {
+ fillDesignation(cable->designation(), cable->designationByHand());
+ m_installation->setText(cable->installation());
+ m_location->setText(cable->location());
+ }
+}
+
+/**
+ @brief CableCreateDialog::fillDesignation
+ Put a name into the field and keep hold of what this window put
+ there: whatever he types over it is a name he chose rather than one
+ the program handed out.
+ @param text the name to show
+ @param by_hand whether that name was typed in by him
+*/
+void CableCreateDialog::fillDesignation(const QString &text, bool by_hand)
+{
+ m_designation->setText(text);
+ m_designation_prefill = text;
+ m_designation_prefill_by_hand = by_hand;
+}
+
+/**
+ @brief CableCreateDialog::nextFreeSlots
+ @param count how many cores are needed
+ @return the lowest core numbers no conductor shows yet
+*/
+QList CableCreateDialog::nextFreeSlots(int count) const
+{
+ QList core_slots;
+ if (!m_cable) return core_slots;
+
+ const int total = m_cable->coreCount();
+ for (int core = 0; core < total && core_slots.size() < count; ++core)
+ {
+ if (!m_cable->hasCore(core)) {
+ core_slots.append(core);
+ }
+ }
+ return core_slots;
+}
+
+/**
+ @brief CableCreateDialog::assignCrossings
+ Give the recognised conductors to the cable, one after the other
+ along the line, so each core takes the colour the type lists at its
+ place. A conductor another cable was holding is taken away from it:
+ a piece of wire belongs to one cable only.
+ @param slots the core numbers being handed out, in drawing order
+*/
+void CableCreateDialog::assignCrossings(const QList &core_slots)
+{
+ if (!m_cable || !m_diagram || !m_diagram->project()) return;
+ QETProject *project = m_diagram->project();
+
+ int position = 0;
+ for (const CableCrossing &crossing : std::as_const(m_crossings))
+ {
+ if (position >= core_slots.size()) break;
+
+ Conductor *conductor = crossing.conductor;
+ if (!conductor) continue;
+
+ //Already one of this cable's cores: it keeps the core it has,
+ //drawing the same cable twice must not give it two.
+ if (conductor->properties().m_cable_uuid == m_cable->uuid())
+ {
+ ++position;
+ continue;
+ }
+
+ CableManager::claimConductor(project, conductor, m_cable);
+
+ //A cable binds a conductor by its identity, so a conductor
+ //read back from a file written before it had a persistent one
+ //is given one to keep from now on.
+ conductor->setUuid(conductor->uuid());
+
+ CableCore core;
+ core.core = core_slots.at(position);
+ core.part = m_part.uuid;
+ core.conductor = conductor->uuid();
+ core.position = crossing.position;
+ m_cable->setCore(core);
+ ++position;
+ }
+}
+
+/**
+ @brief CableCreateDialog::coresTakenFromOthers
+ Which cores of other cables the wires this line crosses already
+ belong to.
+
+ A conductor is one piece of wire, so handing it to the cable being
+ drawn takes that core away from whichever cable held it before. The
+ list is worked out before anything is changed, both to be able to
+ ask the user first and to be able to give those cores back when he
+ undoes the line.
+ @param target the cable about to claim those conductors; a null one
+ is a cable which does not exist yet, and then every core another
+ cable holds is one which would be taken away
+ @return one entry per core which is about to change hands
+*/
+QList CableCreateDialog::coresTakenFromOthers(const QUuid &target) const
+{
+ QList taken;
+ if (!m_diagram || !m_diagram->project()) return taken;
+
+ QETProject *project = m_diagram->project();
+ for (const CableCrossing &crossing : std::as_const(m_crossings))
+ {
+ Conductor *conductor = crossing.conductor;
+ if (!conductor) continue;
+
+ const auto properties = conductor->properties();
+ if (properties.m_cable_uuid.isNull()
+ || properties.m_cable_uuid == target) {
+ continue;
+ }
+
+ Cable *previous = CableManager::cableByUuid(project, properties.m_cable_uuid);
+ if (!previous || !previous->hasCore(properties.m_cable_slot)) continue;
+
+ CableCoreTaken item;
+ item.cable = previous;
+ item.core = previous->core(properties.m_cable_slot);
+ taken.append(item);
+ }
+ return taken;
+}
+
+/**
+ @brief CableCreateDialog::accept
+ Settle the cable the drawing gets: a new one from the chosen type, or
+ the chosen existing one, filled with the recognised conductors.
+
+ Nothing is written before the user has been asked about the cores
+ other cables are about to lose: saying yes to that question has to be
+ the last thing he does before the change really happens, so that
+ saying no leaves the project exactly as it was.
+*/
+void CableCreateDialog::accept()
+{
+ //Changing the type of a cable which already exists has nothing
+ //to do with conductors a line crosses: it takes the chosen type
+ //onto that cable, and that alone.
+ if (changeMode())
+ {
+ acceptTypeChange();
+ return;
+ }
+
+ if (!m_diagram || !m_diagram->project()) {
+ QDialog::reject();
+ return;
+ }
+ QETProject *project = m_diagram->project();
+ const int needed = m_crossings.size();
+ QList core_slots;
+
+ const QModelIndex index = currentSourceIndex();
+ if (!index.isValid() || currentSelectionIsUnsuitable()) return;
+
+ Cable *chosen = nullptr;
+ if (m_tabs->currentIndex() == 0)
+ {
+ if (index.row() >= m_types.records.size()) return;
+ const CableTypeRecord record = m_types.records.at(index.row());
+ const int cores = CableTypeList::coreCount(record);
+ if (cores > 0 && cores < needed) return;
+ }
+ else
+ {
+ chosen = m_existing.value(index.row());
+ if (!chosen) return;
+ if (chosen->freeCoreCount() < needed) return;
+ }
+
+ //The wires this line crosses which another cable already holds
+ //are about to change hands, and the drawing already shows that
+ //as the label of the first cable vanishing: better to say so
+ //here than to let the user discover it afterwards.
+ m_taken = coresTakenFromOthers(chosen ? chosen->uuid() : QUuid());
+ if (!m_taken.isEmpty())
+ {
+ const QString text = tr(
+ "%n crossed conductor(s) already bound to another cable\n\n"
+ "If you confirm, the other cable will lose these cores. Do you want to continue?",
+ "ask before taking cores away from another cable",
+ m_taken.size());
+
+ if (QMessageBox::warning(this, tr("Cable"), text,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No) != QMessageBox::Yes) {
+ m_taken.clear();
+ return;
+ }
+ }
+
+ if (m_tabs->currentIndex() == 0)
+ {
+ const CableTypeRecord record = m_types.records.at(index.row());
+ const int cores = CableTypeList::coreCount(record);
+
+ m_cable = project->newCable();
+ m_cable->setType(CableTypeList::designation(record));
+ //A type saying how many cores it has keeps its own count, so
+ //"5 of 8" stays true; a type saying nothing gets the number of
+ //conductors which were recognised, since that is all we know.
+ m_cable->setCoreCount(qMax(cores, needed));
+ m_cable->setCoreColors(CableTypeList::coreColors(record));
+ m_cable->setDesignation(CableManager::nextDesignation(project, m_diagram.data()));
+ m_cable->addPart(m_part);
+ core_slots = nextFreeSlots(needed);
+ }
+ else
+ {
+ m_cable = chosen;
+ if (!m_cable->hasPart(m_part.uuid)) {
+ m_cable->addPart(m_part);
+ }
+ core_slots = nextFreeSlots(needed);
+ }
+
+ m_cable->setDesignation(m_designation->text().trimmed());
+ //A name this window filled in is the program's, a name he typed
+ //over it is his own -- and it stays his own when he puts the
+ //very same name back into the field
+ m_cable->setDesignationByHand(
+ m_designation->text().trimmed() != m_designation_prefill
+ || m_designation_prefill_by_hand);
+ m_cable->setInstallation(m_installation->text().trimmed());
+ m_cable->setLocation(m_location->text().trimmed());
+
+ assignCrossings(core_slots);
+
+ QDialog::accept();
+}
+
+/**
+ @brief CableCreateDialog::neededCores
+ @return how many cores whatever he picks has to hold at least: the
+ conductors the line just crossed, or -- when a type is being changed
+ -- the cores this cable holds right now
+*/
+int CableCreateDialog::neededCores() const
+{
+ return changeMode() ? m_change->coreCount() : m_crossings.size();
+}
+
+/**
+ @brief CableCreateDialog::typeFilePath
+ @return where the cable type file stands, read the same way the type
+ tab reads it
+*/
+QString CableCreateDialog::typeFilePath() const
+{
+ QString path = CableTypeList::configuredPath();
+ if (path.isEmpty()) {
+ path = CableTypeList::defaultPath();
+ }
+ return path;
+}
+
+/**
+ @brief CableCreateDialog::addTypeRecord
+ Fill in a new line of the cable type file, the way the material
+ window adds one of its own articles: the form is appended to the
+ file, the search is cleared so nothing hides what was just written,
+ and the file is read again so the new line comes back from where it
+ really stands -- and it is the one under his finger.
+*/
+void CableCreateDialog::addTypeRecord()
+{
+ QStringList columns = m_types.columns;
+ if (columns.isEmpty()) {
+ columns = CableTypeList::defaultColumns();
+ }
+
+ CableTypeEntryDialog entry(columns, this);
+ if (entry.exec() != QDialog::Accepted) {
+ return;
+ }
+
+ const QString path = typeFilePath();
+ QString error;
+ if (!CableTypeList::appendRecord(path, entry.record(), &error))
+ {
+ QMessageBox::critical(this,
+ tr("Cannot be written"),
+ tr("Cannot write to the file:\n%1\n%2")
+ .arg(path, error));
+ return;
+ }
+
+ //A search hiding the line he just filled in would leave him
+ //with a form he completed and no line to see.
+ m_new_search->clear();
+ loadTypeFile();
+
+ const CableTypeRecord written = entry.record();
+ for (int row = 0; row < m_types.records.size(); ++row)
+ {
+ if (!(m_types.records.at(row) == written)) continue;
+
+ const QModelIndex source = m_new_model->index(row, 0);
+ m_new_table->setCurrentIndex(m_new_proxy->mapFromSource(source));
+ m_new_table->scrollTo(m_new_table->currentIndex());
+ break;
+ }
+
+ selectionChanged();
+}
+
+/**
+ @brief CableCreateDialog::editTypeRecord
+ Rewrite the line of the cable type file he picked: the very window
+ which adds a line opens, already filled in with what that line says,
+ and only his yes writes it back -- saying no leaves the file exactly
+ as it was.
+
+ What he writes replaces the line he started from and nothing else.
+ The other lines of the file are read again first, so whatever he
+ saved from his spreadsheet in the meantime is written back too
+ rather than being thrown away.
+*/
+void CableCreateDialog::editTypeRecord()
+{
+ const QModelIndex index = currentSourceIndex();
+ if (!index.isValid() || index.row() >= m_types.records.size()) return;
+
+ const CableTypeRecord before = m_types.records.at(index.row());
+
+ QStringList columns = m_types.columns;
+ if (columns.isEmpty()) {
+ columns = CableTypeList::defaultColumns();
+ }
+
+ CableTypeEntryDialog entry(columns, this);
+ entry.setValues(before);
+ if (entry.exec() != QDialog::Accepted) {
+ return;
+ }
+
+ CableTypeRecord after = entry.record();
+ //Cells standing beyond the header belong to the user, and the
+ //form knows nothing about them: they must not vanish from the
+ //line just because they could not be shown.
+ after.extra = before.extra;
+
+ const QString path = typeFilePath();
+ QString error;
+ if (!CableTypeList::updateRecord(path, before, after, &error))
+ {
+ QMessageBox::critical(this,
+ tr("Cannot be written"),
+ tr("Cannot write to the file:\n%1\n%2")
+ .arg(path, error));
+ return;
+ }
+
+ //A search hiding the line he just wrote would leave him with a
+ //form he filled in and no line to see.
+ m_new_search->clear();
+ loadTypeFile();
+
+ for (int row = 0; row < m_types.records.size(); ++row)
+ {
+ if (!(m_types.records.at(row) == after)) continue;
+
+ const QModelIndex source = m_new_model->index(row, 0);
+ m_new_table->setCurrentIndex(m_new_proxy->mapFromSource(source));
+ m_new_table->scrollTo(m_new_table->currentIndex());
+ break;
+ }
+
+ selectionChanged();
+}
+
+/**
+ @brief CableCreateDialog::acceptTypeChange
+ Take the chosen type onto the cable whose type is being changed:
+ its name, how many cores it has and the colour of each of them,
+ together with the cores that cable holds -- the ones beyond the new
+ count leave it, the ones which stay keep their place and their wire,
+ and the added ones wait in the list of cores not placed yet.
+
+ Everything lands as one single undo step, together with whatever he
+ typed into the property fields: the type, the cores and the fields
+ all come back together or not at all.
+*/
+void CableCreateDialog::acceptTypeChange()
+{
+ if (!m_change) {
+ QDialog::reject();
+ return;
+ }
+
+ const QModelIndex index = currentSourceIndex();
+ if (!index.isValid() || index.row() >= m_types.records.size()) return;
+
+ const CableTypeRecord record = m_types.records.at(index.row());
+
+ const QString type = CableTypeList::designation(record);
+ int count = CableTypeList::coreCount(record);
+ //A type saying how many cores it has takes that count; one
+ //saying nothing leaves this cable the size it already is.
+ if (count <= 0) count = m_change->coreCount();
+ const QStringList colors = CableTypeList::coreColors(record);
+
+ const CableTypeState before = m_change->typeState();
+ CableTypeState after = before;
+ after.type = type;
+ after.core_count = count;
+ after.core_colors = colors;
+ after.cores.clear();
+ for (const CableCore &core : std::as_const(before.cores))
+ {
+ if (core.core < count) {
+ after.cores.append(core);
+ }
+ }
+
+ //Cores beyond the new count take their mark off the drawing.
+ //He is only asked about that when a wire really loses its
+ //label: a core which stood nowhere was not on the drawing at
+ //all, and nothing he can see goes away with it.
+ int wired = 0;
+ for (const CableCore &core : std::as_const(before.cores))
+ {
+ if (core.core >= count && !core.conductor.isNull()) {
+ ++wired;
+ }
+ }
+ if (wired > 0)
+ {
+ const QString text = tr(
+ "%n core(s) of this cable bound to a conductor. "
+ "With the chosen type, they leave this cable and their mark "
+ "disappears from the drawing. Continue?",
+ "ask before cores lose the mark they show on the drawing",
+ wired);
+
+ if (QMessageBox::warning(this, tr("Cable"), text,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::Yes) != QMessageBox::Yes) {
+ return;
+ }
+ }
+
+ //The type itself is written by the type step rather than by the
+ //fields, so the two cannot write it twice and undo cannot leave
+ //half of it standing.
+ const CableProperties properties_before = m_change->properties();
+ CableProperties properties_after = properties_before;
+ properties_after.designation = m_designation->text().trimmed();
+ //A name this window filled in is the one the cable already had;
+ //anything he typed over it is a name of his own
+ properties_after.designation_by_hand =
+ properties_after.designation != m_designation_prefill
+ || m_designation_prefill_by_hand;
+ properties_after.installation = m_installation->text().trimmed();
+ properties_after.location = m_location->text().trimmed();
+
+ QList steps;
+ if (properties_after != properties_before) {
+ steps.append(new ChangeCablePropertiesCommand(m_change,
+ properties_before,
+ properties_after));
+ }
+ if (before != after) {
+ steps.append(new ChangeCableTypeCommand(m_change, before, after));
+ }
+
+ //Nothing changed at all: saying yes to an unchanged form is
+ //still saying yes, but it is not a step which has to be kept.
+ if (steps.isEmpty())
+ {
+ QDialog::accept();
+ return;
+ }
+
+ QUndoCommand *step = steps.size() == 1
+ ? steps.takeFirst()
+ : new BatchCommand(steps, QCoreApplication::translate("ChangeCableTypeCommand",
+ "Change the cable type"));
+
+ //The folio is the usual place this step is taken back from. When
+ //no folio is known -- a cable picked without a line -- the
+ //project keeps them all anyway, and only when there is neither
+ //does the change stand without a way back.
+ QUndoStack *stack = m_diagram ? &m_diagram->undoStack() : nullptr;
+ if (!stack)
+ {
+ auto *project = qobject_cast(m_change->parent());
+ if (project && project->undoStack()) {
+ stack = project->undoStack();
+ }
+ }
+
+ if (stack) {
+ stack->push(step);
+ } else {
+ if (properties_after != properties_before) {
+ m_change->setProperties(properties_after);
+ }
+ m_change->applyTypeState(after);
+ delete step;
+ }
+
+ QDialog::accept();
+}
+
+/**
+ @brief CableTypeEntryDialog::CableTypeEntryDialog
+ Build one field per column of the cable type file. Two columns get
+ the shape they need -- a number which counts, and colours written
+ the way the file separates them -- and every other column is a plain
+ field, since it may be one the user invented rather than one
+ QElectroTech knows.
+ @param columns the columns of the file, in file order
+ @param parent
+*/
+CableTypeEntryDialog::CableTypeEntryDialog(const QStringList &columns, QWidget *parent) :
+ QDialog(parent),
+ m_columns(columns)
+{
+ setWindowTitle(tr("New entry"));
+
+ auto *main_layout = new QVBoxLayout(this);
+
+ auto *intro = new QLabel(tr("Enter the cable type to add. Fields "
+ "left empty stay empty in the file."), this);
+ intro->setWordWrap(true);
+ m_intro = intro;
+ main_layout->addWidget(intro);
+
+ auto *grid = new QGridLayout;
+ for (int i = 0; i < m_columns.size(); ++i)
+ {
+ const QString column = m_columns.at(i);
+
+ auto *label = new QLabel(CableTypeList::translatedColumn(column), this);
+ label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
+
+ QWidget *field = nullptr;
+ if (column == QLatin1String("cores"))
+ {
+ auto *spin = new QSpinBox(this);
+ spin->setRange(0, 99);
+ spin->setSpecialValueText(QStringLiteral("—"));
+ spin->setToolTip(tr("How many cores this type has. The dash means "
+ "the file does not specify."));
+ field = spin;
+ }
+ else
+ {
+ auto *edit = new QLineEdit(this);
+ edit->setClearButtonEnabled(true);
+ if (column == QLatin1String("core_colors"))
+ {
+ edit->setPlaceholderText(QStringLiteral("br,sw,gr,bl,gnge"));
+ edit->setToolTip(tr("The color of each core, separated by a "
+ "comma and in the order of the cores."));
+ }
+ field = edit;
+ }
+
+ label->setBuddy(field);
+ grid->addWidget(label, i, 0, Qt::AlignRight | Qt::AlignVCenter);
+ grid->addWidget(field, i, 1);
+ m_fields.append(field);
+ }
+ grid->setColumnStretch(1, 1);
+ main_layout->addLayout(grid);
+
+ auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ buttons->button(QDialogButtonBox::Ok)->setText(tr("Save"));
+ buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
+ connect(buttons, &QDialogButtonBox::accepted, this, &CableTypeEntryDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &CableTypeEntryDialog::reject);
+ main_layout->addWidget(buttons);
+
+ if (!m_fields.isEmpty()) {
+ m_fields.first()->setFocus();
+ }
+
+ setMinimumWidth(420);
+}
+
+/**
+ @brief CableTypeEntryDialog::record
+ @return the line the form describes. Empty fields are left out: an
+ empty column never means anything for a type being created.
+*/
+CableTypeRecord CableTypeEntryDialog::record() const
+{
+ CableTypeRecord record;
+
+ for (int i = 0; i < m_columns.size() && i < m_fields.size(); ++i)
+ {
+ const QString column = m_columns.at(i);
+
+ QString value;
+ if (auto *spin = qobject_cast(m_fields.at(i)))
+ {
+ //Zero means "not said": a type may leave its size to the
+ //drawing, and an empty cell says exactly that.
+ if (spin->value() > 0) {
+ value = QString::number(spin->value());
+ }
+ }
+ else if (auto *edit = qobject_cast(m_fields.at(i)))
+ {
+ value = edit->text();
+ value.remove(QLatin1Char('\r'));
+ value.remove(QLatin1Char('\n'));
+ value = value.trimmed();
+ }
+
+ if (!value.isEmpty()) {
+ record.setValue(column, value);
+ }
+ }
+
+ return record;
+}
+
+/**
+ @brief CableTypeEntryDialog::setValues
+ Fill the form with a line which is already in the file. The window
+ then says it edits rather than adds, so the two can never be told
+ apart by mistake: what he writes is only written when he says yes to
+ it, and only about the line he started from.
+ @param values the line as it stands in the file
+*/
+void CableTypeEntryDialog::setValues(const CableTypeRecord &values)
+{
+ setWindowTitle(tr("Edit entry"));
+ if (m_intro)
+ {
+ m_intro->setText(tr("Edit the cable type selected in the list. "
+ "Fields left empty stay empty in the file."));
+ }
+
+ for (int i = 0; i < m_columns.size() && i < m_fields.size(); ++i)
+ {
+ const QString value = values.value(m_columns.at(i));
+
+ if (auto *spin = qobject_cast(m_fields.at(i)))
+ {
+ bool ok = false;
+ const int count = value.toInt(&ok);
+ spin->setValue(ok ? qBound(spin->minimum(), count, spin->maximum())
+ : spin->minimum());
+ }
+ else if (auto *edit = qobject_cast(m_fields.at(i)))
+ {
+ edit->setText(value);
+ }
+ }
+}
+
+/**
+ @brief CableTypeEntryDialog::accept
+ Refuse an empty form: a type with nothing said about it is not worth
+ a line in the file.
+*/
+void CableTypeEntryDialog::accept()
+{
+ if (record().values.isEmpty())
+ {
+ QMessageBox::warning(this,
+ tr("No details"),
+ tr("Enter at least one detail "
+ "to create an entry."));
+ return;
+ }
+
+ QDialog::accept();
+}
diff --git a/sources/cable/cablecreatedialog.h b/sources/cable/cablecreatedialog.h
new file mode 100644
index 000000000..a18dd3094
--- /dev/null
+++ b/sources/cable/cablecreatedialog.h
@@ -0,0 +1,236 @@
+/*
+ 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 CABLECREATEDIALOG_H
+#define CABLECREATEDIALOG_H
+
+#include "../cablelist/cabletypelist.h"
+#include "cable.h"
+#include "cablemanager.h"
+
+#include
+#include
+#include
+#include
+
+class Diagram;
+class QCheckBox;
+class QDialogButtonBox;
+class QLabel;
+class QLineEdit;
+class QStandardItem;
+class QStandardItemModel;
+class QTableView;
+class QTabWidget;
+class QWidget;
+class QPushButton;
+
+/**
+ @brief Filter keeping the rows the user is looking for.
+
+ Every word he types has to be found somewhere in the row, whatever
+ the column it sits in, the same way the material file is searched.
+ On top of that it can hide the rows which are of no use to the cable
+ being drawn -- the checkbox each of the two tabs carries.
+*/
+class CableFilterProxy : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+ public:
+ static constexpr int UnsuitableRole = Qt::UserRole + 1;
+
+ explicit CableFilterProxy(QObject *parent = nullptr);
+
+ void setTokens(const QStringList &tokens);
+ void setHideUnsuitable(bool hide);
+
+ protected:
+ bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
+
+ private:
+ QStringList m_tokens;
+ bool m_hide_unsuitable = true;
+};
+
+/**
+ @brief The small window filling in one line of the cable type file.
+
+ It is the cable counterpart of the window the material list opens to
+ add one of its own articles: one field per column of the file, in
+ file order, and a column the program doesn't know still gets its
+ field since it belongs to the user rather than to QElectroTech. The
+ two columns a cable type is read by get the shape they need -- a
+ number which counts, and colours written the way the file separates
+ them.
+*/
+class CableTypeEntryDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ /**
+ @param columns the columns of the cable type file, in file
+ order
+ @param parent
+ */
+ explicit CableTypeEntryDialog(const QStringList &columns, QWidget *parent = nullptr);
+
+ ///The line the form describes, empty fields left out
+ CableTypeRecord record() const;
+ /**
+ Fill the form with a line which is already in the file,
+ for the window where that line is edited rather than
+ added: the window then says so, and every field carries
+ what the file holds about that column.
+ @param values the line as it stands in the file
+ */
+ void setValues(const CableTypeRecord &values);
+
+ void accept() override;
+
+ private:
+ QStringList m_columns;
+ ///One field per column, in file order, for the tab key
+ QList m_fields;
+ ///What the window says it is about, changed when editing
+ QLabel *m_intro = nullptr;
+};
+
+/**
+ @brief The dialog which opens as soon as a cable line has been drawn.
+
+ It says how many cores were recognised, then lets the user either
+ pick a type from the cable type file -- a brand new cable -- or pick
+ one of the cables the project already holds and give it more cores.
+ Both tabs carry a search field and the checkbox hiding what cannot be
+ used, exactly like the material selection does.
+
+ The very same window opens to change the type of a cable which
+ already exists: it then shows the type tab alone, filled in with what
+ that cable holds, and takes the chosen type onto the cable -- cores
+ reworked, marks redrawn -- as one single undo step. It carries the
+ button filling in a new line of the cable type file, the way the
+ material window carries its own.
+*/
+class CableCreateDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ CableCreateDialog(Diagram *diagram,
+ const QList &crossings,
+ const CablePartData &part,
+ QWidget *parent = nullptr);
+
+ /**
+ The same window, opened to change the type of a cable which
+ already exists rather than to settle a line just drawn.
+ @param diagram the folio whose undo stack takes the change
+ @param cable the cable whose type is being changed
+ @param parent
+ */
+ CableCreateDialog(Diagram *diagram,
+ Cable *cable,
+ QWidget *parent = nullptr);
+
+ ///The cable the user settled on, null when he cancelled
+ Cable *cable() const {return m_cable;}
+ ///The cores this cable took away from cables the project
+ ///already held, so that undo gives them back
+ QList takenCores() const {return m_taken;}
+
+ void accept() override;
+
+ private:
+ void build();
+ void buildNewTab();
+ void buildExistingTab();
+ void buildPropertyRow();
+ void loadTypeFile();
+ void refreshExisting();
+ void selectionChanged();
+ ///Fill the name field in and remember what this window put there
+ void fillDesignation(const QString &text, bool by_hand);
+ void assignCrossings(const QList &core_slots);
+ QList coresTakenFromOthers(const QUuid &target) const;
+ QList nextFreeSlots(int count) const;
+ QModelIndex currentSourceIndex() const;
+ bool currentSelectionIsUnsuitable() const;
+ QString unsuitableNote() const;
+ QStandardItem *cell(const QString &value, bool unsuitable) const;
+ ///True while this window changes the type of an existing cable
+ bool changeMode() const {return m_change != nullptr;}
+ ///How many cores whatever he picks has to hold at least
+ int neededCores() const;
+ ///Where the cable type file stands
+ QString typeFilePath() const;
+ ///The button filling in a new line of the cable type file
+ void addTypeRecord();
+ ///The button rewriting the line of the cable type file he picked
+ void editTypeRecord();
+ ///Take the chosen type onto the cable being changed
+ void acceptTypeChange();
+
+ //ATTRIBUTES
+ private:
+ QPointer m_diagram;
+ QList m_crossings;
+ CablePartData m_part;
+ Cable *m_cable = nullptr;
+ ///The cable whose type is changed, null while a line is drawn
+ QPointer m_change;
+ ///The cores other cables lost to this one, given back by undo
+ QList m_taken;
+
+ QLabel *m_header = nullptr;
+ QTabWidget *m_tabs = nullptr;
+ QDialogButtonBox *m_buttons = nullptr;
+
+ QLineEdit *m_new_search = nullptr;
+ QCheckBox *m_new_hide = nullptr;
+ QTableView *m_new_table = nullptr;
+ QStandardItemModel *m_new_model = nullptr;
+ CableFilterProxy *m_new_proxy = nullptr;
+ QLabel *m_new_note = nullptr;
+ ///The button rewriting the chosen line of the file, which
+ ///can only work on a line which was picked
+ QPushButton *m_edit_button = nullptr;
+ CableTypeListData m_types;
+
+ QLineEdit *m_existing_search = nullptr;
+ QCheckBox *m_existing_hide = nullptr;
+ QTableView *m_existing_table = nullptr;
+ QStandardItemModel *m_existing_model = nullptr;
+ CableFilterProxy *m_existing_proxy = nullptr;
+ QLabel *m_existing_note = nullptr;
+ QList m_existing;
+
+ QWidget *m_property_row = nullptr;
+ QLineEdit *m_designation = nullptr;
+ /**
+ The name exactly as this window filled it in, and where
+ that name came from: whatever he types over it from then
+ on is a name of his own (see accept())
+ */
+ QString m_designation_prefill;
+ bool m_designation_prefill_by_hand = false;
+ QLineEdit *m_installation = nullptr;
+ QLineEdit *m_location = nullptr;
+};
+
+#endif // CABLECREATEDIALOG_H
diff --git a/sources/cable/cablemanager.cpp b/sources/cable/cablemanager.cpp
new file mode 100644
index 000000000..e3728d5af
--- /dev/null
+++ b/sources/cable/cablemanager.cpp
@@ -0,0 +1,635 @@
+/*
+ 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 "cablemanager.h"
+
+#include "cable.h"
+#include "../autoNum/assignvariables.h"
+#include "../autoNum/numerotationcontextcommands.h"
+#include "../diagram.h"
+#include "../qetgraphicsitem/conductor.h"
+#include "../qetgraphicsitem/element.h"
+#include "../qetgraphicsitem/terminal.h"
+#include "../qetproject.h"
+
+#include
+#include
+
+#include
+#include
+
+namespace {
+
+/**
+ @brief Where two segments cross, if they do at all.
+
+ Done by hand rather than with QLineF::intersects() because the
+ collinear case has to be a miss: a cable drawn exactly on top of a
+ conductor follows it, it does not cross it, and counting it would
+ claim a core the cable never reaches.
+ @param a1 start of the first segment
+ @param a2 end of the first segment
+ @param b1 start of the second segment
+ @param b2 end of the second segment
+ @param result receives the crossing point when there is one
+ @return true when the two segments cross within their own length
+*/
+bool segmentIntersection(const QPointF &a1, const QPointF &a2,
+ const QPointF &b1, const QPointF &b2,
+ QPointF *result)
+{
+ const QPointF r = a2 - a1;
+ const QPointF s = b2 - b1;
+ const QPointF u = b1 - a1;
+
+ const qreal denominator = r.x() * s.y() - r.y() * s.x();
+ if (qFuzzyIsNull(denominator)) {
+ return false;
+ }
+
+ const qreal t = (u.x() * s.y() - u.y() * s.x()) / denominator;
+ const qreal v = (u.x() * r.y() - u.y() * r.x()) / denominator;
+ if (t < 0.0 || t > 1.0 || v < 0.0 || v > 1.0) {
+ return false;
+ }
+
+ if (result) {
+ *result = a1 + t * r;
+ }
+ return true;
+}
+
+/**
+ @brief How far a point stands from a segment, in scene units
+*/
+qreal distanceToSegment(const QPointF &point, const QPointF &a, const QPointF &b)
+{
+ const QPointF ab = b - a;
+ const qreal length_squared = ab.x() * ab.x() + ab.y() * ab.y();
+ if (qFuzzyIsNull(length_squared)) {
+ return QLineF(point, a).length();
+ }
+
+ const QPointF ap = point - a;
+ qreal t = (ap.x() * ab.x() + ap.y() * ab.y()) / length_squared;
+ t = qBound(0.0, t, 1.0);
+ return QLineF(point, a + t * ab).length();
+}
+
+/**
+ @brief Whether some cable of the project already stands under this
+ number.
+
+ A number which is taken must not be handed out twice. That happens
+ while a whole run of cables is numbered one after the other -- none
+ of them has been added to the project by anything but its own
+ creation yet -- and it happens when a rule has simply been given a
+ value which some cable carries already.
+ @param project
+ @param designation the number to look for
+ @return true when a cable of that project stands under it
+*/
+bool designationIsTaken(QETProject *project, const QString &designation)
+{
+ if (!project || designation.isEmpty()) return false;
+
+ for (Cable *cable : project->cables())
+ {
+ if (cable && cable->designation() == designation) {
+ return true;
+ }
+ }
+ return false;
+}
+
+} // namespace
+
+/**
+ @brief CableManager::crossedConductors
+ Tell which conductors a line about to become a cable runs across, in
+ the order they are met along the line. That order is what gives each
+ core its place, and therefore its colour, so it has to be the order
+ of the drawing and not the order the conductors happen to be stored
+ in.
+ @param diagram the folio the line is drawn on
+ @param line the line, in scene coordinates
+ @return the crossings, ordered from the start of the line
+*/
+QList CableManager::crossedConductors(Diagram *diagram, const QLineF &line)
+{
+ QList crossings;
+ if (!diagram) {
+ return crossings;
+ }
+
+ const QPointF start = line.p1();
+ if (qFuzzyCompare(start, line.p2())) {
+ return crossings;
+ }
+
+ const QPointF direction = line.p2() - start;
+ for (Conductor *conductor : diagram->conductors())
+ {
+ QPointF where;
+ if (!crossingOf(diagram, line, conductor, &where)) continue;
+
+ CableCrossing crossing;
+ crossing.conductor = conductor;
+ crossing.position = where;
+ crossings.append(crossing);
+ }
+
+ std::sort(crossings.begin(), crossings.end(),
+ [&start, &direction](const CableCrossing &a_, const CableCrossing &b_) {
+ const QPointF da = a_.position - start;
+ const QPointF db = b_.position - start;
+ const qreal ta = da.x() * direction.x() + da.y() * direction.y();
+ const qreal tb = db.x() * direction.x() + db.y() * direction.y();
+ return ta < tb;
+ });
+
+ return crossings;
+}
+
+/**
+ @brief CableManager::crossingOf
+ Where one line runs across one conductor, if it does at all.
+
+ Done by hand rather than with QLineF::intersects() because the
+ collinear case has to be a miss: a cable drawn exactly on top of a
+ conductor follows it, it does not cross it, and counting it would
+ claim a core the cable never reaches.
+ @param diagram the folio the line is drawn on
+ @param line the line, in scene coordinates
+ @param conductor the wire being asked about
+ @param result receives the crossing point, in scene coordinates
+ @return true when the line really runs across that wire
+*/
+bool CableManager::crossingOf(Diagram *diagram,
+ const QLineF &line,
+ Conductor *conductor,
+ QPointF *result)
+{
+ if (!conductor || !diagram) return false;
+
+ const QPointF start = line.p1();
+ const QPointF end = line.p2();
+ if (qFuzzyCompare(start, end)) return false;
+
+ //Work in the conductor's own coordinates: its path is stored
+ //there, and a conductor is never where it was when it was
+ //created once the whole drawing has been moved about.
+ const QPointF a = conductor->mapFromScene(start);
+ const QPointF b = conductor->mapFromScene(end);
+
+ const QPainterPath path = conductor->path();
+ for (int i = 0; i + 1 < path.elementCount(); ++i)
+ {
+ const QPainterPath::Element &e1 = path.elementAt(i);
+ const QPainterPath::Element &e2 = path.elementAt(i + 1);
+ //A MoveTo ends the segment before it: the jump to the new
+ //subpath is not drawn, so nothing can cross it.
+ if (e2.type == QPainterPath::MoveToElement) {
+ continue;
+ }
+ QPointF where;
+ if (segmentIntersection(a, b,
+ QPointF(e1.x, e1.y),
+ QPointF(e2.x, e2.y),
+ &where))
+ {
+ if (result) {
+ *result = conductor->mapToScene(where);
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ @brief CableManager::distanceTo
+ How far a point of the sheet stands from a wire, in scene units.
+ @param conductor
+ @param scene_pos
+ @return that distance, which is where the wire passes when the point
+ is beside it rather than in front of one of its ends
+*/
+qreal CableManager::distanceTo(Conductor *conductor, const QPointF &scene_pos)
+{
+ if (!conductor) {
+ return std::numeric_limits::max();
+ }
+
+ qreal best = std::numeric_limits::max();
+ const QPainterPath path = conductor->path();
+ for (int i = 0; i + 1 < path.elementCount(); ++i)
+ {
+ const QPainterPath::Element &e1 = path.elementAt(i);
+ const QPainterPath::Element &e2 = path.elementAt(i + 1);
+ if (e2.type == QPainterPath::MoveToElement) continue;
+
+ const qreal d = distanceToSegment(
+ scene_pos,
+ conductor->mapToScene(QPointF(e1.x, e1.y)),
+ conductor->mapToScene(QPointF(e2.x, e2.y)));
+ if (d < best) {
+ best = d;
+ }
+ }
+ return best;
+}
+
+namespace {
+
+/**
+ @brief The end of @a conductor which does not touch @a element.
+
+ Which end that is tells where the wire goes on its own sheet: a wire
+ ending on a report arrow points away from it, and that is the end
+ which says something about where it comes from or where it runs to.
+ @param conductor the wire to look at
+ @param element the report arrow it ends on (may be nullptr)
+ @return the scene position of the other end, (0, 0) when there is none
+*/
+QPointF farEndPosition(const Conductor *conductor, Element *element)
+{
+ const Terminal *far = nullptr;
+ if (conductor->terminal1
+ && conductor->terminal1->parentElement() != element) {
+ far = conductor->terminal1;
+ } else {
+ far = conductor->terminal2 ? conductor->terminal2 : conductor->terminal1;
+ }
+ return far ? far->scenePos() : QPointF();
+}
+
+/**
+ @brief Whether wire @a a runs before wire @a b on its own sheet.
+
+ Both are looked at from @a element: what tells them apart is where
+ their other end goes. Putting the wires which come in to a report
+ and the wires which go out of its partner in this order is what
+ pairs a continuation up with the core it belongs to when several
+ cores run through one and the same report.
+ @param a @param b the two wires to compare
+ @param element the report arrow both end on (may be nullptr)
+ @return true when @a a comes first
+*/
+bool farBefore(const Conductor *a, const Conductor *b, Element *element)
+{
+ const QPointF pa = farEndPosition(a, element);
+ const QPointF pb = farEndPosition(b, element);
+ if (pa.y() != pb.y()) return pa.y() < pb.y();
+ if (pa.x() != pb.x()) return pa.x() < pb.x();
+ return a->uuid() < b->uuid();
+}
+
+} // namespace
+
+/**
+ @brief CableManager::refreshLabels
+ Rewrite the cable field of every conductor of the project from the
+ cables themselves.
+
+ The rule is one-directional and therefore cannot drift: a conductor
+ which some cable claims gets the label that cable generates, a
+ conductor which no cable claims any more -- because the cable was
+ delete, the core was dragged away or the type lost that core -- has
+ the generated label taken away. A conductor which never belonged to
+ a cable keeps whatever it had, since only one with a reference is
+ touched.
+
+ A core which ends on a folio report goes on over there: the wire
+ coming out of the linked arrow shows the same piece of copper, so
+ it belongs to the same core and carries the same cable field. What
+ the links say is worked out from scratch on every pass, and a wire
+ which some cable already claims is never taken away from it, so the
+ reference appears wherever a report carries a core and disappears
+ again by itself as soon as the cable, the core or the link itself
+ is gone. Chains of reports are followed as far as they go, so a
+ cable may run over any number of sheets.
+ @param project
+*/
+void CableManager::refreshLabels(QETProject *project)
+{
+ if (!project) return;
+
+ struct Binding {
+ QUuid cable;
+ int slot = -1;
+ QString label;
+ };
+
+ QHash bindings;
+ for (Cable *cable : project->cables())
+ {
+ if (!cable) continue;
+ for (const CableCore &core : cable->usedCores())
+ {
+ if (core.conductor.isNull()) continue;
+ Binding binding;
+ binding.cable = cable->uuid();
+ binding.slot = core.core;
+ binding.label = cable->labelOfCore(core.core);
+ bindings.insert(core.conductor, binding);
+ }
+ }
+
+ //A core which ends on a folio report continues on the wire which
+ //leaves the linked arrow on the other sheet. That wire shows the
+ //same piece of copper, so it gets the same reference as the core
+ //it continues. The walk repeats while it finds new wires, so a
+ //chain of reports is followed across any number of sheets, and
+ //it only ever fills a wire no cable has claimed yet: two cores
+ //can never fight over the same wire, and a reference worked out
+ //here is taken away again by the same pass as soon as the cable,
+ //the core or the link does.
+ QHash conductors_by_uuid;
+ QHash> endings_on_report;
+ for (Diagram *diagram : project->diagrams())
+ {
+ if (!diagram) continue;
+ for (Conductor *conductor : diagram->conductors())
+ {
+ if (conductor) conductors_by_uuid.insert(conductor->uuid(), conductor);
+ }
+ }
+
+ //Where a wire goes on its own sheet (farBefore, above): putting the
+ //wires which come in and the wires which go out in the same order
+ //is what pairs a continuation up with its core when several cores
+ //run through one and the same report.
+
+ bool grew = true;
+ while (grew)
+ {
+ grew = false;
+ endings_on_report.clear();
+ for (const QUuid &source_uuid : bindings.keys())
+ {
+ Conductor *source = conductors_by_uuid.value(source_uuid);
+ if (!source) continue;
+
+ Terminal *ends[2] = {source->terminal1, source->terminal2};
+ for (Terminal *end : ends)
+ {
+ if (!end) continue;
+ Element *element = end->parentElement();
+ //An arrow taken off the sheet carries nothing over
+ //any more, whatever still links it
+ if (!element || !element->scene()) continue;
+ if (!(element->linkType() & Element::AllReport)) continue;
+
+ QList &in = endings_on_report[element];
+ if (!in.contains(source)) in.append(source);
+ }
+ }
+
+ for (auto it = endings_on_report.constBegin();
+ it != endings_on_report.constEnd(); ++it)
+ {
+ Element *element = it.key();
+ QList in = it.value();
+ std::sort(in.begin(), in.end(),
+ [&](const Conductor *a, const Conductor *b) {
+ return farBefore(a, b, element);
+ });
+
+ for (Element *partner : element->linkedElements())
+ {
+ if (!partner || partner == element || !partner->scene()) continue;
+
+ QList out = partner->conductors();
+ std::sort(out.begin(), out.end(),
+ [&](const Conductor *a, const Conductor *b) {
+ return farBefore(a, b, partner);
+ });
+
+ //One core coming in may branch out into several wires
+ //over there -- they all show the same piece of copper.
+ //Several cores coming in take one wire each, in the
+ //order they run on their own sheet.
+ const int count = (in.size() == 1)
+ ? out.size()
+ : qMin(in.size(), out.size());
+ for (int i = 0; i < count; ++i)
+ {
+ Conductor *continued = out.at(i);
+ if (!continued || bindings.contains(continued->uuid())) continue;
+ const int from = qMin(i, in.size() - 1);
+ bindings.insert(continued->uuid(),
+ bindings.value(in.at(from)->uuid()));
+ grew = true;
+ }
+ }
+ }
+ }
+
+ for (Diagram *diagram : project->diagrams())
+ {
+ if (!diagram) continue;
+ for (Conductor *conductor : diagram->conductors())
+ {
+ if (!conductor) continue;
+
+ const auto it = bindings.constFind(conductor->uuid());
+ if (it != bindings.constEnd())
+ {
+ const ConductorProperties &properties = conductor->properties();
+ conductor->setCableReference(it->label, it->cable, it->slot);
+ }
+ else if (!conductor->properties().m_cable_uuid.isNull())
+ {
+ conductor->setCableReference(QString(), QUuid(), -1);
+ }
+ }
+ }
+}
+
+/**
+ @brief CableManager::claimConductor
+ Make a conductor a core of @a new_owner, taking it away from whichever
+ cable held it before. A conductor is a single piece of wire: two
+ cables cannot both claim it, and the one that loses it must not go
+ on drawing a label for it.
+ @param project
+ @param conductor
+ @param new_owner the cable which is about to use the conductor
+*/
+void CableManager::claimConductor(QETProject *project, Conductor *conductor, Cable *new_owner)
+{
+ if (!project || !conductor) return;
+
+ const QUuid owner_uuid = conductor->properties().m_cable_uuid;
+ if (owner_uuid.isNull() || (new_owner && owner_uuid == new_owner->uuid())) {
+ return;
+ }
+
+ if (Cable *previous = cableByUuid(project, owner_uuid)) {
+ previous->removeCore(conductor->properties().m_cable_slot);
+ }
+}
+
+/**
+ @brief CableManager::cableByUuid
+ @param project
+ @param uuid
+ @return the cable with that identity, or nullptr
+*/
+Cable *CableManager::cableByUuid(QETProject *project, const QUuid &uuid)
+{
+ if (!project || uuid.isNull()) return nullptr;
+ for (Cable *cable : project->cables()) {
+ if (cable && cable->uuid() == uuid) return cable;
+ }
+ return nullptr;
+}
+
+/**
+ @brief CableManager::conductorByUuid
+ @param project
+ @param uuid
+ @return the conductor with that identity, or nullptr. Needed because
+ a cable remembers its cores by identity: only the drawing knows where
+ they ended up.
+*/
+Conductor *CableManager::conductorByUuid(QETProject *project, const QUuid &uuid)
+{
+ if (!project || uuid.isNull()) return nullptr;
+ for (Diagram *diagram : project->diagrams()) {
+ if (!diagram) continue;
+ for (Conductor *conductor : diagram->conductors()) {
+ if (conductor && conductor->uuid() == uuid) return conductor;
+ }
+ }
+ return nullptr;
+}
+
+/**
+ @brief CableManager::nextDesignation
+ Work out the number of the next cable drawn on a folio.
+
+ When the project has a numbering rule for cables, that rule decides:
+ whatever it holds at this moment is the number, read by the very
+ same machinery the element numbering reads its own. When it has
+ none, the plain letter W is handed out: a project which numbers its
+ cables by no rule gets no numbers invented for it, and W -- the one
+ part of the old numbers which was never a number -- is all such a
+ cable is called until a rule is defined.
+
+ A rule whose counter belongs to one folio is read from the beginning
+ of the folio the cable is drawn on, so that the number this cable
+ gets is the first free one of its own folio rather than whatever the
+ project's rule happens to hold after numbering other folios.
+
+ Nothing here moves the project's own rule on: that belongs to the
+ moment the cable really comes into being.
+ @param project
+ @param diagram the folio the cable is being drawn on
+ @return the number
+*/
+QString CableManager::nextDesignation(QETProject *project, Diagram *diagram)
+{
+ NumerotationContext context;
+ if (project) {
+ context = project->cableAutoNum(project->cableCurrentAutoNum());
+ }
+ //The folio-bound parts are put back to the start of that folio;
+ //the stepping of the overload below then walks past every
+ //number of this folio which is already taken
+ context = autonum::resetFolioCounters(context);
+ return nextDesignation(project, diagram, context);
+}
+
+/**
+ @brief CableManager::nextDesignation
+ Work the number of a cable out of a rule which the caller keeps in
+ @p context -- the project's own rule as it stands, or a copy of it
+ which the caller is numbering a whole run of cables with.
+
+ Whatever the rule holds is the number, unless some cable already
+ stands under it: then @p context is stepped on and the rule is read
+ once more, so that a paste which brings in several cables at once
+ numbers each of them differently although the project's own rule
+ does not move until those cables really exist. A rule which cannot
+ step -- one with nothing in it which counts -- hands out what it
+ says and says no more, for what it says is all it knows.
+
+ A project which numbers its cables by no rule is not read at all
+ and is handed the plain letter W instead: no numbers are invented
+ where none have been defined.
+ @param project
+ @param diagram the folio the cable is being drawn on, which every
+ folio-bound part of a rule is read from
+ @param context the rule to read; stepped on when what it says is
+ already taken
+ @return the number
+*/
+QString CableManager::nextDesignation(QETProject *project,
+ Diagram *diagram,
+ NumerotationContext &context)
+{
+ if (!project || !diagram) return QString();
+
+ const QString key = project->cableCurrentAutoNum();
+ //Whether there was a rule to read at all, taken down before the
+ //loop: stepping the rule on empties it, and that must not look
+ //like a project which never had one.
+ const bool ruled = !key.isEmpty() && !context.isEmpty();
+ QString previous;
+
+ for (int attempt = 0; ruled && attempt < 1000 && !context.isEmpty(); ++attempt)
+ {
+ //The rule is read on a copy: reading it fills in what its
+ //folio-bound parts are worth here and now, and that must
+ //not creep into the rule itself.
+ NumerotationContext evaluated = context;
+ const QString formula = autonum::numerotationContextToFormula(evaluated);
+ autonum::sequentialNumbers seq;
+ autonum::setSequential(formula, seq, evaluated, diagram, key);
+ const QString candidate =
+ autonum::AssignVariables::formulaToLabel(formula, seq, diagram);
+
+ if (candidate.isEmpty()) break;
+ if (!designationIsTaken(project, candidate)) return candidate;
+
+ //Taken already: step the rule on and read it once more.
+ if (candidate == previous) return candidate;
+ previous = candidate;
+
+ NumerotationContextCommands ncc(context, diagram);
+ const NumerotationContext following = ncc.next();
+ if (following.size() != context.size()) break;
+ context = following;
+ }
+
+ //No rule at all: the plain letter W, and nothing more. The old
+ //numbers read folio + W + index, and W is the only part of them
+ //which was never a number -- so it is what a project which has
+ //defined no numbering rule calls its cables, until it defines
+ //one.
+ if (!ruled) {
+ return QStringLiteral("W");
+ }
+
+ //A rule which says nothing usable here: no number at all. The
+ //number is what the rule says, and a rule which says nothing
+ //leaves the field to the user rather than having it filled in
+ //with something it never held.
+ return QString();
+}
diff --git a/sources/cable/cablemanager.h b/sources/cable/cablemanager.h
new file mode 100644
index 000000000..00055f6dc
--- /dev/null
+++ b/sources/cable/cablemanager.h
@@ -0,0 +1,113 @@
+/*
+ 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 CABLEMANAGER_H
+#define CABLEMANAGER_H
+
+#include
+#include
+#include
+#include
+
+class Cable;
+class Conductor;
+class Diagram;
+class NumerotationContext;
+class QETProject;
+
+/**
+ @brief One place where a drawn cable line crosses a conductor.
+*/
+struct CableCrossing
+{
+ Conductor *conductor = nullptr;
+ /// Scene coordinates of the crossing, where the slash is drawn
+ QPointF position;
+};
+
+/**
+ @brief The CableManager works out everything which follows from the
+ cables of a project, so nothing has to be maintained by hand.
+
+ It detects which conductors a line about to be drawn crosses, and it
+ turns the cables back into the cable field of every conductor they
+ own -- both directions of one single rule: the cable is the only
+ source of truth.
+*/
+class CableManager
+{
+ public:
+ ///How far a colour label may stand from where a line crosses
+ ///a wire for that wire to count as the one the label stands
+ ///on: one scene unit, and that is already generous. The grid
+ ///is there to put a label exactly on the crossing, and it
+ ///does: a label drawn on a wire, slid along its own line or
+ ///carried there by a paste lands on that crossing to the
+ ///last decimal. The single unit only soaks up the rounding
+ ///which comes of projecting a label onto its line, and a
+ ///terminal which is a hair off the grid. Anything beyond it
+ ///is beside the wire rather than on it, so a label one
+ ///whole grid step away -- however the grid is set -- gives
+ ///up its entry. One number for every place a label is given
+ ///to a wire: drawing one, dragging one and wiring a copy
+ ///must not disagree about it.
+ static constexpr qreal bind_tolerance = 1.0;
+
+ //Drawing
+ static QList crossedConductors(Diagram *diagram, const QLineF &line);
+ static bool crossingOf(Diagram *diagram,
+ const QLineF &line,
+ Conductor *conductor,
+ QPointF *result);
+ static qreal distanceTo(Conductor *conductor, const QPointF &scene_pos);
+
+ //Keeping the drawing in step with the cables
+ static void refreshLabels(QETProject *project);
+ static void claimConductor(QETProject *project, Conductor *conductor, Cable *new_owner);
+
+ //Lookup
+ static Cable *cableByUuid(QETProject *project, const QUuid &uuid);
+ static Conductor *conductorByUuid(QETProject *project, const QUuid &uuid);
+ static QString nextDesignation(QETProject *project, Diagram *diagram);
+ /**
+ Work the number of a next cable out of a rule which is
+ @em not the project's own state but the one @p context
+ holds: the rule is read as it stands there and what it
+ hands out is the number, unless some cable already
+ stands under that number -- which is what happens while
+ a whole run of cables is numbered one after the other
+ before any of them has really been added. Then @p
+ context is stepped on and the rule is read once more.
+
+ When the project numbers its cables by no rule at all,
+ @p context is not read and the plain letter W is handed
+ out instead: no numbers are invented where none have
+ been defined.
+
+ A caller numbering a run of cables keeps the same @p
+ context for all of them, so each of them gets a number
+ of its own; a caller numbering one cable lets it go
+ again afterwards. The project's own rule is never
+ moved on here -- that belongs to the moment the cable
+ comes into being.
+ */
+ static QString nextDesignation(QETProject *project,
+ Diagram *diagram,
+ NumerotationContext &context);
+};
+
+#endif // CABLEMANAGER_H
diff --git a/sources/cable/cablepart.cpp b/sources/cable/cablepart.cpp
new file mode 100644
index 000000000..ef1b7401d
--- /dev/null
+++ b/sources/cable/cablepart.cpp
@@ -0,0 +1,3377 @@
+/*
+ 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 "cablepart.h"
+
+#include "cable.h"
+#include "cablemanager.h"
+#include "cablepropertiesdialog.h"
+#include "editcablecommand.h"
+#include "../autoNum/assignvariables.h"
+#include "../diagram.h"
+#include "../properties/xrefproperties.h"
+#include "../qetapp.h"
+#include "../qetproject.h"
+#include "../qetgraphicsitem/conductor.h"
+#include "../utils/qetutils.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+namespace {
+
+ ///Half size of the slash drawn where a core crosses a conductor
+const qreal slash_half = 3.5;
+ ///How far from a slash or its colour the mouse still counts as holding it
+const qreal slash_grab = 6.0;
+ ///How far the colour of a core may stand from the wire it names:
+ ///close enough to read as one thing, far enough away that the tail of
+ ///a letter like "g" does not run into the wire. The text is turned by
+ ///a quarter, so that tail is its right hand side, which is the side
+ ///facing the wire.
+const qreal core_label_side = 2.5;
+ ///How far above the line the colour of a core is written, so
+ ///the line passes beside the letters instead of through them
+const qreal core_label_gap = 4.0;
+ ///Radius of the blue point drawn at each end of a picked line, and
+ ///how far from such a point the mouse still counts as holding it.
+ ///Ten across, which is the size QET's own handles use.
+const qreal grip_radius = 5.0;
+const qreal grip_grab = 6.0;
+ ///How far from the trunk line the mouse still counts as aiming at
+ ///it while a core waits to be put down there: the core follows the
+ ///line only while the mouse is near enough for the click to read as
+ ///belonging to it, and the slash itself is drawn on the line, at a
+ ///grid step, so what is shown is always what would be kept.
+const qreal place_grab = 30.0;
+
+} // namespace
+
+ ///The one line which waits for the click putting a core down, if any
+QPointer CablePart::s_placing;
+
+/**
+ @brief CablePart::CablePart
+ @param data the section as the cable holds it
+ @param parent
+*/
+CablePart::CablePart(const CablePartData &data, QGraphicsItem *parent) :
+ CablePart(parent)
+{
+ setLine(data.p1, data.p2);
+ m_part_uuid = data.uuid;
+ m_pending_cable = QUuid();
+}
+
+/**
+ @brief CablePart::CablePart
+ An empty part, waiting for its geometry: used while reading a project.
+ @param parent
+*/
+CablePart::CablePart(QGraphicsItem *parent) :
+ QetGraphicsItem(parent)
+{
+ //ItemSendsGeometryChanges is what makes itemChange() hear about
+ //position changes at all -- without it Qt silently never calls
+ //it for a setPos(), and then this item is moved only in the
+ //screen's opinion: the line and its colour labels are drawn at
+ //the new place because the whole item is transformed, while
+ //m_p1/m_p2 and CableCore::position go on naming the old one.
+ //A paste would therefore wire its labels against the coordinates
+ //the line was copied from, and the next copy would write those
+ //stale coordinates into the clipboard -- which is exactly the
+ //paste landing away from the group it belongs to.
+ setFlags(ItemIsMovable | ItemIsSelectable | ItemSendsGeometryChanges);
+ setAcceptHoverEvents(true);
+ m_part_uuid = QUuid::createUuid();
+}
+
+CablePart::~CablePart()
+{
+}
+
+/**
+ @brief CablePart::setCable
+ Bind this section to its cable. The item follows every change of the
+ cable, because the number and the type it draws live there.
+ @param cable
+*/
+void CablePart::setCable(Cable *cable)
+{
+ if (m_cable == cable) {
+ return;
+ }
+ if (m_cable) {
+ disconnect(m_cable, nullptr, this, nullptr);
+ }
+ m_cable = cable;
+ m_pending_cable = QUuid();
+ if (m_cable)
+ {
+ connect(m_cable, &Cable::changed, this, [this]() {
+ prepareGeometryChange();
+ update();
+ });
+ }
+ setUpXrefHooks();
+ prepareGeometryChange();
+ update();
+}
+
+/**
+ @brief CablePart::cable
+ @return the cable this section belongs to, or nullptr
+*/
+Cable *CablePart::cable() const
+{
+ return m_cable.data();
+}
+
+/**
+ @brief CablePart::partUuid
+ @return which section of the cable this item draws
+*/
+QUuid CablePart::partUuid() const
+{
+ return m_part_uuid;
+}
+
+/**
+ @brief CablePart::refreshPending
+ Bind the item to its cable once the whole project is read: a folio is
+ loaded before the cables it holds sections of, so at that moment the
+ cable is only known by its identity.
+*/
+void CablePart::refreshPending()
+{
+ if (m_cable) return;
+ if (!diagram() || !diagram()->project()) return;
+
+ if (Cable *cable = CableManager::cableByUuid(diagram()->project(), m_pending_cable)) {
+ setCable(cable);
+ }
+}
+
+/**
+ @brief CablePart::setLine
+ Change where the trunk line runs. Purely optical: no core is added,
+ none is dropped, nothing is reassigned -- a cable keeps its cores
+ wherever its line happens to run.
+ @param p1 scene coordinates of one end
+ @param p2 scene coordinates of the other end
+*/
+void CablePart::setLine(const QPointF &p1, const QPointF &p2)
+{
+ if (m_p1 == p1 && m_p2 == p2) return;
+ prepareGeometryChange();
+ m_p1 = p1;
+ m_p2 = p2;
+ syncGeometry();
+ update();
+}
+
+/**
+ @brief CablePart::line
+ @return the trunk line, in scene coordinates
+*/
+QLineF CablePart::line() const
+{
+ return QLineF(m_p1, m_p2);
+}
+
+/**
+ @brief CablePart::textFont
+ @return the font a text of the cable is written with when the cable
+ itself asks for none: what the preferences offer on the "Textes"
+ page, and -- until he has picked something there -- the font the
+ folio writes its own texts with, a shade smaller: the way every
+ cable has always been drawn, so that merely opening the
+ preferences changes nothing.
+*/
+QFont CablePart::textFont() const
+{
+ return QETApp::cableTextsFont();
+}
+
+/**
+ @brief CablePart::coreFont
+ @return the font the colours of the cores are written with when the
+ cable itself asks for none -- all of them together, since the
+ colours of one cable are read as one and are never set apart.
+*/
+QFont CablePart::coreFont() const
+{
+ return fontOf(QStringLiteral("cores"));
+}
+
+/**
+ @brief CablePart::fontOf
+ @param key the name of a text of the cable: "designation", "type",
+ "installation", "location", "length" or "cores"
+ @return the font that text is really written with: the one this
+ cable asks for, or -- when it asks for none -- the one the
+ preferences offer
+*/
+QFont CablePart::fontOf(const QString &key) const
+{
+ if (m_cable)
+ {
+ const CableTextFormat format = m_cable->properties().texts.value(key);
+ if (!format.font.isEmpty())
+ {
+ QFont font;
+ if (QETUtils::fontFromString(font, format.font)) {
+ return font;
+ }
+ }
+ }
+
+ //The reference to the other folios this cable runs on has a
+ //font of its own, set with the button next to its text under
+ //Cross-references -- he sets it there and not in the cable itself.
+ //When he has left that alone, the reference follows the texts
+ //of the cable like every other line of the label does.
+ if (key == QLatin1String("xref") && m_cable && diagram() && diagram()->project())
+ {
+ const QString description = diagram()->project()
+ ->defaultXRefProperties(QStringLiteral("cable")).font();
+ if (!description.isEmpty())
+ {
+ QFont font;
+ if (QETUtils::fontFromString(font, description)) {
+ return font;
+ }
+ }
+ }
+
+ return key == QLatin1String("cores") ? QETApp::cableCoreFont() : textFont();
+}
+
+/**
+ @brief CablePart::alignOf
+ How one text lines up at the left end of the trunk line. A cable
+ which says nothing about it follows the preferences, which is where
+ -- until he has said otherwise there either -- every text ends where
+ the line begins, the way it always did.
+ @param key the name of a text of the cable
+ @return Qt::AlignLeft, Qt::AlignHCenter or Qt::AlignRight
+*/
+Qt::Alignment CablePart::alignOf(const QString &key) const
+{
+ int alignment = 0;
+ if (m_cable) {
+ alignment = m_cable->properties().texts.value(key).alignment;
+ }
+ if (!alignment) {
+ return QETApp::cableTextAlignment();
+ }
+ switch (alignment)
+ {
+ case int(Qt::AlignLeft): return Qt::AlignLeft;
+ case int(Qt::AlignHCenter): return Qt::AlignHCenter;
+ case int(Qt::AlignRight): return Qt::AlignRight;
+ default: return Qt::AlignRight;
+ }
+}
+
+/**
+ @brief CablePart::textOffset
+ @return how far the whole label at the left of the line stands from
+ where it has always stood: what the form offers as "Position du
+ texte", and zero for a cable which has never been nudged -- which
+ is where every cable starts, so that merely opening a project
+ moves nothing.
+*/
+QPointF CablePart::textOffset() const
+{
+ return m_cable ? m_cable->properties().text_offset : QPointF();
+}
+
+/**
+ @brief CablePart::coreOffset
+ @return how far the row of colours stands from the slashes it names:
+ the same kind of nudge, kept apart from the label's because the two
+ are set apart in the form and are moved one without the other.
+*/
+QPointF CablePart::coreOffset() const
+{
+ return m_cable ? m_cable->properties().core_offset : QPointF();
+}
+
+/**
+ @brief CablePart::textLeft
+ Where a text of this width starts, at the left end of the trunk
+ line -- the same place for every text, so that a cable whose
+ fields are set apart still reads as one label rather than as five.
+ @param key the name of a text of the cable
+ @param width how wide that text is, in scene units
+ @return the x of the left of the text, in scene coordinates
+*/
+qreal CablePart::textLeft(const QString &key, qreal width) const
+{
+ const qreal line_left = qMin(m_p1.x(), m_p2.x());
+ if (alignOf(key) == Qt::AlignLeft) {
+ return line_left + 6.0;
+ }
+ if (alignOf(key) == Qt::AlignHCenter) {
+ return line_left - width * 0.5;
+ }
+ return line_left - 6.0 - width;
+}
+
+/**
+ @brief CablePart::trunkIsHorizontal
+ @return true when the trunk line runs along the sheet rather than up
+ and down it: which way it runs is what tells the label where it may
+ sit, and where the colour of a core may be written.
+*/
+bool CablePart::trunkIsHorizontal() const
+{
+ return qAbs(m_p2.y() - m_p1.y()) <= qAbs(m_p2.x() - m_p1.x());
+}
+
+/**
+ @brief CablePart::labelLines
+ @return what of the cable the label at the left of the trunk line
+ writes, in the order the lines are stacked: where the cable runs
+ first, then what it is called. Every field the user switched off,
+ and every field he left empty, is left out.
+
+ Neither the type of the cable, nor its length, nor the references to
+ the other folios this cable runs on is among them: all three are
+ written under the line rather than over it -- the type first, the
+ length under it and the references at the very bottom -- so what is
+ above the line stays the description of where the cable belongs and
+ nothing else. See refLines() for those underneath.
+
+ Each line comes with the rectangle it is written in, worked out with
+ its own font and its own way of lining up: those lines may differ
+ from each other as much as he likes, and they are stacked one
+ under the other by their own height rather than by one common size.
+*/
+QList CablePart::labelLines() const
+{
+ if (!m_cable) return QList();
+
+ const CableProperties properties = m_cable->properties();
+ QList lines;
+ const auto write = [this, &lines](const QString &text, const QString &key)
+ {
+ if (text.isEmpty()) return;
+ LabelLine line;
+ line.text = text;
+ line.key = key;
+ line.font = fontOf(key);
+ line.align = alignOf(key);
+ const QFontMetricsF metrics(line.font);
+ line.width = metrics.horizontalAdvance(text);
+ line.height = metrics.height();
+ lines.append(line);
+ };
+
+ if (properties.show_installation) {
+ write(properties.installation, QStringLiteral("installation"));
+ }
+ if (properties.show_location) {
+ write(properties.location, QStringLiteral("location"));
+ }
+ if (properties.show_designation) {
+ write(properties.designation, QStringLiteral("designation"));
+ }
+ if (lines.isEmpty()) return lines;
+
+ qreal total_height = 0.0;
+ for (const LabelLine &line : lines) {
+ total_height += line.height;
+ }
+ const qreal top = trunkIsHorizontal()
+ ? (qMin(m_p1.y(), m_p2.y()) - 2.0 - total_height)
+ : qMin(m_p1.y(), m_p2.y());
+
+ qreal y = top;
+ for (LabelLine &line : lines)
+ {
+ line.rect = QRectF(textLeft(line.key, line.width), y,
+ line.width, line.height);
+ y += line.height;
+ }
+
+ //Where the label as a whole has been nudged to: every line of it
+ //goes with the rest, so those three lines keep reading as one
+ //label rather than as three which have drifted apart.
+ const QPointF nudge = textOffset();
+ if (!nudge.isNull())
+ {
+ for (LabelLine &line : lines) {
+ line.rect.translate(nudge);
+ }
+ }
+ return lines;
+}
+
+/**
+ @brief CablePart::refLines
+ @return one line per other folio this cable runs on, stacked under
+ the length of the cable -- under the type when the length is not
+ drawn, straight under the trunk line when neither is.
+
+ That is the bottom of everything the cable says about itself: above
+ the line stands where it belongs, under the line what it is and how
+ long it runs, and last comes where it goes on. Each line names the
+ section standing there, and each of them is the line he double
+ clicks to be taken to it.
+
+ Every entry comes in the order crossRefs() lists them, so the index
+ of a line here is the index of that reference there.
+*/
+QList CablePart::refLines() const
+{
+ if (!m_cable) return QList();
+
+ QList lines;
+ for (const CrossRef &ref : crossRefs())
+ {
+ if (ref.text.isEmpty()) continue;
+ LabelLine line;
+ line.text = ref.text;
+ line.key = QStringLiteral("xref");
+ line.font = fontOf(line.key);
+ line.align = alignOf(line.key);
+ const QFontMetricsF metrics(line.font);
+ line.width = metrics.horizontalAdvance(line.text);
+ line.height = metrics.height();
+ lines.append(line);
+ }
+ if (lines.isEmpty()) return lines;
+
+ //The same stack as typeRect() and lengthRect() build, only
+ //continued: what those two write between themselves decides how
+ //far down the references start, so no two of them can overlap
+ //whatever fonts he has chosen for them.
+ qreal y = underTop();
+ if (!typeText().isEmpty()) {
+ y += QFontMetricsF(fontOf(QStringLiteral("type"))).height();
+ }
+ if (!lengthText().isEmpty()) {
+ y += QFontMetricsF(fontOf(QStringLiteral("length"))).height();
+ }
+
+ for (LabelLine &line : lines)
+ {
+ line.rect = QRectF(textLeft(line.key, line.width), y,
+ line.width, line.height);
+ y += line.height;
+ }
+
+ const QPointF nudge = textOffset();
+ if (!nudge.isNull())
+ {
+ for (LabelLine &line : lines) {
+ line.rect.translate(nudge);
+ }
+ }
+ return lines;
+}
+
+/**
+ @brief CablePart::crossRefs
+ @return every other folio this cable runs on, one entry each, in the
+ order the folios turn -- empty when the cable runs on this folio
+ alone, which is what a cable normally does.
+
+ The reference needs no data of its own: a cable standing on two
+ folios is one cable drawn as several lines, so every one of those
+ lines knows its siblings and they all get the same list, whichever
+ way they were put together -- a line drawn onto a cable which was
+ already there, or a cable drawn first and joined later.
+*/
+QList CablePart::crossRefs() const
+{
+ QList refs;
+ if (!m_cable || !diagram() || !diagram()->project()) return refs;
+
+ QETProject *project = diagram()->project();
+
+ //Which section stands on which other folio. The first section of
+ //a folio is the one %l and %c name, when the format asks for
+ //them: it is the line he has to look for over there.
+ QHash elsewhere;
+ const QList parts = m_cable->parts();
+ for (const CablePartData &part : parts)
+ {
+ Diagram *other = project->diagramByUuid(part.diagram);
+ if (!other || other == diagram()) continue;
+ if (!elsewhere.contains(other)) {
+ elsewhere.insert(other, part);
+ }
+ }
+ if (elsewhere.isEmpty()) return refs;
+
+ //The format this project asks for such a reference, chosen under
+ //Cross-references for the type "Cable": one text per folio, written
+ //out with that folio's own numbers and fields.
+ const XRefProperties xref = project->defaultXRefProperties(QStringLiteral("cable"));
+ const QString format = xref.masterLabel();
+ if (format.isEmpty()) return refs;
+
+ //The folios are walked over in the order they turn, so the lines
+ //read the way the pages do.
+ const QList folios = project->diagrams();
+ for (Diagram *other : folios)
+ {
+ if (!elsewhere.contains(other)) continue;
+
+ autonum::FormulaContext context;
+ const BorderTitleBlock &border = other->border_and_titleblock;
+ context.folio = border.folio();
+ context.folio_index = other->folioIndex();
+ context.folio_total = border.folioTotal();
+ context.plant = border.plant();
+ context.locmach = border.locmach();
+ context.title_block_fields = border.additionalFields();
+ context.project_properties = project->projectProperties();
+ const CablePartData part = elsewhere.value(other);
+ context.has_element = true;
+ context.element_position = other->convertPosition((part.p1 + part.p2) * 0.5);
+
+ //A fresh set of numbers for every line: two references of
+ //the same cable must not count one after the other.
+ autonum::sequentialNumbers seq;
+ CrossRef ref;
+ ref.diagram = other;
+ ref.part = part.uuid;
+ ref.text = autonum::AssignVariables::formulaToLabel(format, seq, context);
+ if (ref.text.isEmpty()) continue;
+ refs.append(ref);
+ }
+ return refs;
+}
+
+/**
+ @brief CablePart::crossRefAt
+ @param scene_pos a point of the sheet, in scene coordinates
+ @return the index of the reference standing at that very place, -1
+ when none does -- measured on the lines which are really drawn, so
+ the hand of the mouse, the double click and the drawing can never
+ disagree about which line is which.
+*/
+int CablePart::crossRefAt(const QPointF &scene_pos) const
+{
+ int index = 0;
+ for (const LabelLine &line : refLines())
+ {
+ if (line.rect.contains(scene_pos)) return index;
+ ++index;
+ }
+ return -1;
+}
+
+/**
+ @brief CablePart::goToCrossRef
+ Send the mouse to the section of the cable which stands on the folio
+ the reference at that place points at, the way a cross reference of
+ an element does: the folio is shown and the line is picked.
+ @param scene_pos a point of the sheet, in scene coordinates
+ @return true when the click stood on a reference
+*/
+bool CablePart::goToCrossRef(const QPointF &scene_pos)
+{
+ const int index = crossRefAt(scene_pos);
+ if (index < 0) return false;
+
+ const QList refs = crossRefs();
+ if (index >= refs.size()) return false;
+
+ const CrossRef ref = refs.at(index);
+ if (!ref.diagram) return false;
+
+ const QList items = ref.diagram->items();
+ for (QGraphicsItem *item : items)
+ {
+ auto *section = qgraphicsitem_cast(item);
+ if (section && section->partUuid() == ref.part) {
+ QetGraphicsItem::showItem(section);
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ @brief CablePart::pdfRefs
+ Where the PDF export has to put a clickable link for every reference
+ this section writes: the rectangle of the text on this folio, and on
+ the folio it points at the rectangle of the section it names -- the
+ very section a double click on the drawing sends him to (see
+ goToCrossRef), so that the export and the drawing can never disagree
+ about where the link leads.
+
+ One entry per reference drawn, in the order it is drawn, so that the
+ export and the drawing count the same way (see crossRefAt).
+ @return those entries, empty when this cable runs on this folio alone
+*/
+QList CablePart::pdfRefs() const
+{
+ QList out;
+ if (!m_cable) return out;
+
+ const QList lines = refLines();
+ const QList refs = crossRefs();
+ const int count = qMin(lines.size(), refs.size());
+
+ for (int i = 0; i < count; ++i)
+ {
+ const CrossRef &ref = refs.at(i);
+ if (!ref.diagram || lines.at(i).rect.isEmpty()) continue;
+
+ PdfRef link;
+ link.diagram = ref.diagram;
+ link.here = lines.at(i).rect;
+
+ //The section standing over there, which is what the reader
+ //has to be brought to. It is looked for the same way a click
+ //on the drawing looks for it, so link and click agree.
+ const QList items = ref.diagram->items();
+ for (QGraphicsItem *item : items)
+ {
+ auto *section = qgraphicsitem_cast(item);
+ if (section && section->partUuid() == ref.part) {
+ link.there = section->mapRectToScene(section->boundingRect());
+ break;
+ }
+ }
+ out.append(link);
+ }
+ return out;
+}
+
+/**
+ @brief CablePart::setUpXrefHooks
+ Keep the reference lines up to date with what the project does: a
+ folio which is added, removed or moved changes which lines there are
+ and what they say, and so does the format chosen under Cross-references.
+
+ Only this line is redrawn, which is enough: the references of a
+ cable are worked out again from the cable itself every time its
+ label is drawn.
+*/
+void CablePart::setUpXrefHooks()
+{
+ for (const QMetaObject::Connection &connection : std::as_const(m_xref_connections)) {
+ disconnect(connection);
+ }
+ m_xref_connections.clear();
+
+ Diagram *folio = diagram();
+ if (!m_cable || !folio || !folio->project()) return;
+
+ QETProject *project = folio->project();
+ const auto rebuild = [this]() {
+ prepareGeometryChange();
+ setUpXrefHooks();
+ update();
+ };
+
+ m_xref_connections << connect(project, &QETProject::projectDiagramsOrderChanged, this, rebuild);
+ m_xref_connections << connect(project, &QETProject::diagramAdded, this, rebuild);
+ m_xref_connections << connect(project, &QETProject::diagramRemoved, this, rebuild);
+ m_xref_connections << connect(project, &QETProject::XRefPropertiesChanged, this, rebuild);
+
+ //%F names the other folio by its label, which is read off that
+ //folio itself: only when the format really asks for it does that
+ //folio have to be listened to as well.
+ const XRefProperties xref = project->defaultXRefProperties(QStringLiteral("cable"));
+ if (xref.masterLabel().contains(QLatin1String("%F")))
+ {
+ for (Diagram *other : project->diagrams()) {
+ m_xref_connections << connect(other, &Diagram::diagramInformationChanged, this, rebuild);
+ }
+ }
+}
+
+/**
+ @brief CablePart::typeText
+ @return the type of the cable, an empty string when the drawing is
+ not meant to show it or the cable says nothing about it
+*/
+QString CablePart::typeText() const
+{
+ if (!m_cable) return QString();
+
+ const CableProperties properties = m_cable->properties();
+ if (!properties.show_type || properties.type.isEmpty()) {
+ return QString();
+ }
+ return properties.type;
+}
+
+/**
+ @brief CablePart::lengthText
+ @return how long the cable is, an empty string when the drawing is
+ not meant to show it or the cable says nothing about it
+*/
+QString CablePart::lengthText() const
+{
+ if (!m_cable) return QString();
+
+ const CableProperties properties = m_cable->properties();
+ if (!properties.show_length || properties.length.isEmpty()) {
+ return QString();
+ }
+ return properties.length;
+}
+
+/**
+ @brief CablePart::labelRect
+ The rectangle holding the label of the cable, drawn at the left of
+ the trunk line -- in scene coordinates, like the line itself, so the
+ item never has to be moved for its text to follow.
+
+ Across a horizontal line the whole label stands above the line: text
+ straddling the line is what the eyes have to fight with. The type
+ and the length of the cable stand under the line, one under the
+ other, and are therefore not measured here.
+ @return an empty rectangle when the cable says nothing to show
+*/
+QRectF CablePart::labelRect() const
+{
+ QRectF rect;
+ for (const LabelLine &line : labelLines())
+ {
+ rect = rect.isEmpty() ? line.rect : rect.united(line.rect);
+ }
+ return rect;
+}
+
+/**
+ @brief CablePart::underTop
+ Where the first line written under the trunk line starts.
+
+ Across a horizontal line that is just under the line itself; along a
+ line which runs up and down the sheet "under" has no meaning, so it
+ is just under the rest of the label instead -- which keeps those
+ lines at the left of the line either way.
+ @return the y of the top of that first line, in scene coordinates
+*/
+qreal CablePart::underTop() const
+{
+ if (trunkIsHorizontal()) {
+ return qMax(m_p1.y(), m_p2.y()) + 2.0;
+ }
+
+ const QRectF label = labelRect();
+ //labelRect() already carries the nudge of the label, and the
+ //lines under it are nudged once more further down: what is asked
+ //for here is where they would stand without it, so that they go
+ //with the label exactly once rather than twice.
+ return label.isEmpty() ? qMin(m_p1.y(), m_p2.y())
+ : label.bottom() + 2.0 - textOffset().y();
+}
+
+/**
+ @brief CablePart::typeRect
+ The rectangle the type of the cable is written in. It sits under the
+ trunk line rather than with the rest of the label, so the type reads
+ as what the line carries while everything above the line says where
+ the cable belongs -- and it is the first of the lines written under
+ the line, so the length can stand under it.
+ @return an empty rectangle when the type is not drawn
+*/
+QRectF CablePart::typeRect() const
+{
+ const QString type = typeText();
+ if (type.isEmpty()) {
+ return QRectF();
+ }
+
+ const QFontMetricsF metrics(fontOf(QStringLiteral("type")));
+ const qreal width = metrics.horizontalAdvance(type);
+ const qreal height = metrics.height();
+
+ return QRectF(textLeft(QStringLiteral("type"), width), underTop(),
+ width, height).translated(textOffset());
+}
+
+/**
+ @brief CablePart::lengthRect
+ The rectangle the length of the cable is written in. It stands under
+ the type of the cable, which itself stands straight under the trunk
+ line: the type is what the cable is, the length is one more thing
+ said about it. Where the type is not drawn, the length takes its
+ place and sits straight under the line.
+ @return an empty rectangle when the length is not drawn
+*/
+QRectF CablePart::lengthRect() const
+{
+ const QString length = lengthText();
+ if (length.isEmpty()) {
+ return QRectF();
+ }
+
+ const QFontMetricsF metrics(fontOf(QStringLiteral("length")));
+ const qreal width = metrics.horizontalAdvance(length);
+ const qreal height = metrics.height();
+
+ //The type may well be written in another font than this, so how
+ //far down the length hangs has to be measured from the type and
+ //not from itself.
+ qreal top = underTop();
+ if (!typeText().isEmpty()) {
+ top += QFontMetricsF(fontOf(QStringLiteral("type"))).height();
+ }
+
+ return QRectF(textLeft(QStringLiteral("length"), width), top,
+ width, height).translated(textOffset());
+}
+
+/**
+ @brief CablePart::boundingRect
+*/
+QRectF CablePart::boundingRect() const
+{
+ QRectF rect(QPointF(qMin(m_p1.x(), m_p2.x()), qMin(m_p1.y(), m_p2.y())),
+ QPointF(qMax(m_p1.x(), m_p2.x()), qMax(m_p1.y(), m_p2.y())));
+ rect.adjust(-14.0, -14.0, 14.0, 14.0);
+
+ const QRectF label = labelRect();
+ if (!label.isEmpty()) {
+ rect = rect.united(label);
+ }
+ const QRectF type = typeRect();
+ if (!type.isEmpty()) {
+ rect = rect.united(type);
+ }
+ const QRectF length = lengthRect();
+ if (!length.isEmpty()) {
+ rect = rect.united(length);
+ }
+ for (const LabelLine &line : refLines()) {
+ rect = rect.united(line.rect);
+ }
+
+ if (m_cable)
+ {
+ for (const CableCore &core : m_cable->usedCores())
+ {
+ if (core.part != m_part_uuid || !coreDrawn(core)) continue;
+ const QPointF at = (core.core == m_dragged_core) ? m_drag_scene : coreDrawPosition(core);
+ rect = rect.united(coreLabelRect(at, m_cable->colorOfCore(core.core))
+ .adjusted(-3.0, -3.0, 3.0, 3.0));
+ }
+ //The core which is only waiting to be put down is drawn
+ //too, so its colour has to be inside what is repainted --
+ //right from the moment he asks for it, whether the mouse
+ //already stands at the line or still has to come there
+ if (m_pending_core >= 0)
+ {
+ rect = rect.united(coreLabelRect(m_drag_scene,
+ m_cable->colorOfCore(m_pending_core))
+ .adjusted(-3.0, -3.0, 3.0, 3.0));
+ }
+ }
+ return rect;
+}
+
+/**
+ @brief CablePart::coreLabelPivot
+ The point the colour of a core is written around before being turned
+ by a quarter: at the left of its slash -- or rather close enough to
+ the wire that it reads as one thing with it -- and lifted, so its
+ lower end stops just above the line instead of being cut through by
+ it.
+
+ Being turned by a quarter, the tail of a letter like "g" points to
+ the right, which is the side facing the wire: that is what the small
+ distance kept here has to clear.
+ @param at where the slash stands, in scene coordinates
+ @param color the colour being written, e.g. "br"
+ @return the centre of the text, in scene coordinates -- nudged by
+ whatever the form offers as "Position des couleurs", which moves the
+ colours and never the slash: the slash marks where the wire starts,
+ and that is not for a distance to decide.
+*/
+QPointF CablePart::coreLabelPivot(const QPointF &at, const QString &color) const
+{
+ const QFontMetricsF metrics(coreFont());
+ const qreal length = metrics.horizontalAdvance(color);
+ const qreal thickness = metrics.height();
+
+ return QPointF(at.x() - core_label_side - thickness * 0.5,
+ at.y() - core_label_gap - length * 0.5) + coreOffset();
+}
+
+/**
+ @brief CablePart::coreLabelRect
+ The rectangle the colour of a core is written in: the text stands at
+ the left of its slash and is turned by a quarter, so it runs along
+ the line instead of across it.
+ @param at where the slash stands, in scene coordinates
+ @param color the colour being written, e.g. "br"
+ @return the rectangle in scene coordinates, empty when there is
+ nothing to write
+*/
+QRectF CablePart::coreLabelRect(const QPointF &at, const QString &color) const
+{
+ if (color.isEmpty()) return QRectF();
+
+ const QFontMetricsF metrics(coreFont());
+ const qreal width = metrics.horizontalAdvance(color);
+ const qreal height = metrics.height();
+
+ //The text is drawn around this point and then turned by -90°, so
+ //its screen rectangle is height wide and text length tall.
+ const QPointF pivot = coreLabelPivot(at, color);
+ return QRectF(pivot.x() - height * 0.5, pivot.y() - width * 0.5, height, width);
+}
+
+/**
+ @brief CablePart::coreDrawPosition
+ Where a stored core is drawn right now. While the trunk line is
+ being dragged the label stands where it stood, carried along by
+ however far the line has come: the line, the description at its left
+ and the colours of its cores are one thing and move as one thing,
+ so no label ever jumps, ever lands on a neighbour's wire or ever
+ disappears -- and letting go of the line needs no correction.
+ @param core the stored core
+ @return where it is drawn at this moment
+*/
+QPointF CablePart::coreDrawPosition(const CableCore &core) const
+{
+ if (m_line_moved)
+ {
+ for (const CableCore &dragged : std::as_const(m_line_drag_cores))
+ {
+ if (dragged.core == core.core && dragged.part == core.part) {
+ return dragged.position;
+ }
+ }
+ }
+ return core.position;
+}
+
+/**
+ @brief CablePart::coreDrawn
+ Says whether a core is drawn at all. While the line is dragged, the
+ labels are drawn as they stand during that drag -- which is every
+ one of them: carrying the line along never leaves any of them out.
+ @param core the stored core
+ @return true when there is something to draw
+*/
+bool CablePart::coreDrawn(const CableCore &core) const
+{
+ if (!m_line_moved) return true;
+
+ for (const CableCore &dragged : std::as_const(m_line_drag_cores))
+ {
+ if (dragged.core == core.core && dragged.part == core.part) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ @brief CablePart::dragPosition
+ Where the label of a core being dragged is drawn: still exactly
+ where it stood, taken along by however far the mouse has come since
+ it was picked up -- and only by whole grid steps, counted from the
+ place it stood, the same as anything else drawn on the sheet moves.
+
+ So picking a label up never shifts it by a hair, even when the
+ mouse closes on it a little to one side, and letting go can never
+ take it back: what is drawn while he drags is exactly what is kept
+ when he lets go. A pull shorter than half a grid step moves nothing
+ at all, which is how every other item on the folio behaves.
+
+ The label follows the line and nothing else: pulling it upwards,
+ downwards, sideways or diagonally slides it along its own line,
+ never takes it off it, and never past either end of it -- a colour
+ label belongs to the line it is written on. Holding Ctrl lets go of
+ the grid steps, as everywhere else on the sheet.
+ @param scene_pos where the mouse stands, in scene coordinates
+ @return where the label is drawn at this moment
+*/
+QPointF CablePart::dragPosition(const QPointF &scene_pos) const
+{
+ //Where it was picked up: still the place the cable says it
+ //stands, since nothing is written while he drags.
+ QPointF anchor = m_drag_scene;
+ if (m_cable && m_dragged_core >= 0) {
+ const CableCore held = m_cable->core(m_dragged_core);
+ if (held.core == m_dragged_core) anchor = held.position;
+ }
+
+ const QPointF axis = m_p2 - m_p1;
+ const qreal length_squared = axis.x() * axis.x() + axis.y() * axis.y();
+ if (qFuzzyIsNull(length_squared)) return anchor;
+
+ const qreal length = std::sqrt(length_squared);
+ const QPointF shift = scene_pos - m_press_scene;
+ const qreal along = (shift.x() * axis.x() + shift.y() * axis.y()) / length;
+
+ const qreal stepped = stepAlong(along);
+
+ const QPointF carried = anchor + (axis / length) * stepped;
+
+ //And no further than the line itself: a colour label belongs to
+ //the line it is written on and stops at either end of it, so it
+ //goes on standing over a place the line really runs across and
+ //an entry can always follow it. Where it was picked up lay on
+ //the line, so this never moves a label which behaves -- it only
+ //stops one being pulled past the end.
+ const qreal share = ((carried.x() - m_p1.x()) * axis.x()
+ + (carried.y() - m_p1.y()) * axis.y())
+ / length_squared;
+ if (share <= 0.0) return m_p1;
+ if (share >= 1.0) return m_p2;
+ return m_p1 + axis * share;
+}
+
+/**
+ @brief CablePart::stepAlong
+ Snap a distance measured along the line to a grid step, in the
+ coordinates of the sheet: a horizontal line steps along x, a
+ vertical one along y, and holding Ctrl lets go of the grid the same
+ way Diagram::snapToGrid does everywhere else on the sheet.
+ @param along how far from the first end of the line, in scene units
+ @return that distance at a grid step
+*/
+qreal CablePart::stepAlong(qreal along) const
+{
+ const QPointF probe = trunkIsHorizontal() ? QPointF(along, 0.0)
+ : QPointF(0.0, along);
+ const QPointF stepped_probe = Diagram::snapToGrid(probe);
+ return trunkIsHorizontal() ? stepped_probe.x()
+ : stepped_probe.y();
+}
+
+/**
+ @brief CablePart::placePosition
+ Where the mouse would put down a core which is waiting to be put
+ down: the mouse itself projected onto the line, snapped to a grid
+ step and stopped at either end of the line, so what the drawing
+ shows while he aims is exactly what the click leaves behind.
+
+ Unlike dragPosition this asks where the mouse *is* rather than how
+ far it has come, since nothing is held yet to measure that from.
+ @param scene_pos where the mouse stands, in scene coordinates
+ @return where the slash would be drawn
+*/
+QPointF CablePart::placePosition(const QPointF &scene_pos) const
+{
+ const QPointF axis = m_p2 - m_p1;
+ const qreal length_squared = axis.x() * axis.x() + axis.y() * axis.y();
+ if (qFuzzyIsNull(length_squared)) return m_p1;
+
+ const qreal length = std::sqrt(length_squared);
+ const QPointF shift = scene_pos - m_p1;
+ const qreal along = (shift.x() * axis.x() + shift.y() * axis.y()) / length;
+
+ //At a grid step like anything else drawn, and never beyond
+ //either end: a colour label belongs to the line it is written
+ //on and stops where the line stops.
+ const qreal stepped = qBound(0.0, stepAlong(along), length);
+ return m_p1 + (axis / length) * stepped;
+}
+
+/**
+ @brief CablePart::shape
+ Only the trunk line, its text and its slashes are clickable, not the
+ whole rectangle around them: a cable drawn across a dense folio must
+ not steal the clicks meant for what it passes over.
+*/
+QPainterPath CablePart::shape() const
+{
+ QPainterPath trunk;
+ trunk.moveTo(m_p1);
+ trunk.lineTo(m_p2);
+
+ QPainterPathStroker stroker;
+ stroker.setWidth(10.0);
+ stroker.setCapStyle(Qt::FlatCap);
+
+ QPainterPath result = stroker.createStroke(trunk);
+
+ const QRectF label = labelRect();
+ if (!label.isEmpty()) {
+ result.addRect(label);
+ }
+ const QRectF type = typeRect();
+ if (!type.isEmpty()) {
+ result.addRect(type);
+ }
+ const QRectF length = lengthRect();
+ if (!length.isEmpty()) {
+ result.addRect(length);
+ }
+ //The references to the other folios stand under those and are
+ //clicked the same way.
+ for (const LabelLine &line : refLines()) {
+ result.addRect(line.rect);
+ }
+
+ if (m_cable)
+ {
+ for (const CableCore &core : m_cable->usedCores())
+ {
+ if (core.part != m_part_uuid || !coreDrawn(core)) continue;
+ const QPointF at = (core.core == m_dragged_core) ? m_drag_scene : coreDrawPosition(core);
+ //The colour beside the slash is held by the same grab as
+ //the slash: it is read far more often than it is moved,
+ //and clicking the letters has to do what clicking the
+ //slash does. The label stands to the left of the slash,
+ //so the grab reaches that way rather than the other.
+ result.addRect(QRectF(at - QPointF(18.0, slash_grab),
+ QSizeF(24.0, slash_grab * 2.0)));
+ const QRectF text = coreLabelRect(at, m_cable->colorOfCore(core.core));
+ if (!text.isEmpty()) {
+ result.addRect(text.adjusted(-3.0, -3.0, 3.0, 3.0));
+ }
+ }
+ }
+
+ //While the line is picked, its two end points are part of what
+ //may be clicked: they reach out over the one unit of stroke the
+ //trunk itself is, and a finger aiming at a blue point never
+ //lands that exactly.
+ if (isSelected())
+ {
+ result.addEllipse(m_p1, grip_grab, grip_grab);
+ result.addEllipse(m_p2, grip_grab, grip_grab);
+ }
+ return result;
+}
+
+/**
+ @brief CablePart::labelAt
+ Whether the colour label of that core is standing at that point:
+ either its slash, or the text beside it, both of which read as one
+ thing with the wire the core names.
+
+ One core at a time, so that the test can never be answered by a
+ neighbour standing closer to the point -- dropping one colour
+ straight onto another has to be seen as exactly that.
+ @param core which core is being asked about
+ @param local a point of this item, scene coordinates
+ @return true when the point is on that label
+*/
+bool CablePart::labelAt(int core, const QPointF &local) const
+{
+ if (!m_cable) return false;
+
+ const CableCore at_core = m_cable->core(core);
+ if (at_core.core != core || at_core.part != m_part_uuid || !coreDrawn(at_core)) {
+ return false;
+ }
+
+ const QPointF at = coreDrawPosition(at_core);
+ if (QLineF(at, local).length() <= slash_grab) return true;
+
+ const QRectF text = coreLabelRect(at, m_cable->colorOfCore(core));
+ return !text.isEmpty() && text.adjusted(-3.0, -3.0, 3.0, 3.0).contains(local);
+}
+
+/**
+ @brief CablePart::coreAt
+ @param local a point of this item, scene coordinates
+ @return the core whose slash or colour stands there, -1 when none does
+*/
+int CablePart::coreAt(const QPointF &local) const
+{
+ if (!m_cable) return -1;
+
+ for (const CableCore &core : m_cable->usedCores())
+ {
+ if (core.part != m_part_uuid || !coreDrawn(core)) continue;
+ if (core.core != m_dragged_core)
+ {
+ if (labelAt(core.core, local)) return core.core;
+ continue;
+ }
+
+ //The one being dragged is drawn where the mouse holds it
+ //right now, not where the cable still says it stands
+ const QPointF at = m_drag_scene;
+ if (QLineF(at, local).length() <= slash_grab) {
+ return core.core;
+ }
+ const QRectF text = coreLabelRect(at, m_cable->colorOfCore(core.core));
+ if (!text.isEmpty() && text.adjusted(-3.0, -3.0, 3.0, 3.0).contains(local)) {
+ return core.core;
+ }
+ }
+ return -1;
+}
+
+/**
+ @brief CablePart::drawLabel
+ @param painter
+*/
+void CablePart::drawLabel(QPainter *painter, const QColor &ink) const
+{
+ if (!m_cable) return;
+
+ const QList lines = labelLines();
+ const QRectF type_rect = typeRect();
+ const QRectF length_rect = lengthRect();
+ const QList ref_lines = refLines();
+ if (lines.isEmpty() && type_rect.isEmpty() && length_rect.isEmpty()
+ && ref_lines.isEmpty()) return;
+
+ painter->save();
+ painter->setPen(ink);
+
+ //Every line brings the font it is written with: they are not
+ //measured with the same yard, so they may not be written with
+ //one either.
+ for (const LabelLine &line : lines)
+ {
+ painter->setFont(line.font);
+ painter->drawText(line.rect, line.align | Qt::AlignVCenter, line.text);
+ }
+ if (!type_rect.isEmpty()) {
+ painter->setFont(fontOf(QStringLiteral("type")));
+ painter->drawText(type_rect, alignOf(QStringLiteral("type")) | Qt::AlignVCenter,
+ typeText());
+ }
+ if (!length_rect.isEmpty()) {
+ painter->setFont(fontOf(QStringLiteral("length")));
+ painter->drawText(length_rect, alignOf(QStringLiteral("length")) | Qt::AlignVCenter,
+ lengthText());
+ }
+ //Last of all, at the very bottom: which other folios this cable
+ //runs on, one line each.
+ for (const LabelLine &line : ref_lines)
+ {
+ painter->setFont(line.font);
+ painter->drawText(line.rect, line.align | Qt::AlignVCenter, line.text);
+ }
+ painter->restore();
+}
+
+/**
+ @brief CablePart::drawCores
+ One slash per core the cable really wires, with the colour of that
+ core beside it -- the same colour which is written into the cable
+ field of the conductor underneath. Plus, while a core is waiting to
+ be put down, a paler copy of the very same marks wherever the mouse
+ stands near the line.
+ @param painter
+*/
+void CablePart::drawCores(QPainter *painter, const QColor &ink) const
+{
+ if (!m_cable) return;
+
+ for (const CableCore &core : m_cable->usedCores())
+ {
+ if (core.part != m_part_uuid || !coreDrawn(core)) continue;
+
+ const bool dragged = (core.core == m_dragged_core);
+ const QPointF at = dragged ? m_drag_scene : coreDrawPosition(core);
+
+ drawCoreMark(painter, ink, at, m_cable->colorOfCore(core.core));
+ }
+
+ //A core which is waiting to be put down shows where it would
+ //land: the same slash and the same colour as any other, only
+ //paler, since nothing of it is written yet. It shows as soon as
+ //he asks for the core, and not only once the mouse has come
+ //near enough -- he picks it out of a list, and the line may well
+ //be across the screen from where that list stands.
+ if (m_pending_core >= 0)
+ {
+ painter->save();
+ painter->setOpacity(painter->opacity() * 0.6);
+ drawCoreMark(painter, ink, m_drag_scene,
+ m_cable->colorOfCore(m_pending_core));
+ painter->restore();
+ }
+}
+
+/**
+ @brief CablePart::drawCoreMark
+ One slash, with the colour of the core it marks written beside it:
+ the colour stands at the left of its slash and is turned by a
+ quarter, the way a measurement is written along the line it belongs
+ to. The same drawing serves a core which is already on the sheet and
+ one which is only being aimed at.
+ @param painter
+ @param ink the colour everything of this line is drawn with
+ @param at where the slash stands, in scene coordinates
+ @param color the colour of that core, e.g. "br"
+*/
+void CablePart::drawCoreMark(QPainter *painter, const QColor &ink,
+ const QPointF &at, const QString &color) const
+{
+ const QFont core_font = coreFont();
+ const QFontMetricsF metrics(core_font);
+
+ painter->save();
+ painter->setFont(core_font);
+ painter->setPen(QPen(ink, 1.0));
+ painter->drawLine(QPointF(at.x() - slash_half, at.y() + slash_half),
+ QPointF(at.x() + slash_half, at.y() - slash_half));
+
+ if (!color.isEmpty())
+ {
+ const qreal width = metrics.horizontalAdvance(color);
+ const qreal height = metrics.height();
+ const QPointF pivot = coreLabelPivot(at, color);
+
+ painter->save();
+ painter->translate(pivot);
+ painter->rotate(-90.0);
+ painter->drawText(QRectF(-width * 0.5, -height * 0.5, width, height),
+ Qt::AlignCenter, color);
+ painter->restore();
+ }
+ painter->restore();
+}
+
+/**
+ @brief CablePart::paint
+ @param painter
+ @param option
+ @param widget
+*/
+void CablePart::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
+{
+ Q_UNUSED(option)
+ Q_UNUSED(widget)
+
+ painter->setRenderHint(QPainter::Antialiasing, true);
+ painter->setFont(textFont());
+
+ // The folio is painted into a buffer whose lightness is inverted to the
+ // palette afterwards (PaletteGraphicsView): what is black in the buffer
+ // comes out as the Text color, what is white comes out as the sheet.
+ // The palette of the option, on the other hand, is the palette of the
+ // view's widgets, so it names colors which are already final and land
+ // on the sheet color -- an ink taken from it vanishes. Conductors are
+ // drawn the same way: black when they stand, red when they are picked.
+ const QColor ink = isSelected() ? Qt::red : Qt::black;
+
+ //One unit, like every other line of the drawing: a cable trunk is
+ //a line, not a highlight, and it has to line up with the wires it
+ //runs across rather than stand out over them.
+ QPen pen(ink, 1.0);
+ pen.setCapStyle(Qt::FlatCap);
+ pen.setJoinStyle(Qt::MiterJoin);
+ painter->setPen(pen);
+ painter->drawLine(m_p1, m_p2);
+
+ drawLabel(painter, ink);
+
+ drawCores(painter, ink);
+
+ //The two points every line of the drawing shows once it is
+ //picked: one at each end, in the same blue QET's own handles
+ //use. Pulling one of them changes how long the line is, the
+ //same way an end of a conductor is pulled (mousePressEvent
+ //and mouseMoveEvent). They belong to the line and are painted
+ //by it, so nothing else has to be put into the folio for them.
+ if (isSelected())
+ {
+ painter->save();
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(Qt::blue);
+ painter->drawEllipse(m_p1, grip_radius, grip_radius);
+ painter->drawEllipse(m_p2, grip_radius, grip_radius);
+ painter->restore();
+ }
+}
+
+/**
+ @brief CablePart::name
+ @return the cable number, so the cable shows under its own name in
+ every list which names items
+*/
+QString CablePart::name() const
+{
+ return m_cable ? m_cable->designation() : QString();
+}
+
+/**
+ @brief CablePart::syncGeometry
+ Tell the cable where this section runs. The cable is the one place
+ the drawing is read back from, so nothing has to be collected from
+ the folios at saving time.
+*/
+void CablePart::syncGeometry()
+{
+ if (!m_cable) return;
+ m_cable->setPartGeometry(m_part_uuid, m_p1, m_p2);
+}
+
+/**
+ @brief CablePart::mousePressEvent
+ While a core waits to be put down, the click belongs to that: on the
+ line -- or near enough for the mouse to be pointing at it -- the core
+ is left standing there, right click calls it off, and a click anywhere
+ else calls it off as well and goes on doing what it does for everybody
+ (left clicking on a slash or on its colour holds that core, on one of
+ the two blue points at the ends it holds that end, anywhere else on
+ the line it moves the line).
+
+ Otherwise: left clicking on a slash or on its colour holds that core:
+ it can be dragged onto another conductor to give it that conductor.
+
+ On one of the two blue points at the ends of a picked line it holds
+ that end instead, so the line can be made longer or shorter the same
+ way an end of a conductor is pulled (mouseMoveEvent).
+
+ Left clicking anywhere else on the line moves the line.
+ @param event
+*/
+void CablePart::mousePressEvent(QGraphicsSceneMouseEvent *event)
+{
+ //A core which waits to be put down takes the whole click: on
+ //the line it is left standing there, anywhere else -- and on
+ //the right button, which cancels everywhere on the sheet --
+ //nothing is put down. The click itself goes on doing whatever
+ //it does for everybody else.
+ if (m_pending_core >= 0)
+ {
+ if (!m_cable)
+ {
+ cancelPlacement();
+ event->ignore();
+ return;
+ }
+ //Where he aims is read off the click itself as well, so a
+ //dialog closing with the mouse already over the line needs
+ //no move first and a stale position is never taken for a
+ //place he never pointed at.
+ m_drag_scene = placePosition(event->scenePos());
+ m_pending_valid = QLineF(event->scenePos(), m_drag_scene).length()
+ <= place_grab;
+
+ if (event->button() == Qt::LeftButton && m_pending_valid)
+ {
+ const int core = m_pending_core;
+ const QPointF at = m_drag_scene;
+ cancelPlacement();
+ m_just_placed = placeCore(core, at);
+ update();
+ event->accept();
+ return;
+ }
+ const bool cancel = (event->button() == Qt::RightButton);
+ cancelPlacement();
+ if (cancel) {
+ event->accept();
+ } else {
+ event->ignore();
+ }
+ return;
+ }
+
+ //The second of two quick clicks arrives as a double click: by
+ //then the first one has already put a core down, and that must
+ //not read as a request to open the dialog a second time.
+ m_just_placed = false;
+
+ if (event->button() != Qt::LeftButton) {
+ QetGraphicsItem::mousePressEvent(event);
+ return;
+ }
+
+ m_press_scene = event->scenePos();
+ m_move_p1 = m_p1;
+ m_move_p2 = m_p2;
+ m_line_moved = false;
+ m_line_drag_cores.clear();
+ //Nothing is carried along until he picks up a whole line: a
+ //press on a colour or on one of the blue points works on that
+ //one thing and leaves every other line standing.
+ m_carried.clear();
+ //Where every colour label stands when the gesture starts: a
+ //line which is moved carries all of them along with it, and
+ //undo has to know where they were before that. A line which is
+ //stretched does not touch them, and records the same set
+ //unchanged.
+ m_cores_before = m_cable ? m_cable->usedCores() : QList();
+ //Whether the sheet has already been told that the rest of the
+ //selection comes along with this line (see carrySelection): every
+ //press starts from nothing, so the next gesture tells it again.
+ m_mover_started = false;
+ m_told_mover = QPointF();
+
+ //One of the two blue points at the ends, when the line is
+ //picked and only one of the two is under the finger: pulling
+ //it changes how long the line is. The line comes before
+ //whatever stands on it: a colour label parked right at the end
+ //may not take that point away, since the line could not be
+ //made longer from that end any more -- the labels would have
+ //to be pushed aside first, and there is nothing to push them
+ //with. The label is still picked up beside the point, by its
+ //letters or by its slash a little further along the line.
+ //When both ends are that close, which only happens on a very
+ //short line, nothing is stretched: it is moved like any other
+ //line instead.
+ int grip = 0;
+ if (isSelected())
+ {
+ const bool first = QLineF(event->scenePos(), m_p1).length() <= grip_grab;
+ const bool second = QLineF(event->scenePos(), m_p2).length() <= grip_grab;
+ if (first != second) grip = first ? 1 : 2;
+ }
+ m_stretch_end = grip;
+
+ if (grip > 0)
+ {
+ update();
+ event->accept();
+ return;
+ }
+
+ const int core = coreAt(event->scenePos());
+ if (core >= 0)
+ {
+ m_dragged_core = core;
+ //The label stays exactly where it stands until the mouse
+ //really moves: picking it up must not shift it by a hair.
+ m_drag_scene = m_cable->core(core).position;
+ if (!isSelected()) {
+ setSelected(true);
+ }
+ update();
+ event->accept();
+ return;
+ }
+
+ QetGraphicsItem::mousePressEvent(event);
+
+ //Every other line he has picked up at the same time: dragging
+ //one line takes the whole selection with it, and all of them go
+ //back to where they stood if he calls the gesture off. The
+ //selection is read after the click has been dealt with, since
+ //clicking an unselected line leaves that one alone selected.
+ m_carried.clear();
+ if (isSelected() && scene())
+ {
+ const QList selected = scene()->selectedItems();
+ for (QGraphicsItem *item : selected)
+ {
+ auto *other = qgraphicsitem_cast(item);
+ if (!other || other == this || !other->m_cable) continue;
+
+ CarriedLine carried;
+ carried.part = other;
+ carried.p1 = other->m_p1;
+ carried.p2 = other->m_p2;
+ carried.cores = other->m_cable->usedCores();
+ m_carried.append(carried);
+ }
+ }
+}
+
+/**
+ @brief CablePart::mouseMoveEvent
+ Three things the mouse can carry: the colour of a core (which stays
+ on its line and never past either end of it), one of the two blue
+ points at the ends (which makes the line longer or shorter -- that
+ changes nothing else: every colour label goes on standing exactly
+ where the user put it, and the line may not be brought in past the
+ outermost of those labels, which would otherwise be left standing
+ beyond the end of their own line), and the whole line (which is
+ translated as a piece, taking its colour labels with it). The line
+ in each case is snapped to the grid.
+ @param event
+*/
+void CablePart::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
+{
+ //Where a core waiting to be put down would stand: on the line
+ //itself, at a grid step, and drawn from the very first moment
+ //it is asked for -- how near the mouse stands decides nothing
+ //but whether a click keeps it or sends it away (see
+ //mousePressEvent), never whether he can see it.
+ if (m_pending_core >= 0)
+ {
+ if (!m_cable) {
+ cancelPlacement();
+ } else {
+ m_drag_scene = placePosition(event->scenePos());
+ update();
+ }
+ event->accept();
+ return;
+ }
+
+ if (m_dragged_core >= 0)
+ {
+ m_drag_scene = dragPosition(event->scenePos());
+ update();
+ event->accept();
+ return;
+ }
+
+ if (m_stretch_end > 0 && (event->buttons() & Qt::LeftButton))
+ {
+ //The end under the finger follows the mouse, snapped to the
+ //grid like anything else drawn on the folio, and the line
+ //keeps its right angle: the direction the mouse covers the
+ //most decides which way the line runs, the same way the
+ //drawing tool decides it -- so pulling the end of a
+ //horizontal line far enough upwards turns the line
+ //vertical instead of hanging it into the air at an angle.
+ const QPointF fixed = (m_stretch_end == 1) ? m_move_p2 : m_move_p1;
+ QPointF moving = Diagram::snapToGrid(event->scenePos());
+ const QPointF to = moving - fixed;
+
+ bool horizontal;
+ if (qAbs(to.x()) > qAbs(to.y())) horizontal = true;
+ else if (qAbs(to.y()) > qAbs(to.x())) horizontal = false;
+ else horizontal = trunkIsHorizontal();
+ moving = horizontal ? QPointF(moving.x(), fixed.y())
+ : QPointF(fixed.x(), moving.y());
+
+ //Never shorter than his own colour labels: every one of
+ //them which stood on this part of the line when the
+ //gesture began has to go on standing on it, or it would be
+ //left beyond the end of the line it belongs to and could
+ //no longer be wired. He may pull the line out as far as he
+ //likes, and he may bring either end in as far as he likes
+ //up to the outermost of those labels -- which are found
+ //along the line as it runs now, since the direction can
+ //change from one move to the next.
+ const QPointF started_from = (m_stretch_end == 1) ? m_move_p1 : m_move_p2;
+ const qreal at_fixed = horizontal ? fixed.x() : fixed.y();
+ const qreal at_start = horizontal ? started_from.x() : started_from.y();
+ const qreal first = qMin(at_fixed, at_start);
+ const qreal last = qMax(at_fixed, at_start);
+
+ bool any_label = false;
+ qreal low = at_fixed, high = at_fixed;
+ for (const CableCore &core : m_cores_before)
+ {
+ if (core.part != m_part_uuid) continue;
+
+ const qreal at = horizontal ? core.position.x() : core.position.y();
+ if (at < first || at > last) continue;
+
+ if (!any_label) { low = high = at; any_label = true; }
+ else { low = qMin(low, at); high = qMax(high, at); }
+ }
+
+ if (any_label)
+ {
+ //The end sits on the grid, so the limit it stops
+ //against is taken from it on the far side of the
+ //label, never in front of it.
+ QSettings settings;
+ const int step = horizontal
+ ? settings.value(QStringLiteral("diagrameditor/Xgrid"),
+ Diagram::xGrid).toInt()
+ : settings.value(QStringLiteral("diagrameditor/Ygrid"),
+ Diagram::yGrid).toInt();
+ if (step > 0) {
+ high = std::ceil(high / step) * step;
+ low = std::floor(low / step) * step;
+ }
+
+ qreal at_end = horizontal ? moving.x() : moving.y();
+ at_end = at_end > at_fixed ? qMax(at_end, high)
+ : qMin(at_end, low);
+ moving = horizontal ? QPointF(at_end, moving.y())
+ : QPointF(moving.x(), at_end);
+ }
+
+ const QPointF p1 = (m_stretch_end == 1) ? moving : fixed;
+ const QPointF p2 = (m_stretch_end == 1) ? fixed : moving;
+
+ if (p1 != m_p1 || p2 != m_p2)
+ {
+ setLine(p1, p2);
+ m_line_moved = true;
+ //Only the line is stretched: every colour label goes on
+ //standing exactly where the user left it, none is
+ //carried anywhere and none is worked out again. What
+ //the drawing showed of them before the gesture goes on
+ //being what the cable holds -- which is what the
+ //release below records, along with the line.
+ m_line_drag_cores = m_cores_before;
+ update();
+ }
+ event->accept();
+ return;
+ }
+
+ if (isSelected() && event->buttons() & Qt::LeftButton)
+ {
+ //The line follows the mouse, snapped to the grid like anything
+ //else drawn on the folio, and keeps its length: it is purely
+ //optical, so nothing else has to follow it.
+ const QPointF target = Diagram::snapToGrid(
+ m_move_p1 + (event->scenePos() - m_press_scene));
+ const QPointF delta = target - m_move_p1;
+
+ if (target != m_p1 || m_move_p2 + delta != m_p2)
+ {
+ setLine(target, m_move_p2 + delta);
+ m_line_moved = true;
+ //Where every colour label stands on the line at this
+ //moment: carried along by however far the line has
+ //come, so line, description and colours move as one
+ //piece and no label ever jumps or disappears.
+ m_line_drag_cores = m_cores_before;
+ reconcileCores(m_line_drag_cores, delta);
+ update();
+ }
+ //Every other line he picked up with this one comes along,
+ //on every step of the gesture and with the same step: a
+ //selection of lines is one thing to move. It happens even
+ //when the line under his finger itself stands still -- a
+ //pull back to where it began -- or those would be left
+ //standing out where the gesture reached.
+ carryCompanions(delta);
+ //Everything else he marked at the same time comes along too
+ //-- the elements and the texts of the same selection -- so
+ //that one gesture moves all of it rather than two halves of
+ //the selection going different ways.
+ carrySelection(delta);
+ event->accept();
+ return;
+ }
+
+ QetGraphicsItem::mouseMoveEvent(event);
+}
+
+/**
+ @brief CablePart::mouseReleaseEvent
+ @param event
+*/
+void CablePart::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
+{
+ //Nothing has been put down by this gesture, so nothing has to
+ //be settled either: the core is placed by the press itself.
+ if (m_pending_core >= 0)
+ {
+ event->accept();
+ return;
+ }
+
+
+ if (m_dragged_core >= 0 && event->button() == Qt::LeftButton)
+ {
+ const int core = m_dragged_core;
+ m_dragged_core = -1;
+
+ //What was drawn while he dragged is what is kept: the label
+ //goes on standing where he left it, and letting go can never
+ //take it back to where it was picked up. A press which never
+ //went anywhere is a click: it keeps the label exactly where
+ //it already stands and never makes it name some other wire
+ //for having been touched.
+ const CableCore held = m_cable ? m_cable->core(core) : CableCore();
+ const bool moved_label = (held.core == core) && (m_drag_scene != held.position);
+
+ if (moved_label) {
+ rebindCore(core, m_drag_scene);
+ }
+ update();
+ event->accept();
+ return;
+ }
+
+ //The steps of this line, held back until the gesture is over:
+ //when the sheet is moving elements together with the lines, its
+ //own undo waits for them so that one single undo step takes all
+ //of it back.
+ QList pending;
+ bool cancelled = false;
+
+ if (event->button() == Qt::LeftButton && m_line_moved && diagram())
+ {
+ const QPointF after_p1 = m_p1;
+ const QPointF after_p2 = m_p2;
+ //One undo step for one gesture: taking the drag back puts
+ //every line of it exactly where it started -- a line which
+ //moved takes its colour labels back with it, since they are
+ //written on that line, and one which was stretched leaves
+ //them exactly where he put them.
+ if (after_p1 != m_move_p1 || after_p2 != m_move_p2)
+ {
+ QList after = m_line_drag_cores;
+
+ //Stretching only changes how long the line is: no
+ //colour label has been carried anywhere, so nothing of
+ //the cable has to be worked out again, no wire changes
+ //hands and nothing at all is asked. One step which puts
+ //the line -- and only the line -- back where it was.
+ if (m_stretch_end > 0)
+ {
+ pending.append(
+ new MoveCablePartCommand(m_cable, this,
+ m_move_p1, m_move_p2,
+ after_p1, after_p2,
+ m_cores_before, after));
+ }
+ else
+ {
+ //The lines he picked up with this one settle first,
+ //one after the other: a wire one of them lets go of
+ //is then already free for the next one instead of
+ //being asked about as if it were still taken. Each
+ //of them asks before taking anything away from a
+ //cable which is not part of this gesture.
+ QList steps;
+ cancelled = !settleCompanions(steps);
+
+ //What the drawing already showed during the drag goes
+ //into the cable now -- and which wire each core names
+ //is worked out at this moment, and only at it, from
+ //where its colour label has come to rest: that is
+ //what makes the entries of the wires right again
+ //after a move, instead of leaving them standing on
+ //the wires the line has gone away from. No label is
+ //moved for it.
+ if (!cancelled)
+ {
+ readyToSettle(after);
+ cancelled = !settleCores(after);
+ }
+
+ if (cancelled)
+ {
+ //He called the whole gesture off: every line of
+ //it goes back to where he picked it up, nothing
+ //at all is written, and there is no step to undo
+ //-- the drawing is as it was before he started.
+ putCompanionsBack();
+ if (m_cable) {
+ m_cable->setCores(m_cores_before);
+ }
+ setLine(m_move_p1, m_move_p2);
+ for (QUndoCommand *step : steps) {
+ delete step;
+ }
+ }
+ else
+ {
+ if (m_cable) {
+ m_cable->setCores(after);
+ }
+ steps.append(new MoveCablePartCommand(m_cable, this,
+ m_move_p1, m_move_p2,
+ after_p1, after_p2,
+ m_cores_before, after));
+ //Given to the whole gesture rather than pushed
+ //here, since the sheet may still have the rest
+ //of the selection to write down with it.
+ pending = steps;
+ steps.clear();
+ }
+ }
+ }
+ }
+
+ //The lines he picked up with this one go on standing where the
+ //gesture left them, and none of them is carried any more --
+ //whether it was written down or called off.
+ for (const CarriedLine &carried : std::as_const(m_carried))
+ {
+ if (!carried.part) continue;
+ carried.part->m_line_moved = false;
+ carried.part->m_line_drag_cores.clear();
+ carried.part->update();
+ }
+ m_carried.clear();
+
+ m_line_moved = false;
+ m_stretch_end = 0;
+ m_line_drag_cores.clear();
+
+ //One step for one gesture, however mixed the selection is: when
+ //the sheet is moving the elements he marked together with these
+ //lines, its own undo waits for the steps above and pushes
+ //everything at once, so a single undo takes the whole selection
+ //back. When nothing else was marked they are pushed here exactly
+ //as they always were -- and he called the gesture off when
+ //nothing at all is written, the elements included.
+ if (diagram())
+ {
+ ElementsMover &mover = diagram()->elementsMover();
+ if (cancelled)
+ {
+ qDeleteAll(pending);
+ pending.clear();
+ mover.cancelMovement();
+ }
+ else if (!pending.isEmpty() && !mover.isReady())
+ {
+ mover.addExtraCommands(pending);
+ pending.clear();
+ }
+ else if (pending.size() == 1)
+ {
+ diagram()->undoStack().push(pending.takeFirst());
+ }
+ else if (!pending.isEmpty())
+ {
+ //One step however many lines he moved: taking the
+ //gesture back has to take all of it back, or half the
+ //selection would stay where the drag left it.
+ diagram()->undoStack().push(new BatchCommand(
+ pending,
+ QCoreApplication::translate(
+ "BatchCommand", "Move %n cable(s)",
+ "Un geste déplace plusieurs lignes à la fois.",
+ pending.size())));
+ pending.clear();
+ }
+ }
+ else
+ {
+ qDeleteAll(pending);
+ pending.clear();
+ }
+
+ QetGraphicsItem::mouseReleaseEvent(event);
+
+ //The sheet has been told everything this gesture had to tell it:
+ //the next press starts from nothing again (see carrySelection).
+ m_mover_started = false;
+ m_told_mover = QPointF();
+}
+
+/**
+ @brief CablePart::mouseDoubleClickEvent
+ Opening the cable is what a double click on the line is for. On one
+ of the colour labels it does nothing: they are part of a core, and
+ the user asking for the whole cable by clicking twice in the same
+ place never said so. On a line which points at another folio it goes
+ to that folio instead: the reference leads somewhere, and asking
+ twice in it is asking to be taken there.
+ @param event
+*/
+void CablePart::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
+{
+ //The click which put a core down is reported once more as a
+ //double click when he clicks twice fast: it was there to place
+ //something and must not go on to open the dialog.
+ if (m_just_placed)
+ {
+ m_just_placed = false;
+ event->accept();
+ return;
+ }
+
+ if (event->button() == Qt::LeftButton && coreAt(event->scenePos()) >= 0)
+ {
+ event->accept();
+ return;
+ }
+
+ //A line which points at another folio leads there: asking twice
+ //in that very line is asking to be taken to the section standing
+ //over there, the way a cross reference of an element does, rather
+ //than to open this cable.
+ if (event->button() == Qt::LeftButton && goToCrossRef(event->scenePos()))
+ {
+ event->accept();
+ return;
+ }
+ QetGraphicsItem::mouseDoubleClickEvent(event);
+}
+
+/**
+ @brief CablePart::hoverMoveEvent
+ The hand over a line which points at another folio: it says the line
+ leads somewhere before he clicks and finds out. While a core waits to
+ be put down the cross of that gesture is showing, and it stays.
+ @param event
+*/
+void CablePart::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
+{
+ QetGraphicsItem::hoverMoveEvent(event);
+
+ if (m_pending_core >= 0) return;
+
+ if (crossRefAt(event->scenePos()) >= 0) {
+ setCursor(Qt::PointingHandCursor);
+ } else {
+ unsetCursor();
+ }
+}
+
+/**
+ @brief CablePart::rebindCore
+ Let go of a core the user dragged: it lands where he let it go, and
+ whether it takes the wire standing there is decided by what he did
+ rather than by anything being looked up for him.
+
+ Nothing here ever moves the label to reach a wire. It goes on
+ standing at the place he dropped it -- on the line, at the grid,
+ the very place it has been drawn for every moment of the drag -- so
+ that a label never crawls back to the wire he pulled it away from
+ and never goes off to look for one of its own. What changes, if
+ anything, is which wire names it:
+
+ - On a wire this line runs across: the core takes it, and the
+ field of the wire it left is emptied by the refresh which
+ follows.
+ - On a wire another cable holds: he is asked about it first. Yes
+ hands the wire over, and the other cable's core becomes a free
+ slot of that cable -- its slash and its colour leave the sheet
+ with it, which is what he saw happen. No leaves the other cable
+ exactly as it was and makes this core the free one instead: it
+ names no wire at all any more, so its own slash and colour go off
+ the sheet as well, its entry leaves the wire it came from, and it
+ waits as a free slot in the list of existing cables until he puts
+ it back.
+ - Directly on the colour label of another core of the same cable:
+ the two exchange their wires, which is a deliberate exchange --
+ he put one colour on top of the other, so he means the second one
+ to take what the first is giving up.
+ - On a wire another core of this same cable already holds, anywhere
+ but on its label: nothing changes.
+ - On nothing at all: the label stays exactly where he dropped it,
+ and it stops naming a wire -- an entry follows its label and
+ stops where its label stopped, so that no entry is left standing
+ on a wire the label has gone away from. A label which has not
+ been moved keeps what it had, though: picking one up and letting
+ it go again leaves the drawing exactly as it was.
+
+ Only the label he dragged may move: no other core of this cable is
+ hunted down. A core is taken away from its own cable at one single
+ place -- when he says no to the wire he has dropped its label on,
+ so that it names nothing -- and that is the very thing the other
+ cable's core becomes when he says yes to taking its wire. Both
+ answers therefore leave the drawing the same way: a free slot, no
+ slash, no colour, and the entry gone from the wire it stood on.
+
+ Whatever comes out of it is kept as one undo step, the whole set of
+ assignments before and after the gesture.
+ @param core which core is being moved
+ @param dropped_at scene coordinates where the slash was let go
+ @param before_cores the set of assignments as it stood before the
+ core was added to the cable, when a core is being put down rather
+ than moved; nullptr otherwise
+*/
+bool CablePart::rebindCore(int core, const QPointF &dropped_at,
+ const QList *before_cores)
+{
+ if (!m_cable || !diagram() || !diagram()->project()) {
+ return true;
+ }
+
+ const CableCore moved = m_cable->core(core);
+ if (moved.core != core) {
+ return true;
+ }
+
+ //What the cable held before this gesture, so undo can put it
+ //back: the set as it stands while a core already on the sheet
+ //is merely dragged, or -- when a core is being put down for the
+ //first time -- the set from before it was added, which takes
+ //the addition back as well.
+ const QList before = before_cores ? *before_cores
+ : m_cable->usedCores();
+
+ //Where the label has actually been drawn: on the line, at whole
+ //grid steps from where it stood. It is taken as it is and never
+ //worked out again, so letting go cannot shift it by a hair.
+ const QPointF at = dropped_at;
+ CableCore landed = moved;
+ landed.position = at;
+
+ //Which wire of this line stands at that very place, worked out
+ //from where the label stands and never the other way round:
+ //the label is not sent off to look for a wire, it names the
+ //one it happens to be standing on.
+ const QList crossings = CableManager::crossedConductors(
+ diagram(), QLineF(m_p1, m_p2));
+ Conductor *target = wireFor(crossings, moved, at);
+
+ //Set when he refuses a wire another cable holds: this core then
+ //names no wire at all any more and goes back to being a free
+ //slot of the cable, so it is taken out rather than written.
+ bool freed = false;
+
+ if (target)
+ {
+ //Which core of this cable, if any, already stands on that
+ //wire -- everything but the one being dragged.
+ int holder = -1;
+ for (const CableCore &other : m_cable->usedCores())
+ {
+ if (other.core == core || other.part != m_part_uuid) continue;
+ if (other.conductor == target->uuid()) {
+ holder = other.core;
+ }
+ }
+
+ if (holder < 0)
+ {
+ const ClaimAnswer answer = mayClaim(target);
+ if (answer == ClaimAnswer::Cancel)
+ {
+ //He called the drop off: the label goes on standing
+ //where he picked it up, and nothing is written at all.
+ return false;
+ }
+ if (answer == ClaimAnswer::Take)
+ {
+ //A wire is one piece of wire: whichever cable held it
+ //before has to let it go.
+ CableManager::claimConductor(diagram()->project(), target, m_cable.data());
+
+ //A conductor keeps the identity a cable binds it by, even
+ //when it was read back from a file written before it had
+ //one.
+ target->setUuid(target->uuid());
+
+ landed.conductor = target->uuid();
+ }
+ else
+ {
+ //He said no: the other cable goes on holding that
+ //wire, and this label has come to rest on it anyway.
+ //So this core names no wire at all any more and goes
+ //back to being a free slot of this cable -- exactly
+ //what the other cable's core becomes when he says
+ //yes -- taking its slash and its colour off the sheet
+ //with it, while its entry leaves the wire the label
+ //has gone away from. Only the other cable keeps its
+ //wire, and he can put this core back wherever he
+ //likes from the list of existing cables.
+ freed = true;
+ }
+ }
+ else if (labelAt(holder, at))
+ {
+ //One colour dropped straight onto another: the two
+ //exchange their wires, so neither is left standing on
+ //a wire the drawing does not name with it. The dragged
+ //label stays where he put it, on the wire it has just
+ //taken, and the other one comes to the place it gives
+ //up -- that exchange is what he asked for.
+ CableCore swapped = m_cable->core(holder);
+ if (swapped.core == holder)
+ {
+ CableManager::claimConductor(diagram()->project(), target, m_cable.data());
+ target->setUuid(target->uuid());
+
+ landed.conductor = target->uuid();
+
+ swapped.conductor = moved.conductor;
+ swapped.position = moved.position;
+ m_cable->setCore(swapped);
+ }
+ }
+ //Held by another core of this cable but not dropped on its
+ //label: leave everything alone, the dragged label merely
+ //stays where it was let go.
+ }
+ else if (at != moved.position && !moved.conductor.isNull())
+ {
+ //He let go where this line runs across nothing, having
+ //really brought the label there: the entry of the wire it
+ //came from goes with it, while the label itself stays
+ //exactly where he put it and merely names no wire at all
+ //any more. Not moved means not touched at all, so picking
+ //a label up and letting it go again never empties
+ //anything.
+ landed.conductor = QUuid();
+ }
+
+ if (freed)
+ {
+ //No wire names this core any more: it goes back to being a
+ //free slot of the cable and stops being drawn, and the
+ //refresh which follows empties the cable field of the wire
+ //it used to name -- an entry follows its label, and this
+ //label has come to rest where it may not name anything.
+ m_cable->removeCore(core);
+ }
+ else
+ {
+
+ if (landed != moved) {
+ m_cable->setCore(landed);
+ }
+ }
+
+ const QList after = m_cable->usedCores();
+ if (after != before) {
+ //A core which has just been put down for the first time is
+ //not being moved, and the step which takes it back is named
+ //for what it did rather than for where it went.
+ const QString text = before_cores
+ ? QCoreApplication::translate("ChangeCableCoresCommand",
+ "Place a cable core")
+ : QString();
+ diagram()->undoStack().push(
+ new ChangeCableCoresCommand(m_cable, before, after, text));
+ }
+ return true;
+}
+
+/**
+ @brief CablePart::takeCoreOffLine
+ Take one core off this very line: its colour label disappears from
+ the sheet and the core goes back to being a free slot of its cable,
+ listed among the ones which stand nowhere -- the exact opposite of
+ putting one down from that list.
+
+ Only a core standing on this line is taken off, never one of the
+ other sections of the same cable: a cable running over two folios
+ stands on two lines, and the one whose menu was asked for is the one
+ he is working on.
+
+ Whatever it named goes with it: the refresh which follows empties
+ the cable field of the wire it used to name, since an entry follows
+ the label it stands for, and this label no longer stands anywhere.
+
+ One undo step, and a single undo puts the label back exactly where
+ it stood.
+ @param core which core standing on this line is taken off
+ @return true when a core was taken off
+*/
+bool CablePart::takeCoreOffLine(int core)
+{
+ if (!m_cable || !diagram() || !diagram()->project()) return false;
+
+ const CableCore there = m_cable->core(core);
+ if (there.core != core || there.part != m_part_uuid) return false;
+
+
+ const QList before = m_cable->usedCores();
+ m_cable->removeCore(core);
+ const QList after = m_cable->usedCores();
+ if (after == before) return false;
+
+ diagram()->undoStack().push(new ChangeCableCoresCommand(
+ m_cable, before, after,
+ QCoreApplication::translate("ChangeCableCoresCommand",
+ "Remove a core from the line")));
+ return true;
+}
+
+/**
+ @brief CablePart::reconcileCores
+ Where the colour labels of this line stand while the line is being
+ moved: exactly where they stood, taken along by however far the line
+ has come.
+
+ Every label is carried along on every single step of the gesture --
+ the trunk line, the description above it and the colour labels of the
+ cores are one thing and move as one thing. Nothing is worked out
+ again while he drags, so no label ever jumps, ever lands on a
+ neighbour's wire or ever disappears because the line came a little
+ too short: pulling the line back puts every label exactly back where
+ it was.
+
+ The place a label belongs to is worked out afterwards, when the
+ line is let go (settleCores), and while a single core is moved on
+ its own -- gestures which are about one wire rather than about the
+ whole line, and which read the wire off the place of the label
+ rather than sending the label off to find one.
+ @param cores the whole set of assignments, changed in place
+ @param delta how far the line has come in this gesture
+*/
+void CablePart::reconcileCores(QList &cores, const QPointF &delta) const
+{
+ for (CableCore &core : cores)
+ {
+ if (core.part != m_part_uuid) continue;
+
+ core.position += delta;
+ }
+}
+
+/**
+ @brief CablePart::carryCompanions
+ Every other line he picked up with this one comes along, with the
+ same step and with the colours of its own cores: a selection of
+ lines is one thing to move.
+
+ Nothing of them is written while he drags -- they are drawn where
+ the gesture has brought them, exactly the way this line draws its
+ own labels, so pulling back puts every one of them back where it
+ was and letting go works the entries out afterwards.
+ @param delta how far the line under his finger has come
+*/
+void CablePart::carryCompanions(const QPointF &delta)
+{
+ for (const CarriedLine &carried : std::as_const(m_carried))
+ {
+ CablePart *other = carried.part;
+ if (!other) continue;
+
+ other->setLine(carried.p1 + delta, carried.p2 + delta);
+ other->m_line_moved = true;
+ other->m_line_drag_cores = carried.cores;
+ other->reconcileCores(other->m_line_drag_cores, delta);
+ other->update();
+ }
+}
+
+/**
+ @brief CablePart::carrySelection
+ Everything else he marked at the same time comes along too: the
+ elements, the texts and the images of the same selection are moved
+ by the very step this line has taken, so that one gesture moves
+ whatever the selection is made of instead of two halves of it going
+ different ways -- the sheet has its own way of moving those, and
+ this is the only place which knows how far the lines have come.
+
+ The step handed over is the part of the gesture the sheet has not
+ been told yet: what this line measures is how far it has come all in
+ altogether, from where the press found it, while the sheet takes one
+ step at a time.
+ @param total_delta how far this line has come since the press
+*/
+void CablePart::carrySelection(const QPointF &total_delta)
+{
+ if (!diagram()) return;
+
+ ElementsMover &mover = diagram()->elementsMover();
+ if (!m_mover_started)
+ {
+ //Once per gesture rather than once per step: when nothing
+ //but lines is marked the sheet has nothing to move of its
+ //own, and must not be asked to start over at every step.
+ m_mover_started = true;
+ m_told_mover = QPointF();
+ mover.beginMovement(diagram(), this);
+ }
+
+ const QPointF step = total_delta - m_told_mover;
+ m_told_mover = total_delta;
+ if (!step.isNull()) {
+ mover.continueMovement(step);
+ }
+}
+
+/**
+ @brief CablePart::settleCompanions
+ Where every other line of this gesture comes to rest, worked out
+ exactly the way this line's own is: from where its colour labels
+ have come to stand, asking before anything changes hands.
+
+ The lines are settled one after the other rather than all at once,
+ so that a wire one of them lets go of is already free for the next
+ one instead of being asked about as if it were still taken.
+ @param steps receives one undo step per line he let go of
+ @return false when he cancelled one of their questions, in which
+ case nothing at all has to be written and the caller puts every
+ line back where it was picked up
+*/
+bool CablePart::settleCompanions(QList &steps)
+{
+ for (const CarriedLine &carried : std::as_const(m_carried))
+ {
+ CablePart *other = carried.part;
+ if (!other || !other->m_cable) continue;
+
+ QList after = other->m_line_drag_cores;
+ other->readyToSettle(after);
+
+ if (!other->settleCores(after)) {
+ return false;
+ }
+
+ other->m_cable->setCores(after);
+ steps.append(new MoveCablePartCommand(other->m_cable, other,
+ carried.p1, carried.p2,
+ other->m_p1, other->m_p2,
+ carried.cores, after));
+ }
+ return true;
+}
+
+/**
+ @brief CablePart::putCompanionsBack
+ Put every other line of this gesture back to where he picked it
+ up, cores included: the wire fields are rewritten along the way,
+ so that not one entry of the refused move is left standing.
+*/
+void CablePart::putCompanionsBack()
+{
+ for (const CarriedLine &carried : std::as_const(m_carried))
+ {
+ CablePart *other = carried.part;
+ if (!other) continue;
+
+ other->setLine(carried.p1, carried.p2);
+ if (other->m_cable) {
+ other->m_cable->setCores(carried.cores);
+ }
+ other->m_line_moved = false;
+ other->m_line_drag_cores.clear();
+ other->update();
+ }
+}
+
+/**
+ @brief CablePart::beginLineGesture
+ Where this line stood when the sheet began moving the selection it
+ is part of, kept here so that every step of that movement can carry
+ the line -- and its colour labels, which are written on it -- along
+ with it, exactly as if the finger had been on the line itself.
+
+ Nothing is carried along by this line: the sheet carries every line
+ of the selection itself, one after the other, so no line takes its
+ neighbours along a second time.
+*/
+void CablePart::beginLineGesture()
+{
+ m_move_p1 = m_p1;
+ m_move_p2 = m_p2;
+ m_line_moved = false;
+ m_line_drag_cores.clear();
+ m_carried.clear();
+ m_cores_before = m_cable ? m_cable->usedCores() : QList();
+ //Where its own labels stand when the gesture begins: they go on
+ //standing wherever the line is carried to, and undo has to know
+ //where they were before that.
+ m_line_drag_cores = m_cores_before;
+ m_stretch_end = 0;
+ m_dragged_core = -1;
+}
+
+/**
+ @brief CablePart::continueLineGesture
+ Take one more step of a move the sheet is driving: the line follows
+ by that much and every colour label standing on it comes along with
+ it, so line, description and colours move as one piece and no label
+ ever jumps, lands on a neighbour's wire or disappears under his
+ hand.
+ @param delta how far to come, in scene units
+*/
+void CablePart::continueLineGesture(const QPointF &delta)
+{
+ if (!m_cable || delta.isNull()) return;
+
+ setLine(m_p1 + delta, m_p2 + delta);
+ m_line_moved = true;
+ //Step by step rather than measured from where the gesture
+ //began: the sheet hands one step over at a time
+ reconcileCores(m_line_drag_cores, delta);
+ update();
+}
+
+/**
+ @brief CablePart::settleLineGesture
+ Where this line comes to rest, worked out the same way as after a
+ drag of its own: which wire every colour label has come to stand on,
+ asked before anything changes hands, and no label moved for it.
+
+ The step is handed over rather than pushed, so that the movement of
+ the whole selection ends up as one single undo step.
+ @param steps receives the undo step of this line, if any
+ @return false when he called the gesture off -- this line is already
+ back where it began then, and the sheet puts the rest back too
+*/
+bool CablePart::settleLineGesture(QList &steps)
+{
+ if (!m_line_moved) return true;
+ //Back where it began: nothing was carried anywhere, so there is
+ //nothing to work out and nothing to undo
+ if (m_p1 == m_move_p1 && m_p2 == m_move_p2) return true;
+
+ if (!m_cable || !diagram())
+ {
+ abortLineGesture();
+ return true;
+ }
+
+ QList after = m_line_drag_cores;
+ readyToSettle(after);
+
+ if (!settleCores(after))
+ {
+ abortLineGesture();
+ return false;
+ }
+
+ m_cable->setCores(after);
+ steps.append(new MoveCablePartCommand(m_cable, this,
+ m_move_p1, m_move_p2,
+ m_p1, m_p2,
+ m_cores_before, after));
+ return true;
+}
+
+/**
+ @brief CablePart::abortLineGesture
+ Put this line back where the gesture found it: the line goes back
+ to its place and its cable gets the labels it held at that moment,
+ whether they were written down during this gesture or not.
+*/
+void CablePart::abortLineGesture()
+{
+ if (m_line_moved)
+ {
+ if (m_cable) {
+ m_cable->setCores(m_cores_before);
+ }
+ setLine(m_move_p1, m_move_p2);
+ update();
+ }
+ m_line_moved = false;
+ m_line_drag_cores.clear();
+ m_cores_before.clear();
+}
+
+/**
+ @brief CablePart::doneLineGesture
+ The gesture is over for this line: forget where it began, the step
+ which was worked out from it having been handed over already.
+*/
+void CablePart::doneLineGesture()
+{
+ m_line_moved = false;
+ m_line_drag_cores.clear();
+ m_cores_before.clear();
+ m_stretch_end = 0;
+ m_dragged_core = -1;
+}
+
+/**
+ @brief CablePart::readyToSettle
+ Make a set of carried cores ready to be written into the cable:
+ this line's own labels wherever the gesture has brought them, and
+ every other section of the cable exactly as the cable holds it at
+ this moment.
+
+ The other sections are read back rather than carried, because
+ another line of this very gesture may have just settled and
+ written them -- and a core which is no longer there has just been
+ taken by another cable of the gesture, which makes it that
+ cable's free slot rather than one of ours.
+ @param cores the carried set, corrected in place
+*/
+void CablePart::readyToSettle(QList &cores) const
+{
+ if (!m_cable) return;
+
+ for (int i = cores.size() - 1; i >= 0; --i)
+ {
+ const bool mine = cores.at(i).part == m_part_uuid;
+ if (m_cable->hasCore(cores.at(i).core)) {
+ if (!mine) {
+ cores[i] = m_cable->core(cores.at(i).core);
+ }
+ } else {
+ cores.removeAt(i);
+ }
+ }
+}
+
+/**
+ @brief CablePart::wireFor
+ Which wire this line runs across at the very place a colour label
+ stands, so that the entry of a wire follows the label instead of
+ the label being sent off to find a wire.
+
+ The nearest crossing which is close enough wins, and where two are
+ as close as that, the wire the core already names wins the draw: a
+ label standing between two crossings goes on naming the one it had
+ rather than sliding onto the other, while a label dropped straight
+ onto another wire does take that one. Close enough means close
+ enough for the slash and the wire to read as one thing -- and
+ nothing at all that close means no wire: a label is never moved to
+ reach one, it keeps whatever it already names and stands where the
+ user left it.
+ @param crossings every place this line runs across a wire
+ @param core the core being worked out
+ @param at where its label stands, in scene coordinates
+ @return the wire to give the core, or nullptr
+*/
+Conductor *CablePart::wireFor(const QList &crossings,
+ const CableCore &core, const QPointF &at) const
+{
+ const CableCrossing *nearest = nullptr;
+ const qreal tolerance = CableManager::bind_tolerance;
+ qreal nearest_distance = tolerance;
+
+ for (const CableCrossing &crossing : crossings)
+ {
+ if (!crossing.conductor) continue;
+
+ const qreal distance = QLineF(crossing.position, at).length();
+ if (distance > tolerance) continue;
+ if (nearest && distance >= nearest_distance) {
+ //Where it already stands wins a draw, so that a label
+ //between two crossings never slides from one to the other
+ if (distance > nearest_distance
+ || crossing.conductor->uuid() != core.conductor) {
+ continue;
+ }
+ }
+
+ nearest = &crossing;
+ nearest_distance = distance;
+ }
+ return nearest ? nearest->conductor : nullptr;
+}
+
+/**
+ @brief CablePart::askClaim
+ The question every takeover is asked with: three answers, in
+ French, of which the third one is the way out of the whole gesture
+ -- he has let go in the wrong place, and would rather have nothing
+ happen at all than take a wire he did not mean to take.
+ @param parent the window the question is asked in
+ @param title the title of the question
+ @param question what is being asked
+ @return what he answered
+*/
+CablePart::ClaimAnswer CablePart::askClaim(QWidget *parent,
+ const QString &title,
+ const QString &question) const
+{
+ QMessageBox box(QMessageBox::Question, title, question, QMessageBox::NoButton, parent);
+ QPushButton *take = box.addButton(
+ tr("Yes", "Prendre le conducteur à l'autre câble."),
+ QMessageBox::YesRole);
+ QPushButton *leave = box.addButton(
+ tr("No", "Laisser le conducteur à l'autre câble, on ne change rien."),
+ QMessageBox::NoRole);
+ QPushButton *cancel = box.addButton(
+ tr("Cancel", "Annuler le geste entier : rien n'est écrit, la ligne et ses couleurs reviennent où elles étaient."),
+ QMessageBox::RejectRole);
+ //The safe answer is the one under his finger, and the escape
+ //key throws the whole gesture away rather than taking a wire.
+ box.setDefaultButton(leave);
+ box.setEscapeButton(cancel);
+ box.exec();
+
+ const QAbstractButton *clicked = box.clickedButton();
+ if (clicked == take) return ClaimAnswer::Take;
+ if (clicked == leave) return ClaimAnswer::Leave;
+ return ClaimAnswer::Cancel;
+}
+
+/**
+ @brief CablePart::mayClaim
+ Whether a wire may be given to this cable. A wire another cable
+ already describes is asked about first: taking it empties the cable
+ field of that other cable, and that is not something a drop or a
+ movement does behind the back of the user.
+ @param conductor the wire about to be taken
+ @return what he answered
+*/
+CablePart::ClaimAnswer CablePart::mayClaim(Conductor *conductor) const
+{
+ if (!conductor || !m_cable || !diagram() || !diagram()->project()) {
+ return ClaimAnswer::Leave;
+ }
+
+ const QUuid holder = conductor->properties().m_cable_uuid;
+ if (holder.isNull() || holder == m_cable->uuid()) return ClaimAnswer::Take;
+
+ const Cable *other = CableManager::cableByUuid(diagram()->project(), holder);
+ const QString other_name = other ? other->designation() : QString();
+
+ const QString text = other_name.isEmpty()
+ ? tr("This conductor is already described by another cable. Do you want to assign it to cable %1?",
+ "Un conducteur ne peut appartenir qu'à un câble : on demande avant de le prendre à l'autre.")
+ : tr("This conductor is already described by cable %1. Do you want to assign it to cable %2?",
+ "Un conducteur ne peut appartenir qu'à un câble : on demande avant de le prendre à l'autre.");
+
+ QWidget *parent = nullptr;
+ if (!diagram()->views().isEmpty()) {
+ parent = diagram()->views().constFirst();
+ }
+
+ return askClaim(parent,
+ tr("Conductor already assigned", "Un conducteur ne peut appartenir qu'à un câble."),
+ other_name.isEmpty() ? text.arg(m_cable->designation())
+ : text.arg(other_name, m_cable->designation()));
+}
+
+/**
+ @brief CablePart::mayClaimSeveral
+ The same question as mayClaim(), but asked once for a whole handful
+ of conductors: pulling a line across several wires which another
+ cable already describes must ask about them too, rather than either
+ quietly taking them or quietly leaving them -- one question, one
+ answer, and the other cable releases every wire he says yes to.
+ @param conductors the wires which other cables hold
+ @return what he answered
+*/
+CablePart::ClaimAnswer CablePart::mayClaimSeveral(const QList &conductors) const
+{
+ if (conductors.isEmpty() || !m_cable || !diagram() || !diagram()->project()) {
+ return ClaimAnswer::Leave;
+ }
+
+ //Which cables hold those wires: he is told the name of one of
+ //them, and that is the one he is most likely to be looking for.
+ QString other_name;
+ QSet others;
+ for (const Conductor *conductor : conductors)
+ {
+ const QUuid holder = conductor->properties().m_cable_uuid;
+ if (holder.isNull() || holder == m_cable->uuid()) continue;
+ others.insert(holder);
+ }
+ if (others.isEmpty()) return ClaimAnswer::Take;
+
+ if (others.size() == 1) {
+ const Cable *other = CableManager::cableByUuid(diagram()->project(), *others.constBegin());
+ if (other) other_name = other->designation();
+ }
+ //Several other cables at once: no one name would say it all.
+ const bool unnamed = (others.size() != 1) || other_name.isEmpty();
+
+ const int n = conductors.size();
+ const QString title = n <= 1
+ ? tr("Conductor already assigned", "Un conducteur ne peut appartenir qu'à un câble.")
+ : tr("Conductors already assigned", "Plusieurs conducteurs sont pris en une seule fois.", n);
+
+ const QString text = n <= 1
+ ? (unnamed
+ ? tr("This conductor is already described by another cable. Do you want to assign it to cable %1?",
+ "Un conducteur ne peut appartenir qu'à un câble : on demande avant de le prendre à l'autre.")
+ : tr("This conductor is already described by cable %1. Do you want to assign it to cable %2?",
+ "Un conducteur ne peut appartenir qu'à un câble : on demande avant de le prendre à l'autre."))
+ : (unnamed
+ ? tr("%n conductors are already described by other cables. Do you want to assign them to cable %1?",
+ "On prend plusieurs conducteurs à la fois : on demande avant de les prendre aux autres câbles.", n)
+ : tr("%n conductors are already described by cable %1. Do you want to assign them to cable %2?",
+ "On prend plusieurs conducteurs à la fois : on demande avant de les prendre à l'autre câble.", n));
+
+ const QString question = unnamed
+ ? text.arg(m_cable->designation())
+ : text.arg(other_name, m_cable->designation());
+
+ QWidget *parent = nullptr;
+ if (!diagram()->views().isEmpty()) {
+ parent = diagram()->views().constFirst();
+ }
+
+ return askClaim(parent, title, question);
+}
+
+/**
+ @brief CablePart::settleCores
+ Which wire each core names once the line has come to rest: read
+ off where the colour labels have stood themselves, so that moving a
+ line as a whole updates the entries of the wires instead of leaving
+ them behind on the wires it has gone away from.
+
+ While he drags, every label is merely carried along, so nothing
+ jumps, nothing lands on a neighbour's wire and nothing disappears
+ because the line came a little short -- the drawing stays readable
+ over the whole gesture, and pulling the line back puts every label
+ exactly back where it was. Which wire each core names is worked out
+ once, at the very end, and always from the place of the label: a
+ label standing on a wire gives that wire to its core, and the wire
+ it came from keeps no binding, so the refresh which follows empties
+ its cable field. Not one label is moved here. A label which came
+ to rest beside a wire rather than on it -- a whole grid step away,
+ which is as far as one pull of the line can carry it -- lets go of
+ that wire, because it no longer stands on it: an entry is always
+ where its colour label stands, and the grid is what puts a label
+ down on a crossing in the first place, so standing on that
+ crossing is what counts. When the line has been drawn off into an
+ empty part of the sheet, where it crosses no wire at all, every one
+ of its cores lets go, so that no entry is left standing on a wire
+ the label has gone away from. Pulling the line back puts every
+ label on a crossing again, and with it the entries they had.
+
+ The whole set is worked out at once, and not core after core in the
+ order they happen to be stored. Otherwise a label coming to rest on
+ a wire another label of this very line is standing on at this moment
+ would be turned away for a core which is moving off that very wire
+ in the same gesture -- and its entry would go on standing on a wire
+ the line has gone away from, which is exactly the untidy entry that
+ has to disappear. A label which does not move keeps its wire all the
+ same, and where two labels want one wire, the first of them has it
+ and the other keeps what it had.
+
+ A core which has no wire at all yet -- a copy put down on a folio
+ it was not wired on, because a copy is carried whole even where
+ there is nothing to wire -- takes the wire its own label stands on,
+ if there is one: pulling a copied line into place is what wires it.
+
+ Only wires nobody else holds are taken -- and when the line comes
+ to rest on wires another cable already describes, he is asked about
+ them once, before anything is taken, naming the other cable and
+ answering no unless he says yes. The answer covers the whole move:
+ either those wires change hands and the other cable releases them,
+ or none of them does.
+
+ Whichever way he answers, a core whose label has come to rest on
+ such a wire and names none of its own any more stops being a core
+ of its cable: it goes back to being a free slot, so that neither its
+ slash nor its colour is drawn on a wire which is not its own, and
+ its entry leaves the wire the label went away from -- an entry
+ following its label just as it does everywhere else, so that no
+ entry is left standing where no colour label stands. Yes makes the
+ other cable's core the free one instead; No makes it this cable's
+ own, and the other cable keeps its wire either way. That is why
+ both answers look the same on the sheet: one label standing there,
+ and a free slot in the list of existing cables.
+
+ And he may call the whole gesture off: nothing is then written at
+ all, the line goes back to where he picked it up, and the drawing
+ is once more what it was before he started.
+
+ @param cores the whole set of assignments, changed in place
+ @return false when he cancelled the gesture
+*/
+bool CablePart::settleCores(QList &cores) const
+{
+ if (!m_cable || !diagram()) return true;
+
+ const QList crossings = CableManager::crossedConductors(
+ diagram(), QLineF(m_p1, m_p2));
+ //A line which runs across nothing at all is not a reason to call
+ //the move off: it is the moment every core of this section lets
+ //go of the wire it came from, since an entry belongs to a wire
+ //the cable still touches. So the walk below happens either way,
+ //and then simply comes out with nothing to keep.
+
+ //Which wire every core ends up with is worked out for the whole
+ //set before a single one of them is written down -- a dry run
+ //first, which merely collects the wires of other cables on the
+ //way, so that he is asked about all of them before any of them
+ //changes hands.
+ //
+ //Working it out for the set as a whole, and not core by core in
+ //the order they happen to be stored, is what makes the entries
+ //come out right: a label coming to rest on a wire another label
+ //of this very line is standing on at this moment must have that
+ //wire, because the other label is moving off it in the same
+ //gesture -- whereas a label which does not move, and keeps the
+ //wire it names, must not be robbed of it.
+ const auto settle_once = [this, &crossings](
+ QList &list,
+ const bool dry_run,
+ QList *foreign_wires,
+ const bool take_foreign)
+ {
+ //A wire another cable already describes only changes hands
+ //when he has said yes to taking it away from that one.
+ const auto is_foreign = [this](Conductor *wire) {
+ return wire && !wire->properties().m_cable_uuid.isNull()
+ && wire->properties().m_cable_uuid != m_cable->uuid();
+ };
+
+ //The wire each colour label of this line is standing on
+ //right now -- worked out for every one of them first, so
+ //that which wire a core gets does not depend on which core
+ //is looked at first. None at all when a label stands on no
+ //wire, and then that core lets go of whatever it named.
+ QHash wanted;
+ for (const CableCore &core : list) {
+ if (core.part != m_part_uuid) continue;
+ wanted.insert(core.core, wireFor(crossings, core, core.position));
+ }
+
+ //Who lets go of nothing: a label standing on its own wire,
+ //and every label of another part of this cable, which this
+ //line never takes anything from. A label standing on no
+ //wire at all is not among them: it lets go of what it
+ //named, a whole grid step away from a crossing being
+ //already beside that wire rather than on it.
+ QSet keepers;
+ //Who ends up naming no wire at all: a label which has come
+ //to rest on a wire another cable holds, and he has said no
+ //to taking it. Such a core goes back to being a free slot of
+ //this cable -- exactly what the other cable's core becomes
+ //when he says yes -- so neither its slash nor its colour is
+ //drawn on a wire which is not its own, and its entry leaves
+ //the wire the label came from with it. The other cable keeps
+ //its wire either way.
+ QSet refuses;
+ for (const CableCore &core : list)
+ {
+ if (core.part != m_part_uuid) continue;
+ Conductor *wire = wanted.value(core.core, nullptr);
+ //Stands on no wire of this line: not a keeper, so it
+ //lets go of whatever it named further down.
+ if (!wire) continue;
+
+ if (wire->uuid() == core.conductor) {
+ keepers.insert(core.core);
+ continue;
+ }
+ if (!take_foreign && is_foreign(wire)) {
+ refuses.insert(core.core);
+ }
+ }
+
+ //Which core ends up with which wire, over and over until it
+ //stops moving: a core sent away from its own wire sends the
+ //one waiting for that wire away too, and that one may in
+ //turn free the wire the first one was after. The keepers
+ //only ever grow, so this cannot go round for ever.
+ QSet given_wires;
+ bool grew = true;
+ while (grew)
+ {
+ grew = false;
+ given_wires.clear();
+
+ //The wires nobody is letting go of
+ QSet staying;
+ for (const CableCore &core : list)
+ {
+ if (core.conductor.isNull()) continue;
+ if (core.part != m_part_uuid || keepers.contains(core.core)) {
+ staying.insert(core.conductor);
+ }
+ }
+
+ for (const CableCore &core : list)
+ {
+ if (core.part != m_part_uuid
+ || keepers.contains(core.core)
+ || refuses.contains(core.core)) continue;
+
+ Conductor *wanted_wire = wanted.value(core.core, nullptr);
+ if (!wanted_wire) continue; //stands on no wire: a keeper already
+
+ const QUuid uuid = wanted_wire->uuid();
+ if (staying.contains(uuid) || given_wires.contains(uuid))
+ {
+ //Another label of this line goes on standing on it,
+ //or has just been given it: this one keeps the wire
+ //it names now rather than taking it away from under
+ //a label which has come to rest on it.
+ keepers.insert(core.core);
+ grew = true;
+ continue;
+ }
+ given_wires.insert(uuid);
+ }
+ }
+
+ //The whole set has come to rest; now it can be written down
+ //without anything shifting under the feet of the next core.
+ //Cores which name no wire at all any more are collected on
+ //the way and taken out of the set only once the walk is over,
+ //since a list cannot be shortened while it is being walked.
+ QList freed;
+ for (CableCore &core : list)
+ {
+ if (core.part != m_part_uuid) continue;
+
+ Conductor *wire = wanted.value(core.core, nullptr);
+
+ if (refuses.contains(core.core))
+ {
+ //He said no, so this core names no wire at all any
+ //more: it goes back to being a free slot of this
+ //cable, exactly what the other cable's core becomes
+ //when he says yes to taking its wire. Neither its
+ //slash nor its colour stays standing on a wire which
+ //is not its own, and its entry leaves the wire the
+ //label came from with it -- while the other cable
+ //keeps its wire either way.
+ if (!dry_run) {
+ freed << core.core;
+ }
+ continue;
+ }
+
+ if (!wire)
+ {
+ //The label has come to rest where this line crosses
+ //nothing: the entry of the wire it came from goes
+ //with it, while the label itself stays exactly
+ //where he put it and merely names no wire at all
+ //any more -- an entry follows its label, and stops
+ //where its label stopped. A whole grid step away
+ //from a crossing is already beside that wire
+ //rather than on it, which is the case he means.
+ if (!dry_run && !core.conductor.isNull()) {
+ core.conductor = QUuid();
+ }
+ continue;
+ }
+
+ if (keepers.contains(core.core))
+ {
+ continue;
+ }
+
+ const QUuid uuid = wire->uuid();
+ //Another cable holds this wire: it only changes hands
+ //when he has said yes to taking it away from that one.
+ const bool foreign = is_foreign(wire);
+ if (foreign && foreign_wires) {
+ foreign_wires->append(wire);
+ }
+ if (!dry_run) {
+
+ wire->setUuid(uuid);
+ //A wire is one piece of wire: whichever cable held it
+ //before has to let it go, or its entry would stand on
+ //the wire all the same.
+ if (foreign) {
+ CableManager::claimConductor(diagram()->project(), wire, m_cable.data());
+ wire->setUuid(uuid);
+ }
+ core.conductor = uuid;
+ //The wire this core has just left keeps no binding, so
+ //the refresh which follows empties its cable field: an
+ //entry follows its label, and stops where its label
+ //stopped
+ }
+ }
+
+ //A core which names no wire is not a core of this cable any
+ //more: it goes back to being a free slot, and takes its slash
+ //and its colour off the sheet with it -- the very thing the
+ //other cable's core becomes when he says yes to taking its
+ //wire, so that both answers behave the same way.
+ for (int i = list.size() - 1; i >= 0; --i) {
+ if (list.at(i).part == m_part_uuid
+ && freed.contains(list.at(i).core)) {
+ list.removeAt(i);
+ }
+ }
+ };
+
+ QList trial = cores;
+ QList foreign_wires;
+ settle_once(trial, true, &foreign_wires, true);
+
+ //The whole move is asked about at once, and the wires of other
+ //cables are taken either all together or not at all.
+ ClaimAnswer answer = ClaimAnswer::Leave;
+ if (!foreign_wires.isEmpty()) {
+ answer = mayClaimSeveral(foreign_wires);
+ if (answer == ClaimAnswer::Cancel) return false;
+ }
+
+ settle_once(cores, false, nullptr, answer == ClaimAnswer::Take);
+ return true;
+}
+
+/**
+ @brief CablePart::itemChange
+ Fold any translation of the item into its own geometry: this item
+ works in scene coordinates and keeps itself at the origin, so that
+ one number -- the trunk line -- is all the cable ever has to store.
+
+ The colour labels come along with it. A line moved as a whole --
+ by a paste, by a duplicate, or together with the rest of a selection
+ -- is one thing, and its labels belong to it, so they travel rather
+ than staying behind at the place the line was copied from. The trunk
+ line dragged by its own gesture does not come through here: it is
+ given its new geometry directly, which is what lets that gesture
+ carry the labels step by step and settle them only at the end.
+ @param change
+ @param value
+ @return
+*/
+QVariant CablePart::itemChange(GraphicsItemChange change, const QVariant &value)
+{
+ if (change == ItemSelectedHasChanged)
+ {
+ //The two blue points at the ends come and go with the
+ //selection, so the moment a line is picked it has to be
+ //drawn again -- otherwise the points only appear at the
+ //next redraw of any other kind.
+ update();
+ }
+ if (change == ItemSceneHasChanged)
+ {
+ //The folio this line stands in is what its references to the
+ //other folios of the cable are worked out from, and it is
+ //also what has to be listened to: only now, once there is
+ //one, can they be kept up to date -- and once this line
+ //leaves that folio, they have to stop.
+ setUpXrefHooks();
+ }
+ if (change == ItemPositionHasChanged)
+ {
+ const QPointF new_pos = value.toPointF();
+ if (!new_pos.isNull())
+ {
+ prepareGeometryChange();
+ m_p1 += new_pos;
+ m_p2 += new_pos;
+ if (m_cable) {
+ m_cable->movePartCores(m_part_uuid, new_pos);
+ }
+ syncGeometry();
+ update();
+ QGraphicsItem::setPos(QPointF());
+ }
+ }
+ return QetGraphicsItem::itemChange(change, value);
+}
+
+/**
+ @brief CablePart::editProperty
+ Open the dialog which edits the properties of the cable this line
+ belongs to -- the number, the type, the installation, the place. They
+ belong to the cable, so editing them here changes them for every line
+ of that cable, on every folio.
+*/
+void CablePart::editProperty()
+{
+ if (!m_cable || !diagram() || diagram()->isReadOnly()) return;
+
+ //Asking for the whole cable is another thing than putting one
+ //of its cores down: anything which waited for a click stops
+ //waiting -- on this line or on any other, since only one of
+ //them can wait at a time -- and he can start over from this
+ //dialog.
+ cancelPlacing();
+
+ CablePropertiesDialog dialog(m_cable, diagram(), this,
+ diagram()->views().isEmpty()
+ ? nullptr : diagram()->views().constFirst());
+ dialog.exec();
+}
+
+/**
+ @brief CablePart::startPlacingCore
+ Give this line the mouse and wait for the click which puts that core
+ of its cable down: the core follows the line wherever the mouse comes
+ near it, drawn where the click would leave it -- on the line, at a
+ grid step, and never past either end of it.
+
+ It is always this very section which takes the core, never the first
+ section of the cable: a cable running over two folios stands on two
+ lines, and the one whose dialog was asked for is the one he is
+ working on.
+ @param core which core of the cable is being put down
+*/
+void CablePart::startPlacingCore(int core)
+{
+ if (!m_cable || !diagram() || diagram()->isReadOnly()) return;
+ if (core < 0 || core >= m_cable->coreCount() || m_cable->hasCore(core)) return;
+ if (m_dragged_core >= 0 || m_stretch_end > 0 || m_line_moved) return;
+ if (m_pending_core == core)
+ {
+ //He asks again for the very core which is waiting already:
+ //there is nothing to arm a second time, but the mouse still
+ //goes to the line -- asking twice has to behave exactly like
+ //asking once, and the mark is where he is being sent to.
+ sendMouseToLine();
+ return;
+ }
+
+ cancelPlacement();
+
+ //Only one line at a time may wait for its click, and it is the
+ //one whose dialog was asked for: a line standing on another
+ //folio lets go as well, so Escape and the right button always
+ //find the very line he is working on.
+ cancelPlacing();
+
+ m_pending_core = core;
+ s_placing = this;
+ m_drag_scene = m_p1;
+ m_pending_valid = false;
+
+ //Where the mouse stands at this very moment, so that a dialog
+ //which happened to cover the line does not have to be closed by
+ //a click the core would take for itself -- and then straight to
+ //the line, which is where the next click belongs.
+ sendMouseToLine();
+
+ if (!isSelected()) {
+ setSelected(true);
+ }
+ grabMouse();
+ setCursor(Qt::CrossCursor);
+ update();
+}
+
+/**
+ @brief CablePart::sendMouseToLine
+ Send the mouse to this line, onto the grid step the mark of the
+ waiting core stands on, so that the very next click is the click
+ which puts that core down: he picks the core out of a list, and the
+ line may well be across the screen from where that list stands.
+
+ The mouse goes wherever the line stands, as long as that line is
+ standing in some window he can see -- there is nothing else to ask.
+ It is never the mark which is held back: he asked for this core, the
+ very next click is the one which puts it down, and the mouse is
+ meant to be there when he clicks.
+*/
+void CablePart::sendMouseToLine()
+{
+ if (!scene() || scene()->views().isEmpty()) return;
+
+ QGraphicsView *view = scene()->views().constFirst();
+ const QPointF scene_pos = view->mapToScene(view->mapFromGlobal(QCursor::pos()));
+ m_drag_scene = placePosition(scene_pos);
+ m_pending_valid = QLineF(scene_pos, m_drag_scene).length() <= place_grab;
+ view->setFocus();
+
+ //The window he is actually looking at: a folio may stand in
+ //more than one window, and only the one which is on screen can
+ //be sent the mouse. The mark itself is drawn on the line
+ //whenever he asks for the core, seen or not -- whether a click
+ //keeps it or sends it away is worked out from the click itself
+ //(see mousePressEvent), so nothing here may stop the mouse
+ //from going where he just told it to go.
+ QGraphicsView *shown = nullptr;
+ for (QGraphicsView *candidate : scene()->views())
+ {
+ if (candidate && candidate->isVisible()) {
+ shown = candidate;
+ break;
+ }
+ }
+
+ if (shown)
+ {
+ QWidget *viewport = shown->viewport();
+ const QPoint there = viewport->mapFrom(shown, shown->mapFromScene(m_drag_scene));
+ const QPoint target = viewport->mapToGlobal(there);
+ const QPointF mark = m_drag_scene;
+ QPointer self(this);
+
+ QCursor::setPos(target);
+
+ //And again a little later: a pop-up which has just closed
+ //keeps the mouse in hand for a moment longer, and the system
+ //only lets go once the menu is really gone from the screen.
+ //A jump asked for earlier than that is dropped on the floor,
+ //while one asked for afterwards always goes through -- so the
+ //very same jump is asked for three times, as long as this
+ //line is still the one waiting and its mark still stands on
+ //that very spot.
+ const int waits[] = {0, 70, 250};
+ for (const int wait : waits)
+ {
+ QTimer::singleShot(wait, viewport, [self, target, mark]() {
+ if (!self || self->m_pending_core < 0 || self->m_drag_scene != mark) return;
+
+ //The same pop-up may have kept the mouse out of
+ //this line's hands while it was closing: the line
+ //waits for the click, so it has to be the one
+ //holding the mouse.
+ if (self->scene() && self->scene()->mouseGrabberItem() != self.data()) {
+ self->grabMouse();
+ }
+
+ QCursor::setPos(target);
+ });
+ }
+ }
+ update();
+
+}
+
+/**
+ @brief CablePart::cancelPlacement
+ Stop waiting for the click which would put a core down, without
+ putting anything down anywhere: the core goes back to being a free
+ one in the dialog it came from.
+*/
+void CablePart::cancelPlacement()
+{
+ if (s_placing == this) s_placing = nullptr;
+ if (m_pending_core < 0) return;
+
+ m_pending_core = -1;
+ m_pending_valid = false;
+ ungrabMouse();
+ unsetCursor();
+ update();
+}
+
+/**
+ @brief CablePart::placingPart
+ @return the line which waits for the click putting a core down right
+ now, or nullptr when no line does
+*/
+CablePart *CablePart::placingPart()
+{
+ return s_placing.data();
+}
+
+/**
+ @brief CablePart::cancelPlacing
+ End the wait of whichever line stands in it, without putting anything
+ down anywhere: the core goes back to being a free one.
+ @return true when a line was waiting for its click
+*/
+bool CablePart::cancelPlacing()
+{
+ if (!s_placing) return false;
+
+ s_placing->cancelPlacement();
+ return true;
+}
+
+/**
+ @brief CablePart::placeCore
+ Put a core which is not on the sheet yet onto this line, standing
+ where the mouse says: on a wire this line runs across it takes that
+ wire like any other label would -- including asking before taking
+ one another cable holds -- and where the line runs across nothing it
+ merely stands and names no wire at all.
+
+ The core belongs to this very section, so a cable standing on two
+ folios gets it on the line he asked for rather than on the first one.
+ @param core which core of the cable is being put down
+ @param at where its label is to stand, in scene coordinates
+ @return false when nothing was put down
+*/
+bool CablePart::placeCore(int core, const QPointF &at)
+{
+ if (!m_cable || !diagram() || diagram()->isReadOnly()) return false;
+ if (core < 0 || core >= m_cable->coreCount()) return false;
+ if (m_cable->hasCore(core)) return false;
+
+ //Every core the cable holds at this moment: the addition is
+ //kept as one step of undo together with whatever the click
+ //makes of it, so that taking it back leaves no trace.
+ const QList before = m_cable->usedCores();
+
+ CableCore fresh;
+ fresh.core = core;
+ fresh.part = m_part_uuid;
+ fresh.conductor = QUuid();
+ fresh.position = at;
+ m_cable->setCore(fresh);
+
+ if (!rebindCore(core, at, &before))
+ {
+ //He called the question off, so nothing is put down at
+ //all: the core goes back to being a free one, and no step
+ //of undo was written for what never happened.
+ m_cable->setCores(before);
+ return false;
+ }
+ return true;
+}
diff --git a/sources/cable/cablepart.h b/sources/cable/cablepart.h
new file mode 100644
index 000000000..a3b8744fa
--- /dev/null
+++ b/sources/cable/cablepart.h
@@ -0,0 +1,513 @@
+/*
+ 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 CABLEPART_H
+#define CABLEPART_H
+
+#include "cable.h"
+#include "cablemanager.h"
+#include "../qetgraphicsitem/qetgraphicsitem.h"
+
+#include
+#include
+#include
+#include
+#include
+
+class Cable;
+class CablePartData;
+class Conductor;
+class QUndoCommand;
+class QWidget;
+
+/**
+ @brief The CablePart class draws one section of a cable: the trunk
+ line of one folio, the number and the type of the cable at its left,
+ and a slash with the colour of a core wherever that core crosses a
+ conductor.
+
+ All of it is a view of the Cable: the geometry lives in the cable
+ itself, so lengthening the trunk line is purely optical. What a core
+ wires is read off where its colour label stands: dropping a label
+ gives that core the wire it lands on, and letting go of a moved line
+ works the entries out the same way, from where the labels have come
+ to rest. A label is never sent off to look for a wire and never
+ moved to reach one -- it goes on standing where the user put it --
+ and no gesture at all takes a core away from its cable.
+*/
+class CablePart : public QetGraphicsItem
+{
+ Q_OBJECT
+
+ public:
+ explicit CablePart(const CablePartData &data, QGraphicsItem *parent = nullptr);
+ explicit CablePart(QGraphicsItem *parent = nullptr);
+ ~CablePart() override;
+
+ enum {Type = UserType + 1012};
+ int type() const override {return Type;}
+
+ void setCable(Cable *cable);
+ Cable *cable() const;
+ QUuid partUuid() const;
+ ///Bind the item to its cable once the project is loaded
+ void refreshPending();
+
+ void setLine(const QPointF &p1, const QPointF &p2);
+ QLineF line() const;
+ QPointF firstPoint() const {return m_p1;}
+ QPointF secondPoint() const {return m_p2;}
+
+ QRectF boundingRect() const override;
+ QPainterPath shape() const override;
+ void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
+ QString name() const override;
+
+ void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
+ void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
+ void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
+ void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
+ ///Shows the hand over a line which points at another folio
+ void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;
+ ///Opens the dialog editing the properties of the whole cable
+ void editProperty() override;
+
+ /**
+ Give this line the mouse and wait for the next click to put
+ down a core of its cable which is not on the sheet yet: the
+ core follows this line wherever the mouse comes near it and
+ the click leaves it standing there, snapped to the grid like
+ anything else drawn.
+
+ It is always this very section which takes it, never the
+ first section of the cable: a cable running over two folios
+ stands on two lines, and the one whose dialog was asked for
+ is the one being worked on.
+ @param core which core of the cable is being put down
+ */
+ void startPlacingCore(int core);
+ ///Stop waiting for that click, putting nothing down at all
+ void cancelPlacement();
+
+ /**
+ The line which waits for the click putting one of its cores
+ down at this very moment, anywhere in the application, or
+ nullptr when no line does.
+
+ Only one line at a time may wait -- taking the core always
+ belongs to the line whose dialog was asked for -- so Escape
+ and the right button can simply ask here and always act on
+ the right one, however many folios are open.
+ */
+ static CablePart *placingPart();
+ /**
+ End that wait wherever it stands, without putting anything
+ down anywhere: the core goes back to being a free one.
+
+ Used by the sheet itself, which gets the right button and
+ Escape before any item does (see DiagramView).
+ @return true when a line was waiting for its click
+ */
+ static bool cancelPlacing();
+
+ /**
+ Which core of this cable has its colour label standing at
+ that very place on the sheet -- asked by the sheet itself
+ when the right button comes down on a line, so that the
+ menu which opens belongs to that one core rather than to
+ the whole folio.
+ @param scene_pos a point of the sheet, in scene coordinates
+ @return the core standing there, -1 when none does
+ */
+ int coreAt(const QPointF &scene_pos) const;
+ /**
+ Take one core off this line: it names no wire any more and
+ goes back to being a free slot of its cable, listed among
+ the ones which stand nowhere -- the very opposite of
+ putting one down from that list.
+
+ Kept as one undo step, so a single undo puts the label
+ back exactly where it stood.
+ @param core which core standing on this line is taken off
+ @return true when a core was taken off
+ */
+ bool takeCoreOffLine(int core);
+
+ /**
+ Which wire this line runs across at the very place a colour
+ label stands -- so that the entry of a wire follows the label
+ instead of the label being sent off to find a wire. Also asked
+ by a copy being put down (CableCopy::wire), which must not
+ move any label to reach a wire either.
+ @param crossings every place this line runs across a wire
+ @param core the core being worked out
+ @param at where its label stands, in scene coordinates
+ @return the wire to give the core, or nullptr
+ */
+ Conductor *wireFor(const QList &crossings,
+ const CableCore &core, const QPointF &at) const;
+
+ /**
+ Move this line from outside: the sheet moving a whole
+ selection of elements and lines at once asks every line
+ of the selection to come along, one step after the other,
+ exactly as if the finger had been on the line itself, so
+ that one gesture moves everything which is marked instead
+ of two halves of the selection going different ways.
+
+ The calls come in order -- begin once, continue as often
+ as the gesture needs, settle once for the whole
+ selection, then either done or abort -- and a line which
+ never moves simply has no undo step to hand over.
+ */
+ void beginLineGesture();
+ ///Take one more step of such a move, by that much
+ void continueLineGesture(const QPointF &delta);
+ /**
+ Where this line comes to rest, worked out the same way as
+ after a drag of its own: which wire every colour label now
+ stands on, asked before anything changes hands. The step
+ of this line is handed over rather than pushed, so that
+ the movement as a whole ends up as one single undo step.
+ @param steps receives the undo step of this line, if any
+ @return false when he called the gesture off
+ */
+ bool settleLineGesture(QList &steps);
+ ///Put this line back where the gesture found it
+ void abortLineGesture();
+ ///The gesture is over for this line: forget where it began
+ void doneLineGesture();
+
+ /**
+ One cross reference of this section as the PDF export
+ needs it: where the text of the reference stands on this
+ folio, and on the folio it points at where the section it
+ names stands -- so that exporting makes it a clickable
+ link the way a cross reference of an element already is.
+
+ Both rectangles are in scene coordinates, each of its own
+ folio.
+ */
+ struct PdfRef
+ {
+ ///The folio the reference points at
+ Diagram *diagram = nullptr;
+ ///Where the text of the reference stands on this folio
+ QRectF here;
+ ///Where the section it names stands on that folio
+ QRectF there;
+ };
+ QList pdfRefs() const;
+
+ protected:
+ QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
+
+ private:
+ /**
+ One line of the label at the left of the trunk line:
+ what is written, which field of the cable it comes from
+ and -- worked out from what that field asks for -- the
+ font it is written with, the way it lines up and the
+ rectangle it sits in.
+
+ Every line carries its own font, because every field
+ may have been given one of its own: Anlage, Ort and
+ Désignation stand in one block, and their lines may
+ very well no longer all be the same size once he has
+ set them apart.
+ */
+ struct LabelLine
+ {
+ QString text;
+ QString key;
+ QFont font;
+ Qt::Alignment align = Qt::AlignRight;
+ qreal width = 0.0;
+ qreal height = 0.0;
+ QRectF rect;
+ };
+
+ /**
+ One line of the label which points at a folio this cable
+ runs on besides this one: that folio, the section of the
+ cable standing there, and the text this project asks for
+ such a reference -- the format chosen for the type
+ "Cable" under Cross-references, written out once per folio,
+ in the order the folios turn.
+ */
+ struct CrossRef
+ {
+ ///The other folio, which always exists while listed
+ Diagram *diagram = nullptr;
+ ///Which section of the cable stands on it
+ QUuid part;
+ ///The text as it is written on the drawing
+ QString text;
+ };
+
+ QFont textFont() const;
+ ///The font the colours of the cores are written with
+ QFont coreFont() const;
+ ///The font one named text of this cable is written with
+ QFont fontOf(const QString &key) const;
+ ///How one named text of this cable lines up at the line
+ Qt::Alignment alignOf(const QString &key) const;
+ /**
+ How far the whole label at the left of the line is
+ nudged on the drawing: over the line, under it, left
+ or right of the end of it. A nudge only, which moves
+ the texts and never the line.
+ @return the distance in scene units
+ */
+ QPointF textOffset() const;
+ /**
+ How far the row of colours is nudged on the drawing:
+ again a distance on the texts only, the slashes and
+ the entries behind them staying where they stand.
+ @return the distance in scene units
+ */
+ QPointF coreOffset() const;
+ /**
+ Where a text of this width starts, at the left end of
+ the trunk line: before the line when it is set to end
+ there (the way it always did), on the line when it is
+ centred, after it when it starts there.
+ @param key the field the text comes from
+ @param width how wide the text is, in scene units
+ @return the x of the left of the text
+ */
+ qreal textLeft(const QString &key, qreal width) const;
+ bool trunkIsHorizontal() const;
+ QList labelLines() const;
+ /**
+ The references to the other folios this cable runs on,
+ one line each, stacked under the length of the cable:
+ at the very bottom of everything the line says.
+ @return those lines, in the order crossRefs() lists them
+ */
+ QList refLines() const;
+ /**
+ Every other folio this cable runs on, one entry each, in
+ the order the folios turn -- empty when the cable runs on
+ this folio alone, which is the usual case.
+ */
+ QList crossRefs() const;
+ /**
+ Which of those references stands at that very place, so
+ that both the hand of the mouse and a double click know
+ which line they are about.
+ @param scene_pos a point of the sheet, in scene coordinates
+ @return the index of that reference, -1 when none stands there
+ */
+ int crossRefAt(const QPointF &scene_pos) const;
+ /**
+ Send the mouse to the section of the cable which stands
+ on the folio the reference at that place points at -- the
+ very leap an element's cross reference takes, straight to
+ the place to look at.
+ @param scene_pos a point of the sheet, in scene coordinates
+ @return true when the click stood on a reference
+ */
+ bool goToCrossRef(const QPointF &scene_pos);
+ /**
+ Keep the reference lines up to date with the project: a
+ folio which is added, removed or moved changes what they
+ say, and so does the format chosen under Cross-references.
+ */
+ void setUpXrefHooks();
+ QString typeText() const;
+ QString lengthText() const;
+ QRectF labelRect() const;
+ qreal underTop() const;
+ QRectF typeRect() const;
+ QRectF lengthRect() const;
+ QRectF coreLabelRect(const QPointF &at, const QString &color) const;
+ QPointF coreLabelPivot(const QPointF &at, const QString &color) const;
+ QPointF coreDrawPosition(const CableCore &core) const;
+ bool coreDrawn(const CableCore &core) const;
+ QPointF dragPosition(const QPointF &scene_pos) const;
+ /**
+ Where the mouse would put down a core which waits to be
+ put down: the mouse projected onto the line itself, at a
+ grid step, and never past either end of the line -- the
+ same coordinates a dragged label would be drawn at, only
+ worked out from the mouse rather than from a gesture.
+ @param scene_pos where the mouse stands, in scene coordinates
+ @return where the slash would be drawn
+ */
+ QPointF placePosition(const QPointF &scene_pos) const;
+ /**
+ Snap a distance measured along the line to a grid step, in
+ the coordinates of the sheet -- so a horizontal line steps
+ along x and a vertical one along y. Holding Ctrl lets go of
+ the grid, as everywhere else on the sheet.
+ @param along how far from the first end of the line
+ @return that distance at a grid step
+ */
+ qreal stepAlong(qreal along) const;
+ void drawLabel(QPainter *painter, const QColor &ink) const;
+ void drawCores(QPainter *painter, const QColor &ink) const;
+ ///Draw the slash of one core and the colour written beside it
+ void drawCoreMark(QPainter *painter, const QColor &ink,
+ const QPointF &at, const QString &color) const;
+ void syncGeometry();
+ bool labelAt(int core, const QPointF &scene_pos) const;
+ ///Answer to the question of taking a wire another cable holds
+ enum class ClaimAnswer {
+ ///Take it: the other cable lets it go
+ Take,
+ ///Leave it: nothing changes hands, the rest still happens
+ Leave,
+ ///Cancel: this whole gesture is called off, as if undone
+ Cancel
+ };
+ /**
+ @param core which core is being let go
+ @param dropped_at where its label comes to rest
+ @param before every core the cable held before this one was
+ added to it, so that the one step of undo takes the addition
+ back as well; nullptr while a core which is already on the
+ sheet is merely moved
+ @return false when the user called the whole thing off
+ */
+ bool rebindCore(int core, const QPointF &dropped_at,
+ const QList *before = nullptr);
+ /**
+ Put a core which is not on the sheet yet onto this line: it
+ stands where the mouse says, and if a wire runs across that
+ very place it names that wire the same way a dragged label
+ would. Called off means back to being a free core, with one
+ step of undo to take it away again.
+ @param core which core of the cable is being put down
+ @param at where its label is to stand, in scene coordinates
+ @return false when nothing was put down
+ */
+ bool placeCore(int core, const QPointF &at);
+ /**
+ Send the mouse to this line, to the grid step the mark of
+ the waiting core stands on, so that the very next click is
+ the one which puts the core down -- wherever he asked for
+ the core from, and whether he asks for this very one a
+ second time or not. The mouse goes there only where the
+ line is really in sight: a folio hidden behind another
+ one, or off the edge of the view, is left alone rather
+ than jumped to.
+ */
+ void sendMouseToLine();
+ void reconcileCores(QList &cores, const QPointF &delta) const;
+ bool settleCores(QList &cores) const;
+ ClaimAnswer mayClaim(Conductor *conductor) const;
+ ClaimAnswer mayClaimSeveral(const QList &conductors) const;
+ ClaimAnswer askClaim(QWidget *parent,
+ const QString &title,
+ const QString &question) const;
+
+ private:
+ ///The one line which waits for the click putting a core down,
+ ///null once there is none: taking the core belongs to the line
+ ///whose dialog was asked for, never to the first line of the
+ ///cable, and the sheet has to be able to cancel that wait
+ ///without knowing which scene it stands in
+ static QPointer s_placing;
+ QPointer m_cable;
+ ///Identity of the cable this part belongs to, kept until the
+ ///project has loaded it and setCable() can be called
+ QUuid m_pending_cable;
+ QUuid m_part_uuid;
+ QPointF m_p1;
+ QPointF m_p2;
+
+ ///Core being dragged by its slash, -1 when none
+ int m_dragged_core = -1;
+ ///Core waiting to be put down by the next click, -1 when none
+ int m_pending_core = -1;
+ ///True while the mouse stands close enough to this line for
+ ///the waiting core to be put down where it shows
+ bool m_pending_valid = false;
+ ///True between the click which put a core down and the second
+ ///click Qt counts as a double click, so that a click which was
+ ///there to place something does not open the dialog as well
+ bool m_just_placed = false;
+ ///Where the dragged slash follows the mouse
+ QPointF m_drag_scene;
+ QPointF m_press_scene;
+ QPointF m_move_p1;
+ QPointF m_move_p2;
+ ///True while the trunk line is being dragged, so undo knows
+ ///there is a move to take back
+ bool m_line_moved = false;
+ ///Which end of the trunk line is being pulled by one of its
+ ///blue points: 0 none, 1 the first end, 2 the second
+ int m_stretch_end = 0;
+ ///Every core as it stands while the line is being dragged:
+ ///every label is carried along by however far the line has
+ ///come, so the line, its description and the colours of its
+ ///cores move as one thing and no label ever jumps, lands on
+ ///a neighbour's wire or disappears under his hand
+ QList m_line_drag_cores;
+ ///Every core as it was when the line was picked up, so undo
+ ///can put the labels back with the line
+ QList m_cores_before;
+ ///Another line he picked up at the same time: a drag takes
+ ///the whole selection with it, so each of them is carried
+ ///along and each of them goes back to where it stood if he
+ ///calls the gesture off
+ struct CarriedLine
+ {
+ CablePart *part = nullptr;
+ ///Where that line ran when the gesture began
+ QPointF p1;
+ QPointF p2;
+ ///What its cable held when the gesture began
+ QList cores;
+ };
+ QList m_carried;
+
+ ///True when the sheet has been told that the rest of the
+ ///selection comes along with this line (see carrySelection),
+ ///so that it is told once per gesture rather than once per
+ ///step -- and never again once it had nothing to move
+ bool m_mover_started = false;
+ ///How far of this gesture the sheet has already been told
+ QPointF m_told_mover;
+
+ ///The lines which keep the references to the other folios of
+ ///this cable up to date, taken down and taken again whenever
+ ///this section comes to stand in another folio
+ QList m_xref_connections;
+
+ void carryCompanions(const QPointF &delta);
+ /**
+ Everything else he marked at the same time comes along
+ too -- the elements, the texts and the images of the same
+ selection -- by the very step this line has taken, so
+ that one gesture moves all of it instead of the lines
+ going one way and the rest another.
+
+ What this line measures is how far it has come all in
+ altogether, from where the press found it, while the
+ sheet takes a step at a time: so only the part it has not
+ been told yet is handed over.
+ @param total_delta how far this line has come since the press
+ */
+ void carrySelection(const QPointF &total_delta);
+ bool settleCompanions(QList &steps);
+ void putCompanionsBack();
+ void readyToSettle(QList &cores) const;
+};
+
+#endif // CABLEPART_H
diff --git a/sources/cable/cablepropertiesdialog.cpp b/sources/cable/cablepropertiesdialog.cpp
new file mode 100644
index 000000000..f489c4a44
--- /dev/null
+++ b/sources/cable/cablepropertiesdialog.cpp
@@ -0,0 +1,279 @@
+/*
+ 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 "cablepropertiesdialog.h"
+
+#include "cablecreatedialog.h"
+#include "cablepart.h"
+#include "cablepropertieswidget.h"
+#include "editcablecommand.h"
+#include "../diagram.h"
+#include "../qeticons.h"
+
+#include
+#include
+#include
+#include
+#include
+#include